croupier: Improved Board View

- improved board view by using Row and Column instead of
  Stack + Positioned
- Added Transitions to the Board view for Pass/Take/Trick
- Added an advanced transition for trick-taking powered by UI logic
  to draw cards in "non-standard" places during the animation.
- CroupierProfileComponent now has a placeholder form and looks
  like a Card
- The Board now fits on most screens, even in landscape mode.
  CroupierProfileComponents will become mini if the space is too small.

The Board View is pretty much done with this, so I'll move on to the
Score View next.

Change-Id: I492968667cb53c2ad21c41193ecc4b52e21e3e59
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 174a157..27e4520 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -2,16 +2,20 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-import 'card_collection.dart'
-    show CardCollectionComponent, CardCollectionOrientation;
-import '../logic/card.dart' as logic_card;
-import '../logic/game/game.dart' show Game, GameType;
-import '../logic/hearts/hearts.dart' show HeartsGame;
-
-import 'dart:math' as math;
+import 'dart:async';
 
 import 'package:flutter/material.dart';
 
+import '../logic/card.dart' as logic_card;
+import '../logic/croupier.dart' show Croupier;
+import '../logic/croupier_settings.dart' show CroupierSettings;
+import '../logic/game/game.dart' show Game, GameType, NoArgCb;
+import '../logic/hearts/hearts.dart' show HeartsGame;
+import 'card.dart' as component_card;
+import 'card_collection.dart'
+    show CardCollectionComponent, CardCollectionOrientation;
+import 'croupier_profile.dart' show CroupierProfileComponent;
+
 const double defaultBoardHeight = 400.0;
 const double defaultBoardWidth = 400.0;
 const double defaultCardHeight = 40.0;
@@ -21,7 +25,7 @@
 /// While other Widgets may be drawn to accomodate space, a Board is meant to
 /// consume a specific amount of space on the screen, which allows for more
 /// control when positioning elements within the Board's area.
-abstract class Board extends StatelessComponent {
+abstract class Board extends StatefulComponent {
   final Game game;
   final double _height;
   final double _width;
@@ -44,158 +48,257 @@
 /// The HeartsBoard represents the Hearts table view, which shows the number of
 /// cards each player has, and the cards they are currently playing.
 class HeartsBoard extends Board {
-  HeartsBoard(HeartsGame game,
+  final Croupier croupier;
+  final NoArgCb trueSetState;
+
+  HeartsBoard(Croupier croupier, this.trueSetState,
       {double height, double width, double cardHeight, double cardWidth})
-      : super(game,
+      : super(croupier.game,
             height: height,
             width: width,
             cardHeight: cardHeight,
-            cardWidth: cardWidth);
+            cardWidth: cardWidth),
+        croupier = croupier {
+    assert(this.game is HeartsGame);
+  }
+
+  HeartsBoardState createState() => new HeartsBoardState();
+}
+
+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 = 750; // 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) {
-    List<Widget> pile = new List<Widget>();
-
-    _addHandsToPile(pile);
-    _addProfilesToPile(pile);
-    _addPlaysToPile(pile);
+    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: this.height, width: this.width, child: new Stack(pile));
+        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,
+              left: (config.width - config.cardWidth) / 2,
+              child: _buildTrick(0)), // bottom
+          new Positioned(
+              top: (config.height - config.cardHeight) / 2,
+              left: config.width * -0.5,
+              child: _buildTrick(1)), // left
+          new Positioned(
+              top: config.height * -0.5,
+              left: (config.width - config.cardWidth) / 2,
+              child: _buildTrick(2)), // top
+          new Positioned(
+              top: (config.height - config.cardHeight) / 2,
+              left: config.width * 1.5,
+              child: _buildTrick(3)) // right
+        ]));
   }
 
-  // Show the hands of each player (facedown) around the perimeter of the board.
-  void _addHandsToPile(List<Widget> pile) {
-    HeartsGame game = this.game;
+  Widget _buildBoardLayout() {
+    return new Container(
+        height: config.height,
+        width: config.width,
+        child: new Column([
+          new Flexible(child: _buildPlayer(2), flex: 5),
+          new Flexible(
+              child: new Row([
+                new Flexible(child: _buildPlayer(1), flex: 3),
+                new Flexible(child: _buildCenterCards(), flex: 4),
+                new Flexible(child: _buildPlayer(3), flex: 3)
+              ],
+                  alignItems: FlexAlignItems.center,
+                  justifyContent: FlexJustifyContent.spaceAround),
+              flex: 9),
+          new Flexible(child: _buildPlayer(0), flex: 5)
+        ],
+            alignItems: FlexAlignItems.center,
+            justifyContent: FlexJustifyContent.spaceAround));
+  }
 
-    for (int i = 0; i < 4; i++) {
-      List<logic_card.Card> cards =
-          game.cardCollections[i + HeartsGame.OFFSET_HAND];
-      CardCollectionOrientation ori = i % 2 == 0
-          ? CardCollectionOrientation.horz
-          : CardCollectionOrientation.vert;
+  Widget _buildPlayer(int playerNumber) {
+    bool wide = (config.width >= config.height);
 
-      bool wide = (this.width >= this.height);
-      double smallerSide = wide ? this.height : this.width;
-      double sizeRatio = 0.60;
-      double cccSize = sizeRatio * smallerSide;
+    List<Widget> widgets = [
+      _getProfile(playerNumber, wide),
+      _getHand(playerNumber),
+      _getPass(playerNumber)
+    ];
 
-      CardCollectionComponent ccc = new CardCollectionComponent(
-          cards, false, ori,
-          width: i % 2 == 0 ? cccSize : null,
-          height: i % 2 != 0 ? cccSize : null,
-          rotation: -math.PI / 2 * i,
-          useKeys: true);
-      Widget w;
-      switch (i) {
-        case 2:
-          w = new Positioned(
-              top: 0.0, left: (this.width - cccSize) / 2.0, child: ccc);
-          break;
-        case 3:
-          w = new Positioned(
-              top: (this.height - cccSize) / 2.0, left: 0.0, child: ccc);
-          break;
-        case 0:
-          w = new Positioned(
-              // TODO(alexfandrianto): 1.7 is a magic number, but it just looks right somehow.
-              // This could be due to the margins from each card collection.
-              top: this.height - 1.7 * this.cardHeight,
-              left: (this.width - cccSize) / 2.0,
-              child: ccc);
-          break;
-        case 1:
-          w = new Positioned(
-              top: (this.height - cccSize) / 2.0,
-              left: this.width - 1.7 * this.cardWidth,
-              child: ccc);
-          break;
-        default:
-          assert(false);
-      }
-      pile.add(w);
+    if (playerNumber % 2 == 0) {
+      return new Row(widgets,
+          alignItems: FlexAlignItems.center,
+          justifyContent: FlexJustifyContent.center);
+    } else {
+      return new Column(widgets,
+          alignItems: FlexAlignItems.center,
+          justifyContent: FlexJustifyContent.center);
     }
   }
 
-  // Create and add Player Profile widgets to the board.
-  void _addProfilesToPile(List<Widget> pile) {
-    // TODO(alexfandrianto): Show player profiles.
-    // I need to access each player's CroupierSettings here.
+  Widget _getProfile(int playerNumber, bool isWide) {
+    int userID = config.croupier.userIDFromPlayerNumber(playerNumber);
+
+    bool isMini = isWide && config.cardHeight * 2 > config.height * 0.25;
+
+    CroupierSettings cs; // If cs is null, a placeholder is used instead.
+    if (userID != null) {
+      cs = config.croupier.settings_everyone[userID];
+    }
+    return new CroupierProfileComponent(
+        settings: cs, height: config.height * 0.15, isMini: isMini);
   }
 
-  // Add 4 play slots. If the board is wider than it is tall, we need to have
-  // A flat diamond (where the center 2 cards are stacked on top of each other).
-  // If the board is taller than it is wide, then we want a tall diamond. The
-  // center 2 cards should be horizontally adjacent.
-  // TODO(alexfandrianto): Once I get the player profile settings, I can set
-  // the background color of each play slot.
-  void _addPlaysToPile(List<Widget> pile) {
-    HeartsGame game = this.game;
+  Widget _getHand(int playerNumber) {
+    double sizeRatio = 0.30;
+    double cccSize = sizeRatio * config.width;
 
-    for (int i = 0; i < 4; i++) {
-      List<logic_card.Card> cards =
-          game.cardCollections[i + HeartsGame.OFFSET_PLAY];
+    return new CardCollectionComponent(
+        config.game.cardCollections[playerNumber + HeartsGame.OFFSET_HAND],
+        false,
+        CardCollectionOrientation.horz,
+        width: cccSize,
+        widthCard: config.cardWidth,
+        heightCard: config.cardHeight,
+        useKeys: true);
+  }
 
-      double MARGIN = 10.0;
-      CardCollectionComponent ccc = new CardCollectionComponent(
-          cards, true, CardCollectionOrientation.show1,
-          width: this.cardWidth,
-          widthCard: this.cardWidth,
-          height: this.cardHeight,
-          heightCard: this.cardHeight,
-          rotation: -math.PI / 2 * i,
-          useKeys: true);
-      Widget w;
+  Widget _getPass(int playerNumber) {
+    double sizeRatio = 0.10;
+    double cccSize = sizeRatio * config.width;
 
-      double left02 = (this.width - this.cardWidth) / 2;
-      double top13 = (this.height - this.cardHeight) / 2.0;
+    HeartsGame game = config.game;
+    return new CardCollectionComponent(
+        game.cardCollections[
+            game.getTakeTarget(playerNumber) + HeartsGame.OFFSET_PASS],
+        false,
+        CardCollectionOrientation.horz,
+        backgroundColor: Colors.grey[300],
+        width: cccSize,
+        widthCard: config.cardWidth / 2,
+        heightCard: config.cardHeight / 2,
+        useKeys: true);
+  }
 
-      double baseTop = (this.height - (this.cardHeight * 2 + MARGIN)) / 2;
-      double baseLeft = (this.width - (this.cardWidth * 2 + MARGIN)) / 2;
-      double dHeight = (this.cardHeight + MARGIN) / 2;
-      double dWidth = (this.cardWidth + MARGIN) / 2;
+  Widget _buildCenterCards() {
+    bool wide = (config.width >= config.height);
 
-      if (this.width >= this.height) {
-        switch (i) {
-          case 2:
-            w = new Positioned(top: baseTop, left: left02, child: ccc);
-            break;
-          case 3:
-            w = new Positioned(top: top13, left: baseLeft - dWidth, child: ccc);
-            break;
-          case 0:
-            w = new Positioned(
-                top: baseTop + dHeight * 2, left: left02, child: ccc);
-            break;
-          case 1:
-            w = new Positioned(
-                top: top13, left: baseLeft + dWidth * 3, child: ccc);
-            break;
-          default:
-            assert(false);
-        }
-      } else {
-        switch (i) {
-          case 2:
-            w = new Positioned(
-                top: baseTop - dHeight, left: left02, child: ccc);
-            break;
-          case 3:
-            w = new Positioned(top: top13, left: baseLeft, child: ccc);
-            break;
-          case 0:
-            w = new Positioned(
-                top: baseTop + dHeight * 3, left: left02, child: ccc);
-            break;
-          case 1:
-            w = new Positioned(
-                top: top13, left: baseLeft + dHeight * 2, child: ccc);
-            break;
-          default:
-            assert(false);
-        }
-      }
-
-      pile.add(w);
+    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);
     }
   }
+
+  Widget _buildCenterCard(int playerNumber) {
+    HeartsGame game = config.game;
+    List<logic_card.Card> cards =
+        game.cardCollections[playerNumber + HeartsGame.OFFSET_PLAY];
+    if (trickTaking) {
+      cards = 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);
+  }
+
+  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) {
+      cards = new List.from(cards.sublist(0, cards.length - 4));
+    }
+
+    return new CardCollectionComponent(
+        cards, true, CardCollectionOrientation.show1,
+        widthCard: config.cardWidth,
+        heightCard: config.cardHeight,
+        useKeys: true,
+        animationType: component_card.CardAnimationType.LONG);
+  }
 }
diff --git a/lib/components/card.dart b/lib/components/card.dart
index b2f5e71..52a85de 100644
--- a/lib/components/card.dart
+++ b/lib/components/card.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 '../logic/card.dart' as logic_card;
-
 import 'dart:async';
 
 import 'package:flutter/animation.dart';
@@ -11,7 +9,9 @@
 import 'package:flutter/rendering.dart';
 import 'package:vector_math/vector_math_64.dart' as vector_math;
 
-enum CardAnimationType { NONE, OLD_TO_NEW, IN_TOP }
+import '../logic/card.dart' as logic_card;
+
+enum CardAnimationType { NONE, NORMAL, LONG }
 
 enum CardUIType { CARD, ZCARD }
 
@@ -40,7 +40,7 @@
   final double width;
   final double height;
   final double rotation;
-  final bool animateEntrance;
+  final CardAnimationType animationType;
   final double z;
 
   // These points are in local coordinates.
@@ -54,7 +54,7 @@
         width = dataComponent.width ?? 40.0,
         height = dataComponent.height ?? 40.0,
         rotation = dataComponent.rotation,
-        animateEntrance = dataComponent.animateEntrance,
+        animationType = dataComponent.animationType,
         z = dataComponent.z;
 
   ZCardState createState() => new ZCardState();
@@ -68,7 +68,7 @@
   final double rotation;
   final bool useKey;
   final bool visible;
-  final bool animateEntrance;
+  final CardAnimationType animationType;
   final double z;
 
   Card(logic_card.Card card, this.faceUp,
@@ -77,9 +77,10 @@
       this.rotation: 0.0,
       bool useKey: false,
       this.visible: true,
-      this.animateEntrance: true,
+      CardAnimationType animationType,
       this.z})
-      : card = card,
+      : animationType = animationType ?? CardAnimationType.NONE,
+        card = card,
         width = width ?? 40.0,
         height = height ?? 40.0,
         useKey = useKey,
@@ -94,7 +95,7 @@
         rotation: rotation,
         useKey: false,
         visible: visible ?? this.visible,
-        animateEntrance: false,
+        animationType: CardAnimationType.NONE,
         z: z);
   }
 
@@ -108,7 +109,7 @@
         c.rotation == rotation &&
         c.useKey == useKey &&
         c.visible == visible &&
-        c.animateEntrance == animateEntrance &&
+        c.animationType == animationType &&
         c.z == z;
   }
 
@@ -168,7 +169,7 @@
     _pointQueue.add(config.endingPosition);
     _performance = new ValuePerformance<Point>(
         variable: new AnimatedValue<Point>(Point.origin, curve: Curves.ease),
-        duration: const Duration(milliseconds: 250));
+        duration: this.animationDuration);
     _performance.addStatusListener((PerformanceStatus status) {
       if (status == PerformanceStatus.completed) {
         _animationIndex++;
@@ -184,6 +185,20 @@
     }
   }
 
+  Duration get animationDuration {
+    switch (config.animationType) {
+      case CardAnimationType.NONE:
+        return const Duration(milliseconds: 0);
+      case CardAnimationType.NORMAL:
+        return const Duration(milliseconds: 250);
+      case CardAnimationType.LONG:
+        return const Duration(milliseconds: 750);
+      default:
+        print(config.animationType);
+        assert(false);
+    }
+  }
+
   // These microtasks are being scheduled on every build change.
   // Theoretically, this is too often, but to be safe, it is also good to do it.
   @override
@@ -206,7 +221,8 @@
   void _updatePosition() {
     _cardUpdateScheduled =
         false; // allow the next attempt to schedule _updatePosition to succeed.
-    if (!config.animateEntrance || _pointQueue.length == 1) {
+    if (config.animationType == CardAnimationType.NONE ||
+        _pointQueue.length == 1) {
       Point endingLocation = config.endingPosition;
       _performance.variable
         ..begin = endingLocation
@@ -238,6 +254,7 @@
         ..value = startingLocation
         ..end = endingLocation;
       _performance.progress = 0.0;
+      _performance.duration = this.animationDuration;
       _performance.play();
     }
   }
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
index 9ece2cd..1a2f93f 100644
--- a/lib/components/card_collection.dart
+++ b/lib/components/card_collection.dart
@@ -44,6 +44,7 @@
   final Color _altColor;
   final double rotation; // This angle is in radians.
   final bool useKeys; // If set, every Card created in this collection will be keyed.
+  final component_card.CardAnimationType animationType;
 
   DropType get acceptType => _acceptType ?? DropType.none;
   Color get backgroundColor => _backgroundColor ?? Colors.grey[500];
@@ -61,7 +62,8 @@
       Color backgroundColor,
       Color altColor,
       this.rotation: 0.0,
-      this.useKeys: false})
+      this.useKeys: false,
+      this.animationType: component_card.CardAnimationType.NORMAL})
       : _acceptType = acceptType,
         _backgroundColor = backgroundColor,
         _altColor = altColor;
@@ -311,7 +313,8 @@
           visible:
               !config.useKeys, // TODO(alexfandrianto): Is there a case where you want an invisible card and a key?
           useKey: config.useKeys,
-          z: 0.0 + i);
+          z: 0.0 + i,
+          animationType: config.animationType);
 
       cardComponents.add(c);
     }
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 132a09d..211bd7b 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -24,19 +24,6 @@
 }
 
 class CroupierComponentState extends State<CroupierComponent> {
-  @override
-  void initState() {
-    super.initState();
-    // TODO(alexfandrianto): ui.window.size.width and ui.window.size.height?
-
-    // Croupier (logic) needs this in case of syncbase watch updates.
-    config.croupier.informUICb = _informUICb;
-  }
-
-  void _informUICb() {
-    setState(() {});
-  }
-
   NoArgCb makeSetStateCallback(logic_croupier.CroupierState s,
       [var data = null]) {
     return () => setState(() {
@@ -45,13 +32,6 @@
   }
 
   Widget build(BuildContext context) {
-    // TODO(alexfandrianto): A better way to do this is to show the splash
-    // screen while the Store is initializing.
-    // https://github.com/vanadium/issues/issues/958
-    if (config.croupier.settings == null) {
-      return _buildSplashScreen();
-    }
-
     switch (config.croupier.state) {
       case logic_croupier.CroupierState.Welcome:
         // in which we show them a UI to start a new game, join a game, or change some settings.
@@ -101,12 +81,11 @@
             .forEach((String _, logic_game.GameStartData gsd) {
           CroupierSettings cs = config.croupier.settings_everyone[gsd.ownerID];
           // cs could be null if this settings data hasn't synced yet.
-          if (cs != null) {
-            profileWidgets.add(new FlatButton(
-                child: new CroupierProfileComponent(cs),
-                onPressed: makeSetStateCallback(
-                    logic_croupier.CroupierState.ArrangePlayers, gsd)));
-          }
+          // If so, a placeholder is shown instead.
+          profileWidgets.add(new GestureDetector(
+              child: new CroupierProfileComponent(settings: cs),
+              onTap: makeSetStateCallback(
+                  logic_croupier.CroupierState.ArrangePlayers, gsd)));
         });
         // in which players wait for game invitations to arrive.
         return new Container(
@@ -127,9 +106,8 @@
         config.croupier.players_found.forEach((int userID, _) {
           CroupierSettings cs = config.croupier.settings_everyone[userID];
           // cs could be null if this settings data hasn't synced yet.
-          if (cs != null) {
-            profileWidgets.add(new CroupierProfileComponent(cs));
-          }
+          // If so, a placeholder is shown instead.
+          profileWidgets.add(new CroupierProfileComponent(settings: cs));
         });
 
         // TODO(alexfandrianto): You can only start the game once there are enough players.
@@ -160,10 +138,4 @@
         return null;
     }
   }
-
-  // TODO(alexfandrianto): Can we do better than this?
-  Widget _buildSplashScreen() {
-    return new Container(
-        child: new Text("Loading Croupier...", style: style.Text.titleStyle));
-  }
 }
diff --git a/lib/components/croupier_profile.dart b/lib/components/croupier_profile.dart
index 0a9390a..3e2a369 100644
--- a/lib/components/croupier_profile.dart
+++ b/lib/components/croupier_profile.dart
@@ -9,15 +9,44 @@
 
 class CroupierProfileComponent extends StatelessComponent {
   final CroupierSettings settings;
-  CroupierProfileComponent(this.settings);
+  final double height;
+  final double width;
+  final bool isMini;
+
+  static const double padAmount = 4.0;
+
+  CroupierProfileComponent(
+      {CroupierSettings settings, this.height, this.width, this.isMini: false})
+      : settings = settings ?? new CroupierSettings.placeholder();
 
   Widget build(BuildContext context) {
-    return new Container(
-        decoration:
-            new BoxDecoration(backgroundColor: new Color(settings.color)),
-        child: new Column([
-          new AssetImage(name: CroupierSettings.makeAvatarUrl(settings.avatar)),
-          new Text(settings.name, style: style.Text.liveNow)
-        ]));
+    if (!isMini) {
+      return new Card(
+          color: new Color(settings.color),
+          child: new Container(
+              height: this.height,
+              width: this.width,
+              padding: const EdgeDims.all(padAmount),
+              child: new Column([
+                new AssetImage(
+                    name: CroupierSettings.makeAvatarUrl(settings.avatar)),
+                new Text(settings.name, style: style.Text.liveNow)
+              ], 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),
+              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)));
+    }
   }
 }
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 258a09a..075d104 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -6,22 +6,22 @@
 
 import 'dart:math' as math;
 
+import 'package:flutter/animation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+
 import '../logic/card.dart' as logic_card;
 import '../logic/croupier.dart' show Croupier;
 import '../logic/croupier_settings.dart' show CroupierSettings;
 import '../logic/game/game.dart' show Game, GameType;
 import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase, HeartsType;
 import '../logic/solitaire/solitaire.dart' show SolitaireGame, SolitairePhase;
+import '../styles/common.dart' as style;
 import 'board.dart' show HeartsBoard;
 import 'card.dart' as component_card;
 import 'card_collection.dart'
     show CardCollectionComponent, DropType, CardCollectionOrientation, AcceptCb;
 import 'croupier_profile.dart' show CroupierProfileComponent;
-import '../styles/common.dart' as style;
-
-import 'package:flutter/animation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
 
 part 'hearts/hearts.part.dart';
 part 'proto/proto.part.dart';
@@ -56,7 +56,9 @@
   // This callback is used to force the UI to draw when state changes occur
   // outside of the UIs control (e.g., synced data).
   void update() {
-    setState(() {});
+    if (this.mounted) {
+      setState(() {});
+    }
   }
 
   // A helper that most subclasses use in order to quit their respective games.
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 0c5aa4f..f9c3808 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -86,6 +86,7 @@
           visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PLAY + i);
           visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PASS + i);
           visibleCardCollectionIndexes.add(HeartsGame.OFFSET_HAND + i);
+          visibleCardCollectionIndexes.add(HeartsGame.OFFSET_TRICK + i);
         }
       }
     }
@@ -278,19 +279,11 @@
     switch (game.phase) {
       case HeartsPhase.StartGame:
       case HeartsPhase.Deal:
-        kids.add(new Text("DEAL PHASE"));
-        kids.add(_makeButton('Deal', game.dealCards));
+        kids.add(new Text("Waiting for Deal..."));
         break;
       case HeartsPhase.Pass:
-        kids.add(new Text("PASS PHASE"));
-        kids.add(showBoard());
-        break;
       case HeartsPhase.Take:
-        kids.add(new Text("TAKE PHASE"));
-        kids.add(showBoard());
-        break;
       case HeartsPhase.Play:
-        kids.add(new Text("PLAY PHASE"));
         kids.add(showBoard());
         break;
       case HeartsPhase.Score:
@@ -305,8 +298,7 @@
   }
 
   Widget showBoard() {
-    HeartsGame game = config.game;
-    return new HeartsBoard(game,
+    return new HeartsBoard(config.croupier, this.update,
         width: config.width, height: 0.80 * config.height);
   }
 
@@ -376,7 +368,7 @@
     if (userID != null) {
       cs = config.croupier.settings_everyone[userID];
     }
-    return new CroupierProfileComponent(cs);
+    return new CroupierProfileComponent(settings: cs);
   }
 
   Widget showScore() {
diff --git a/lib/components/main_route.dart b/lib/components/main_route.dart
index 5a0a715..3aecfbe 100644
--- a/lib/components/main_route.dart
+++ b/lib/components/main_route.dart
@@ -19,7 +19,28 @@
 }
 
 class MainRouteState extends State<MainRoute> {
+  @override
+  void initState() {
+    super.initState();
+    // Croupier (logic) needs this in case of syncbase watch updates.
+    config.croupier.informUICb = _informUICb;
+  }
+
+  void _informUICb() {
+    if (this.mounted) {
+      setState(() {});
+    }
+  }
+
   Widget build(BuildContext context) {
+    // TODO(alexfandrianto): A better way to do this is to show the splash
+    // screen while the Store is initializing.
+    // https://github.com/vanadium/issues/issues/958
+    if (config.croupier.settings == null) {
+      print("Splash screen side");
+      return _buildSplashScreen();
+    }
+    print("Scaffold side");
     return new Scaffold(
         key: _scaffoldKey,
         toolBar: new ToolBar(
@@ -29,6 +50,13 @@
         body: new Material(child: new CroupierComponent(config.croupier)));
   }
 
+  // TODO(alexfandrianto): Can we do better than this?
+  Widget _buildSplashScreen() {
+    return new Container(
+        decoration: style.Box.liveNow,
+        child: new Text("Loading Croupier...", style: style.Text.titleStyle));
+  }
+
   void _showDrawer() {
     showDrawer(
         context: context,
diff --git a/lib/components/solitaire/solitaire.part.dart b/lib/components/solitaire/solitaire.part.dart
index 933922f..95c8486 100644
--- a/lib/components/solitaire/solitaire.part.dart
+++ b/lib/components/solitaire/solitaire.part.dart
@@ -151,7 +151,7 @@
           heightCard: cardSize,
           dragChildren: true,
           useKeys: true),
-      new InkWell(
+      new GestureDetector(
           child: new CardCollectionComponent(
               game.cardCollections[SolitaireGame.OFFSET_DRAW],
               false,
@@ -164,7 +164,7 @@
 
     List<Widget> row2 = new List<Widget>();
     for (int i = 0; i < 7; i++) {
-      row2.add(new InkWell(
+      row2.add(new GestureDetector(
           child: new CardCollectionComponent(
               game.cardCollections[SolitaireGame.OFFSET_DOWN + i],
               false,
diff --git a/lib/logic/croupier_settings.dart b/lib/logic/croupier_settings.dart
index 4c5501a..52244c2 100644
--- a/lib/logic/croupier_settings.dart
+++ b/lib/logic/croupier_settings.dart
@@ -19,6 +19,13 @@
     _randomInitialization();
   }
 
+  CroupierSettings.placeholder() {
+    userID = 0;
+    avatar = "";
+    name = "Loading...";
+    color = 0xcfcccccc;
+  }
+
   CroupierSettings.fromJSONString(String json) {
     var data = JSON.decode(json);
     userID = data["userID"];
diff --git a/lib/logic/hearts/hearts_command.part.dart b/lib/logic/hearts/hearts_command.part.dart
index 350eb9a..bfd62da 100644
--- a/lib/logic/hearts/hearts_command.part.dart
+++ b/lib/logic/hearts/hearts_command.part.dart
@@ -229,7 +229,7 @@
               "Cannot process take commands when not in Take phase");
         }
         int takerId = int.parse(parts[0]);
-        int senderPile = game._getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
+        int senderPile = game.getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
         List<Card> handT = game.cardCollections[takerId];
         List<Card> handS = game.cardCollections[senderPile];
         handT.addAll(handS);
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index fde65f7..68aebb1 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -112,8 +112,8 @@
     }
   }
 
-  int get takeTarget => _getTakeTarget(playerNumber);
-  int _getTakeTarget(takerId) {
+  int get takeTarget => getTakeTarget(playerNumber);
+  int getTakeTarget(takerId) {
     switch (roundNumber % 4) {
       // is a 4-cycle
       case 0:
@@ -254,7 +254,10 @@
   // It won't be possible to set the readiness for other players, except via the GameLog.
   void setReadyUI() {
     assert(phase == HeartsPhase.Score || phase == HeartsPhase.StartGame);
-    gamelog.add(new HeartsCommand.ready(playerNumber));
+    //gamelog.add(new HeartsCommand.ready(playerNumber));
+    for (int i = 0; i < 4; i++) {
+      gamelog.add(new HeartsCommand.ready(i));
+    }
   }
 
   @override
diff --git a/lib/main.dart b/lib/main.dart
index 1f45727..265fdbd 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -3,6 +3,7 @@
 // license that can be found in the LICENSE file.
 
 import 'package:flutter/material.dart';
+import 'dart:async';
 
 import 'logic/croupier.dart' show Croupier;
 import 'components/settings_route.dart' show SettingsRoute;
@@ -37,5 +38,10 @@
 }
 
 void main() {
-  runApp(new CroupierApp());
+  // TODO(alexfandrianto): Perhaps my app will run better if I initialize more
+  // things here instead of in Croupier. I added this 500 ms delay because the
+  // tablet was sometimes not rendering without it (repainting too early?).
+  new Future.delayed(const Duration(milliseconds: 500), () {
+    runApp(new CroupierApp());
+  });
 }