Merge "croupier: Add Player Split View"
diff --git a/images/avatars/Club.png b/images/avatars/Club.png
index d09abc2..d91ded6 100644
--- a/images/avatars/Club.png
+++ b/images/avatars/Club.png
Binary files differ
diff --git a/images/avatars/Diamond.png b/images/avatars/Diamond.png
index 83733ff..4c541c6 100644
--- a/images/avatars/Diamond.png
+++ b/images/avatars/Diamond.png
Binary files differ
diff --git a/images/avatars/Heart.png b/images/avatars/Heart.png
index 2212ebb..e98084a 100644
--- a/images/avatars/Heart.png
+++ b/images/avatars/Heart.png
Binary files differ
diff --git a/images/avatars/Spade.png b/images/avatars/Spade.png
index 13358da..2956888 100644
--- a/images/avatars/Spade.png
+++ b/images/avatars/Spade.png
Binary files differ
diff --git a/images/avatars/player1.jpeg b/images/avatars/player1.jpeg
index 34d57b0..e1c08d8 100644
--- a/images/avatars/player1.jpeg
+++ b/images/avatars/player1.jpeg
Binary files differ
diff --git a/images/avatars/player3.jpeg b/images/avatars/player3.jpeg
index 4d0ce50..bff779a 100644
--- a/images/avatars/player3.jpeg
+++ b/images/avatars/player3.jpeg
Binary files differ
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 1ee925c..6491981 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import 'dart:async';
-
 import 'package:flutter/material.dart';
 
 import '../logic/card.dart' as logic_card;
@@ -11,9 +9,10 @@
 import '../logic/croupier_settings.dart' show CroupierSettings;
 import '../logic/game/game.dart' show Game, GameType, NoArgCb;
 import '../logic/hearts/hearts.dart' show HeartsGame;
+import '../styles/common.dart' as style;
 import 'card.dart' as component_card;
 import 'card_collection.dart'
-    show CardCollectionComponent, CardCollectionOrientation;
+    show CardCollectionComponent, CardCollectionOrientation, DropType, AcceptCb;
 import 'croupier_profile.dart' show CroupierProfileComponent;
 
 const double defaultBoardHeight = 400.0;
@@ -49,10 +48,20 @@
 /// cards each player has, and the cards they are currently playing.
 class HeartsBoard extends Board {
   final Croupier croupier;
-  final NoArgCb trueSetState;
+  final bool isMini;
+  final AcceptCb gameAcceptCallback;
+  final bool trickTaking;
+  final List<List<logic_card.Card>> playedCards;
 
-  HeartsBoard(Croupier croupier, this.trueSetState,
-      {double height, double width, double cardHeight, double cardWidth})
+  HeartsBoard(Croupier croupier,
+      {double height,
+      double width,
+      double cardHeight,
+      double cardWidth,
+      this.isMini: false,
+      this.gameAcceptCallback,
+      this.trickTaking,
+      this.playedCards})
       : super(croupier.game,
             height: height,
             width: width,
@@ -66,97 +75,122 @@
 }
 
 class HeartsBoardState extends State<HeartsBoard> {
-  bool trickTaking = false;
-  List<List<logic_card.Card>> playedCards = new List<List<logic_card.Card>>(4);
-
-  static const int SHOW_TRICK_DURATION = 2000; // ms
-
-  @override
-  void initState() {
-    super.initState();
-
-    _fillPlayedCards();
-  }
-
-  // Make copies of the played cards.
-  void _fillPlayedCards() {
-    for (int i = 0; i < 4; i++) {
-      playedCards[i] = new List<logic_card.Card>.from(
-          config.game.cardCollections[i + HeartsGame.OFFSET_PLAY]);
-    }
-  }
-
-  // If there were 3 played cards before and now there are 0...
-  bool _detectTrick() {
-    HeartsGame game = config.game;
-    int lastNumPlayed = playedCards.where((List<logic_card.Card> list) {
-      return list.length > 0;
-    }).length;
-    return lastNumPlayed == 3 && game.numPlayed == 0;
-  }
-
-  // Make a copy of the missing played card.
-  void _fillMissingPlayedCard() {
-    HeartsGame game = config.game;
-    List<logic_card.Card> trickPile =
-        game.cardCollections[game.lastTrickTaker + HeartsGame.OFFSET_TRICK];
-
-    // Find the index of the missing play card.
-    int missing;
-    for (int j = 0; j < 4; j++) {
-      if (playedCards[j].length == 0) {
-        missing = j;
-        break;
-      }
-    }
-
-    // Use the trickPile to get this card.
-    playedCards[missing] = <logic_card.Card>[
-      trickPile[trickPile.length - 4 + missing]
-    ];
-  }
-
   Widget build(BuildContext context) {
-    if (!trickTaking) {
-      if (_detectTrick()) {
-        trickTaking = true;
-        _fillMissingPlayedCard();
-        // Unfortunately, ZCards are drawn on the game layer,
-        // so instead of setState, we must use trueSetState.
-        new Future.delayed(const Duration(milliseconds: SHOW_TRICK_DURATION),
-            () {
-          trickTaking = false;
-          config.trueSetState();
-        });
-      } else {
-        _fillPlayedCards();
-      }
-    }
-
     return new Container(
         height: config.height,
         width: config.width,
         child: new Stack([
-          new Positioned(top: 0.0, left: 0.0, child: _buildBoardLayout()),
           new Positioned(
-              top: config.height * 1.5,
+              top: 0.0,
+              left: 0.0,
+              child: config.isMini
+                  ? _buildMiniBoardLayout()
+                  : _buildBoardLayout()),
+          new Positioned(
+              top: config.height * 5.5,
               left: (config.width - config.cardWidth) / 2,
-              child: _buildTrick(0)), // bottom
+              child: _buildTrick(
+                  config.isMini ? rotateByGamePlayerNumber(0) : 0)), // bottom
           new Positioned(
               top: (config.height - config.cardHeight) / 2,
-              left: config.width * -0.5,
-              child: _buildTrick(1)), // left
+              left: config.width * -4.5,
+              child: _buildTrick(
+                  config.isMini ? rotateByGamePlayerNumber(1) : 1)), // left
           new Positioned(
-              top: config.height * -0.5,
+              top: config.height * -4.5,
               left: (config.width - config.cardWidth) / 2,
-              child: _buildTrick(2)), // top
+              child: _buildTrick(
+                  config.isMini ? rotateByGamePlayerNumber(2) : 2)), // top
           new Positioned(
               top: (config.height - config.cardHeight) / 2,
-              left: config.width * 1.5,
-              child: _buildTrick(3)) // right
+              left: config.width * 5.5,
+              child: _buildTrick(
+                  config.isMini ? rotateByGamePlayerNumber(3) : 3)) // right
         ]));
   }
 
+  int rotateByGamePlayerNumber(int i) {
+    return (i + config.game.playerNumber) % 4;
+  }
+
+  Widget _buildMiniBoardLayout() {
+    return new Container(
+        height: config.height,
+        width: config.width,
+        child: new Center(
+            child: new Row([
+          new Flexible(
+              flex: 1,
+              child: new Center(
+                  child: _buildAvatarSlotCombo(rotateByGamePlayerNumber(1)))),
+          new Flexible(
+              flex: 1,
+              child: new Column([
+                new Flexible(
+                    flex: 1,
+                    child: _buildAvatarSlotCombo(rotateByGamePlayerNumber(2))),
+                new Flexible(
+                    flex: 1,
+                    child: _buildAvatarSlotCombo(rotateByGamePlayerNumber(0)))
+              ])),
+          new Flexible(
+              flex: 1,
+              child: new Center(
+                  child: _buildAvatarSlotCombo(rotateByGamePlayerNumber(3))))
+        ])));
+  }
+
+  Widget _buildAvatarSlotCombo(int playerNumber) {
+    HeartsGame game = config.game as HeartsGame;
+    int p = game.playerNumber;
+
+    List<Widget> items = new List<Widget>();
+    bool isMe = playerNumber == p;
+    bool isPlayerTurn = playerNumber == game.whoseTurn && !config.trickTaking;
+
+    List<logic_card.Card> showCard =
+        game.cardCollections[playerNumber + HeartsGame.OFFSET_PLAY];
+
+    if (config.trickTaking) {
+      showCard = config.playedCards[playerNumber];
+    }
+
+    items.add(new Positioned(
+        top: 0.0,
+        left: 0.0,
+        child: new CardCollectionComponent(
+            showCard, true, CardCollectionOrientation.show1,
+            useKeys: true,
+            animationType: component_card.CardAnimationType.NONE,
+            acceptCallback: config.gameAcceptCallback,
+            acceptType: isMe && isPlayerTurn ? DropType.card : DropType.none,
+            widthCard: config.cardWidth - 6.0,
+            heightCard: config.cardHeight - 6.0,
+            backgroundColor:
+                isPlayerTurn ? style.theme.accentColor : Colors.grey[500],
+            altColor: isPlayerTurn ? Colors.grey[200] : Colors.grey[600])));
+
+    bool hasPlayed =
+        game.cardCollections[playerNumber + HeartsGame.OFFSET_PLAY].length > 0;
+    if (!hasPlayed) {
+      items.add(new Positioned(
+          top: 0.0,
+          left: 0.0,
+          child: new IgnorePointer(
+              child: new CroupierProfileComponent(
+                  settings:
+                      config.croupier.settingsFromPlayerNumber(playerNumber),
+                  height: config.cardHeight,
+                  width: config.cardWidth,
+                  isMini: true))));
+    }
+
+    return new Container(
+        width: config.cardWidth,
+        height: config.cardHeight,
+        child: new Stack(items));
+  }
+
   Widget _buildBoardLayout() {
     return new Container(
         height: config.height,
@@ -269,32 +303,35 @@
     HeartsGame game = config.game;
     List<logic_card.Card> cards =
         game.cardCollections[playerNumber + HeartsGame.OFFSET_PLAY];
-    if (trickTaking) {
-      cards = playedCards[playerNumber];
+    if (config.trickTaking) {
+      cards = config.playedCards[playerNumber];
     }
 
-    return new CardCollectionComponent(
-        cards, true, CardCollectionOrientation.show1,
-        widthCard: config.cardWidth * 1.25,
-        heightCard: config.cardHeight * 1.25,
-        backgroundColor:
-            game.whoseTurn == playerNumber ? Colors.blue[500] : null,
-        useKeys: true);
+    return new Container(
+        decoration: game.whoseTurn == playerNumber ? style.Box.liveNow : null,
+        child: new CardCollectionComponent(
+            cards, true, CardCollectionOrientation.show1,
+            widthCard: config.cardWidth * 2,
+            heightCard: config.cardHeight * 2,
+            useKeys: true));
   }
 
   Widget _buildTrick(int playerNumber) {
     HeartsGame game = config.game;
+
     List<logic_card.Card> cards =
         game.cardCollections[playerNumber + HeartsGame.OFFSET_TRICK];
     // If took trick, exclude the last 4 cards for the trick taking animation.
-    if (trickTaking && playerNumber == game.lastTrickTaker) {
+    if (config.trickTaking && playerNumber == game.lastTrickTaker) {
       cards = new List.from(cards.sublist(0, cards.length - 4));
     }
 
+    double sizeFactor = config.isMini ? 1.0 : 2.0;
+
     return new CardCollectionComponent(
         cards, true, CardCollectionOrientation.show1,
-        widthCard: config.cardWidth,
-        heightCard: config.cardHeight,
+        widthCard: config.cardWidth * sizeFactor,
+        heightCard: config.cardHeight * sizeFactor,
         useKeys: true,
         animationType: component_card.CardAnimationType.LONG);
   }
diff --git a/lib/components/card.dart b/lib/components/card.dart
index c222020..68f683a 100644
--- a/lib/components/card.dart
+++ b/lib/components/card.dart
@@ -182,7 +182,7 @@
       case CardAnimationType.NORMAL:
         return const Duration(milliseconds: 250);
       case CardAnimationType.LONG:
-        return const Duration(milliseconds: 750);
+        return const Duration(milliseconds: 1500);
       default:
         print("Unexpected animation type: ${config.animationType}");
         assert(false);
@@ -216,6 +216,7 @@
         ..value = endingLocation
         ..end = endingLocation;
       _performance.progress = 0.0;
+      _animationIndex = _pointQueue.length - 1;
       return;
     }
 
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 8c151fb..71c59e1 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -17,6 +17,8 @@
 
 typedef void NoArgCb();
 
+GlobalObjectKey _gameKey = new GlobalObjectKey("CroupierGameKey");
+
 class CroupierComponent extends StatefulComponent {
   final logic_croupier.Croupier croupier;
 
@@ -117,7 +119,8 @@
               makeSetStateCallback(logic_croupier.CroupierState.Welcome)();
             },
                 width: ui.window.size.width,
-                height: ui.window.size.height - ui.window.padding.top));
+                height: ui.window.size.height - ui.window.padding.top,
+                key: _gameKey));
       default:
         assert(false);
         return null;
diff --git a/lib/components/croupier_profile.dart b/lib/components/croupier_profile.dart
index 3e2a369..ffd0869 100644
--- a/lib/components/croupier_profile.dart
+++ b/lib/components/croupier_profile.dart
@@ -5,7 +5,6 @@
 import 'package:flutter/material.dart';
 
 import '../logic/croupier_settings.dart' show CroupierSettings;
-import '../styles/common.dart' as style;
 
 class CroupierProfileComponent extends StatelessComponent {
   final CroupierSettings settings;
@@ -21,32 +20,29 @@
 
   Widget build(BuildContext context) {
     if (!isMini) {
-      return new Card(
-          color: new Color(settings.color),
-          child: new Container(
-              height: this.height,
-              width: this.width,
-              padding: const EdgeDims.all(padAmount),
+      return new Container(
+          height: this.height,
+          width: this.width,
+          padding: const EdgeDims.all(padAmount),
+          child: new Card(
+              color: new Color(settings.color),
               child: new Column([
                 new AssetImage(
                     name: CroupierSettings.makeAvatarUrl(settings.avatar)),
-                new Text(settings.name, style: style.Text.liveNow)
+                new Text(settings.name)
               ], justifyContent: FlexJustifyContent.spaceAround)));
     } else {
-      return new Card(
-          color: new Color(settings.color),
-          child: new Container(
-              width: this.width,
-              height: this.height,
-              padding: const EdgeDims.all(padAmount),
+      return new Container(
+          width: this.width,
+          height: this.height,
+          padding: const EdgeDims.all(padAmount),
+          child: new Card(
+              color: new Color(settings.color),
               child: new Row([
                 new AssetImage(
                     name: CroupierSettings.makeAvatarUrl(settings.avatar),
-                    width: this.width != null ? this.width - padAmount : null,
-                    height:
-                        this.height != null ? this.height - padAmount : null,
                     fit: ImageFit.scaleDown)
-              ], justifyContent: FlexJustifyContent.collapse)));
+              ], justifyContent: FlexJustifyContent.spaceAround)));
     }
   }
 }
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 6049c6c..b9f844e 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -5,6 +5,7 @@
 library game_component;
 
 import 'dart:math' as math;
+import 'dart:async';
 
 import 'package:flutter/animation.dart';
 import 'package:flutter/material.dart';
@@ -35,7 +36,9 @@
   final double width;
   final double height;
 
-  GameComponent(this.croupier, this.gameEndCallback, {this.width, this.height});
+  GameComponent(this.croupier, this.gameEndCallback,
+      {Key key, this.width, this.height})
+      : super(key: key);
 }
 
 abstract class GameComponentState<T extends GameComponent> extends State<T> {
@@ -167,6 +170,7 @@
       // Don't show a card if it isn't part of a visible collection.
       if (!visibleCardCollectionIndexes.contains(config.game.findCard(c))) {
         cardLevelMap.remove(c); // It is an old card, which we can clean up.
+        assert(!cardLevelMap.containsKey(c));
         return;
       }
 
@@ -196,17 +200,17 @@
 }
 
 GameComponent createGameComponent(Croupier croupier, NoArgCb gameEndCallback,
-    {double width, double height}) {
+    {Key key, double width, double height}) {
   switch (croupier.game.gameType) {
     case GameType.Proto:
       return new ProtoGameComponent(croupier, gameEndCallback,
-          width: width, height: height);
+          key: key, width: width, height: height);
     case GameType.Hearts:
       return new HeartsGameComponent(croupier, gameEndCallback,
-          width: width, height: height);
+          key: key, width: width, height: height);
     case GameType.Solitaire:
       return new SolitaireGameComponent(croupier, gameEndCallback,
-          width: width, height: height);
+          key: key, width: width, height: height);
     default:
       // We're probably not ready to serve the other games yet.
       assert(false);
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 20ed05f..8cd0689 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -6,8 +6,8 @@
 
 class HeartsGameComponent extends GameComponent {
   HeartsGameComponent(Croupier croupier, NoArgCb cb,
-      {double width, double height})
-      : super(croupier, cb, width: width, height: height);
+      {Key key, double width, double height})
+      : super(croupier, cb, key: key, width: width, height: height);
 
   HeartsGameComponentState createState() => new HeartsGameComponentState();
 }
@@ -18,11 +18,62 @@
   List<logic_card.Card> passingCards3 = new List<logic_card.Card>();
 
   HeartsType _lastViewType;
+  bool _showSplitView = false;
+  bool trickTaking = false;
+  List<List<logic_card.Card>> playedCards = new List<List<logic_card.Card>>(4);
+
+  static const int SHOW_TRICK_DURATION = 2000; // ms
 
   @override
   void initState() {
     super.initState();
+
+    // 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)) {
+      _showSplitView = true;
+    }
     _reset();
+
+    _fillPlayedCards();
+  }
+
+  // Make copies of the played cards.
+  void _fillPlayedCards() {
+    for (int i = 0; i < 4; i++) {
+      playedCards[i] = new List<logic_card.Card>.from(
+          config.game.cardCollections[i + HeartsGame.OFFSET_PLAY]);
+    }
+  }
+
+  // If there were 3 played cards before and now there are 0...
+  bool _detectTrick() {
+    HeartsGame game = config.game;
+    int lastNumPlayed = playedCards.where((List<logic_card.Card> list) {
+      return list.length > 0;
+    }).length;
+    return lastNumPlayed == 3 && game.numPlayed == 0;
+  }
+
+  // Make a copy of the missing played card.
+  void _fillMissingPlayedCard() {
+    HeartsGame game = config.game;
+    List<logic_card.Card> trickPile =
+        game.cardCollections[game.lastTrickTaker + HeartsGame.OFFSET_TRICK];
+
+    // Find the index of the missing play card.
+    int missing;
+    for (int j = 0; j < 4; j++) {
+      if (playedCards[j].length == 0) {
+        missing = j;
+        break;
+      }
+    }
+
+    // Use the trickPile to get this card.
+    playedCards[missing] = <logic_card.Card>[
+      trickPile[trickPile.length - 4 + missing]
+    ];
   }
 
   @override
@@ -72,10 +123,17 @@
                 .add(HeartsGame.OFFSET_HAND + playerNum);
             break;
           case HeartsPhase.Play:
-            visibleCardCollectionIndexes
-                .add(HeartsGame.OFFSET_HAND + playerNum);
-            visibleCardCollectionIndexes
-                .add(HeartsGame.OFFSET_PLAY + playerNum);
+            for (int i = 0; i < 4; i++) {
+              if (_showSplitView || i == playerNum) {
+                visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PLAY + i);
+              }
+              if (_showSplitView) {
+                visibleCardCollectionIndexes
+                    .add(HeartsGame.OFFSET_HAND + playerNum);
+                visibleCardCollectionIndexes.add(HeartsGame.OFFSET_TRICK + i);
+              }
+            }
+
             break;
           default:
             break;
@@ -173,6 +231,7 @@
       try {
         HeartsGame game = config.game as HeartsGame;
         game.passCards(_combinePassing());
+        game.debugString = null;
       } catch (e) {
         print("You can't do that! ${e.toString()}");
         config.game.debugString = e.toString();
@@ -190,6 +249,7 @@
         _clearPassing();
         HeartsGame game = config.game as HeartsGame;
         game.takeCards();
+        game.debugString = null;
       } catch (e) {
         print("You can't do that! ${e.toString()}");
         config.game.debugString = e.toString();
@@ -203,6 +263,7 @@
       String reason = game.canPlay(game.playerNumber, card);
       if (reason == null) {
         game.move(card, dest);
+        game.debugString = null;
       } else {
         print("You can't do that! ${reason}");
         game.debugString = reason;
@@ -214,6 +275,7 @@
     setState(() {
       HeartsGame game = config.game as HeartsGame;
       game.jumpToScorePhaseDebug();
+      game.debugString = null;
     });
   }
 
@@ -300,63 +362,132 @@
   }
 
   Widget showBoard() {
-    return new HeartsBoard(config.croupier, this.update,
-        width: config.width, height: 0.80 * config.height);
+    return new HeartsBoard(config.croupier,
+        width: config.width,
+        height: 0.80 * config.height,
+        trickTaking: trickTaking,
+        playedCards: playedCards);
+  }
+
+  String _getName(int playerNumber) {
+    return config.croupier.settingsFromPlayerNumber(playerNumber)?.name;
+  }
+
+  String _getStatus() {
+    HeartsGame game = config.game;
+
+    // Who's turn is it?
+    String name = _getName(game.whoseTurn) ?? "Player ${game.whoseTurn}";
+    String status =
+        game.whoseTurn == game.playerNumber ? "Your turn" : "${name}'s turn";
+
+    // Override if someone is taking a trick.
+    if (this.trickTaking) {
+      String trickTaker =
+          _getName(game.lastTrickTaker) ?? "Player ${game.lastTrickTaker}";
+      status = "${trickTaker}'s trick";
+    }
+
+    // Override if there is a debug string.
+    if (config.game.debugString != null) {
+      status = config.game.debugString;
+    }
+
+    return status;
+  }
+
+  Widget _buildStatusBar() {
+    return new Container(
+        padding: new EdgeDims.all(10.0),
+        decoration:
+            new BoxDecoration(backgroundColor: style.theme.primaryColor),
+        child: new Row([
+          new Text(_getStatus(), style: style.Text.largeStyle),
+          new IconButton(icon: "action/swap_vert", onPressed: () {
+            setState(() {
+              _showSplitView = !_showSplitView;
+            });
+          })
+        ], justifyContent: FlexJustifyContent.spaceBetween));
+  }
+
+  Widget _buildFullMiniBoard() {
+    return new Container(
+        width: config.width * 0.5,
+        height: config.height * 0.25,
+        child: new HeartsBoard(config.croupier,
+            width: config.width * 0.5,
+            height: config.height * 0.25,
+            cardWidth: config.height * 0.1,
+            cardHeight: config.height * 0.1,
+            isMini: true,
+            gameAcceptCallback: _makeGameMoveCallback,
+            trickTaking: trickTaking,
+            playedCards: playedCards));
   }
 
   Widget showPlay() {
     HeartsGame game = config.game as HeartsGame;
+    int p = game.playerNumber;
 
     List<Widget> cardCollections = new List<Widget>();
 
-    // Note that this shouldn't normally be shown.
-    // Since this is a duplicate card collection, it will not have keyed cards.
-    List<Widget> plays = new List<Widget>();
-    for (int i = 0; i < 4; i++) {
-      plays.add(new CardCollectionComponent(
-          game.cardCollections[i + HeartsGame.OFFSET_PLAY],
-          true,
-          CardCollectionOrientation.show1,
-          width: config.width));
+    if (_showSplitView) {
+      if (!trickTaking) {
+        if (_detectTrick()) {
+          trickTaking = true;
+          _fillMissingPlayedCard();
+          // Unfortunately, ZCards are drawn on the game layer,
+          // so instead of setState, we must use trueSetState.
+          new Future.delayed(const Duration(milliseconds: SHOW_TRICK_DURATION),
+              () {
+            setState(() {
+              trickTaking = false;
+            });
+          });
+        } else {
+          _fillPlayedCards();
+        }
+      }
+      cardCollections.add(new Container(
+          decoration:
+              new BoxDecoration(backgroundColor: style.theme.primaryColor),
+          child: new Column([_buildFullMiniBoard(), _buildStatusBar()])));
+    } else {
+      Widget playArea = new Container(
+          decoration: new BoxDecoration(backgroundColor: Colors.teal[500]),
+          width: config.width,
+          child: new Center(
+              child: new CardCollectionComponent(
+                  game.cardCollections[p + HeartsGame.OFFSET_PLAY],
+                  true,
+                  CardCollectionOrientation.show1,
+                  useKeys: true,
+                  animationType: component_card.CardAnimationType.NONE,
+                  acceptCallback: _makeGameMoveCallback,
+                  acceptType:
+                      p == game.whoseTurn ? DropType.card : DropType.none,
+                  backgroundColor:
+                      p == game.whoseTurn ? Colors.white : Colors.grey[500],
+                  altColor: p == game.whoseTurn
+                      ? Colors.grey[200]
+                      : Colors.grey[600])));
+
+      cardCollections.add(new Container(
+          decoration:
+              new BoxDecoration(backgroundColor: style.theme.primaryColor),
+          child: new Column([_buildStatusBar(), playArea])));
     }
-    cardCollections.add(new Container(
-        decoration: new BoxDecoration(backgroundColor: Colors.teal[600]),
-        width: config.width,
-        child:
-            new Flex(plays, justifyContent: FlexJustifyContent.spaceAround)));
-
-    int p = game.playerNumber;
-
-    Widget playArea = new Container(
-        decoration: new BoxDecoration(backgroundColor: Colors.teal[500]),
-        width: config.width,
-        child: new Center(
-            child: new CardCollectionComponent(
-                game.cardCollections[p + HeartsGame.OFFSET_PLAY],
-                true,
-                CardCollectionOrientation.show1,
-                useKeys: true,
-                acceptCallback: _makeGameMoveCallback,
-                acceptType: p == game.whoseTurn ? DropType.card : DropType.none,
-                width: config.width,
-                backgroundColor:
-                    p == game.whoseTurn ? Colors.white : Colors.grey[500],
-                altColor: p == game.whoseTurn
-                    ? Colors.grey[200]
-                    : Colors.grey[600])));
-    cardCollections.add(playArea);
 
     List<logic_card.Card> cards = game.cardCollections[p];
     CardCollectionComponent c = new CardCollectionComponent(
         cards, game.playerNumber == p, CardCollectionOrientation.suit,
-        dragChildren: game.whoseTurn == p,
+        dragChildren: true, // Can drag, but may not have anywhere to drop
         comparator: _compareCards,
         width: config.width,
-        useKeys: true);
+        useKeys: _showSplitView);
     cardCollections.add(c); // flex
 
-    cardCollections.add(new Text("Player ${game.whoseTurn}'s turn"));
-    cardCollections.add(new Text(game.debugString));
     cardCollections.add(_makeDebugButtons());
 
     return new Column(cardCollections,
@@ -372,7 +503,7 @@
     } else if (!game.isPlayer || game.ready[game.playerNumber]) {
       w = new Text("Waiting for other players...");
     } else {
-      w = _makeButton('Ready?', game.setReadyUI);
+      w = _makeButton('New Round', game.setReadyUI);
     }
 
     bool isTall = MediaQuery.of(context).orientation == Orientation.portrait;
@@ -456,8 +587,6 @@
       List<logic_card.Card> hand,
       AcceptCb cb,
       NoArgCb buttoncb) {
-    HeartsGame game = config.game as HeartsGame;
-
     bool draggable = (cb != null);
     bool completed = (buttoncb == null);
 
@@ -487,12 +616,8 @@
         altColor: Colors.grey[700],
         useKeys: true);
 
-    return new Column(<Widget>[
-      topArea,
-      handArea,
-      new Text(game.debugString),
-      _makeDebugButtons()
-    ], justifyContent: FlexJustifyContent.spaceBetween);
+    return new Column(<Widget>[topArea, handArea, _makeDebugButtons()],
+        justifyContent: FlexJustifyContent.spaceBetween);
   }
 
   Widget _topCardWidget(List<logic_card.Card> cards, AcceptCb cb) {
diff --git a/lib/components/proto/proto.part.dart b/lib/components/proto/proto.part.dart
index a09f6f8..79e1e7f 100644
--- a/lib/components/proto/proto.part.dart
+++ b/lib/components/proto/proto.part.dart
@@ -6,8 +6,8 @@
 
 class ProtoGameComponent extends GameComponent {
   ProtoGameComponent(Croupier croupier, NoArgCb cb,
-      {double width, double height})
-      : super(croupier, cb, width: width, height: height);
+      {Key key, double width, double height})
+      : super(croupier, cb, key: key, width: width, height: height);
 
   ProtoGameComponentState createState() => new ProtoGameComponentState();
 }
diff --git a/lib/components/solitaire/solitaire.part.dart b/lib/components/solitaire/solitaire.part.dart
index 95c8486..a9833db 100644
--- a/lib/components/solitaire/solitaire.part.dart
+++ b/lib/components/solitaire/solitaire.part.dart
@@ -6,8 +6,8 @@
 
 class SolitaireGameComponent extends GameComponent {
   SolitaireGameComponent(Croupier croupier, NoArgCb cb,
-      {double width, double height})
-      : super(croupier, cb, width: width, height: height);
+      {Key key, double width, double height})
+      : super(croupier, cb, key: key, width: width, height: height);
 
   SolitaireGameComponentState createState() =>
       new SolitaireGameComponentState();
diff --git a/lib/logic/game/game_def.part.dart b/lib/logic/game/game_def.part.dart
index 5d53ab3..c7544e5 100644
--- a/lib/logic/game/game_def.part.dart
+++ b/lib/logic/game/game_def.part.dart
@@ -107,7 +107,7 @@
   }
 
   bool debugMode = false;
-  String debugString = 'hello?';
+  String debugString;
 
   NoArgCb updateCallback; // Used to inform components of when a change has occurred. This is especially important when something non-UI related changes what should be drawn.
 
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index 477de00..a635060 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -415,13 +415,10 @@
       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.";
+      return "You must play the 2 of Clubs";
     }
     if (this.numPlayed == 0 && isHeartsCard(c) && !heartsBroken) {
-      return "Cannot lead with a heart when the suit has not been broken yet.";
+      return "Hearts have not been broken";
     }
     if (this.leadingCard != null) {
       String leadingSuit = getCardSuit(this.leadingCard);
@@ -429,9 +426,12 @@
       if (this.numPlayed >= 1 &&
           leadingSuit != otherSuit &&
           hasSuit(player, leadingSuit)) {
-        return "Must follow with a ${leadingSuit}.";
+        return "You must follow suit";
       }
     }
+    if (trickNumber == 0 && isPenaltyCard(c)) {
+      return "No penalty cards on 1st trick";
+    }
     return null;
   }
 
diff --git a/lib/styles/common.dart b/lib/styles/common.dart
index b5473c1..73e731b 100644
--- a/lib/styles/common.dart
+++ b/lib/styles/common.dart
@@ -40,8 +40,10 @@
 class Box {
   static final BoxDecoration liveNow = new BoxDecoration(
       border: new Border.all(color: theme.accentColor), borderRadius: 2.0);
-  static final BoxDecoration border =
-      new BoxDecoration(border: new Border.all(), borderRadius: 2.0);
+  static final BoxDecoration border = new BoxDecoration(
+      border: new Border.all(color: theme.primaryColor), borderRadius: 2.0);
+  static final BoxDecoration borderInactive = new BoxDecoration(
+      border: new Border.all(color: Colors.grey[300]), borderRadius: 2.0);
 }
 
 ThemeData theme = new ThemeData(
diff --git a/manifest.yaml b/manifest.yaml
index bfa1022..07fe25a 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -4,6 +4,7 @@
   - name: action/help
   - name: action/settings
   - name: av/play_arrow
+  - name: action/swap_vert
   - name: navigation/arrow_back
   - name: navigation/menu
 assets: