Merge branch 'master' of /usr/local/google/home/alexfandrianto/sky
diff --git a/.gitignore b/.gitignore
index b7e2fde..583a08c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,17 @@
-/.v23
\ No newline at end of file
+/.v23
+
+# Don’t commit the following directories created by pub.
+.buildlog
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6bcebad
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+# Get the packages used by the dart project, according to pubspec.yaml
+# Can also use `pub get`, but Sublime occasionally reverts me to an ealier version.
+# Only `pub upgrade` can escape such a thing.
+get-packages: pubspec.yaml
+ pub upgrade
+
+TEST_FILES := $(shell find test -name *.dart ! -name *.part.dart)
+
+check-fmt:
+ dartfmt -n lib/main.dart $(TEST_FILES)
+
+lint:
+ dartanalyzer lib/main.dart
+ dartanalyzer $(TEST_FILES)
+
+start:
+ ./packages/sky/sky_tool start
+
+install: get-packages
+ ./packages/sky/sky_tool start --install
+
+# Could use `pub run test` too, but I like seeing every assertion print out.
+test:
+ dart --checked $(TEST_FILES)
+
+clean:
+ rm -rf packages
+
+.PHONY: check-fmt lint start install test clean
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b01c72b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+This is the basic Getting Started with Sky + some Widgets.
+
+To run this, just do
+./packages/sky/sky_tool start
+
+when your phone is connected.
+Add --install to the end if this is the first run.
+
+You may also want to debug any problems. Use dartanalyzer.
+dartanalyzer lib/main.dart
\ No newline at end of file
diff --git a/lib/card.dart b/lib/card.dart
new file mode 100644
index 0000000..48d815e
--- /dev/null
+++ b/lib/card.dart
@@ -0,0 +1,113 @@
+library card;
+
+import 'package:sky/widgets/basic.dart';
+import 'dart:sky' as sky;
+import 'package:vector_math/vector_math.dart' as vector_math;
+
+import 'my_button.dart';
+
+class Card {
+ String deck;
+ String identifier;
+
+ Card(this.deck, this.identifier);
+ Card.fromString(String data) {
+ List<String> parts = data.split(" ");
+ assert(parts.length == 2);
+ this
+ ..deck = parts[0]
+ ..identifier = parts[1];
+ }
+
+ toString() => "${deck} ${identifier}";
+
+ get string => toString();
+}
+
+class CardComponent extends StatefulComponent {
+ // Stateful components
+ double dx;
+ double dy;
+ bool faceUp;
+ bool scrolling;
+
+ final Card card;
+ final Function pointerUpCb;
+
+ static Widget imageFromCard(Card c, bool faceUp) {
+ String imageName = "${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
+ return new NetworkImage(src: imageName);
+ }
+
+ CardComponent(this.card, bool faceUp, [this.pointerUpCb = null]) {
+ this.faceUp = faceUp;
+ dx = 0.0;
+ dy = 0.0;
+ scrolling = false;
+ }
+
+ void syncFields(CardComponent other) {
+ this.dx = other.dx;
+ this.dy = other.dy;
+ this.faceUp = other.faceUp;
+ }
+
+ void _onPressed(sky.Event e) {
+ setState(() {
+ this.faceUp = !this.faceUp;
+ });
+ }
+
+ void _onPointerDown(sky.Event e) {
+ setState(() {
+ scrolling = true;
+ });
+ }
+
+ void _onPointerMove(sky.Event e) {
+ sky.PointerEvent ge = e as sky.PointerEvent;
+ setState(() {
+ dx += ge.dx;
+ dy += ge.dy;
+ });
+ }
+
+ void _onPointerUp(sky.Event e) {
+ //sky.PointerEvent pe = e as sky.PointerEvent;
+ setState(() {
+ if (this.pointerUpCb != null) {
+ pointerUpCb(this.dx, this.dy, this.faceUp);
+ }
+ this.dx = 0.0;
+ this.dy = 0.0;
+ this.faceUp = true;
+ scrolling = false;
+ });
+ }
+
+ Widget build() {
+ return new Container(
+ child: /*new Container(
+ child: */new MyButton(
+ child: imageFromCard(this.card, faceUp),
+ onPressed: _onPressed,
+ onPointerDown: _onPointerDown,
+ onPointerMove: _onPointerMove,
+ onPointerUp: _onPointerUp
+ ),
+ padding: const EdgeDims.all(8.0),
+ //margin: const EdgeDims.symmetric(horizontal: 8.0),
+ decoration: new BoxDecoration(
+ backgroundColor: (this.scrolling ? const Color(0xFFFF0000) : const Color(0xFF0000FF)),
+ borderRadius: 5.0
+ ),
+ transform: new vector_math.Matrix4.identity().translate(dx, dy)
+ );
+ }
+}
+
+// I think we should be free to move cards around the screen as we please.
+// However, that doesn't mean we can tap them willy nilly or let go of them so easily.
+// I propose that.
+// Card follows you onScroll.
+// But onPressed and on
\ No newline at end of file
diff --git a/lib/classic/down/c1.png b/lib/classic/down/c1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c1.png
Binary files differ
diff --git a/lib/classic/down/c10.png b/lib/classic/down/c10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c10.png
Binary files differ
diff --git a/lib/classic/down/c2.png b/lib/classic/down/c2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c2.png
Binary files differ
diff --git a/lib/classic/down/c3.png b/lib/classic/down/c3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c3.png
Binary files differ
diff --git a/lib/classic/down/c4.png b/lib/classic/down/c4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c4.png
Binary files differ
diff --git a/lib/classic/down/c5.png b/lib/classic/down/c5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c5.png
Binary files differ
diff --git a/lib/classic/down/c6.png b/lib/classic/down/c6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c6.png
Binary files differ
diff --git a/lib/classic/down/c7.png b/lib/classic/down/c7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c7.png
Binary files differ
diff --git a/lib/classic/down/c8.png b/lib/classic/down/c8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c8.png
Binary files differ
diff --git a/lib/classic/down/c9.png b/lib/classic/down/c9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c9.png
Binary files differ
diff --git a/lib/classic/down/cj.png b/lib/classic/down/cj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/cj.png
Binary files differ
diff --git a/lib/classic/down/ck.png b/lib/classic/down/ck.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/ck.png
Binary files differ
diff --git a/lib/classic/down/cq.png b/lib/classic/down/cq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/cq.png
Binary files differ
diff --git a/lib/classic/down/d1.png b/lib/classic/down/d1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d1.png
Binary files differ
diff --git a/lib/classic/down/d10.png b/lib/classic/down/d10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d10.png
Binary files differ
diff --git a/lib/classic/down/d2.png b/lib/classic/down/d2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d2.png
Binary files differ
diff --git a/lib/classic/down/d3.png b/lib/classic/down/d3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d3.png
Binary files differ
diff --git a/lib/classic/down/d4.png b/lib/classic/down/d4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d4.png
Binary files differ
diff --git a/lib/classic/down/d5.png b/lib/classic/down/d5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d5.png
Binary files differ
diff --git a/lib/classic/down/d6.png b/lib/classic/down/d6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d6.png
Binary files differ
diff --git a/lib/classic/down/d7.png b/lib/classic/down/d7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d7.png
Binary files differ
diff --git a/lib/classic/down/d8.png b/lib/classic/down/d8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d8.png
Binary files differ
diff --git a/lib/classic/down/d9.png b/lib/classic/down/d9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d9.png
Binary files differ
diff --git a/lib/classic/down/dj.png b/lib/classic/down/dj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/dj.png
Binary files differ
diff --git a/lib/classic/down/dk.png b/lib/classic/down/dk.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/dk.png
Binary files differ
diff --git a/lib/classic/down/dq.png b/lib/classic/down/dq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/dq.png
Binary files differ
diff --git a/lib/classic/down/h1.png b/lib/classic/down/h1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h1.png
Binary files differ
diff --git a/lib/classic/down/h10.png b/lib/classic/down/h10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h10.png
Binary files differ
diff --git a/lib/classic/down/h2.png b/lib/classic/down/h2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h2.png
Binary files differ
diff --git a/lib/classic/down/h3.png b/lib/classic/down/h3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h3.png
Binary files differ
diff --git a/lib/classic/down/h4.png b/lib/classic/down/h4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h4.png
Binary files differ
diff --git a/lib/classic/down/h5.png b/lib/classic/down/h5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h5.png
Binary files differ
diff --git a/lib/classic/down/h6.png b/lib/classic/down/h6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h6.png
Binary files differ
diff --git a/lib/classic/down/h7.png b/lib/classic/down/h7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h7.png
Binary files differ
diff --git a/lib/classic/down/h8.png b/lib/classic/down/h8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h8.png
Binary files differ
diff --git a/lib/classic/down/h9.png b/lib/classic/down/h9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h9.png
Binary files differ
diff --git a/lib/classic/down/hj.png b/lib/classic/down/hj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/hj.png
Binary files differ
diff --git a/lib/classic/down/hk.png b/lib/classic/down/hk.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/hk.png
Binary files differ
diff --git a/lib/classic/down/hq.png b/lib/classic/down/hq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/hq.png
Binary files differ
diff --git a/lib/classic/down/s1.png b/lib/classic/down/s1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s1.png
Binary files differ
diff --git a/lib/classic/down/s10.png b/lib/classic/down/s10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s10.png
Binary files differ
diff --git a/lib/classic/down/s2.png b/lib/classic/down/s2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s2.png
Binary files differ
diff --git a/lib/classic/down/s3.png b/lib/classic/down/s3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s3.png
Binary files differ
diff --git a/lib/classic/down/s4.png b/lib/classic/down/s4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s4.png
Binary files differ
diff --git a/lib/classic/down/s5.png b/lib/classic/down/s5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s5.png
Binary files differ
diff --git a/lib/classic/down/s6.png b/lib/classic/down/s6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s6.png
Binary files differ
diff --git a/lib/classic/down/s7.png b/lib/classic/down/s7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s7.png
Binary files differ
diff --git a/lib/classic/down/s8.png b/lib/classic/down/s8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s8.png
Binary files differ
diff --git a/lib/classic/down/s9.png b/lib/classic/down/s9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s9.png
Binary files differ
diff --git a/lib/classic/down/sj.png b/lib/classic/down/sj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/sj.png
Binary files differ
diff --git a/lib/classic/down/sk.png b/lib/classic/down/sk.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/sk.png
Binary files differ
diff --git a/lib/classic/down/sq.png b/lib/classic/down/sq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/sq.png
Binary files differ
diff --git a/lib/classic/up/c1.png b/lib/classic/up/c1.png
new file mode 100644
index 0000000..5f90e5a
--- /dev/null
+++ b/lib/classic/up/c1.png
Binary files differ
diff --git a/lib/classic/up/c10.png b/lib/classic/up/c10.png
new file mode 100644
index 0000000..7768ba3
--- /dev/null
+++ b/lib/classic/up/c10.png
Binary files differ
diff --git a/lib/classic/up/c2.png b/lib/classic/up/c2.png
new file mode 100644
index 0000000..fd60560
--- /dev/null
+++ b/lib/classic/up/c2.png
Binary files differ
diff --git a/lib/classic/up/c3.png b/lib/classic/up/c3.png
new file mode 100644
index 0000000..853f0b8
--- /dev/null
+++ b/lib/classic/up/c3.png
Binary files differ
diff --git a/lib/classic/up/c4.png b/lib/classic/up/c4.png
new file mode 100644
index 0000000..ce8ea7a
--- /dev/null
+++ b/lib/classic/up/c4.png
Binary files differ
diff --git a/lib/classic/up/c5.png b/lib/classic/up/c5.png
new file mode 100644
index 0000000..328b4b3
--- /dev/null
+++ b/lib/classic/up/c5.png
Binary files differ
diff --git a/lib/classic/up/c6.png b/lib/classic/up/c6.png
new file mode 100644
index 0000000..3bc97f4
--- /dev/null
+++ b/lib/classic/up/c6.png
Binary files differ
diff --git a/lib/classic/up/c7.png b/lib/classic/up/c7.png
new file mode 100644
index 0000000..de94e1e
--- /dev/null
+++ b/lib/classic/up/c7.png
Binary files differ
diff --git a/lib/classic/up/c8.png b/lib/classic/up/c8.png
new file mode 100644
index 0000000..5caff1c
--- /dev/null
+++ b/lib/classic/up/c8.png
Binary files differ
diff --git a/lib/classic/up/c9.png b/lib/classic/up/c9.png
new file mode 100644
index 0000000..0419781
--- /dev/null
+++ b/lib/classic/up/c9.png
Binary files differ
diff --git a/lib/classic/up/cj.png b/lib/classic/up/cj.png
new file mode 100644
index 0000000..fe2bc6b
--- /dev/null
+++ b/lib/classic/up/cj.png
Binary files differ
diff --git a/lib/classic/up/ck.png b/lib/classic/up/ck.png
new file mode 100644
index 0000000..7e85b06
--- /dev/null
+++ b/lib/classic/up/ck.png
Binary files differ
diff --git a/lib/classic/up/cq.png b/lib/classic/up/cq.png
new file mode 100644
index 0000000..24e9efb
--- /dev/null
+++ b/lib/classic/up/cq.png
Binary files differ
diff --git a/lib/classic/up/d1.png b/lib/classic/up/d1.png
new file mode 100644
index 0000000..5169ec4
--- /dev/null
+++ b/lib/classic/up/d1.png
Binary files differ
diff --git a/lib/classic/up/d10.png b/lib/classic/up/d10.png
new file mode 100644
index 0000000..fe94ead
--- /dev/null
+++ b/lib/classic/up/d10.png
Binary files differ
diff --git a/lib/classic/up/d2.png b/lib/classic/up/d2.png
new file mode 100644
index 0000000..211a24a
--- /dev/null
+++ b/lib/classic/up/d2.png
Binary files differ
diff --git a/lib/classic/up/d3.png b/lib/classic/up/d3.png
new file mode 100644
index 0000000..9abcace
--- /dev/null
+++ b/lib/classic/up/d3.png
Binary files differ
diff --git a/lib/classic/up/d4.png b/lib/classic/up/d4.png
new file mode 100644
index 0000000..81ebd47
--- /dev/null
+++ b/lib/classic/up/d4.png
Binary files differ
diff --git a/lib/classic/up/d5.png b/lib/classic/up/d5.png
new file mode 100644
index 0000000..e9505ea
--- /dev/null
+++ b/lib/classic/up/d5.png
Binary files differ
diff --git a/lib/classic/up/d6.png b/lib/classic/up/d6.png
new file mode 100644
index 0000000..eaa1f6e
--- /dev/null
+++ b/lib/classic/up/d6.png
Binary files differ
diff --git a/lib/classic/up/d7.png b/lib/classic/up/d7.png
new file mode 100644
index 0000000..dc0b272
--- /dev/null
+++ b/lib/classic/up/d7.png
Binary files differ
diff --git a/lib/classic/up/d8.png b/lib/classic/up/d8.png
new file mode 100644
index 0000000..c736dc3
--- /dev/null
+++ b/lib/classic/up/d8.png
Binary files differ
diff --git a/lib/classic/up/d9.png b/lib/classic/up/d9.png
new file mode 100644
index 0000000..4f6d187
--- /dev/null
+++ b/lib/classic/up/d9.png
Binary files differ
diff --git a/lib/classic/up/dj.png b/lib/classic/up/dj.png
new file mode 100644
index 0000000..3fce1d7
--- /dev/null
+++ b/lib/classic/up/dj.png
Binary files differ
diff --git a/lib/classic/up/dk.png b/lib/classic/up/dk.png
new file mode 100644
index 0000000..307cd29
--- /dev/null
+++ b/lib/classic/up/dk.png
Binary files differ
diff --git a/lib/classic/up/dq.png b/lib/classic/up/dq.png
new file mode 100644
index 0000000..5e26a9b
--- /dev/null
+++ b/lib/classic/up/dq.png
Binary files differ
diff --git a/lib/classic/up/ec.png b/lib/classic/up/ec.png
new file mode 100644
index 0000000..d91730c
--- /dev/null
+++ b/lib/classic/up/ec.png
Binary files differ
diff --git a/lib/classic/up/h1.png b/lib/classic/up/h1.png
new file mode 100644
index 0000000..53681ca
--- /dev/null
+++ b/lib/classic/up/h1.png
Binary files differ
diff --git a/lib/classic/up/h10.png b/lib/classic/up/h10.png
new file mode 100644
index 0000000..b49eb67
--- /dev/null
+++ b/lib/classic/up/h10.png
Binary files differ
diff --git a/lib/classic/up/h2.png b/lib/classic/up/h2.png
new file mode 100644
index 0000000..ad333a0
--- /dev/null
+++ b/lib/classic/up/h2.png
Binary files differ
diff --git a/lib/classic/up/h3.png b/lib/classic/up/h3.png
new file mode 100644
index 0000000..9f5c26c
--- /dev/null
+++ b/lib/classic/up/h3.png
Binary files differ
diff --git a/lib/classic/up/h4.png b/lib/classic/up/h4.png
new file mode 100644
index 0000000..66f3e57
--- /dev/null
+++ b/lib/classic/up/h4.png
Binary files differ
diff --git a/lib/classic/up/h5.png b/lib/classic/up/h5.png
new file mode 100644
index 0000000..4f9408c
--- /dev/null
+++ b/lib/classic/up/h5.png
Binary files differ
diff --git a/lib/classic/up/h6.png b/lib/classic/up/h6.png
new file mode 100644
index 0000000..df43bc8
--- /dev/null
+++ b/lib/classic/up/h6.png
Binary files differ
diff --git a/lib/classic/up/h7.png b/lib/classic/up/h7.png
new file mode 100644
index 0000000..4d9026b
--- /dev/null
+++ b/lib/classic/up/h7.png
Binary files differ
diff --git a/lib/classic/up/h8.png b/lib/classic/up/h8.png
new file mode 100644
index 0000000..16cd43f
--- /dev/null
+++ b/lib/classic/up/h8.png
Binary files differ
diff --git a/lib/classic/up/h9.png b/lib/classic/up/h9.png
new file mode 100644
index 0000000..4a03d61
--- /dev/null
+++ b/lib/classic/up/h9.png
Binary files differ
diff --git a/lib/classic/up/hj.png b/lib/classic/up/hj.png
new file mode 100644
index 0000000..84a446f
--- /dev/null
+++ b/lib/classic/up/hj.png
Binary files differ
diff --git a/lib/classic/up/hk.png b/lib/classic/up/hk.png
new file mode 100644
index 0000000..2d47a44
--- /dev/null
+++ b/lib/classic/up/hk.png
Binary files differ
diff --git a/lib/classic/up/hq.png b/lib/classic/up/hq.png
new file mode 100644
index 0000000..02f7fef
--- /dev/null
+++ b/lib/classic/up/hq.png
Binary files differ
diff --git a/lib/classic/up/index.html b/lib/classic/up/index.html
new file mode 100644
index 0000000..5144375
--- /dev/null
+++ b/lib/classic/up/index.html
@@ -0,0 +1,108 @@
+<HTML>
+<HEAD>
+<META NAME="description" CONTENT="A full deck of Playing Card Icons">
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=ISO-8859-1">
+<META NAME="keywords" CONTENT="Playing Cards, deck of cards, deck, cards, icons, images">
+<TITLE>Playing Cards</TITLE>
+</HEAD>
+<BODY BGCOLOR=#006633 TEXT=#e1ffd7 LINK=#FFFFFF VLINK=#FFFFFF>
+<CENTER>
+<TABLE BORDER=0 CELLSPACING=10 CELLPADDING=10>
+<TR>
+<TD>
+<IMG SRC="b1pt.png"><BR>
+<IMG SRC="b1fh.png"><BR>
+<IMG SRC="b1pb.png"><BR>
+</TD>
+<TD>
+<IMG SRC="b1pl.png">
+<IMG SRC="b1fv.png">
+<IMG SRC="b1pr.png">
+</TD>
+<TD><IMG SRC="jb.png"></TD>
+<TD VALIGN=CENTER><STRONG>Playing Cards</STRONG></TD>
+<TD><IMG SRC="jr.png"></TD>
+<TD>
+<IMG SRC="b2pl.png">
+<IMG SRC="b2fv.png">
+<IMG SRC="b2pr.png">
+</TD>
+<TD>
+<IMG SRC="b2pt.png"><BR>
+<IMG SRC="b2fh.png"><BR>
+<IMG SRC="b2pb.png"><BR>
+</TD>
+</TR>
+<TR></TR>
+</TABLE>
+<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=0>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="c1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="cj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="cq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="ck.png"></TD>
+</TR>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="h1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="hj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="hq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="hk.png"></TD>
+</TR>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="s1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="sj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="sq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="sk.png"></TD>
+</TR>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="d1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="dj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="dq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="dk.png"></TD>
+</TR>
+</TABLE>
+<TABLE BORDER=0 CELLSPACING=10>
+<TR>
+<TD ALIGN=CENTER><FONT SIZE=2><BR>
+These images were created using <A HREF="http://www.mindworkshop.com/alchemy/alchemy.html">GIFCon</A>.
+</FONT></TD>
+</TR>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/lib/classic/up/jb.png b/lib/classic/up/jb.png
new file mode 100644
index 0000000..0d9f8bb
--- /dev/null
+++ b/lib/classic/up/jb.png
Binary files differ
diff --git a/lib/classic/up/jr.png b/lib/classic/up/jr.png
new file mode 100644
index 0000000..5555316
--- /dev/null
+++ b/lib/classic/up/jr.png
Binary files differ
diff --git a/lib/classic/up/s1.png b/lib/classic/up/s1.png
new file mode 100644
index 0000000..573838f
--- /dev/null
+++ b/lib/classic/up/s1.png
Binary files differ
diff --git a/lib/classic/up/s10.png b/lib/classic/up/s10.png
new file mode 100644
index 0000000..48169da
--- /dev/null
+++ b/lib/classic/up/s10.png
Binary files differ
diff --git a/lib/classic/up/s2.png b/lib/classic/up/s2.png
new file mode 100644
index 0000000..43447ca
--- /dev/null
+++ b/lib/classic/up/s2.png
Binary files differ
diff --git a/lib/classic/up/s3.png b/lib/classic/up/s3.png
new file mode 100644
index 0000000..15ec14f
--- /dev/null
+++ b/lib/classic/up/s3.png
Binary files differ
diff --git a/lib/classic/up/s4.png b/lib/classic/up/s4.png
new file mode 100644
index 0000000..859c85e
--- /dev/null
+++ b/lib/classic/up/s4.png
Binary files differ
diff --git a/lib/classic/up/s5.png b/lib/classic/up/s5.png
new file mode 100644
index 0000000..a9aed79
--- /dev/null
+++ b/lib/classic/up/s5.png
Binary files differ
diff --git a/lib/classic/up/s6.png b/lib/classic/up/s6.png
new file mode 100644
index 0000000..3e23667
--- /dev/null
+++ b/lib/classic/up/s6.png
Binary files differ
diff --git a/lib/classic/up/s7.png b/lib/classic/up/s7.png
new file mode 100644
index 0000000..4108d84
--- /dev/null
+++ b/lib/classic/up/s7.png
Binary files differ
diff --git a/lib/classic/up/s8.png b/lib/classic/up/s8.png
new file mode 100644
index 0000000..d2032f0
--- /dev/null
+++ b/lib/classic/up/s8.png
Binary files differ
diff --git a/lib/classic/up/s9.png b/lib/classic/up/s9.png
new file mode 100644
index 0000000..d8a4a33
--- /dev/null
+++ b/lib/classic/up/s9.png
Binary files differ
diff --git a/lib/classic/up/sj.png b/lib/classic/up/sj.png
new file mode 100644
index 0000000..a6cdec1
--- /dev/null
+++ b/lib/classic/up/sj.png
Binary files differ
diff --git a/lib/classic/up/sk.png b/lib/classic/up/sk.png
new file mode 100644
index 0000000..38287b5
--- /dev/null
+++ b/lib/classic/up/sk.png
Binary files differ
diff --git a/lib/classic/up/sq.png b/lib/classic/up/sq.png
new file mode 100644
index 0000000..27c08c0
--- /dev/null
+++ b/lib/classic/up/sq.png
Binary files differ
diff --git a/lib/components/board.dart b/lib/components/board.dart
new file mode 100644
index 0000000..22baa95
--- /dev/null
+++ b/lib/components/board.dart
@@ -0,0 +1,112 @@
+import './card.dart' show Card;
+import '../logic/card.dart' as logic_card;
+import 'package:sky/widgets.dart' as widgets;
+import 'package:vector_math/vector_math.dart' as vector_math;
+import 'dart:math' as math;
+import 'package:sky/theme/colors.dart' as colors;
+
+const double cardHeight = 96.0;
+const double cardWidth = 71.0;
+
+class CardCluster extends widgets.Component {
+ List<int> cards; // the indicies of the cards in the center, in clockwise order
+ int startingPos;
+ CardCluster(this.startingPos, this.cards);
+
+ widgets.Widget build() {
+ var widgetsList = [];
+ for (int i = 0; i < cards.length; i++) {
+ var posMod = (startingPos + i) % 4;
+ switch (posMod) {
+ case 0:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI).translate(0.0, -cardHeight / 2),
+ child: new Card(logic_card.Card.All[cards[i]], true)
+ ));
+ break;
+ case 1:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI/2.0).translate(0.0, cardWidth/2),
+ child: new Card(logic_card.Card.All[cards[i]], true)
+ ));
+ break;
+ case 2:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().translate(-cardWidth, cardWidth / 2),
+ child: new Card(logic_card.Card.All[cards[i]], true)
+ ));
+ break;
+ case 3:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI/2.0).translate(0.0, -cardHeight/2),
+ child: new Card(logic_card.Card.All[cards[i]], true)
+ ));
+ break;
+ }
+ }
+ return new widgets.Container(child: new widgets.Stack(widgetsList));
+ }
+}
+
+class PlayerHand extends widgets.Component {
+ int count;
+ PlayerHand(this.count);
+
+ widgets.Widget build() {
+ List<widgets.Positioned> cards = [];
+ for (int i = 0; i < count; i++) {
+ cards.add(new widgets.Positioned(child: new Card(logic_card.Card.All[0], false),
+ top: 0.0,
+ left: cardWidth*i/2.0));
+ }
+ return new widgets.Stack(cards);
+ }
+}
+
+class Board extends widgets.Component {
+ CardCluster centerCluster;
+ List<PlayerHand> hands; // counts of cards in players hands, in clockwise order
+
+ Board(int firstCardPlayedPosition, List<int> cards, List<int> playerHandCount) :
+ centerCluster = new CardCluster(firstCardPlayedPosition, cards) {
+ assert(playerHandCount.length == 4);
+ hands = new List<PlayerHand>();
+ for (int count in playerHandCount) {
+ hands.add(new PlayerHand(count));
+ }
+ }
+
+ widgets.Widget build() {
+ return new widgets.Container(
+ decoration: new widgets.BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new widgets.Stack(
+ [
+ new widgets.Positioned(child: hands[0],
+ top: 0.0,
+ left: 250.0),
+ new widgets.Positioned(child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI/2.0),
+ child: hands[1]
+ ),
+ left: 100.0,
+ top: 400.0),
+ new widgets.Positioned(child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI),
+ child: hands[2]
+ ),
+ top: 820.0,
+ left: 350.0),
+ new widgets.Positioned(child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI/2.0),
+ child: hands[3]
+ ),
+ left: 500.0,
+ top: 400.0),
+ new widgets.Positioned(child: centerCluster,
+ top: 400.0,
+ left: 300.0),
+ ]
+ )
+ );
+ }
+}
diff --git a/lib/components/card.dart b/lib/components/card.dart
new file mode 100644
index 0000000..f482ca9
--- /dev/null
+++ b/lib/components/card.dart
@@ -0,0 +1,20 @@
+import '../logic/card.dart' as logic_card;
+import 'package:sky/widgets.dart' as widgets;
+
+class Card extends widgets.Component {
+ logic_card.Card card;
+ bool faceUp;
+
+ Card(this.card, this.faceUp);
+
+ widgets.Widget build() {
+ return new widgets.Listener(
+ child: imageFromCard(card, faceUp)
+ );
+ }
+
+ static widgets.Widget imageFromCard(logic_card.Card c, bool faceUp) {
+ String imageName = "${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
+ return new widgets.NetworkImage(src: imageName);
+ }
+}
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
new file mode 100644
index 0000000..27d0bcc
--- /dev/null
+++ b/lib/components/card_collection.dart
@@ -0,0 +1,118 @@
+import '../logic/card.dart' as logic_card;
+import 'card.dart' show Card;
+import 'draggable.dart' show Draggable;
+import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets.dart' show DragTarget;
+import 'package:sky/theme/colors.dart' as colors;
+
+enum Orientation {
+ vert, horz, fan, show1
+}
+
+class CardCollectionComponent extends StatefulComponent {
+ List<logic_card.Card> cards;
+ Orientation orientation;
+ bool faceUp;
+ Function parentCallback;
+
+ String status = 'bar';
+
+ CardCollectionComponent(this.cards, this.faceUp, this.orientation, this.parentCallback);
+
+ void syncConstructorArguments(CardCollectionComponent other) {
+ //assert(false); // Why do we need to do this?
+ //status = other.status;
+ cards = other.cards;
+ orientation = other.orientation;
+ faceUp = other.faceUp;
+ parentCallback = other.parentCallback;
+ }
+
+ void _handleAccept(Card data) {
+ setState(() {
+ status = 'ACCEPT ${data.card.toString()}';
+ parentCallback(data.card, this.cards);
+ });
+ }
+
+ List<Widget> flexCards(List<Widget> cardWidgets) {
+ List<Widget> flexWidgets = new List<Widget>();
+ cardWidgets.forEach((cardWidget) => flexWidgets.add(new Flexible(child: cardWidget)));
+ return flexWidgets;
+ }
+
+ Widget wrapCards(List<Widget> cardWidgets) {
+ switch (this.orientation) {
+ case Orientation.vert:
+ return new Flex(flexCards(cardWidgets), direction: FlexDirection.vertical);
+ case Orientation.horz:
+ return new Flex(flexCards(cardWidgets));
+ case Orientation.fan:
+ // unimplemented, so we'll fall through to show1, for now.
+ // Probably a Stack + Positioned
+ case Orientation.show1:
+ return new Stack(cardWidgets);
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ Widget build() {
+ // Let's just do horizontal for now, it's too complicated otherwise.
+
+ /*double cardDelta = card_constants.CARD_WIDTH;
+ if (cards.length > 6) {
+ //cardDelta = card_constants.CARD_WIDTH / cards.length; // just make it tiny
+ cardDelta -= card_constants.CARD_WIDTH * (cards.length - 6) / cards.length;
+ }*/
+
+ List<Widget> cardComponents = new List<Widget>();
+ cardComponents.add(new Text(status));
+ for (int i = 0; i < cards.length; i++) {
+ // Positioned seems correct, but it causes an error when rendering. Constraints aren't matched?
+ /*cardComponents.add(new Positioned(
+ top: 0.0,
+ // left: i * cardDelta,
+ child: new Draggable<Card>(Card(cards[i], faceUp))
+ ));*/
+ /*cardComponents.add(new Transform(
+ transform: new vector_math.Matrix4.identity().translate(i * cardDelta, 40.0),
+ child: new CardComponent(cards[i], faceUp)
+ ));*/
+ cardComponents.add(new Draggable<Card>(new Card(cards[i], faceUp))); // flex
+ }
+
+
+ // Just draw a stack of cards...
+ //return new Stack(cardComponents);
+
+
+ /*List<Widget> cardComponents = new List<Widget>();
+ for (int i = 0; i < cards.length; i++) {
+ cardComponents.add(new Draggable<Card>(new Card(cards[i], faceUp)));
+ }
+ return new Flex(cardComponents);*/
+
+ // Let's draw a stack of cards with DragTargets.
+ return new DragTarget<Card>(
+ onAccept: _handleAccept,
+ builder: (List<Card> data, _) {
+ print(this.cards.length);
+ print(data);
+ return new Container(
+ decoration: new BoxDecoration(
+ border: new Border.all(
+ width: 3.0,
+ color: data.isEmpty ? colors.white : colors.Blue[500]
+ ),
+ backgroundColor: data.isEmpty ? colors.Grey[500] : colors.Green[500]
+ ),
+ height: 80.0,
+ margin: new EdgeDims.all(10.0),
+ child: wrapCards(cardComponents)//new Stack(cardComponents)
+ );
+ }
+ );
+ }
+}
diff --git a/lib/components/card_constants.dart b/lib/components/card_constants.dart
new file mode 100644
index 0000000..380718c
--- /dev/null
+++ b/lib/components/card_constants.dart
@@ -0,0 +1,2 @@
+const double CARD_HEIGHT = 120.0;
+const double CARD_WIDTH = 90.0;
\ No newline at end of file
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
new file mode 100644
index 0000000..b06ae24
--- /dev/null
+++ b/lib/components/croupier.dart
@@ -0,0 +1,81 @@
+import '../logic/croupier.dart' as logic_croupier;
+import '../logic/game.dart' as logic_game;
+import 'package:sky/widgets.dart' show FlatButton;
+import 'package:sky/widgets/basic.dart';
+import 'game.dart' show GameComponent;
+import 'dart:sky' as sky;
+
+class CroupierComponent extends StatefulComponent {
+ logic_croupier.Croupier croupier;
+
+ CroupierComponent(this.croupier) : super();
+
+ void syncConstructorArguments(CroupierComponent other) {
+ croupier = other.croupier;
+ }
+
+ Function setStateCallbackFactory(logic_croupier.CroupierState s, [var data = null]) {
+ return () => setState(() {
+ croupier.setState(s, data);
+ });
+ }
+
+ Widget build() {
+ switch (croupier.state) {
+ case logic_croupier.CroupierState.Welcome:
+ // in which we show them a UI to start a new game, join a game, or change some settings.
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: new Flex([
+ new FlatButton(
+ child: new Text('Create Game'),
+ onPressed: setStateCallbackFactory(logic_croupier.CroupierState.ChooseGame)
+ ),
+ new FlatButton(
+ child: new Text('Join Game')
+ ),
+ new FlatButton(
+ child: new Text('Settings')
+ )
+ ], direction: FlexDirection.vertical
+ )
+ );
+ case logic_croupier.CroupierState.Settings:
+ return null; // in which we let them pick an avatar, name, and color. And return to the previous screen after (NOT IMPLEMENTED YET)
+ case logic_croupier.CroupierState.ChooseGame:
+ // in which we let them pick a game out of the many possible games... There aren't that many.
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: new Flex([
+ new FlatButton(
+ child: new Text('Proto'),
+ onPressed: setStateCallbackFactory(logic_croupier.CroupierState.PlayGame, logic_game.GameType.Proto)
+ ),
+ new FlatButton(
+ child: new Text('Hearts'),
+ onPressed: setStateCallbackFactory(logic_croupier.CroupierState.PlayGame, logic_game.GameType.Hearts)
+ ),
+ new FlatButton(
+ child: new Text('Poker')
+ ),
+ new FlatButton(
+ child: new Text('Solitaire')
+ )
+ ], direction: FlexDirection.vertical
+ )
+ );
+ case logic_croupier.CroupierState.AwaitGame:
+ return null; // in which players wait for game invitations to arrive.
+ case logic_croupier.CroupierState.ArrangePlayers:
+ return null; // If needed, lists the players around and what devices they'd like to use.
+ case logic_croupier.CroupierState.PlayGame:
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: new GameComponent(croupier.game) // Asks the game UI to draw itself.
+ );
+ default:
+ assert(false);
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/components/draggable.dart b/lib/components/draggable.dart
new file mode 100644
index 0000000..4415645
--- /dev/null
+++ b/lib/components/draggable.dart
@@ -0,0 +1,63 @@
+import 'package:sky/widgets.dart' as widgets;
+import 'dart:sky' as sky;
+import 'package:vector_math/vector_math.dart' as vector_math;
+
+class Draggable<T extends widgets.Widget> extends widgets.StatefulComponent {
+ widgets.DragController dragController;
+ widgets.Offset displacement = widgets.Offset.zero;
+ T child;
+
+ Draggable(this.child);
+
+ void syncConstructorArguments(Draggable other) {
+ child = other.child;
+ }
+
+ widgets.Widget build() {
+ return new widgets.Listener(
+ onPointerDown: _startDrag,
+ onPointerMove: _updateDrag,
+ onPointerCancel: _cancelDrag,
+ onPointerUp: _drop,
+ child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().translate(displacement.dx, displacement.dy),
+ child: child)
+ );
+ }
+
+ widgets.EventDisposition _startDrag(sky.PointerEvent event) {
+ setState(() {
+ dragController = new widgets.DragController(this.child);
+ dragController.update(new widgets.Point(event.x, event.y));
+ displacement = widgets.Offset.zero;
+ });
+ return widgets.EventDisposition.consumed;
+ }
+
+ widgets.EventDisposition _updateDrag(sky.PointerEvent event) {
+ setState(() {
+ dragController.update(new widgets.Point(event.x, event.y));
+ displacement += new widgets.Offset(event.dx, event.dy);
+ });
+ return widgets.EventDisposition.consumed;
+ }
+
+ widgets.EventDisposition _cancelDrag(sky.PointerEvent event) {
+ setState(() {
+ dragController.cancel();
+ dragController = null;
+ });
+ return widgets.EventDisposition.consumed;
+ }
+
+ widgets.EventDisposition _drop(sky.PointerEvent event) {
+ setState(() {
+ dragController.update(new widgets.Point(event.x, event.y));
+ dragController.drop();
+ dragController = null;
+
+ displacement = widgets.Offset.zero;
+ });
+ return widgets.EventDisposition.consumed;
+ }
+}
diff --git a/lib/components/game.dart b/lib/components/game.dart
new file mode 100644
index 0000000..dc0daf2
--- /dev/null
+++ b/lib/components/game.dart
@@ -0,0 +1,173 @@
+import '../logic/card.dart' show Card;
+import '../logic/game.dart' show Game, GameType, Viewer, HeartsGame, HeartsPhase;
+import 'card_collection.dart' show CardCollectionComponent, Orientation;
+import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets.dart' show FlatButton;
+import 'package:sky/theme/colors.dart' as colors;
+import 'board.dart' show Board;
+
+class GameComponent extends StatefulComponent {
+ Game game;
+
+ GameComponent(this.game) {
+ game.updateCallback = update;
+ }
+
+ void update() {
+ setState(() {});
+ }
+
+ void syncConstructorArguments(GameComponent other) {
+ this.game = other.game;
+ }
+
+ Widget build() {
+ switch (game.gameType) {
+ case GameType.Proto:
+ return buildProto();
+ case GameType.Hearts:
+ return buildHearts();
+ case GameType.Board:
+ // Does NOT work in checked mode since it has a Stack of Positioned Stack with Positioned Widgets.
+ return new Board(1, [2,3,4], [1, 2, 3, 4]);
+ default:
+ return null; // unsupported
+ }
+ }
+
+ _switchPlayersCallback() {
+ setState(() {
+ game.playerNumber = (game.playerNumber + 1) % 4;
+ });
+ }
+
+ _updateGameCallback(Card card, List<Card> dest) {
+ setState(() {
+ try {
+ game.move(card, dest);
+ } catch(e) {
+ print("You can't do that! ${e.toString()}");
+ game.debugString = e.toString();
+ }
+ });
+ }
+
+ Widget buildProto() {
+ List<Widget> cardCollections = new List<Widget>();
+
+ // debugString
+ cardCollections.add(new Text(game.debugString));
+
+ for (int i = 0; i < 4; i++) {
+ List<Card> cards = game.cardCollections[i];
+ CardCollectionComponent c = new CardCollectionComponent(cards, game.playerNumber == i, Orientation.horz, _updateGameCallback);
+
+ /*cardCollections.add(new Positioned(
+ top: i * (card_constants.CARD_HEIGHT + 20.0),
+ child: c
+ ));*/
+
+ /*cardCollections.add(new Transform(
+ transform: new vector_math.Matrix4.identity().translate(0.0, i * (card_constants.CARD_HEIGHT + 20.0)),
+ child: c
+ ));*/
+
+ cardCollections.add(c); // flex
+ }
+
+ // game.cardCollections[4] is a discard pile
+ /*cardCollections.add(new Transform(
+ transform: new vector_math.Matrix4.identity().translate(0.0, 4 * (card_constants.CARD_HEIGHT + 20.0)),
+ child: new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Green[500], borderRadius: 5.0),
+ child: new CardCollectionComponent(game.cardCollections[4], true, Orientation.horz, _parentHandleAccept)
+ )
+ ));*/
+ /*cardCollections.add(new Positioned(
+ top: 4 * (card_constants.CARD_HEIGHT + 20.0),
+ child: new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Green[500], borderRadius: 5.0),
+ child: new CardCollectionComponent(game.cardCollections[4], true, Orientation.horz)
+ )
+ ));*/
+
+ cardCollections.add(new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Green[500], borderRadius: 5.0),
+ child: new CardCollectionComponent(game.cardCollections[4], true, Orientation.show1, _updateGameCallback)
+ ));
+
+ // game.cardCollections[5] is just not shown
+
+ cardCollections.add(new FlatButton(
+ child: new Text('Switch View'),
+ onPressed: _switchPlayersCallback
+ ));
+
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex(cardCollections, direction: FlexDirection.vertical)//new Stack(cardCollections)
+ );
+ }
+
+ Widget _makeSwitchViewButton() =>_makeButton('Switch View', _switchPlayersCallback);
+
+ Widget _makeButton(String text, Function callback) {
+ return new FlatButton(
+ child: new Text(text),
+ onPressed: callback
+ );
+ }
+
+ Widget buildHearts() {
+ HeartsGame game = this.game as HeartsGame;
+
+ switch (game.phase) {
+ case HeartsPhase.Deal:
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex([
+ new Text('Player ${game.playerNumber}'),
+ _makeButton('Deal', game.dealCards),
+ _makeSwitchViewButton()
+ ], direction: FlexDirection.vertical)
+ );
+ case HeartsPhase.Pass:
+ case HeartsPhase.Take:
+ case HeartsPhase.Play:
+ case HeartsPhase.Score:
+ return showBoard();
+ default:
+ assert(false); // What?
+ return null;
+ }
+ }
+
+ Widget showBoard() {
+ HeartsGame game = this.game as HeartsGame;
+
+ List<Widget> cardCollections = new List<Widget>();
+
+ cardCollections.add(new Text(game.debugString));
+
+ for (int i = 0; i < 4; i++) {
+ List<Card> cards = game.cardCollections[i];
+ CardCollectionComponent c = new CardCollectionComponent(cards, game.playerNumber == i, Orientation.horz, _updateGameCallback);
+ cardCollections.add(c); // flex
+ }
+
+ cardCollections.add(new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Green[500], borderRadius: 5.0),
+ child: new CardCollectionComponent(game.cardCollections[4], true, Orientation.show1, _updateGameCallback)
+ ));
+
+ cardCollections.add(new FlatButton(
+ child: new Text('Switch View'),
+ onPressed: _switchPlayersCallback
+ ));
+
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex(cardCollections, direction: FlexDirection.vertical)
+ );
+ }
+}
diff --git a/lib/logic/card.dart b/lib/logic/card.dart
new file mode 100644
index 0000000..3f53685
--- /dev/null
+++ b/lib/logic/card.dart
@@ -0,0 +1,75 @@
+import 'dart:collection';
+
+class Card {
+ final String deck;
+ final String identifier;
+
+ Card(this.deck, this.identifier);
+ Card.fromString(String cardData) : deck = cardData.split(" ")[0], identifier = cardData.split(" ")[1];
+
+ bool operator ==(Object other) {
+ if (other is! Card) return false;
+ Card o = other as Card;
+ return deck == o.deck && identifier == o.identifier;
+ }
+ int get hashCode => 37 * (deck.hashCode + 41 * identifier.hashCode);
+
+ static final List<Card> All = new UnmodifiableListView<Card>([
+ new Card("classic", "c1"),
+ new Card("classic", "c2"),
+ new Card("classic", "c3"),
+ new Card("classic", "c4"),
+ new Card("classic", "c5"),
+ new Card("classic", "c6"),
+ new Card("classic", "c7"),
+ new Card("classic", "c8"),
+ new Card("classic", "c9"),
+ new Card("classic", "c10"),
+ new Card("classic", "cj"),
+ new Card("classic", "cq"),
+ new Card("classic", "ck"),
+ new Card("classic", "d1"),
+ new Card("classic", "d2"),
+ new Card("classic", "d3"),
+ new Card("classic", "d4"),
+ new Card("classic", "d5"),
+ new Card("classic", "d6"),
+ new Card("classic", "d7"),
+ new Card("classic", "d8"),
+ new Card("classic", "d9"),
+ new Card("classic", "d10"),
+ new Card("classic", "dj"),
+ new Card("classic", "dq"),
+ new Card("classic", "dk"),
+ new Card("classic", "h1"),
+ new Card("classic", "h2"),
+ new Card("classic", "h3"),
+ new Card("classic", "h4"),
+ new Card("classic", "h5"),
+ new Card("classic", "h6"),
+ new Card("classic", "h7"),
+ new Card("classic", "h8"),
+ new Card("classic", "h9"),
+ new Card("classic", "h10"),
+ new Card("classic", "hj"),
+ new Card("classic", "hq"),
+ new Card("classic", "hk"),
+ new Card("classic", "s1"),
+ new Card("classic", "s2"),
+ new Card("classic", "s3"),
+ new Card("classic", "s4"),
+ new Card("classic", "s5"),
+ new Card("classic", "s6"),
+ new Card("classic", "s7"),
+ new Card("classic", "s8"),
+ new Card("classic", "s9"),
+ new Card("classic", "s10"),
+ new Card("classic", "sj"),
+ new Card("classic", "sq"),
+ new Card("classic", "sk"),
+ ]);
+
+ toString() => "${deck} ${identifier}";
+
+ get string => toString();
+}
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
new file mode 100644
index 0000000..9a548cc
--- /dev/null
+++ b/lib/logic/croupier.dart
@@ -0,0 +1,65 @@
+import 'game.dart' show Game, GameType;
+
+enum CroupierState {
+ Welcome, Settings, ChooseGame, AwaitGame, ArrangePlayers, PlayGame
+}
+
+class Croupier {
+ CroupierState state;
+ Settings settings;
+ Game game; // null until chosen
+
+ Croupier() {
+ state = CroupierState.Welcome;
+ // settings = new Settings.load(?); // Give it in the croupier constructor. The app itself should load this info.
+ }
+
+ // Sets the next part of croupier state.
+ // Depending on the originating state, data can contain extra information that we need.
+ void setState(CroupierState nextState, var data) {
+ switch (state) {
+ case CroupierState.Welcome:
+ // data should be empty.
+ assert(data == null);
+ break;
+ case CroupierState.Settings:
+ // data should be empty.
+ // All settings changes affect the croupier settings directly without changing app state.
+ assert(data == null);
+ break;
+ case CroupierState.ChooseGame:
+ // data should be the game id here.
+ GameType gt = data as GameType;
+ game = new Game(gt, 0); // Start as player 0 of whatever game type.
+ break;
+ case CroupierState.AwaitGame:
+ // data would probably be the game id again.
+ GameType gt = data as GameType;
+ game = new Game(gt, 0); // Start as player 0 of whatever game type.
+ break;
+ case CroupierState.ArrangePlayers:
+ // data should be empty.
+ // All rearrangements affect the Game's player number without changing app state.
+ break;
+ case CroupierState.PlayGame:
+ // data should be empty.
+ // The signal to start really isn't anything special.
+ break;
+ default:
+ assert(false);
+ }
+
+ state = nextState;
+ }
+}
+
+class Settings {
+ String avatar;
+ String name;
+ String color; // in hex?
+
+ Settings(this.avatar, this.name, this.color);
+
+ // Settings.load(String data) {}
+ // String save() { return null; }
+}
\ No newline at end of file
diff --git a/lib/logic/game.dart b/lib/logic/game.dart
new file mode 100644
index 0000000..b5f7d36
--- /dev/null
+++ b/lib/logic/game.dart
@@ -0,0 +1,786 @@
+import 'card.dart' show Card;
+import 'dart:math' as math;
+
+// Note: Proto and Board are "fake" games intended to demonstrate what we can do.
+// Proto is just a drag cards around "game".
+// Board is meant to show how one _could_ layout a game of Hearts. This one is not hooked up very well yet.
+enum GameType {
+ Proto, Hearts, Poker, Solitaire, Board
+}
+
+/// A game consists of multiple decks and tracks a single deck of cards.
+/// It also handles events; when cards are dragged to and from decks.
+class Game {
+ final GameType gameType;
+ final List<List<Card>> cardCollections = new List<List<Card>>();
+ final List<Card> deck = new List<Card>.from(Card.All);
+
+ final math.Random random = new math.Random();
+ final GameLog gamelog = new GameLog();
+ int playerNumber;
+ String debugString = 'hello?';
+
+ Function updateCallback; // Used to inform components of when a change has occurred. This is especially important when something non-UI related changes what should be drawn.
+
+ factory Game(GameType gt, int pn) {
+ switch (gt) {
+ case GameType.Proto:
+ return new ProtoGame(pn);
+ case GameType.Hearts:
+ return new HeartsGame(pn);
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ // A super constructor, don't call this unless you're a subclass.
+ Game._create(this.gameType, this.playerNumber, int numCollections) {
+ gamelog.setGame(this);
+ for (int i = 0; i < numCollections; i++) {
+ cardCollections.add(new List<Card>());
+ }
+ }
+
+ List<Card> deckPeek(int numCards) {
+ assert(deck.length >= numCards);
+ List<Card> cards = new List<Card>.from(deck.take(numCards));
+ return cards;
+ }
+
+ // Which card collection has the card?
+ int findCard(Card card) {
+ for (int i = 0; i < cardCollections.length; i++) {
+ if (cardCollections[i].contains(card)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ void resetCards() {
+ for (int i = 0; i < cardCollections.length; i++) {
+ cardCollections[i].clear();
+ }
+ deck.addAll(Card.All);
+ }
+
+ // UNIMPLEMENTED: Let subclasses override this?
+ // Or is it improper to do so?
+ void move(Card card, List<Card> dest) {}
+
+ // UNIMPLEMENTED: Override this to implement game-specific logic after each event.
+ void triggerEvents() {}
+}
+
+class ProtoGame extends Game {
+ ProtoGame(int playerNumber) : super._create(GameType.Proto, playerNumber, 6) {
+ // playerNumber would be used in a real game, but I have to ignore it for debugging.
+ // It would determine faceUp/faceDown status.faceDown
+
+ // TODO: Set the number of piles created to either 9 (1x per player, 1 discard, 4 play piles) or 12 (2x per player, 4 play piles)
+ // But for now, we will deal with 6. 1x per player, 1 discard, and 1 undrawn pile.
+
+ // We do some arbitrary things here... Just for setup.
+ deck.shuffle();
+ deal(0, 8);
+ deal(1, 5);
+ deal(2, 4);
+ deal(3, 1);
+ }
+
+ void deal(int playerId, int numCards) {
+ gamelog.add(new ProtoCommand.deal(playerId, this.deckPeek(numCards)));
+ }
+
+ // Overrides Game's move method with the "move" logic for the card dragging prototype.
+ void move(Card card, List<Card> dest) {
+ // The first step is to find the card. Where is it?
+ // then we can remove it and add to the dest.
+ debugString = 'Moving... ${card.toString()}';
+ int i = findCard(card);
+ if (i == -1) {
+ debugString = 'NO... ${card.toString()}';
+ return;
+ }
+ int destId = cardCollections.indexOf(dest);
+
+ gamelog.add(new ProtoCommand.pass(i, destId, <Card>[card]));
+
+ debugString = 'Move ${i} ${card.toString()}';
+ print(debugString);
+ }
+}
+
+enum HeartsPhase {
+ Deal, Pass, Take, Play, Score
+}
+
+class HeartsGame extends Game {
+ static const PLAYER_A = 0;
+ static const PLAYER_B = 1;
+ static const PLAYER_C = 2;
+ static const PLAYER_D = 3;
+ static const PLAYER_A_PLAY = 4;
+ static const PLAYER_B_PLAY = 5;
+ static const PLAYER_C_PLAY = 6;
+ static const PLAYER_D_PLAY = 7;
+ static const PLAYER_A_TRICK = 8;
+ static const PLAYER_B_TRICK = 9;
+ static const PLAYER_C_TRICK = 10;
+ static const PLAYER_D_TRICK = 11;
+ static const PLAYER_A_PASS = 12;
+ static const PLAYER_B_PASS = 13;
+ static const PLAYER_C_PASS = 14;
+ static const PLAYER_D_PASS = 15;
+
+ static const OFFSET_HAND = 0;
+ static const OFFSET_PLAY = 4;
+ static const OFFSET_TRICK = 8;
+ static const OFFSET_PASS = 12;
+
+ static const MAX_SCORE = 100; // Play until someone gets to 100.
+
+ // Note: These cards are final because the "classic" deck has 52 cards.
+ // It is up to the renderer to reskin those cards as needed.
+ final Card TWO_OF_CLUBS = new Card("classic", "c2");
+ final Card QUEEN_OF_SPADES = new Card("classic", "sq");
+
+ HeartsPhase _phase = HeartsPhase.Deal;
+ HeartsPhase get phase => _phase;
+ void set phase(HeartsPhase other) {
+ print('setting phase from ${_phase} to ${other}');
+ _phase = other;
+ }
+ int roundNumber = 0;
+ int lastTrickTaker;
+ bool heartsBroken;
+ int trickNumber;
+
+ // Used by the score screen to track scores and see which players are ready to continue to the next round.
+ List<int> scores = [0, 0, 0, 0];
+ List<bool> ready;
+
+ HeartsGame(int playerNumber) : super._create(GameType.Hearts, playerNumber, 16) {
+ resetGame();
+ }
+
+ void resetGame() {
+ this.resetCards();
+ heartsBroken = false;
+ lastTrickTaker = null;
+ trickNumber = 0;
+ }
+
+ void dealCards() {
+ deck.shuffle();
+ deal(PLAYER_A, 13);
+ deal(PLAYER_B, 13);
+ deal(PLAYER_C, 13);
+ deal(PLAYER_D, 13);
+ }
+
+ int get passTarget {
+ switch (roundNumber % 4) { // is a 4-cycle
+ case 0:
+ return (playerNumber - 1) % 4; // passLeft
+ case 1:
+ return (playerNumber + 1) % 4; // passRight
+ case 2:
+ return (playerNumber + 2) % 4; // passAcross
+ case 3:
+ return null; // no player to pass to
+ default:
+ assert(false);
+ return null;
+ }
+ }
+ int get takeTarget => _getTakeTarget(playerNumber);
+ int _getTakeTarget(takerId) {
+ switch (roundNumber % 4) { // is a 4-cycle
+ case 0:
+ return (takerId + 1) % 4; // takeRight
+ case 1:
+ return (takerId - 1) % 4; // takeLeft
+ case 2:
+ return (takerId + 2) % 4; // taleAcross
+ case 3:
+ return null; // no player to pass to
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ // Please only call this in the Play phase. Otherwise, it's pretty useless.
+ int get whoseTurn {
+ if (phase != HeartsPhase.Play) {
+ return null;
+ }
+ return (lastTrickTaker + this.numPlayed) % 4;
+ }
+
+ int getCardValue(Card c) {
+ String remainder = c.identifier.substring(1);
+ switch (remainder) {
+ case "1": // ace
+ return 14;
+ case "k":
+ return 13;
+ case "q":
+ return 12;
+ case "j":
+ return 11;
+ default:
+ return int.parse(remainder);
+ }
+ }
+
+ String getCardSuit(Card c) {
+ return c.identifier[0];
+ }
+ bool isHeartsCard(Card c) {
+ return getCardSuit(c) == 'h' && c.deck == 'classic';
+ }
+ bool isQSCard(Card c) {
+ return c == QUEEN_OF_SPADES;
+ }
+ bool isFirstCard(Card c) {
+ return c == TWO_OF_CLUBS;
+ }
+
+ bool isPenaltyCard(Card c) {
+ return isQSCard(c) || isHeartsCard(c);
+ }
+
+ bool hasSuit(int player, String suit) {
+ Card matchesSuit = this.cardCollections[player + OFFSET_HAND].firstWhere(
+ (Card element) => (getCardSuit(element) == suit),
+ orElse: () => null
+ );
+ return matchesSuit != null;
+ }
+
+ Card get leadingCard {
+ if(this.numPlayed >= 1) {
+ return cardCollections[this.lastTrickTaker + OFFSET_PLAY][0];
+ }
+ return null;
+ }
+ int get numPlayed {
+ int count = 0;
+ for (int i = 0; i < 4; i++) {
+ if (cardCollections[i + OFFSET_PLAY].length == 1) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ bool get hasGameEnded => this.scores.reduce(math.max) >= HeartsGame.MAX_SCORE;
+
+ bool get allDealt => cardCollections[PLAYER_A].length == 13 &&
+ cardCollections[PLAYER_B].length == 13 &&
+ cardCollections[PLAYER_C].length == 13 &&
+ cardCollections[PLAYER_D].length == 13;
+
+ bool get allPassed => cardCollections[PLAYER_A_PASS].length == 3 &&
+ cardCollections[PLAYER_B_PASS].length == 3 &&
+ cardCollections[PLAYER_C_PASS].length == 3 &&
+ cardCollections[PLAYER_D_PASS].length == 3;
+ bool get allTaken => cardCollections[PLAYER_A_PASS].length == 0 &&
+ cardCollections[PLAYER_B_PASS].length == 0 &&
+ cardCollections[PLAYER_C_PASS].length == 0 &&
+ cardCollections[PLAYER_D_PASS].length == 0;
+ bool get allPlayed => this.numPlayed == 4;
+
+ bool get allReady => ready[0] && ready[1] && ready[2] && ready[3];
+ void setReady(int playerId) {
+ ready[playerId] = true;
+ }
+ void unsetReady() {
+ ready = <bool>[false, false, false, false];
+ }
+
+ void deal(int playerId, int numCards) {
+ gamelog.add(new HeartsCommand.deal(playerId, this.deckPeek(numCards)));
+ }
+
+ // Note that this will be called by the UI.
+ // It won't be possible to pass for other players, except via the GameLog.
+ void passCards(List<Card> cards) {
+ assert(phase == HeartsPhase.Pass && this.passTarget != null);
+ if (cards.length != 3) {
+ throw new StateError('3 cards expected, but got: ${cards.toString()}');
+ }
+ gamelog.add(new HeartsCommand.pass(playerNumber, cards));
+ }
+
+ // Note that this will be called by the UI.
+ // It won't be possible to take cards for other players, except via the GameLog.
+ void takeCards() {
+ assert(phase == HeartsPhase.Take && this.takeTarget != null);
+ List<Card> cards = this.cardCollections[takeTarget + OFFSET_PASS];
+ assert(cards.length == 3);
+
+ gamelog.add(new HeartsCommand.take(playerNumber));
+ }
+
+ // Note that this will be called by the UI.
+ // It won't be possible to set the readiness for other players, except via the GameLog.
+ void setReadyUI() {
+ assert(phase == HeartsPhase.Score);
+ gamelog.add(new HeartsCommand.ready(playerNumber));
+ }
+
+ // Note that this will be called by the UI.
+ // TODO: Does this really need to be overridden? That seems like bad structure in GameComponent.
+ // Overrides Game's move method with the "move" logic for Hearts. Used for drag-drop.
+ // Note that this can only be called in the Play Phase of your turn.
+ // The UI will handle the drag-drop of the Pass Phase with its own state.
+ // The UI will initiate pass separately.
+ void move(Card card, List<Card> dest) {
+ assert(phase == HeartsPhase.Play && whoseTurn == playerNumber);
+
+ int i = findCard(card);
+ if (i == -1) {
+ throw new StateError('card does not exist or was not dealt: ${card.toString()}');
+ }
+ int destId = cardCollections.indexOf(dest);
+ if (destId == -1) {
+ throw new StateError('destination list does not exist: ${dest.toString()}');
+ }
+ if (destId != playerNumber + OFFSET_PLAY) {
+ throw new StateError('player ${playerNumber} is not playing to the correct list: ${destId}');
+ }
+
+ gamelog.add(new HeartsCommand.play(playerNumber, card));
+
+ debugString = 'Play ${i} ${card.toString()}';
+ print(debugString);
+ }
+
+ // Overridden from Game for Hearts-specific logic:
+ // Switch from Pass to Take phase when all 4 players are passing.
+ // Switch from Take to Play phase when all 4 players have taken.
+ // During Play, if all 4 players play a card, move the tricks around.
+ // During Play, once all cards are gone and last trick is taken, go to Score phase (compute score and possibly end game).
+ // Switch from Score to Deal phase when all 4 players indicate they are ready.
+ void triggerEvents() {
+ switch (this.phase) {
+ case HeartsPhase.Deal:
+ if (this.allDealt) {
+ if (this.passTarget != null) {
+ phase = HeartsPhase.Pass;
+ } else {
+ // All cards are dealt. The person who "won" the last trick goes first.
+ // In this case, we'll just pretend it's the person with the 2 of clubs.
+ this.lastTrickTaker = this.findCard(TWO_OF_CLUBS);
+ phase = HeartsPhase.Play;
+ }
+ }
+ return;
+ case HeartsPhase.Pass:
+ if (this.allPassed) {
+ phase = HeartsPhase.Take;
+ }
+ return;
+ case HeartsPhase.Take:
+ if (this.allTaken) {
+ // All cards are dealt. The person who "won" the last trick goes first.
+ // In this case, we'll just pretend it's the person with the 2 of clubs.
+ this.lastTrickTaker = this.findCard(TWO_OF_CLUBS);
+ phase = HeartsPhase.Play;
+ }
+ return;
+ case HeartsPhase.Play:
+ if (this.allPlayed) {
+ // Determine who won this trick.
+ int winner = this.determineTrickWinner();
+
+ // Move the cards to their trick list. Also check if hearts was broken.
+ // Note: Some variants of Hearts allows the QUEEN_OF_SPADES to break hearts too.
+ for (int i = 0; i < 4; i++) {
+ List<Card> play = this.cardCollections[i + OFFSET_PLAY];
+ if (!heartsBroken && isHeartsCard(play[0])) {
+ heartsBroken = true;
+ }
+ this.cardCollections[winner + OFFSET_TRICK].addAll(play); // or add(play[0])
+ play.clear();
+ }
+
+ // Set them as the next person to go.
+ this.lastTrickTaker = winner;
+ this.trickNumber++;
+
+ // Additionally, if that was the last trick, move onto the score phase.
+ if (this.trickNumber == 13) {
+ phase = HeartsPhase.Score;
+ this.prepareScore();
+ }
+ }
+ return;
+ case HeartsPhase.Score:
+ if (!this.hasGameEnded && this.allReady) {
+ this.roundNumber++;
+ phase = HeartsPhase.Deal;
+ this.resetGame();
+ }
+ return;
+ default:
+ assert(false);
+ }
+ }
+
+ // Returns null or the reason that the player cannot play the card.
+ String canPlay(int player, Card c) {
+ if (phase != HeartsPhase.Play) {
+ return "It is not the Play phase of Hearts.";
+ }
+ if (!cardCollections[player].contains(c)) {
+ return "Player ${player} does not have the card (${c.toString()})";
+ }
+ if (this.whoseTurn != player) {
+ return "It is not Player ${player}'s turn.";
+ }
+ if (trickNumber == 0 && this.numPlayed == 0 && c != TWO_OF_CLUBS) {
+ return "Player ${player} must play the two of clubs.";
+ }
+ if (trickNumber == 0 && isPenaltyCard(c)) {
+ return "Cannot play a penalty card on the first round of Hearts.";
+ }
+ if (this.numPlayed == 0 && isHeartsCard(c) && !heartsBroken) {
+ return "Cannot lead with a heart when the suit has not been broken yet.";
+ }
+ if (this.leadingCard != null) {
+ String leadingSuit = getCardSuit(this.leadingCard);
+ String otherSuit = getCardSuit(c);
+ if (this.numPlayed >= 1 && leadingSuit != otherSuit && hasSuit(player, leadingSuit)) {
+ return "Must follow with a ${leadingSuit}.";
+ }
+ }
+ return null;
+ }
+
+ int determineTrickWinner() {
+ String leadingSuit = this.getCardSuit(this.leadingCard);
+ int highestIndex;
+ int highestValue; // oh no, aces are highest.
+ for (int i = 0; i < 4; i++) {
+ Card c = cardCollections[i + OFFSET_PLAY][0];
+ int value = this.getCardValue(c);
+ String suit = this.getCardSuit(c);
+ if (suit == leadingSuit && (highestIndex == null || highestValue < value)) {
+ highestIndex = i;
+ highestValue = value;
+ }
+ }
+
+ return highestIndex;
+ }
+ void prepareScore() {
+ this.unsetReady();
+ this.updateScore();
+
+ // At this point, it's up to the UI to determine what to do if the game is 'over'.
+ // Check this.hasGameEnded to determine if that is the case. Logically, there is nothing for this game to do.
+ }
+
+ void updateScore() {
+ // Count up points and check if someone shot the moon.
+ int shotMoon = null;
+ for (int i = 0; i < 4; i++) {
+ int delta = computeScore(i);
+ this.scores[i] += delta;
+ if (delta == 26) { // Shot the moon!
+ shotMoon = i;
+ }
+ }
+
+ // If someone shot the moon, apply the proper score adjustments here.
+ if (shotMoon != null) {
+ for (int i = 0; i < 4; i++) {
+ if (shotMoon == i) {
+ this.scores[i] -= 26;
+ } else {
+ this.scores[i] += 26;
+ }
+ }
+ }
+ }
+
+ int computeScore(int player) {
+ int total = 0;
+ List<Card> trickCards = this.cardCollections[player + OFFSET_TRICK];
+ for (int i = 0; i < trickCards.length; i++) {
+ Card c = trickCards[i];
+ if (isHeartsCard(c)) {
+ total++;
+ }
+ if (isQSCard(c)) {
+ total += 13;
+ }
+ }
+ return total;
+ }
+}
+
+
+class GameLog {
+ Game game;
+ List<GameCommand> log = new List<GameCommand>();
+ int position = 0;
+
+ void setGame(Game g) {
+ this.game = g;
+ }
+
+ // This adds and executes the GameCommand.
+ void add(GameCommand gc) {
+ log.add(gc);
+
+ while (position < log.length) {
+ log[position].execute(game);
+ game.triggerEvents();
+ if (game.updateCallback != null) {
+ game.updateCallback();
+ }
+ position++;
+ }
+ }
+}
+
+abstract class GameCommand {
+ void execute(Game game);
+}
+
+class HeartsCommand extends GameCommand {
+ final String data; // This will be parsed.
+
+ // Usually this constructor is used when reading from a log/syncbase.
+ HeartsCommand(this.data);
+
+ // The following constructors are used for the player generating the HeartsCommand.
+ HeartsCommand.deal(int playerId, List<Card> cards) :
+ this.data = computeDeal(playerId, cards);
+
+ HeartsCommand.pass(int senderId, List<Card> cards) :
+ this.data = computePass(senderId, cards);
+
+ HeartsCommand.take(int takerId) :
+ this.data = computeTake(takerId);
+
+ HeartsCommand.play(int playerId, Card c) :
+ this.data = computePlay(playerId, c);
+
+ HeartsCommand.ready(int playerId) :
+ this.data = computeReady(playerId);
+
+ static computeDeal(int playerId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Deal:${playerId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computePass(int senderId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Pass:${senderId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computeTake(int takerId) {
+ return "Take:${takerId}:END";
+ }
+ static computePlay(int playerId, Card c) {
+ return "Play:${playerId}:${c.toString()}:END";
+ }
+ static computeReady(int playerId) {
+ return "Ready:${playerId}:END";
+ }
+
+ void execute(Game g) {
+ HeartsGame game = g as HeartsGame;
+
+ print("HeartsCommand is executing: ${data}");
+ List<String> parts = data.split(":");
+ switch (parts[0]) {
+ case "Deal":
+ if (game.phase != HeartsPhase.Deal) {
+ throw new StateError("Cannot process deal commands when not in Deal phase");
+ }
+ // Deal appends cards to playerId's hand.
+ int playerId = int.parse(parts[1]);
+ List<Card> hand = game.cardCollections[playerId];
+ if (hand.length + parts.length - 3 > 13) {
+ throw new StateError("Cannot deal more than 13 cards to a hand");
+ }
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 2; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(game.deck, hand, c);
+ }
+ return;
+ case "Pass":
+ if (game.phase != HeartsPhase.Pass) {
+ throw new StateError("Cannot process pass commands when not in Pass phase");
+ }
+ // Pass moves a set of cards from senderId to receiverId.
+ int senderId = int.parse(parts[1]);
+ int receiverId = senderId + HeartsGame.OFFSET_PASS;
+ List<Card> handS = game.cardCollections[senderId];
+ List<Card> handR = game.cardCollections[receiverId];
+
+ int numPassing = parts.length - 3;
+ if (numPassing != 3) {
+ throw new StateError("Must pass 3 cards, attempted ${numPassing}");
+ }
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 2; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(handS, handR, c);
+ }
+ return;
+ case "Take":
+ if (game.phase != HeartsPhase.Take) {
+ throw new StateError("Cannot process take commands when not in Take phase");
+ }
+ int takerId = int.parse(parts[1]);
+ int senderPile = game._getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
+ List<Card> handT = game.cardCollections[takerId];
+ List<Card> handS = game.cardCollections[senderPile];
+ handT.addAll(handS);
+ handS.clear();
+ return;
+ case "Play":
+ if (game.phase != HeartsPhase.Play) {
+ throw new StateError("Cannot process play commands when not in Play phase");
+ }
+
+ // Play the card from the player's hand to their play pile.
+ int playerId = int.parse(parts[1]);
+ int targetId = playerId + HeartsGame.OFFSET_PLAY;
+ List<Card> hand = game.cardCollections[playerId];
+ List<Card> discard = game.cardCollections[targetId];
+
+ Card c = new Card.fromString(parts[2]);
+
+ // If the card isn't valid, then we have an error.
+ String reason = game.canPlay(playerId, c);
+ if (reason != null) {
+ throw new StateError("Player ${playerId} cannot play ${c.toString()} because ${reason}");
+ }
+ this.transfer(hand, discard, c);
+ return;
+ case "Ready":
+ if (game.hasGameEnded) {
+ throw new StateError("Game has already ended. Start a new one to play again.");
+ }
+ if (game.phase != HeartsPhase.Score) {
+ throw new StateError("Cannot process ready commands when not in Score phase");
+ }
+ int playerId = int.parse(parts[1]);
+ game.setReady(playerId);
+ return;
+ default:
+ print(data);
+ assert(false); // How could this have happened?
+ }
+ }
+
+ void transfer(List<Card> sender, List<Card> receiver, Card c) {
+ if (!sender.contains(c)) {
+ throw new StateError("Sender ${sender.toString()} lacks Card ${c.toString()}");
+ }
+ sender.remove(c);
+ receiver.add(c);
+ }
+}
+
+class ProtoCommand extends GameCommand {
+ final String data; // This will be parsed.
+
+ // Usually this constructor is used when reading from a log/syncbase.
+ ProtoCommand(this.data);
+
+ // The following constructors are used for the player generating the ProtoCommand.
+ ProtoCommand.deal(int playerId, List<Card> cards) :
+ this.data = computeDeal(playerId, cards);
+
+ // TODO: receiverId is actually implied by the game round. So it may end up being removable.
+ ProtoCommand.pass(int senderId, int receiverId, List<Card> cards) :
+ this.data = computePass(senderId, receiverId, cards);
+
+ ProtoCommand.play(int playerId, Card c) :
+ this.data = computePlay(playerId, c);
+
+ static computeDeal(int playerId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Deal:${playerId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computePass(int senderId, int receiverId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Pass:${senderId}:${receiverId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computePlay(int playerId, Card c) {
+ return "Play:${playerId}:${c.toString()}:END";
+ }
+
+ void execute(Game game) {
+ print("ProtoCommand is executing: ${data}");
+ List<String> parts = data.split(":");
+ switch (parts[0]) {
+ case "Deal":
+ // Deal appends cards to playerId's hand.
+ int playerId = int.parse(parts[1]);
+ List<Card> hand = game.cardCollections[playerId];
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 2; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(game.deck, hand, c);
+ }
+ return;
+ case "Pass":
+ // Pass moves a set of cards from senderId to receiverId.
+ int senderId = int.parse(parts[1]);
+ int receiverId = int.parse(parts[2]);
+ List<Card> handS = game.cardCollections[senderId];
+ List<Card> handR = game.cardCollections[receiverId];
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 3; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(handS, handR, c);
+ }
+ return;
+ case "Play":
+ // In this case, move it to the designated discard pile.
+ // For now, the discard pile is pile #4. This may change.
+ int playerId = int.parse(parts[1]);
+ List<Card> hand = game.cardCollections[playerId];
+
+ Card c = new Card.fromString(parts[2]);
+ this.transfer(hand, game.cardCollections[4], c);
+ return;
+ default:
+ print(data);
+ assert(false); // How could this have happened?
+ }
+ }
+
+ void transfer(List<Card> sender, List<Card> receiver, Card c) {
+ assert(sender.contains(c));
+ sender.remove(c);
+ receiver.add(c);
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..173d822
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,66 @@
+//import 'package:sky/widgets/basic.dart';
+
+//import 'card.dart' as card;
+//import 'my_button.dart';
+//import 'dart:sky' as sky;
+//import 'package:vector_math/vector_math.dart' as vector_math;
+//import 'package:sky/theme/colors.dart' as colors;
+import 'package:sky/widgets.dart';
+
+//import 'logic/game.dart' show Game, HeartsCommand;
+//import 'components/game.dart' show GameComponent;
+import 'logic/croupier.dart' show Croupier;
+import 'components/croupier.dart' show CroupierComponent;
+
+//import 'dart:io';
+//import 'dart:convert';
+//import 'dart:async';
+
+class CroupierApp extends App {
+ Croupier croupier;
+
+ CroupierApp() : super() {
+ this.croupier = new Croupier();
+ }
+
+ Widget build() {
+ return new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: const Color(0xFF0000FF),
+ borderRadius: 5.0
+ ),
+ child: new CroupierComponent(this.croupier)
+ );
+ }
+}
+
+void main() {
+ print('started');
+ CroupierApp app = new CroupierApp();
+
+ // Had difficulty reading from a file, so I can use this to simulate it.
+ // Seems like only NetworkImage exists, but why not also have NetworkFile?
+ /*List<String> commands = <String>[
+ "Deal:0:classic h1:classic h2:classic h3:classic h4:END",
+ "Deal:1:classic d1:classic d2:classic d3:classic d4:END",
+ "Deal:2:classic s1:classic s2:classic s3:classic s4:END",
+ "Deal:3:classic c1:classic c2:classic c3:classic c4:END",
+ "Pass:0:1:classic h2:classic h3:END",
+ "Pass:1:2:classic d1:classic d4:END",
+ "Play:0:classic h1:END",
+ "Play:1:classic d3:END",
+ "Play:2:classic d4:END",
+ "Play:3:classic c2:END"
+ ];
+ new Future.delayed(new Duration(seconds: 2)).then((_) {
+ for (var i = 0; i < commands.length; i++) {
+ new Future.delayed(new Duration(seconds: 1*i)).then((_) {
+ app.game.gamelog.add(new HeartsCommand(commands[i]));
+ });
+ }
+ });*/
+
+
+ runApp(app);
+ print('running');
+}
\ No newline at end of file
diff --git a/lib/menu.png b/lib/menu.png
new file mode 100644
index 0000000..38ee213
--- /dev/null
+++ b/lib/menu.png
Binary files differ
diff --git a/lib/my_button.dart b/lib/my_button.dart
new file mode 100644
index 0000000..c04bf97
--- /dev/null
+++ b/lib/my_button.dart
@@ -0,0 +1,63 @@
+import 'package:sky/widgets/basic.dart';
+
+final BoxDecoration _decoration = new BoxDecoration(
+ backgroundColor: const Color(0xFFFF00FF),
+ borderRadius: 5.0/*,
+ gradient: new LinearGradient(
+ endPoints: [ Point.origin, const Point(0.0, 36.0) ],
+ colors: [ const Color(0xFFEEEEEE), const Color(0xFFCCCCCC) ]
+ )*/
+);
+
+class MyButton extends Component {
+ final Widget child;
+ final Function onPressed;
+ final Function onPointerDown;
+ final Function onPointerMove;
+ final Function onPointerUp;
+
+ MyButton({this.child, this.onPressed, this.onPointerDown, this.onPointerMove, this.onPointerUp});
+
+ Container makeContainer() {
+ return new Container(
+ //height: 36.0,
+ //padding: const EdgeDims.all(8.0),
+ //margin: const EdgeDims.symmetric(horizontal: 8.0),
+ decoration: _decoration,
+ child: new Center(
+ child: this.child
+ )
+ );
+ }
+
+ Widget build() {
+ return new Listener(
+ // Listeners have these possibly fields https://github.com/domokit/sky_engine/blob/2e8843893b9c1cef0f0f9d9e00d384fca7a70d23/sky/packages/sky/lib/widgets/framework.dart
+ onGestureTap: (e) {
+ print('MyButton was tapped!');
+ if (this.onPressed != null) {
+ this.onPressed(e);
+ }
+ },
+ onPointerDown: (e) {
+ print('MyButton was scrolled!');
+ if (this.onPointerDown != null) {
+ this.onPointerDown(e);
+ }
+ },
+ onPointerMove: (e) {
+ print('MyButton continues to be scrolled!');
+ if (this.onPointerMove != null) {
+ this.onPointerMove(e);
+ }
+ },
+ onPointerUp: (e) {
+ print('MyButton pointer up!');
+ if (this.onPointerUp != null) {
+ this.onPointerUp(e);
+ }
+ },
+ child: makeContainer()
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/search.png b/lib/search.png
new file mode 100644
index 0000000..9c81036
--- /dev/null
+++ b/lib/search.png
Binary files differ
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..298989f
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,5 @@
+name: your_app_name
+dependencies:
+ sky: any
+ sky_tools: any
+ test: any
\ No newline at end of file
diff --git a/test/game_log_hearts_test.txt b/test/game_log_hearts_test.txt
new file mode 100644
index 0000000..de1b3b2
--- /dev/null
+++ b/test/game_log_hearts_test.txt
@@ -0,0 +1,508 @@
+# Deal
+Deal:0:classic h1:classic h2:classic h3:classic h4:classic h5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:1:classic d1:classic d2:classic d3:classic d4:classic d5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+Deal:2:classic s1:classic s2:classic s3:classic s4:classic s5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:3:classic c1:classic c2:classic c3:classic c4:classic c5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+
+# Pass
+Pass:3:classic c1:classic c2:classic c3:END
+Pass:2:classic s1:classic s2:classic s3:END
+Pass:0:classic h1:classic h2:classic h3:END
+Pass:1:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# 0 has all diamonds except for h4 and h5
+# 1 has all spades except for d4 and d5
+# 2 has all clubs except for s4 and s5
+# 3 has all hearts except for c4 and c5
+
+# Trick 1 (2 leads with 2 of clubs)
+Play:2:classic c2:END
+Play:3:classic c4:END
+Play:0:classic d1:END
+Play:1:classic s1:END
+
+# Trick 2 (3 won last round with 4 of clubs)
+Play:3:classic c5:END
+Play:0:classic d2:END
+Play:1:classic s2:END
+Play:2:classic c1:END
+
+# Trick 3 (2 won with ace of clubs)
+Play:2:classic s4:END
+Play:3:classic h1:END
+Play:0:classic h5:END
+Play:1:classic s3:END
+
+# Trick 4 (2 won with s4)
+Play:2:classic s5:END
+Play:3:classic hk:END
+Play:0:classic h4:END
+Play:1:classic sk:END
+
+# Trick 5 (1 won with sk)
+Play:1:classic d5:END
+Play:2:classic ck:END
+Play:3:classic hq:END
+Play:0:classic d3:END
+
+# Trick 6 (1 won with d5)
+Play:1:classic d4:END
+Play:2:classic cq:END
+Play:3:classic hj:END
+Play:0:classic dk:END
+
+# Trick 7 (0 won with dk)
+Play:0:classic dq:END
+Play:1:classic sq:END
+Play:2:classic c3:END
+Play:3:classic h2:END
+
+# Trick 8 (0 won with dq)
+Play:0:classic dj:END
+Play:1:classic sj:END
+Play:2:classic cj:END
+Play:3:classic h3:END
+
+# Trick 9 (0 won with dj)
+Play:0:classic d10:END
+Play:1:classic s10:END
+Play:2:classic c10:END
+Play:3:classic h10:END
+
+# Trick 10 (0 won with d10)
+Play:0:classic d9:END
+Play:1:classic s9:END
+Play:2:classic c9:END
+Play:3:classic h9:END
+
+# Trick 11 (0 won with d9)
+Play:0:classic d8:END
+Play:1:classic s8:END
+Play:2:classic c8:END
+Play:3:classic h8:END
+
+# Trick 12 (0 won with d8)
+Play:0:classic d7:END
+Play:1:classic s7:END
+Play:2:classic c7:END
+Play:3:classic h7:END
+
+# Trick 13 (0 won with d7)
+Play:0:classic d6:END
+Play:1:classic s6:END
+Play:2:classic c6:END
+Play:3:classic h6:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# The score is [21, 3, 2, 0]
+
+# 2nd Round here
+
+# Deal
+Deal:0:classic d1:classic d2:classic d3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic h1:classic h2:classic h3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic s1:classic s2:classic s3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic c1:classic c2:classic c3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:1:classic h1:classic h2:classic h3:END
+Pass:2:classic s1:classic s2:classic s3:END
+Pass:3:classic c1:classic c2:classic c3:END
+Pass:0:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 3rd Round here
+
+# Deal
+Deal:0:classic h1:classic h2:classic h3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic s1:classic s2:classic s3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic c1:classic c2:classic c3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic d1:classic d2:classic d3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:0:classic h1:classic h2:classic h3:END
+Pass:1:classic s1:classic s2:classic s3:END
+Pass:2:classic c1:classic c2:classic c3:END
+Pass:3:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 4th round here
+
+# Deal
+Deal:0:classic c1:classic c2:classic c3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic d1:classic d2:classic d3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic h1:classic h2:classic h3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic s1:classic s2:classic s3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 5th round here
+
+# Deal
+Deal:0:classic s1:classic s2:classic s3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic c1:classic c2:classic c3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic d1:classic d2:classic d3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic h1:classic h2:classic h3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:3:classic h1:classic h2:classic h3:END
+Pass:0:classic s1:classic s2:classic s3:END
+Pass:1:classic c1:classic c2:classic c3:END
+Pass:2:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Game is over!
\ No newline at end of file
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
new file mode 100644
index 0000000..85fc692
--- /dev/null
+++ b/test/hearts_test.dart
@@ -0,0 +1,445 @@
+import "package:test/test.dart";
+import "../lib/logic/game.dart";
+import "../lib/logic/card.dart";
+
+import "dart:io";
+
+void main() {
+ group("Initialization", () {
+ HeartsGame game = new HeartsGame(0);
+ test("Dealing", () {
+ game.dealCards(); // What the dealer actually runs to get cards to everybody.
+
+ // By virtue of creating the game, HeartsGame should have 4 collections with 13 cards and 8 collections with 0 cards each.
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to A");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to B");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to C");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_HAND].length, equals(13), reason: "Dealt 13 cards to D");
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_PLAY].length, equals(0), reason: "Not playing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_PASS].length, equals(0), reason: "Not passing yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_TRICK].length, equals(0), reason: "No tricks yet");
+ });
+ });
+
+ // For this test, the cards may end up being duplicate or inconsistent.
+ group("Scoring", () {
+ HeartsGame game = new HeartsGame(0);
+ test("Compute/Prepare Score", () {
+ // In this situation, what's the score?
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[
+ new Card("classic", "dq"),
+ new Card("classic", "dk"),
+ new Card("classic", "h1"),
+ new Card("classic", "h2"),
+ new Card("classic", "h3"),
+ new Card("classic", "h4")
+ ];
+
+ expect(game.computeScore(HeartsGame.PLAYER_A), equals(4), reason: "Player A has 4 hearts");
+
+ // In this alternative situation, what's the score?
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK] = <Card>[
+ new Card("classic", "h6"),
+ new Card("classic", "h7"),
+ new Card("classic", "h8"),
+ new Card("classic", "h9"),
+ new Card("classic", "h10"),
+ new Card("classic", "hj"),
+ new Card("classic", "hq"),
+ new Card("classic", "hk"),
+ new Card("classic", "s1"),
+ new Card("classic", "s2")
+ ];
+
+ expect(game.computeScore(HeartsGame.PLAYER_B), equals(8), reason: "Player B has 8 hearts.");
+
+ // Should prepare C as well.
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK] = <Card>[
+ new Card("classic", "h5"),
+ new Card("classic", "sq")
+ ];
+ expect(game.computeScore(HeartsGame.PLAYER_C), equals(14), reason: "Player C has 1 heart and the queen of spades.");
+
+ // Now, update the score, modifying game.scores.
+ game.updateScore();
+ expect(game.scores, equals([4, 8, 14, 0]));
+
+ // Do it again.
+ game.updateScore();
+ expect(game.scores, equals([8, 16, 28, 0]));
+
+ // Shoot the moon!
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_D_TRICK] = Card.All;
+ game.updateScore();
+ expect(game.scores, equals([34, 42, 54, 0]));
+ });
+ });
+
+ group("Game Over", () {
+ HeartsGame game = new HeartsGame(0);
+
+ test("Has the game ended? Yes", () {
+ // Check if the game has ended. Should be yes.
+ game.scores = <int>[HeartsGame.MAX_SCORE + 5, 40, 35, 0];
+ expect(game.hasGameEnded, isTrue);
+ });
+ test("Has the game ended? No", () {
+ // Check if the game has ended. Should be no.
+ game.scores = <int>[HeartsGame.MAX_SCORE - 5, 40, 35, 0];
+ expect(game.hasGameEnded, isFalse);
+ });
+ });
+
+ // At this point, we should prepare the canonical game by setting up state and
+ // performing a single action or set of actions.
+ // Reads from a log, so we will go through logical game mechanics.
+ group("Card Manipulation", () {
+ HeartsGame game = new HeartsGame(0);
+
+ // Note: This could have been a non-file (in-memory), but it's fine to use a file too.
+ File file = new File("test/game_log_hearts_test.txt");
+ List<String> commands = file.readAsStringSync().split("\n");
+ int commandIndex = 0;
+
+ void runCommand() {
+ String c = commands[commandIndex];
+ commandIndex++;
+ if (c == "" || c[0] == "#") { // Essentially, this case allows empty lines and comments.
+ runCommand();
+ } else {
+ game.gamelog.add(new HeartsCommand(c));
+ }
+ }
+
+ test("Deal Phase", () {
+ expect(game.phase, equals(HeartsPhase.Deal));
+
+ // Deal consists of 4 deal commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands.
+ List<Card> expectedAHand = new List<Card>.from(Card.All.getRange(26, 26+5))..addAll(Card.All.getRange(13+5, 26));
+ List<Card> expectedBHand = new List<Card>.from(Card.All.getRange(13, 13+5))..addAll(Card.All.getRange(39+5, 52));
+ List<Card> expectedCHand = new List<Card>.from(Card.All.getRange(39, 39+5))..addAll(Card.All.getRange(0+5, 13));
+ List<Card> expectedDHand = new List<Card>.from(Card.All.getRange(0, 0+5))..addAll(Card.All.getRange(26+5, 39));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ });
+ test("Pass Phase", () {
+ expect(game.phase, equals(HeartsPhase.Pass));
+
+ // Pass consists of 4 pass commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands and passes.
+ List<Card> expectedAHand = new List<Card>.from(Card.All.getRange(26+3, 26+5))..addAll(Card.All.getRange(13+5, 26));
+ List<Card> expectedBHand = new List<Card>.from(Card.All.getRange(13+3, 13+5))..addAll(Card.All.getRange(39+5, 52));
+ List<Card> expectedCHand = new List<Card>.from(Card.All.getRange(39+3, 39+5))..addAll(Card.All.getRange(0+5, 13));
+ List<Card> expectedDHand = new List<Card>.from(Card.All.getRange(0+3, 0+5))..addAll(Card.All.getRange(26+5, 39));
+ List<Card> expectedAPass = new List<Card>.from(Card.All.getRange(26, 26+3));
+ List<Card> expectedBPass = new List<Card>.from(Card.All.getRange(13, 13+3));
+ List<Card> expectedCPass = new List<Card>.from(Card.All.getRange(39, 39+3));
+ List<Card> expectedDPass = new List<Card>.from(Card.All.getRange(0, 0+3));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_A_PASS], equals(expectedAPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_B_PASS], equals(expectedBPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_C_PASS], equals(expectedCPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_D_PASS], equals(expectedDPass));
+ });
+ test("Take Phase", () {
+ expect(game.phase, equals(HeartsPhase.Take));
+
+ // Take consists of 4 take commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands again.
+ // Note: I will eventually want to do a sorted comparison or set comparison instead.
+ List<Card> expectedAHand = new List<Card>.from(Card.All.getRange(26+3, 26+5))
+ ..addAll(Card.All.getRange(13+5, 26))
+ ..addAll(Card.All.getRange(13, 13+3));
+ List<Card> expectedBHand = new List<Card>.from(Card.All.getRange(13+3, 13+5))
+ ..addAll(Card.All.getRange(39+5, 52))
+ ..addAll(Card.All.getRange(39, 39+3));
+ List<Card> expectedCHand = new List<Card>.from(Card.All.getRange(39+3, 39+5))
+ ..addAll(Card.All.getRange(0+5, 13))
+ ..addAll(Card.All.getRange(0, 0+3));
+ List<Card> expectedDHand = new List<Card>.from(Card.All.getRange(0+3, 0+5))
+ ..addAll(Card.All.getRange(26+5, 39))
+ ..addAll(Card.All.getRange(26, 26+3));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+
+ });
+ test("Play Phase - Trick 1", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 1 consists of 4 play commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm the winner of the round.
+ expect(game.lastTrickTaker, equals(3), reason: "Player 3 played 4 of Clubs");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4), reason: "Player 3 won 1 trick.");
+ });
+ test("Play Phase - Trick 2", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 2 consists of 4 play commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm the winner of the round.
+ expect(game.lastTrickTaker, equals(2), reason: "Player 2 played Ace of Clubs");
+ expect(game.cardCollections[HeartsGame.PLAYER_C_TRICK].length, equals(4), reason: "Player 2 won 1 trick.");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4), reason: "Player 3 won 1 trick.");
+
+ });
+ test("Play Phase - Trick 13", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 13 consists of 44 play commands.
+ // Read line by line until the game is "over".
+ for (int i = 8; i < 52; i++) {
+ runCommand();
+ }
+
+ // Assert that hands/plays/passes are empty.
+ expect(game.cardCollections[HeartsGame.PLAYER_A + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+ expect(game.cardCollections[HeartsGame.PLAYER_B + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+ expect(game.cardCollections[HeartsGame.PLAYER_C + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+ expect(game.cardCollections[HeartsGame.PLAYER_D + HeartsGame.OFFSET_HAND].length, equals(0), reason: "Played all cards");
+
+ // Check that all 52 cards are in tricks.
+ expect(game.lastTrickTaker, equals(0), reason: "Player 0 won the last trick.");
+ expect(game.cardCollections[HeartsGame.PLAYER_A_TRICK].length, equals(4*8), reason: "Player 0 won 8 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_B_TRICK].length, equals(4*2), reason: "Player 1 won 2 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_C_TRICK].length, equals(4*2), reason: "Player 2 won 2 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4), reason: "Player 3 won 1 trick.");
+ });
+ test("Score Phase", () {
+ expect(game.phase, equals(HeartsPhase.Score));
+
+ // Check score to ensure it matches the expectation.
+ expect(game.scores, equals([21, 3, 2, 0]));
+
+ // Score consists of 4 ready commands.
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+
+ // Back to the deal phase once everyone indicates that they are ready.
+ expect(game.phase, equals(HeartsPhase.Deal));
+ });
+ test("Score Phase - end of game", () {
+ expect(game.hasGameEnded, isFalse);
+
+ // 2nd Round: 4 deal, 4 pass, 4 take, 52 play, 4 ready
+ // Player A will shoot the moon for all remaining games (for simplicity).
+ for (int i = 0; i < 68; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0, 3+26, 2+26, 0+26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 3rd Round: 4 deal, 4 pass, 4 take, 52 play, 4 ready
+ for (int i = 0; i < 68; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0+0, 3+26+26, 2+26+26, 0+26+26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 4th Round: 4 deal, 52 play, 4 ready
+ for (int i = 0; i < 60; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0+0+0, 3+26+26+26, 2+26+26+26, 0+26+26+26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 5th round: 4 deal, 4 pass, 4 take, 52 play. Game is over, so no ready phase.
+ for (int i = 0; i < 64; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21+0+0+0+0, 3+26+26+26+26, 2+26+26+26+26, 0+26+26+26+26]));
+ expect(game.hasGameEnded, isTrue); // assumes game ends after about 100 points.
+ });
+ });
+
+ group("Card Manipulation - Error Cases", () {
+ test("Dealing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.phase = HeartsPhase.Score;
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Dealing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, <Card>[new Card("fake", "not real")]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Dealing - too many cards dealt", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 15))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 5))));
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(5, 15))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(0, 4))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(13, 16))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - wrong number of cards", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(0, 2))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(0, new List<Card>.from(Card.All.getRange(0, 4))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Taking - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.take(3));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Play;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[13]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (not 2 of clubs as first card)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (no penalty on first round)", () {
+ // NOTE: It is actually possible to be forced to play a penalty card on round 1.
+ // But the odds are miniscule, so this rule will be enforced.
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[26]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - wrong turn", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13])); // player 0's turn, not player 1's.
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (suit mismatch)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 12))..add(Card.All[25])));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(12, 25))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[13])); // should play 12
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (hearts not broken yet)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0, new List<Card>.from(Card.All.getRange(0, 12))..add(Card.All[38])));
+ game.gamelog.add(new HeartsCommand.deal(1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2, new List<Card>.from(Card.All.getRange(26, 38))..add(Card.All[12])));
+ game.gamelog.add(new HeartsCommand.deal(3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[12])); // 2 won!
+ game.gamelog.add(new HeartsCommand.play(3, Card.All[39]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[26])); // But 2 can't lead with a hearts.
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ });
+}
\ No newline at end of file