This is the firstpass (untested) implementation of Hearts in Croupier.
The UI has not been updated to correspond to this new information either.
diff --git a/Makefile b/Makefile
index b56dd51..c905372 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,25 @@
-lint:
-	dartanalyzer lib/main.dart
+# 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:
+	pub upgrade
 
-start:
+TEST_FILES := $(shell find test -name *.dart ! -name *.part.dart)
+
+check-fmt:
+	dartfmt -n lib/main.dart $(TEST_FILES)
+
+lint: get-packages
+	dartanalyzer lib/main.dart
+	dartanalyzer $(TEST_FILES)
+
+start: get-packages
 	./packages/sky/sky_tool start
 
-install:
+install: get-packages
 	./packages/sky/sky_tool start --install
+
+test: get-packages
+	pub run test
+
+clean:
+	rm -rf packages
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
index a7d4ba8..27d0bcc 100644
--- a/lib/components/card_collection.dart
+++ b/lib/components/card_collection.dart
@@ -19,7 +19,7 @@
 
   CardCollectionComponent(this.cards, this.faceUp, this.orientation, this.parentCallback);
 
-  void syncFields(CardCollectionComponent other) {
+  void syncConstructorArguments(CardCollectionComponent other) {
     //assert(false); // Why do we need to do this?
     //status = other.status;
     cards = other.cards;
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 38909a8..e33e015 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -10,7 +10,7 @@
 
   CroupierComponent(this.croupier) : super();
 
-  void syncFields(CroupierComponent other) {
+  void syncConstructorArguments(CroupierComponent other) {
     croupier = other.croupier;
   }
 
diff --git a/lib/components/draggable.dart b/lib/components/draggable.dart
index 21be29b..4415645 100644
--- a/lib/components/draggable.dart
+++ b/lib/components/draggable.dart
@@ -9,7 +9,7 @@
 
   Draggable(this.child);
 
-  void syncFields(Draggable other) {
+  void syncConstructorArguments(Draggable other) {
     child = other.child;
   }
 
diff --git a/lib/components/game.dart b/lib/components/game.dart
index ab5eea5..1f384b2 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -17,7 +17,7 @@
     setState(() {});
   }
 
-  void syncFields(GameComponent other) {
+  void syncConstructorArguments(GameComponent other) {
     this.game = other.game;
   }
 
diff --git a/lib/logic/game.dart b/lib/logic/game.dart
index df02af9..1a450f2 100644
--- a/lib/logic/game.dart
+++ b/lib/logic/game.dart
@@ -58,9 +58,19 @@
     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 {
@@ -80,7 +90,7 @@
   }
 
   void deal(int playerId, int numCards) {
-    gamelog.add(new HeartsCommand.deal(playerId, this.deckPeek(numCards)));
+    gamelog.add(new ProtoCommand.deal(playerId, this.deckPeek(numCards)));
   }
 
   // Overrides Game's move method with the "move" logic for the card dragging prototype.
@@ -95,50 +105,407 @@
     }
     int destId = cardCollections.indexOf(dest);
 
-    gamelog.add(new HeartsCommand.pass(i, destId, <Card>[card]));
+    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 {
-  HeartsGame(int playerNumber) : super._create(GameType.Hearts, 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
+  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;
 
-    // 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.
+  static const OFFSET_HAND = 0;
+  static const OFFSET_PLAY = 4;
+  static const OFFSET_TRICK = 8;
+  static const OFFSET_PASS = 12;
 
-    // We do some arbitrary things here... Just for setup.
+  // 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;
+  int lastTrickTaker;
+  bool heartsBroken;
+
+  // 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();
+  }
+
+  void prepareRound() {
+    if (roundNumber == null) {
+      roundNumber = 0;
+    } else {
+      roundNumber++;
+    }
+
+    phase = HeartsPhase.Deal;
+
+    this.resetCards();
+    heartsBroken = false;
+    lastTrickTaker = null;
     deck.shuffle();
-    deal(0, 8);
-    deal(1, 5);
-    deal(2, 4);
-    deal(3, 1);
+    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 {
+    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 {
+    switch (roundNumber % 4) { // is a 4-cycle
+      case 0:
+        return (playerNumber + 1) % 4; // takeRight
+      case 1:
+        return (playerNumber - 1) % 4; // takeLeft
+      case 2:
+        return (playerNumber + 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;
+    }
+    if (trickNumber == 0) {
+      return (this.findCard(TWO_OF_CLUBS) + this.numPlayed) % 4;
+    } else {
+      return (lastTrickTaker + this.numPlayed) % 4;
+    }
+  }
+
+  int getCardValue(Card c) {
+    String remainder = c.identifier.substring(1);
+    switch (remainder) {
+      case "0": // 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 {
+    assert(this.numPlayed == 1);
+    for (int i = 0; i < 4; i++) {
+      if (cardCollections[i + OFFSET_HAND].length == 1) {
+        return cardCollections[i + OFFSET_HAND][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) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  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)));
   }
 
-  // Overrides Game's move method with the "move" logic for Hearts.
+  // 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 ArgumentError('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) {
-    // 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()}';
+    assert(phase == HeartsPhase.Play && whoseTurn == playerNumber);
+
     int i = findCard(card);
     if (i == -1) {
-      debugString = 'NO... ${card.toString()}';
-      return;
+      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.pass(i, destId, <Card>[card]));
+    gamelog.add(new HeartsCommand.play(playerNumber, card));
 
-    debugString = 'Move ${i} ${card.toString()}';
+    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:
+        return;
+      case HeartsPhase.Pass:
+        if (this.allPassed) {
+          phase = HeartsPhase.Take;
+        }
+        return;
+      case HeartsPhase.Take:
+        if (this.allTaken) {
+          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;
+
+          // Additionally, if that was the last trick, move onto the score phase.
+          if (this.trickNumber == 13) {
+            this.prepareScore();
+          }
+        }
+        return;
+      case HeartsPhase.Score:
+        if (this.allReady) {
+          this.prepareRound();
+        }
+        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 (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}.";
+    }
+    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();
+
+    phase = HeartsPhase.Score;
+
+    // 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;
+  }
 }
 
 
@@ -157,6 +524,7 @@
 
     while (position < log.length) {
       log[position].execute(game);
+      game.triggerEvents();
       if (game.updateCallback != null) {
         game.updateCallback();
       }
@@ -169,7 +537,6 @@
   void execute(Game game);
 }
 
-
 class HeartsCommand extends GameCommand {
   final String data; // This will be parsed.
 
@@ -180,13 +547,146 @@
   HeartsCommand.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.
-  HeartsCommand.pass(int senderId, int receiverId, List<Card> cards) :
-    this.data = computePass(senderId, receiverId, 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];
+
+        // 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];
+
+        // 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.takeTarget + HeartsGame.OFFSET_PASS;
+        List<Card> handS = game.cardCollections[senderPile];
+        List<Card> handT = game.cardCollections[takerId];
+        handS.addAll(handT);
+        handT.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.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) {
+    assert(sender.contains(c));
+    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}:");
@@ -206,7 +706,7 @@
   }
 
   void execute(Game game) {
-    print("HeartsCommand is executing: ${data}");
+    print("ProtoCommand is executing: ${data}");
     List<String> parts = data.split(":");
     switch (parts[0]) {
       case "Deal":
diff --git a/lib/main.dart b/lib/main.dart
index ef523a3..64cef29 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -29,6 +29,7 @@
 }
 
 void main() {
+  print('started');
   CroupierApp app = new CroupierApp();
 
   // Had difficulty reading from a file, so I can use this to simulate it.
@@ -55,4 +56,5 @@
 
 
   runApp(app);
+  print('running');
 }
\ No newline at end of file
diff --git a/pubspec.yaml b/pubspec.yaml
index a8ad234..298989f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,4 +1,5 @@
 name: your_app_name
 dependencies:
   sky: any
-  sky_tools: any
\ No newline at end of file
+  sky_tools: any
+  test: any
\ No newline at end of file
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
new file mode 100644
index 0000000..ffdb360
--- /dev/null
+++ b/test/hearts_test.dart
@@ -0,0 +1,77 @@
+import "package:test/test.dart";
+import "../lib/logic/game.dart";
+
+void main() {
+  HeartsGame game = new HeartsGame(0);
+
+  group("Card Manipulation", () {
+    test("Dealing", () {
+      // 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", () {
+
+    });
+  });
+  group("Card Manipulation - Error Cases", () {
+    test("Dealing - missing card", () {
+
+    });
+    test("Dealing - wrong number of cards", () {
+
+    });
+    test("Dealing - wrong phase", () {
+
+    });
+    test("Passing - missing card", () {
+
+    });
+    test("Passing - wrong number of cards", () {
+
+    });
+    test("Passing - wrong phase", () {
+
+    });
+    test("Playing - missing card", () {
+
+    });
+    test("Playing - invalid card (not 2 of clubs as first card)", () {
+
+    });
+    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)", () {
+
+    });
+    test("Playing - wrong turn", () {
+
+    });
+    test("Playing - wrong phase", () {
+
+    });
+  });
+  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.
+    });
+  });
+}
\ No newline at end of file