croupier: Augmented Board View Interactions

Multiple Related Changes were added in this CL
* The Game now requires an "Ask" command to occur before a card can be
  "Play"ed.
* The Board will now show a "Play" button when it is your turn so that
  you can play the card you've queued up.
* The Board also shows "Take" to take the trick when you've won it.
  Note: The player's device will no longer show this unless in split
  view.
* When playing without a table, "Ask" is not required.
* Board was moved around a bit to make space for these buttons.

Change-Id: I47368f90c63bd166a5cd53329d30ec61ab95d438
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 09a3b68..3ed6e9e 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -79,7 +79,7 @@
 }
 
 class HeartsBoardState extends State<HeartsBoard> {
-  static const double PROFILE_SIZE = 0.18; // multiplier of config.height
+  static const double PROFILE_SIZE = 0.17; // multiplier of config.height
 
   // Every time the counter changes, a sound will be played.
   // For example, in the pass/take phase, the counter does this:
@@ -392,65 +392,121 @@
   }
 
   Widget _buildBoardLayout() {
-    return new Container(
-        height: config.height,
-        width: config.width,
-        child: new Column([
-          new Flexible(child: _playerProfile(2, PROFILE_SIZE), flex: 0),
-          new Flexible(child: _showTrickText(2), flex: 0),
-          new Flexible(
-              child: new Row([
-                new Flexible(child: _playerProfile(1, PROFILE_SIZE), flex: 0),
-                new Flexible(child: _showTrickText(1), flex: 0),
-                new Flexible(child: _buildCenterCards(), flex: 1),
-                new Flexible(child: _showTrickText(3), flex: 0),
-                new Flexible(child: _playerProfile(3, PROFILE_SIZE), flex: 0)
-              ],
-                  alignItems: FlexAlignItems.center,
-                  justifyContent: FlexJustifyContent.spaceAround),
-              flex: 1),
-          new Flexible(child: _showTrickText(0), flex: 0),
-          new Flexible(child: _playerProfile(0, PROFILE_SIZE), flex: 0)
-        ],
-            alignItems: FlexAlignItems.center,
-            justifyContent: FlexJustifyContent.spaceAround));
+    int activePlayer = config.game.allPlayed
+        ? config.game.determineTrickWinner()
+        : config.game.whoseTurn;
+
+    return new GestureDetector(
+        onTap: config.game.asking ? null : config.game.askUI,
+        child: new Container(
+            height: config.height,
+            width: config.width,
+            decoration: new BoxDecoration(
+                border: new Border(
+                    top: new BorderSide(
+                        color: activePlayer == 2
+                            ? style.theme.accentColor
+                            : style.transparentColor,
+                        width: 5.0),
+                    right: new BorderSide(
+                        color: activePlayer == 3
+                            ? style.theme.accentColor
+                            : style.transparentColor,
+                        width: 5.0),
+                    left: new BorderSide(
+                        color: activePlayer == 1
+                            ? style.theme.accentColor
+                            : style.transparentColor,
+                        width: 5.0),
+                    bottom: new BorderSide(
+                        color: activePlayer == 0
+                            ? style.theme.accentColor
+                            : style.transparentColor,
+                        width: 5.0))),
+            child: new Column([
+              new Flexible(child: _playerProfile(2, PROFILE_SIZE), flex: 0),
+              new Flexible(child: _showTrickText(2), flex: 0),
+              new Flexible(
+                  child: new Row([
+                    new Flexible(
+                        child: _playerProfile(1, PROFILE_SIZE), flex: 0),
+                    new Flexible(child: _showTrickText(1), flex: 0),
+                    new Flexible(
+                        child: new Center(child: _buildCenterCards()), flex: 1),
+                    new Flexible(child: _showTrickText(3), flex: 0),
+                    new Flexible(
+                        child: _playerProfile(3, PROFILE_SIZE), flex: 0)
+                  ],
+                      alignItems: FlexAlignItems.center,
+                      justifyContent: FlexJustifyContent.spaceAround),
+                  flex: 1),
+              new Flexible(child: _showTrickText(0), flex: 0),
+              new Flexible(child: _playerProfile(0, PROFILE_SIZE), flex: 0)
+            ],
+                alignItems: FlexAlignItems.center,
+                justifyContent: FlexJustifyContent.spaceAround)));
   }
 
   Widget _buildCenterCards() {
-    bool wide = (config.width >= config.height);
+    //bool wide = (config.width >= config.height);
 
-    if (wide) {
-      return new Row([
-        _buildCenterCard(1),
-        new Column([_buildCenterCard(2), _buildCenterCard(0)],
-            alignItems: FlexAlignItems.center,
-            justifyContent: FlexJustifyContent.spaceAround),
-        _buildCenterCard(3)
-      ],
-          alignItems: FlexAlignItems.center,
-          justifyContent: FlexJustifyContent.spaceAround);
-    } else {
-      return new Column([
-        _buildCenterCard(2),
-        new Row([_buildCenterCard(1), _buildCenterCard(3)],
-            alignItems: FlexAlignItems.center,
-            justifyContent: FlexJustifyContent.spaceAround),
-        _buildCenterCard(0)
-      ],
-          alignItems: FlexAlignItems.center,
-          justifyContent: FlexJustifyContent.spaceAround);
+    double height = config.cardHeight * this._centerScaleFactor;
+    double width = config.cardWidth * this._centerScaleFactor;
+    Widget centerPiece =
+        new Container(height: height, width: width, child: new Block([]));
+    if (config.game.allPlayed) {
+      int rotateNum = config.game.determineTrickWinner();
+
+      centerPiece = _rotate(
+          new Container(
+              height: height,
+              width: width,
+              child: new RaisedButton(
+                  child: new Text("Take", style: style.Text.largeStyle),
+                  onPressed: config.game.takeTrickUI,
+                  color: style.theme.accentColor)),
+          rotateNum);
     }
+
+    return new Column([
+      new Flexible(
+          child: new Row([
+        new Flexible(child: new Block([])),
+        new Flexible(child: new Center(child: _buildCenterCard(2))),
+        new Flexible(child: new Block([])),
+      ],
+              alignItems: FlexAlignItems.center,
+              justifyContent: FlexJustifyContent.center)),
+      new Flexible(
+          child: new Row([
+        new Flexible(child: new Center(child: _buildCenterCard(1))),
+        new Flexible(child: new Block([centerPiece])),
+        new Flexible(child: new Center(child: _buildCenterCard(3))),
+      ],
+              alignItems: FlexAlignItems.center,
+              justifyContent: FlexJustifyContent.center)),
+      new Flexible(
+          child: new Row([
+        new Flexible(child: new Block([])),
+        new Flexible(child: new Center(child: _buildCenterCard(0))),
+        new Flexible(child: new Block([])),
+      ],
+              alignItems: FlexAlignItems.center,
+              justifyContent: FlexJustifyContent.center))
+    ],
+        alignItems: FlexAlignItems.center,
+        justifyContent: FlexJustifyContent.center);
   }
 
   double get _centerScaleFactor {
     bool wide = (config.width >= config.height);
-    double heightUsage = (1 - 2 * PROFILE_SIZE);
+    double heightUsed = 2 * PROFILE_SIZE;
 
     if (wide) {
-      return config.height * heightUsage / (config.cardHeight * 3);
+      return config.height * (1 - heightUsed) / (config.cardHeight * 4);
     } else {
-      return (config.width - config.height * heightUsage) /
-          (config.cardWidth * 3);
+      return (config.width - (1.5 * config.height * heightUsed)) /
+          (config.cardWidth * 4);
     }
   }
 
@@ -462,13 +518,38 @@
     bool hasPlayed = cards.length > 0;
     bool isTurn = game.whoseTurn == playerNumber && !hasPlayed;
 
-    return new CardCollectionComponent(
-        cards, true, CardCollectionOrientation.show1,
-        widthCard: config.cardWidth * this._centerScaleFactor,
-        heightCard: config.cardHeight * this._centerScaleFactor,
-        rotation: _rotationAngle(playerNumber),
-        useKeys: true,
-        backgroundColor: isTurn ? style.theme.accentColor : null);
+    double height = config.cardHeight * this._centerScaleFactor;
+    double width = config.cardWidth * this._centerScaleFactor;
+
+    List<Widget> stackWidgets = <Widget>[
+      new Positioned(
+          top: 0.0,
+          left: 0.0,
+          child: new CardCollectionComponent(
+              cards, true, CardCollectionOrientation.show1,
+              widthCard: width - 6,
+              heightCard: height - 6,
+              rotation: _rotationAngle(playerNumber),
+              useKeys: true))
+    ];
+
+    if (isTurn) {
+      stackWidgets.add(new Positioned(
+          top: 0.0,
+          left: 0.0,
+          child: _rotate(
+              new Container(
+                  height: height,
+                  width: width,
+                  child: new RaisedButton(
+                      child: new Text("Play", style: style.Text.largeStyle),
+                      onPressed: config.game.asking ? null : config.game.askUI,
+                      color: style.theme.accentColor)),
+              playerNumber)));
+    }
+
+    return new Container(
+        height: height, width: width, child: new Stack(stackWidgets));
   }
 
   // The off-screen cards consist of trick cards and play cards.
@@ -500,7 +581,7 @@
     }
 
     return new CardCollectionComponent(
-        cards, isPlay, CardCollectionOrientation.show1,
+        cards, alreadyPlaying, CardCollectionOrientation.show1,
         widthCard: config.cardWidth * sizeFactor,
         heightCard: config.cardHeight * sizeFactor,
         useKeys: true,
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index f69b819..066cbec 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -30,12 +30,14 @@
 
     // If someone sat at the table, they would have the value 4.
     // If nobody sat at the table, then we should show the split view.
-    if (!config.croupier.players_found.values.contains(4)) {
+    if (!_isBoardPresent) {
       _showSplitView = true;
     }
     _reset();
   }
 
+  bool get _isBoardPresent => config.croupier.players_found.values.contains(4);
+
   @override
   void _reset() {
     super._reset();
@@ -51,9 +53,11 @@
 
   bool get _shouldUnbuffer {
     HeartsGame game = config.game;
+    bool hasPermission = game.asking || !_isBoardPresent;
     return game.whoseTurn == game.playerNumber &&
         bufferedPlay.length > 0 &&
-        !bufferedPlaying;
+        !bufferedPlaying &&
+        hasPermission;
   }
 
   @override
@@ -263,6 +267,10 @@
           _clearBufferedPlay();
           bufferedPlay.add(card);
         } else {
+          // Automatically ask (for when there is no board).
+          if (!game.asking && !_isBoardPresent) {
+            game.askUI();
+          }
           game.move(card, dest);
           config.sounds.play("whooshOut");
         }
@@ -454,7 +462,8 @@
     switch (game.phase) {
       case HeartsPhase.Play:
         if (game.allPlayed &&
-            game.determineTrickWinner() == game.playerNumber) {
+            game.determineTrickWinner() == game.playerNumber &&
+            _showSplitView) {
           statusBarWidgets.add(new Flexible(
               flex: 0,
               child: new GestureDetector(
diff --git a/lib/logic/hearts/hearts_command.part.dart b/lib/logic/hearts/hearts_command.part.dart
index fe6826b..6bb709a 100644
--- a/lib/logic/hearts/hearts_command.part.dart
+++ b/lib/logic/hearts/hearts_command.part.dart
@@ -30,6 +30,9 @@
       : super("Play", computePlay(playerId, c),
             simultaneity: SimulLevel.TURN_BASED);
 
+  HeartsCommand.ask()
+      : super("Ask", computeAsk(), simultaneity: SimulLevel.TURN_BASED);
+
   HeartsCommand.takeTrick()
       : super("TakeTrick", computeTakeTrick(),
             simultaneity: SimulLevel.TURN_BASED);
@@ -48,6 +51,8 @@
         return SimulLevel.INDEPENDENT;
       case "Play":
         return SimulLevel.TURN_BASED;
+      case "Ask":
+        return SimulLevel.TURN_BASED;
       case "TakeTrick":
         return SimulLevel.TURN_BASED;
       case "Ready":
@@ -83,6 +88,10 @@
     return "${playerId}:${c.toString()}:END";
   }
 
+  static String computeAsk() {
+    return "END";
+  }
+
   static String computeTakeTrick() {
     return "END";
   }
@@ -155,6 +164,11 @@
           return false;
         }
 
+        // Can play if the game is asking for a card to be played.
+        if (!game.asking) {
+          return false;
+        }
+
         // Play the card from the player's hand to their play pile.
         int playerId = int.parse(parts[0]);
         int targetId = playerId + HeartsGame.OFFSET_PLAY;
@@ -170,6 +184,14 @@
         }
         bool canTransfer = this.transferCheck(hand, discard, c);
         return canTransfer;
+      case "Ask":
+        if (game.phase != HeartsPhase.Play) {
+          return false;
+        }
+        if (game.allPlayed) {
+          return false;
+        }
+        return !game.asking; // Can ask if you're not asking.
       case "TakeTrick":
         if (game.phase != HeartsPhase.Play) {
           return false;
@@ -257,6 +279,11 @@
               "Cannot process play commands when not in Play phase");
         }
 
+        // Can play if the game is asking for a card to be played.
+        if (!game.asking) {
+          throw new StateError("Cannot play if not asking");
+        }
+
         // Play the card from the player's hand to their play pile.
         int playerId = int.parse(parts[0]);
         int targetId = playerId + HeartsGame.OFFSET_PLAY;
@@ -272,6 +299,20 @@
               "Player ${playerId} cannot play ${c.toString()} because ${reason}");
         }
         this.transfer(hand, discard, c);
+        game.asking = false;
+        return;
+      case "Ask":
+        if (game.phase != HeartsPhase.Play) {
+          throw new StateError(
+              "Cannot process ask commands when not in Play phase");
+        }
+        if (game.asking) {
+          throw new StateError("Cannot ask while already asking");
+        }
+        if (game.allPlayed) {
+          throw new StateError("Cannot ask if all cards are played");
+        }
+        game.asking = true;
         return;
       case "TakeTrick":
         if (game.phase != HeartsPhase.Play) {
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index f4bb0e5..7e1b708 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -60,6 +60,7 @@
   int lastTrickTaker;
   bool heartsBroken;
   int trickNumber;
+  bool asking; // Is the game ready to play a card?
 
   // 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];
@@ -78,6 +79,7 @@
     heartsBroken = false;
     lastTrickTaker = null;
     trickNumber = 0;
+    asking = false;
   }
 
   void dealCards() {
@@ -278,6 +280,16 @@
   }
 
   // Note that this will be called by the UI.
+  void askUI() {
+    assert(phase == HeartsPhase.Play);
+    if (this.asking) {
+      print("Already asked...");
+      return; // just don't call it again.
+    }
+    gamelog.add(new HeartsCommand.ask());
+  }
+
+  // Note that this will be called by the UI.
   void takeTrickUI() {
     assert(phase == HeartsPhase.Play);
     assert(this.allPlayed);
diff --git a/test/game_log_hearts_test.txt b/test/game_log_hearts_test.txt
index 7a3a9db..67126c1 100644
--- a/test/game_log_hearts_test.txt
+++ b/test/game_log_hearts_test.txt
@@ -22,93 +22,145 @@
 # 3 has all hearts except for c4 and c5
 
 # Trick 1 (2 leads with 2 of clubs)
+Ask|END
 Play|2:classic c2:END
+Ask|END
 Play|3:classic c4:END
+Ask|END
 Play|0:classic d1:END
+Ask|END
 Play|1:classic s1:END
 TakeTrick|END
 
 # Trick 2 (3 won last round with 4 of clubs)
+Ask|END
 Play|3:classic c5:END
+Ask|END
 Play|0:classic d2:END
+Ask|END
 Play|1:classic s2:END
+Ask|END
 Play|2:classic c1:END
 TakeTrick|END
 
 # Trick 3 (2 won with ace of clubs)
+Ask|END
 Play|2:classic s4:END
+Ask|END
 Play|3:classic h1:END
+Ask|END
 Play|0:classic h5:END
+Ask|END
 Play|1:classic s3:END
 TakeTrick|END
 
 # Trick 4 (2 won with s4)
+Ask|END
 Play|2:classic s5:END
+Ask|END
 Play|3:classic hk:END
+Ask|END
 Play|0:classic h4:END
+Ask|END
 Play|1:classic sk:END
 TakeTrick|END
 
 # Trick 5 (1 won with sk)
+Ask|END
 Play|1:classic d5:END
+Ask|END
 Play|2:classic ck:END
+Ask|END
 Play|3:classic hq:END
+Ask|END
 Play|0:classic d3:END
 TakeTrick|END
 
 # Trick 6 (1 won with d5)
+Ask|END
 Play|1:classic d4:END
+Ask|END
 Play|2:classic cq:END
+Ask|END
 Play|3:classic hj:END
+Ask|END
 Play|0:classic dk:END
 TakeTrick|END
 
 # Trick 7 (0 won with dk)
+Ask|END
 Play|0:classic dq:END
+Ask|END
 Play|1:classic sq:END
+Ask|END
 Play|2:classic c3:END
+Ask|END
 Play|3:classic h2:END
 TakeTrick|END
 
 # Trick 8 (0 won with dq)
+Ask|END
 Play|0:classic dj:END
+Ask|END
 Play|1:classic sj:END
+Ask|END
 Play|2:classic cj:END
+Ask|END
 Play|3:classic h3:END
 TakeTrick|END
 
 # Trick 9 (0 won with dj)
+Ask|END
 Play|0:classic d10:END
+Ask|END
 Play|1:classic s10:END
+Ask|END
 Play|2:classic c10:END
+Ask|END
 Play|3:classic h10:END
 TakeTrick|END
 
 # Trick 10 (0 won with d10)
+Ask|END
 Play|0:classic d9:END
+Ask|END
 Play|1:classic s9:END
+Ask|END
 Play|2:classic c9:END
+Ask|END
 Play|3:classic h9:END
 TakeTrick|END
 
 # Trick 11 (0 won with d9)
+Ask|END
 Play|0:classic d8:END
+Ask|END
 Play|1:classic s8:END
+Ask|END
 Play|2:classic c8:END
+Ask|END
 Play|3:classic h8:END
 TakeTrick|END
 
 # Trick 12 (0 won with d8)
+Ask|END
 Play|0:classic d7:END
+Ask|END
 Play|1:classic s7:END
+Ask|END
 Play|2:classic c7:END
+Ask|END
 Play|3:classic h7:END
 TakeTrick|END
 
 # Trick 13 (0 won with d7)
+Ask|END
 Play|0:classic d6:END
+Ask|END
 Play|1:classic s6:END
+Ask|END
 Play|2:classic c6:END
+Ask|END
 Play|3:classic h6:END
 TakeTrick|END
 
@@ -141,93 +193,145 @@
 Take|3:END
 
 # Trick 1
+Ask|END
 Play|0:classic c2:END
+Ask|END
 Play|1:classic d2:END
+Ask|END
 Play|2:classic d4:END
+Ask|END
 Play|3:classic s2:END
 TakeTrick|END
 
 # Trick 2
+Ask|END
 Play|0:classic c3:END
+Ask|END
 Play|1:classic d3:END
+Ask|END
 Play|2:classic h3:END
+Ask|END
 Play|3:classic s3:END
 TakeTrick|END
 
 # Trick 3
+Ask|END
 Play|0:classic c4:END
+Ask|END
 Play|1:classic s4:END
+Ask|END
 Play|2:classic h2:END
+Ask|END
 Play|3:classic h4:END
 TakeTrick|END
 
 # Trick 4
+Ask|END
 Play|0:classic c5:END
+Ask|END
 Play|1:classic d5:END
+Ask|END
 Play|2:classic h5:END
+Ask|END
 Play|3:classic s5:END
 TakeTrick|END
 
 # Trick 5
+Ask|END
 Play|0:classic c6:END
+Ask|END
 Play|1:classic d6:END
+Ask|END
 Play|2:classic h6:END
+Ask|END
 Play|3:classic s6:END
 TakeTrick|END
 
 # Trick 6
+Ask|END
 Play|0:classic c7:END
+Ask|END
 Play|1:classic d7:END
+Ask|END
 Play|2:classic h7:END
+Ask|END
 Play|3:classic s7:END
 TakeTrick|END
 
 # Trick 7
+Ask|END
 Play|0:classic c8:END
+Ask|END
 Play|1:classic d8:END
+Ask|END
 Play|2:classic h8:END
+Ask|END
 Play|3:classic s8:END
 TakeTrick|END
 
 # Trick 8
+Ask|END
 Play|0:classic c9:END
+Ask|END
 Play|1:classic d9:END
+Ask|END
 Play|2:classic h9:END
+Ask|END
 Play|3:classic s9:END
 TakeTrick|END
 
 # Trick 9
+Ask|END
 Play|0:classic c1:END
+Ask|END
 Play|1:classic d1:END
+Ask|END
 Play|2:classic h1:END
+Ask|END
 Play|3:classic s1:END
 TakeTrick|END
 
 # Trick 10
+Ask|END
 Play|0:classic c10:END
+Ask|END
 Play|1:classic d10:END
+Ask|END
 Play|2:classic h10:END
+Ask|END
 Play|3:classic s10:END
 TakeTrick|END
 
 # Trick 11
+Ask|END
 Play|0:classic cj:END
+Ask|END
 Play|1:classic dj:END
+Ask|END
 Play|2:classic hj:END
+Ask|END
 Play|3:classic sj:END
 TakeTrick|END
 
 # Trick 12
+Ask|END
 Play|0:classic cq:END
+Ask|END
 Play|1:classic dq:END
+Ask|END
 Play|2:classic hq:END
+Ask|END
 Play|3:classic sq:END
 TakeTrick|END
 
 # Trick 13
+Ask|END
 Play|0:classic ck:END
+Ask|END
 Play|1:classic dk:END
+Ask|END
 Play|2:classic hk:END
+Ask|END
 Play|3:classic sk:END
 TakeTrick|END
 
@@ -258,93 +362,145 @@
 Take|3:END
 
 # Trick 1
+Ask|END
 Play|0:classic c2:END
+Ask|END
 Play|1:classic d2:END
+Ask|END
 Play|2:classic d4:END
+Ask|END
 Play|3:classic s2:END
 TakeTrick|END
 
 # Trick 2
+Ask|END
 Play|0:classic c3:END
+Ask|END
 Play|1:classic d3:END
+Ask|END
 Play|2:classic h3:END
+Ask|END
 Play|3:classic s3:END
 TakeTrick|END
 
 # Trick 3
+Ask|END
 Play|0:classic c4:END
+Ask|END
 Play|1:classic s4:END
+Ask|END
 Play|2:classic h2:END
+Ask|END
 Play|3:classic h4:END
 TakeTrick|END
 
 # Trick 4
+Ask|END
 Play|0:classic c5:END
+Ask|END
 Play|1:classic d5:END
+Ask|END
 Play|2:classic h5:END
+Ask|END
 Play|3:classic s5:END
 TakeTrick|END
 
 # Trick 5
+Ask|END
 Play|0:classic c6:END
+Ask|END
 Play|1:classic d6:END
+Ask|END
 Play|2:classic h6:END
+Ask|END
 Play|3:classic s6:END
 TakeTrick|END
 
 # Trick 6
+Ask|END
 Play|0:classic c7:END
+Ask|END
 Play|1:classic d7:END
+Ask|END
 Play|2:classic h7:END
+Ask|END
 Play|3:classic s7:END
 TakeTrick|END
 
 # Trick 7
+Ask|END
 Play|0:classic c8:END
+Ask|END
 Play|1:classic d8:END
+Ask|END
 Play|2:classic h8:END
+Ask|END
 Play|3:classic s8:END
 TakeTrick|END
 
 # Trick 8
+Ask|END
 Play|0:classic c9:END
+Ask|END
 Play|1:classic d9:END
+Ask|END
 Play|2:classic h9:END
+Ask|END
 Play|3:classic s9:END
 TakeTrick|END
 
 # Trick 9
+Ask|END
 Play|0:classic c1:END
+Ask|END
 Play|1:classic d1:END
+Ask|END
 Play|2:classic h1:END
+Ask|END
 Play|3:classic s1:END
 TakeTrick|END
 
 # Trick 10
+Ask|END
 Play|0:classic c10:END
+Ask|END
 Play|1:classic d10:END
+Ask|END
 Play|2:classic h10:END
+Ask|END
 Play|3:classic s10:END
 TakeTrick|END
 
 # Trick 11
+Ask|END
 Play|0:classic cj:END
+Ask|END
 Play|1:classic dj:END
+Ask|END
 Play|2:classic hj:END
+Ask|END
 Play|3:classic sj:END
 TakeTrick|END
 
 # Trick 12
+Ask|END
 Play|0:classic cq:END
+Ask|END
 Play|1:classic dq:END
+Ask|END
 Play|2:classic hq:END
+Ask|END
 Play|3:classic sq:END
 TakeTrick|END
 
 # Trick 13
+Ask|END
 Play|0:classic ck:END
+Ask|END
 Play|1:classic dk:END
+Ask|END
 Play|2:classic hk:END
+Ask|END
 Play|3:classic sk:END
 TakeTrick|END
 
@@ -363,93 +519,145 @@
 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
+Ask|END
 Play|0:classic c2:END
+Ask|END
 Play|1:classic d2:END
+Ask|END
 Play|2:classic d4:END
+Ask|END
 Play|3:classic s2:END
 TakeTrick|END
 
 # Trick 2
+Ask|END
 Play|0:classic c3:END
+Ask|END
 Play|1:classic d3:END
+Ask|END
 Play|2:classic h3:END
+Ask|END
 Play|3:classic s3:END
 TakeTrick|END
 
 # Trick 3
+Ask|END
 Play|0:classic c4:END
+Ask|END
 Play|1:classic s4:END
+Ask|END
 Play|2:classic h2:END
+Ask|END
 Play|3:classic h4:END
 TakeTrick|END
 
 # Trick 4
+Ask|END
 Play|0:classic c5:END
+Ask|END
 Play|1:classic d5:END
+Ask|END
 Play|2:classic h5:END
+Ask|END
 Play|3:classic s5:END
 TakeTrick|END
 
 # Trick 5
+Ask|END
 Play|0:classic c6:END
+Ask|END
 Play|1:classic d6:END
+Ask|END
 Play|2:classic h6:END
+Ask|END
 Play|3:classic s6:END
 TakeTrick|END
 
 # Trick 6
+Ask|END
 Play|0:classic c7:END
+Ask|END
 Play|1:classic d7:END
+Ask|END
 Play|2:classic h7:END
+Ask|END
 Play|3:classic s7:END
 TakeTrick|END
 
 # Trick 7
+Ask|END
 Play|0:classic c8:END
+Ask|END
 Play|1:classic d8:END
+Ask|END
 Play|2:classic h8:END
+Ask|END
 Play|3:classic s8:END
 TakeTrick|END
 
 # Trick 8
+Ask|END
 Play|0:classic c9:END
+Ask|END
 Play|1:classic d9:END
+Ask|END
 Play|2:classic h9:END
+Ask|END
 Play|3:classic s9:END
 TakeTrick|END
 
 # Trick 9
+Ask|END
 Play|0:classic c1:END
+Ask|END
 Play|1:classic d1:END
+Ask|END
 Play|2:classic h1:END
+Ask|END
 Play|3:classic s1:END
 TakeTrick|END
 
 # Trick 10
+Ask|END
 Play|0:classic c10:END
+Ask|END
 Play|1:classic d10:END
+Ask|END
 Play|2:classic h10:END
+Ask|END
 Play|3:classic s10:END
 TakeTrick|END
 
 # Trick 11
+Ask|END
 Play|0:classic cj:END
+Ask|END
 Play|1:classic dj:END
+Ask|END
 Play|2:classic hj:END
+Ask|END
 Play|3:classic sj:END
 TakeTrick|END
 
 # Trick 12
+Ask|END
 Play|0:classic cq:END
+Ask|END
 Play|1:classic dq:END
+Ask|END
 Play|2:classic hq:END
+Ask|END
 Play|3:classic sq:END
 TakeTrick|END
 
 # Trick 13
+Ask|END
 Play|0:classic ck:END
+Ask|END
 Play|1:classic dk:END
+Ask|END
 Play|2:classic hk:END
+Ask|END
 Play|3:classic sk:END
 TakeTrick|END
 
@@ -480,93 +688,145 @@
 Take|3:END
 
 # Trick 1
+Ask|END
 Play|0:classic c2:END
+Ask|END
 Play|1:classic d2:END
+Ask|END
 Play|2:classic d4:END
+Ask|END
 Play|3:classic s2:END
 TakeTrick|END
 
 # Trick 2
+Ask|END
 Play|0:classic c3:END
+Ask|END
 Play|1:classic d3:END
+Ask|END
 Play|2:classic h3:END
+Ask|END
 Play|3:classic s3:END
 TakeTrick|END
 
 # Trick 3
+Ask|END
 Play|0:classic c4:END
+Ask|END
 Play|1:classic s4:END
+Ask|END
 Play|2:classic h2:END
+Ask|END
 Play|3:classic h4:END
 TakeTrick|END
 
 # Trick 4
+Ask|END
 Play|0:classic c5:END
+Ask|END
 Play|1:classic d5:END
+Ask|END
 Play|2:classic h5:END
+Ask|END
 Play|3:classic s5:END
 TakeTrick|END
 
 # Trick 5
+Ask|END
 Play|0:classic c6:END
+Ask|END
 Play|1:classic d6:END
+Ask|END
 Play|2:classic h6:END
+Ask|END
 Play|3:classic s6:END
 TakeTrick|END
 
 # Trick 6
+Ask|END
 Play|0:classic c7:END
+Ask|END
 Play|1:classic d7:END
+Ask|END
 Play|2:classic h7:END
+Ask|END
 Play|3:classic s7:END
 TakeTrick|END
 
 # Trick 7
+Ask|END
 Play|0:classic c8:END
+Ask|END
 Play|1:classic d8:END
+Ask|END
 Play|2:classic h8:END
+Ask|END
 Play|3:classic s8:END
 TakeTrick|END
 
 # Trick 8
+Ask|END
 Play|0:classic c9:END
+Ask|END
 Play|1:classic d9:END
+Ask|END
 Play|2:classic h9:END
+Ask|END
 Play|3:classic s9:END
 TakeTrick|END
 
 # Trick 9
+Ask|END
 Play|0:classic c1:END
+Ask|END
 Play|1:classic d1:END
+Ask|END
 Play|2:classic h1:END
+Ask|END
 Play|3:classic s1:END
 TakeTrick|END
 
 # Trick 10
+Ask|END
 Play|0:classic c10:END
+Ask|END
 Play|1:classic d10:END
+Ask|END
 Play|2:classic h10:END
+Ask|END
 Play|3:classic s10:END
 TakeTrick|END
 
 # Trick 11
+Ask|END
 Play|0:classic cj:END
+Ask|END
 Play|1:classic dj:END
+Ask|END
 Play|2:classic hj:END
+Ask|END
 Play|3:classic sj:END
 TakeTrick|END
 
 # Trick 12
+Ask|END
 Play|0:classic cq:END
+Ask|END
 Play|1:classic dq:END
+Ask|END
 Play|2:classic hq:END
+Ask|END
 Play|3:classic sq:END
 TakeTrick|END
 
 # Trick 13
+Ask|END
 Play|0:classic ck:END
+Ask|END
 Play|1:classic dk:END
+Ask|END
 Play|2:classic hk:END
+Ask|END
 Play|3:classic sk:END
 TakeTrick|END
 
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
index d5300e1..95ed019 100644
--- a/test/hearts_test.dart
+++ b/test/hearts_test.dart
@@ -304,11 +304,10 @@
     test("Play Phase - Trick 1", () {
       expect(game.phase, equals(HeartsPhase.Play));
 
-      // Play Trick 1 consists of 4 play commands + 1 take trick command.
-      runCommand();
-      runCommand();
-      runCommand();
-      runCommand();
+      // Play Trick 1 consists of 4 ask + 4 play + 1 take trick command.
+      for (int i = 0; i < 8; i++) {
+        runCommand();
+      }
 
       // Confirm that everyone has played before taking the trick
       expect(game.allPlayed, isTrue);
@@ -323,11 +322,10 @@
     test("Play Phase - Trick 2", () {
       expect(game.phase, equals(HeartsPhase.Play));
 
-      // Play Trick 2 consists of 4 play commands + 1 take trick command.
-      runCommand();
-      runCommand();
-      runCommand();
-      runCommand();
+      // Play Trick 2 consists of 4 ask + 4 play + 1 take trick command.
+      for (int i = 0; i < 8; i++) {
+        runCommand();
+      }
 
       // Confirm that everyone has played before taking the trick
       expect(game.allPlayed, isTrue);
@@ -344,9 +342,9 @@
     test("Play Phase - Trick 13", () {
       expect(game.phase, equals(HeartsPhase.Play));
 
-      // Play Trick 13 consists of 44 play commands  + 11 take trick command.
+      // Play Trick 13 consists of 44 ask + 44 play + 11 take trick command.
       // Read line by line until the game is "over".
-      for (int i = 10; i < 65; i++) {
+      for (int i = 18; i < 117; i++) {
         runCommand();
       }
 
@@ -409,24 +407,24 @@
     test("Score Phase - end of game", () {
       expect(game.hasGameEnded, isFalse);
 
-      // 2nd Round: 4 deal, 4 pass, 4 take, 52 play, 13 take trick, 4 ready
+      // 2nd Round: 4 deal, 4 pass, 4 take, 52 ask, 52 play, 13 take trick, 4 ready
       // Player A will shoot the moon for all remaining games (for simplicity).
-      for (int i = 0; i < 81; i++) {
+      for (int i = 0; i < 133; 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, 13 take trick, 4 ready
-      for (int i = 0; i < 81; i++) {
+      // 3rd Round: 4 deal, 4 pass, 4 take, 52 ask, 52 play, 13 take trick, 4 ready
+      for (int i = 0; i < 133; 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, 13 take trick, 4 ready
-      for (int i = 0; i < 73; i++) {
+      // 4th Round: 4 deal, 52 ask, 52 play, 13 take trick, 4 ready
+      for (int i = 0; i < 125; i++) {
         runCommand();
       }
       expect(
@@ -440,8 +438,9 @@
       expect(game.deltaScores, equals([0, 26, 26, 26]));
       expect(game.hasGameEnded, isFalse);
 
-      // 5th round: 4 deal, 4 pass, 4 take, 52 play, 13 take trick. Game is over, so no ready phase.
-      for (int i = 0; i < 77; i++) {
+      // 5th round: 4 deal, 4 pass, 4 take, 52 ask, 52 play, 13 take trick.
+      // Game is over, so no ready phase.
+      for (int i = 0; i < 129; i++) {
         runCommand();
       }
       expect(
@@ -541,6 +540,26 @@
         game.gamelog.add(new HeartsCommand.take(3));
       }, throwsA(new isInstanceOf<StateError>()));
     });
+    test("Asking - wrong phase", () {
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
+      game.phase = HeartsPhase.Deal;
+      game.gamelog.add(new HeartsCommand.deal(
+          0, new List<Card>.from(Card.All.getRange(0, 13))));
+      expect(() {
+        game.gamelog.add(new HeartsCommand.ask());
+      }, throwsA(new isInstanceOf<StateError>()));
+    });
+    test("Asking - already asking", () {
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
+      game.phase = HeartsPhase.Deal;
+      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.ask());
+      expect(() {
+        game.gamelog.add(new HeartsCommand.ask());
+      }, throwsA(new isInstanceOf<StateError>()));
+    });
     test("Playing - wrong phase", () {
       HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
@@ -550,12 +569,23 @@
         game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
       }, throwsA(new isInstanceOf<StateError>()));
     });
+    test("Playing - not asking", () {
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
+      game.phase = HeartsPhase.Deal;
+      game.gamelog.add(new HeartsCommand.deal(
+          0, new List<Card>.from(Card.All.getRange(0, 13))));
+      game.phase = HeartsPhase.Play;
+      expect(() {
+        game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+      }, throwsA(new isInstanceOf<StateError>()));
+    });
     test("Playing - missing card", () {
       HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
       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.ask());
       expect(() {
         game.gamelog.add(new HeartsCommand.play(0, Card.All[13]));
       }, throwsA(new isInstanceOf<StateError>()));
@@ -567,6 +597,7 @@
           0, new List<Card>.from(Card.All.getRange(0, 13))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       expect(() {
         game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
       }, throwsA(new isInstanceOf<StateError>()));
@@ -586,8 +617,11 @@
           3, new List<Card>.from(Card.All.getRange(39, 52))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+      game.gamelog.add(new HeartsCommand.ask());
       expect(() {
         game.gamelog.add(new HeartsCommand.play(2, Card.All[26]));
       }, throwsA(new isInstanceOf<StateError>()));
@@ -605,6 +639,7 @@
           3, new List<Card>.from(Card.All.getRange(39, 52))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       expect(() {
         game.gamelog.add(new HeartsCommand.play(
             1, Card.All[13])); // player 0's turn, not player 1's.
@@ -623,7 +658,9 @@
           3, new List<Card>.from(Card.All.getRange(39, 52))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+      game.gamelog.add(new HeartsCommand.ask());
       expect(() {
         game.gamelog
             .add(new HeartsCommand.play(0, Card.All[13])); // should play 12
@@ -642,17 +679,22 @@
           3, new List<Card>.from(Card.All.getRange(39, 52))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(2, Card.All[12])); // 2 won!
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(3, Card.All[39]));
       game.gamelog.add(new HeartsCommand.takeTrick());
+      game.gamelog.add(new HeartsCommand.ask());
       expect(() {
         game.gamelog.add(new HeartsCommand.play(
             2, Card.All[26])); // But 2 can't lead with a hearts.
       }, throwsA(new isInstanceOf<StateError>()));
     });
-    test("Playing - trick not taken yet", () {
+    test("Asking - trick not taken yet", () {
       HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
       game.gamelog.add(new HeartsCommand.deal(
@@ -671,15 +713,18 @@
           3, new List<Card>.from(Card.All.getRange(39, 52))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(2, Card.All[12])); // 2 won!
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(3, Card.All[39]));
 
-      // Do not take trick. Play a card instead.
+      // Do not take trick. Ask instead.
       expect(() {
-        game.gamelog.add(new HeartsCommand.play(
-            2, Card.All[11])); // But 2 can't play until trick is taken
+        game.gamelog.add(new HeartsCommand.ask());
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - take trick wrong phase", () {
@@ -702,9 +747,13 @@
           3, new List<Card>.from(Card.All.getRange(39, 52))));
       game.phase = HeartsPhase.Play;
       game.lastTrickTaker = 0;
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+      game.gamelog.add(new HeartsCommand.ask());
       game.gamelog.add(new HeartsCommand.play(2, Card.All[12]));
+      game.gamelog.add(new HeartsCommand.ask());
       expect(() {
         game.gamelog.add(new HeartsCommand.takeTrick()); // too soon
       }, throwsA(new isInstanceOf<StateError>()));