The implementation has been fixed up and lots of tests added.

The next step is to add the UI for Hearts on top of this.
Unfortunately, with a single device, it may not look very good.
diff --git a/Makefile b/Makefile
index c905372..6bcebad 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 # Get the packages used by the dart project, according to pubspec.yaml
-# I don't know why but pub get reverts me... Or perhaps Sublime does?
-get-packages:
+# 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)
@@ -8,18 +9,21 @@
 check-fmt:
 	dartfmt -n lib/main.dart $(TEST_FILES)
 
-lint: get-packages
+lint:
 	dartanalyzer lib/main.dart
 	dartanalyzer $(TEST_FILES)
 
-start: get-packages
+start:
 	./packages/sky/sky_tool start
 
 install: get-packages
 	./packages/sky/sky_tool start --install
 
-test: get-packages
-	pub run test
+# 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/lib/components/croupier.dart b/lib/components/croupier.dart
index e33e015..b06ae24 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -69,7 +69,10 @@
       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 GameComponent(croupier.game); // Asks the game UI to draw itself.
+        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;
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 1f384b2..dc0daf2 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -1,5 +1,5 @@
 import '../logic/card.dart' show Card;
-import '../logic/game.dart' show Game, GameType, Viewer;
+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;
@@ -43,7 +43,12 @@
 
   _updateGameCallback(Card card, List<Card> dest) {
     setState(() {
-      game.move(card, dest);
+      try {
+        game.move(card, dest);
+      } catch(e) {
+        print("You can't do that! ${e.toString()}");
+        game.debugString = e.toString();
+      }
     });
   }
 
@@ -104,7 +109,42 @@
     );
   }
 
+  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));
diff --git a/lib/logic/game.dart b/lib/logic/game.dart
index 1a450f2..b5f7d36 100644
--- a/lib/logic/game.dart
+++ b/lib/logic/game.dart
@@ -1,5 +1,5 @@
 import 'card.dart' show Card;
-import 'dart:math' show Random;
+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".
@@ -15,7 +15,7 @@
   final List<List<Card>> cardCollections = new List<List<Card>>();
   final List<Card> deck = new List<Card>.from(Card.All);
 
-  final Random random = new Random();
+  final math.Random random = new math.Random();
   final GameLog gamelog = new GameLog();
   int playerNumber;
   String debugString = 'hello?';
@@ -139,51 +139,45 @@
   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;
-  int roundNumber;
+  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) {
-    prepareRound();
+    resetGame();
   }
 
-  void prepareRound() {
-    if (roundNumber == null) {
-      roundNumber = 0;
-    } else {
-      roundNumber++;
-    }
-
-    phase = HeartsPhase.Deal;
-
+  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);
-
-    if (this.passTarget != null) {
-      phase = HeartsPhase.Pass;
-    } else {
-      phase = HeartsPhase.Play;
-    }
-  }
-
-  int get trickNumber {
-    return 13 - cardCollections[0].length;
   }
 
   int get passTarget {
@@ -201,14 +195,15 @@
         return null;
     }
   }
-  int get takeTarget {
+  int get takeTarget => _getTakeTarget(playerNumber);
+  int _getTakeTarget(takerId) {
     switch (roundNumber % 4) { // is a 4-cycle
       case 0:
-        return (playerNumber + 1) % 4; // takeRight
+        return (takerId + 1) % 4; // takeRight
       case 1:
-        return (playerNumber - 1) % 4; // takeLeft
+        return (takerId - 1) % 4; // takeLeft
       case 2:
-        return (playerNumber + 2) % 4; // taleAcross
+        return (takerId + 2) % 4; // taleAcross
       case 3:
         return null; // no player to pass to
       default:
@@ -222,17 +217,13 @@
     if (phase != HeartsPhase.Play) {
       return null;
     }
-    if (trickNumber == 0) {
-      return (this.findCard(TWO_OF_CLUBS) + this.numPlayed) % 4;
-    } else {
-      return (lastTrickTaker + this.numPlayed) % 4;
-    }
+    return (lastTrickTaker + this.numPlayed) % 4;
   }
 
   int getCardValue(Card c) {
     String remainder = c.identifier.substring(1);
     switch (remainder) {
-      case "0": // ace
+      case "1": // ace
         return 14;
       case "k":
         return 13;
@@ -271,25 +262,28 @@
   }
 
   Card get leadingCard {
-    assert(this.numPlayed == 1);
-    for (int i = 0; i < 4; i++) {
-      if (cardCollections[i + OFFSET_HAND].length == 1) {
-        return cardCollections[i + OFFSET_HAND][0];
-      }
+    if(this.numPlayed >= 1) {
+      return cardCollections[this.lastTrickTaker + OFFSET_PLAY][0];
     }
-    assert(false);
     return null;
   }
   int get numPlayed {
     int count = 0;
     for (int i = 0; i < 4; i++) {
-      if (cardCollections[i + OFFSET_HAND].length == 1) {
+      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 &&
@@ -317,7 +311,7 @@
   void passCards(List<Card> cards) {
     assert(phase == HeartsPhase.Pass && this.passTarget != null);
     if (cards.length != 3) {
-      throw new ArgumentError('3 cards expected, but got: ${cards.toString()}');
+      throw new StateError('3 cards expected, but got: ${cards.toString()}');
     }
     gamelog.add(new HeartsCommand.pass(playerNumber, cards));
   }
@@ -375,6 +369,16 @@
   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) {
@@ -383,6 +387,9 @@
         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;
@@ -404,16 +411,20 @@
 
           // 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.allReady) {
-          this.prepareRound();
+        if (!this.hasGameEnded && this.allReady) {
+          this.roundNumber++;
+          phase = HeartsPhase.Deal;
+          this.resetGame();
         }
         return;
       default:
@@ -438,13 +449,15 @@
     if (trickNumber == 0 && isPenaltyCard(c)) {
       return "Cannot play a penalty card on the first round of Hearts.";
     }
-    if (isHeartsCard(c) && !heartsBroken) {
+    if (this.numPlayed == 0 && isHeartsCard(c) && !heartsBroken) {
       return "Cannot lead with a heart when the suit has not been broken yet.";
     }
-    String leadingSuit = getCardSuit(this.leadingCard);
-    String otherSuit = getCardSuit(c);
-    if (this.numPlayed >= 1 && leadingSuit != otherSuit && hasSuit(player, leadingSuit)) {
-      return "Must follow with a ${leadingSuit}.";
+    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;
   }
@@ -467,9 +480,13 @@
   }
   void prepareScore() {
     this.unsetReady();
+    this.updateScore();
 
-    phase = HeartsPhase.Score;
+    // 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++) {
@@ -596,6 +613,9 @@
         // 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++) {
@@ -613,6 +633,11 @@
         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]);
@@ -624,11 +649,11 @@
           throw new StateError("Cannot process take commands when not in Take phase");
         }
         int takerId = int.parse(parts[1]);
-        int senderPile = game.takeTarget + HeartsGame.OFFSET_PASS;
-        List<Card> handS = game.cardCollections[senderPile];
+        int senderPile = game._getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
         List<Card> handT = game.cardCollections[takerId];
-        handS.addAll(handT);
-        handT.clear();
+        List<Card> handS = game.cardCollections[senderPile];
+        handT.addAll(handS);
+        handS.clear();
         return;
       case "Play":
         if (game.phase != HeartsPhase.Play) {
@@ -651,6 +676,9 @@
         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");
         }
@@ -664,7 +692,9 @@
   }
 
   void transfer(List<Card> sender, List<Card> receiver, Card c) {
-    assert(sender.contains(c));
+    if (!sender.contains(c)) {
+      throw new StateError("Sender ${sender.toString()} lacks Card ${c.toString()}");
+    }
     sender.remove(c);
     receiver.add(c);
   }
diff --git a/lib/main.dart b/lib/main.dart
index 64cef29..173d822 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -24,7 +24,13 @@
   }
 
   Widget build() {
-    return new CroupierComponent(this.croupier);
+    return new Container(
+      decoration: new BoxDecoration(
+        backgroundColor: const Color(0xFF0000FF),
+        borderRadius: 5.0
+      ),
+      child: new CroupierComponent(this.croupier)
+    );
   }
 }
 
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
index ffdb360..85fc692 100644
--- a/test/hearts_test.dart
+++ b/test/hearts_test.dart
@@ -1,77 +1,445 @@
 import "package:test/test.dart";
 import "../lib/logic/game.dart";
+import "../lib/logic/card.dart";
+
+import "dart:io";
 
 void main() {
-  HeartsGame game = new HeartsGame(0);
-
-  group("Card Manipulation", () {
+  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.
-
-    });
-    test("Passing", () {
-
-    });
-    test("Playing", () {
-
+      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 - missing card", () {
-
-    });
-    test("Dealing - wrong number of cards", () {
-
-    });
     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("Passing - missing card", () {
-
+    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("Passing - wrong number of cards", () {
-
+    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.
-    });
-    test("Playing - invalid card (suit mismatch)", () {
-
-    });
-    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, 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 - wrong phase", () {
-
+    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>()));
     });
-  });
-  group("Scoring", () {
-    test("Count Points", () {
-      // In this situation, what's the score?
-    });
-    test("Count Points 2", () {
-      // In this alternative situation, what's the score?
-    });
-  });
-  group("Game Over", () {
-    test("Has the game ended? Yes", () {
-      // Check if the game has ended. Should be yes.
-    });
-    test("Has the game ended? No", () {
-      // Check if the game has ended. Should be no.
+    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