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