croupier: Add Arrange Players

- A very rudimentary arrange players screen was added.
- Since this screen gets in the way of developing Hearts, a workaround
  using Debug Mode was added.
- playerNumber was also removed since this is now set by arrange
  players. the tests are adjusted to correpond with this.

Change-Id: I205ecc867ef1ba0ac7a83c97db65da54cc649107
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 861efa4..1ee925c 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -199,14 +199,11 @@
   }
 
   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];
-    }
+    // If cs is null, a placeholder is used instead.
+    CroupierSettings cs =
+        config.croupier.settingsFromPlayerNumber(playerNumber);
     return new CroupierProfileComponent(
         settings: cs, height: config.height * 0.15, isMini: isMini);
   }
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 415e8f7..b164dd1 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -106,27 +106,14 @@
             padding: new EdgeDims.only(top: ui.window.padding.top),
             child: new Column(gameAdWidgets));
       case logic_croupier.CroupierState.ArrangePlayers:
-        List<Widget> profileWidgets = new List<Widget>();
-        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 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.
         return new Container(
             padding: new EdgeDims.only(top: ui.window.padding.top),
             child: new Column([
-              new FlatButton(child: new Text('Start Game'), onPressed: () {
-                makeSetStateCallback(logic_croupier.CroupierState.PlayGame)();
-                config.croupier.game.startGameSignal();
-              }),
-              new Grid(profileWidgets, maxChildExtent: 150.0),
+              _buildArrangePlayers(),
               new FlatButton(
                   child: new Text('Back'),
                   onPressed: makeSetStateCallback(
-                      logic_croupier.CroupierState.ChooseGame))
+                      logic_croupier.CroupierState.Welcome))
             ]));
       case logic_croupier.CroupierState.PlayGame:
         return new Container(
@@ -142,4 +129,58 @@
         return null;
     }
   }
+
+  // Show the player profiles. If needs arrangement, then the profile is only
+  // shown if the person has not sat down yet.
+  Widget _buildPlayerProfiles(bool needsArrangement) {
+    List<Widget> profileWidgets = new List<Widget>();
+    config.croupier.players_found.forEach((int userID, int playerNumber) {
+      if (!needsArrangement || playerNumber == null) {
+        CroupierSettings cs = config.croupier.settings_everyone[userID];
+        // cs could be null if this settings data hasn't synced yet.
+        // If so, a placeholder is shown instead.
+        profileWidgets.add(new CroupierProfileComponent(settings: cs));
+      }
+    });
+
+    return new Grid(profileWidgets, maxChildExtent: 120.0);
+  }
+
+  Widget _buildArrangePlayers() {
+    List<Widget> allWidgets = new List<Widget>();
+
+    logic_game.GameArrangeData gad = config.croupier.game.gameArrangeData;
+    Iterable<int> playerNumbers = config.croupier.players_found.values;
+
+    // Allow games that can start with these players to begin.
+    // Debug Mode should also go through.
+    NoArgCb onPressed;
+    if (gad.canStart(playerNumbers) || config.croupier.debugMode) {
+      onPressed = () {
+        makeSetStateCallback(logic_croupier.CroupierState.PlayGame)();
+
+        // Since playerNumber starts out as null, we should set it to -1 if
+        // the person pressed Start Game without sitting.
+        if (config.croupier.game.playerNumber == null) {
+          config.croupier.game.playerNumber = -1;
+        }
+        config.croupier.game.startGameSignal();
+      };
+    }
+
+    // Always include the Start Game Button.
+    allWidgets.add(
+        new FlatButton(child: new Text('Start Game'), onPressed: onPressed));
+
+    // Games that need arrangement can show their game arrange component.
+    if (gad.needsArrangement) {
+      allWidgets.add(component_game.createGameArrangeComponent(config.croupier,
+          width: ui.window.size.width, height: ui.window.size.height / 2));
+    }
+
+    // Then show the profile widgets of those who have joined the game.
+    allWidgets.add(_buildPlayerProfiles(gad.needsArrangement));
+
+    return new Column(allWidgets);
+  }
 }
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 075d104..6049c6c 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -12,7 +12,6 @@
 
 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;
@@ -215,6 +214,25 @@
   }
 }
 
+abstract class GameArrangeComponent extends StatelessComponent {
+  final Croupier croupier;
+  final double width;
+  final double height;
+  GameArrangeComponent(this.croupier, {this.width, this.height});
+}
+
+GameArrangeComponent createGameArrangeComponent(Croupier croupier,
+    {double width, double height}) {
+  switch (croupier.game.gameType) {
+    case GameType.Hearts:
+      return new HeartsArrangeComponent(croupier, width: width, height: height);
+    default:
+      // We can't arrange this game.
+      throw new UnimplementedError(
+          "We cannot arrange the game type: ${croupier.game.gameType}");
+  }
+}
+
 /// CardAnimationData contains the relevant information for a ZCard to be built.
 /// It uses the comp_card's properties, the oldPoint, newPoint, and z-index to
 /// determine how it needs to animate.
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index cc4e406..c3f03e5 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -103,6 +103,9 @@
         game.viewType = HeartsType.Board;
       } else {
         game.viewType = HeartsType.Player;
+        if (!game.isPlayer) {
+          game.playerNumber = 0; // avoid accidental red screen
+        }
       }
     });
   }
@@ -360,23 +363,13 @@
         justifyContent: FlexJustifyContent.spaceBetween);
   }
 
-  Widget _getProfileComponent(int playerNumber) {
-    int userID = config.croupier.userIDFromPlayerNumber(playerNumber);
-
-    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);
-  }
-
   Widget showScore() {
     HeartsGame game = config.game as HeartsGame;
 
     Widget w;
     if (game.hasGameEnded) {
       w = new Text("Game Over!");
-    } else if (game.ready[game.playerNumber]) {
+    } else if (!game.isPlayer || game.ready[game.playerNumber]) {
       w = new Text("Waiting for other players...");
     } else {
       w = _makeButton('Ready?', game.setReadyUI);
@@ -415,7 +408,10 @@
 
       scores.add(new Flexible(
           child: new Flex([
-            new Flexible(child: _getProfileComponent(i), flex: 1),
+            new Flexible(
+                child: new CroupierProfileComponent(
+                    settings: config.croupier.settingsFromPlayerNumber(i)),
+                flex: 1),
             new Flexible(
                 child: new Center(
                     child:
@@ -566,3 +562,77 @@
         hasTaken ? null : _makeGameTakeCallback);
   }
 }
+
+class HeartsArrangeComponent extends GameArrangeComponent {
+  HeartsArrangeComponent(Croupier croupier, {double width, double height})
+      : super(croupier, width: width, height: height);
+
+  bool get hasSat => croupier.game.playerNumber != null;
+
+  Widget build(BuildContext context) {
+    int numAtTable = croupier.players_found.values
+        .where((int playerNumber) => playerNumber == 4)
+        .length;
+
+    return new Container(
+        decoration: style.Box.liveNow,
+        height: height,
+        width: width,
+        child: new Column([
+          new Flexible(
+              flex: 1,
+              child: new Row(
+                  [_buildEmptySlot(), _buildSlot("Player", 2), _buildEmptySlot()],
+                  justifyContent: FlexJustifyContent.spaceAround,
+                  alignItems: FlexAlignItems.stretch)),
+          new Flexible(
+              flex: 1,
+              child: new Row([
+                _buildSlot("Player", 1),
+                _buildSlot("Table: ${numAtTable}", 4),
+                _buildSlot("Player", 3)
+              ],
+                  justifyContent: FlexJustifyContent.spaceAround,
+                  alignItems: FlexAlignItems.stretch)),
+          new Flexible(
+              flex: 1,
+              child: new Row(
+                  [_buildEmptySlot(), _buildSlot("Player", 0), _buildEmptySlot()],
+                  justifyContent: FlexJustifyContent.spaceAround,
+                  alignItems: FlexAlignItems.stretch))
+        ],
+            justifyContent: FlexJustifyContent.spaceAround,
+            alignItems: FlexAlignItems.stretch));
+  }
+
+  Widget _buildEmptySlot() {
+    return new Flexible(flex: 1, child: new Text(""));
+  }
+
+  Widget _buildSlot(String name, int index) {
+    NoArgCb onTap = () {
+      croupier.settings_manager.setPlayerNumber(croupier.game.gameID, index);
+    };
+    Widget slotWidget = new Text(name, style: style.Text.hugeStyle);
+
+    bool seatTaken =
+        index >= 0 && index < 4 && croupier.players_found.containsValue(index);
+    if (seatTaken) {
+      onTap = null;
+      slotWidget = new CroupierProfileComponent(
+          settings: croupier.settingsFromPlayerNumber(index));
+    } else if (hasSat) {
+      onTap = null;
+    }
+
+    return new Flexible(
+        flex: 1,
+        child: new GestureDetector(
+            child: new Card(
+                color: croupier.game.playerNumber == index
+                    ? style.theme.accentColor
+                    : null,
+                child: new Center(child: slotWidget)),
+            onTap: onTap));
+  }
+}
diff --git a/lib/logic/create_game.dart b/lib/logic/create_game.dart
index ae08b66..1a2f95c 100644
--- a/lib/logic/create_game.dart
+++ b/lib/logic/create_game.dart
@@ -7,17 +7,17 @@
 import 'proto/proto.dart' as proto_impl;
 import 'solitaire/solitaire.dart' as solitaire_impl;
 
-game_impl.Game createGame(game_impl.GameType gt, int pn, bool debugMode,
+game_impl.Game createGame(game_impl.GameType gt, bool debugMode,
     {int gameID, bool isCreator}) {
   switch (gt) {
     case game_impl.GameType.Proto:
-      return new proto_impl.ProtoGame(pn, gameID: gameID, isCreator: isCreator)
+      return new proto_impl.ProtoGame(gameID: gameID, isCreator: isCreator)
         ..debugMode = debugMode;
     case game_impl.GameType.Hearts:
-      return new hearts_impl.HeartsGame(pn,
-          gameID: gameID, isCreator: isCreator)..debugMode = debugMode;
+      return new hearts_impl.HeartsGame(gameID: gameID, isCreator: isCreator)
+        ..debugMode = debugMode;
     case game_impl.GameType.Solitaire:
-      return new solitaire_impl.SolitaireGame(pn,
+      return new solitaire_impl.SolitaireGame(
           gameID: gameID, isCreator: isCreator)..debugMode = debugMode;
     default:
       assert(false);
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 4f9789e..98dc9a6 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -78,10 +78,21 @@
         orElse: () => null);
   }
 
+  CroupierSettings settingsFromPlayerNumber(int playerNumber) {
+    int userID = userIDFromPlayerNumber(playerNumber);
+    if (userID != null) {
+      return settings_everyone[userID];
+    }
+    return null;
+  }
+
   void _updatePlayerFoundCb(String playerID, String playerNum) {
     int id = int.parse(playerID);
     if (playerNum == null) {
-      games_found.remove(id);
+      if (!players_found.containsKey(id)) {
+        // The player exists but has not sat down yet.
+        players_found[id] = null;
+      }
     } else {
       int playerNumber = int.parse(playerNum);
       players_found[id] = playerNumber;
@@ -113,8 +124,7 @@
 
         // data should be the game id here.
         GameType gt = data as GameType;
-        game = cg.createGame(gt, 0, this.debugMode,
-            isCreator: true); // Start as player 0 of whatever game type.
+        game = cg.createGame(gt, this.debugMode, isCreator: true);
 
         _advertiseFuture = settings_manager
             .createGameSyncgroup(gameTypeToString(gt), game.gameID)
@@ -128,6 +138,7 @@
         // Note that if we were in join game, we must have been scanning.
         _scanFuture.then((_) {
           settings_manager.stopScanSettings();
+          games_found.clear();
           _scanFuture = null;
         });
 
@@ -138,9 +149,8 @@
 
         // data would probably be the game id again.
         GameStartData gsd = data as GameStartData;
-        game = cg.createGame(
-            stringToGameType(gsd.type), gsd.playerNumber, this.debugMode,
-            gameID: gsd.gameID); // Start as player 0 of whatever game type.
+        game = cg.createGame(stringToGameType(gsd.type), this.debugMode,
+            gameID: gsd.gameID);
         String sgName;
         games_found.forEach((String name, GameStartData g) {
           if (g == gsd) {
@@ -150,6 +160,10 @@
         assert(sgName != null);
 
         settings_manager.joinGameSyncgroup(sgName, gsd.gameID);
+        players_found[gsd.ownerID] = null;
+        if (!game.gameArrangeData.needsArrangement) {
+          settings_manager.setPlayerNumber(gsd.gameID, 0);
+        }
         break;
       case CroupierState.ArrangePlayers:
         // Note that if we were arranging players, we might have been advertising.
@@ -174,8 +188,8 @@
     // A simplified way of clearing out the games and players found.
     // They will need to be re-discovered in the future.
     if (nextState == CroupierState.Welcome) {
-      games_found = new Map<String, GameStartData>();
-      players_found = new Map<int, int>();
+      games_found.clear();
+      players_found.clear();
     } else if (nextState == CroupierState.JoinGame) {
       // Start scanning for games since that's what's next for you.
       _scanFuture =
diff --git a/lib/logic/game/game_def.part.dart b/lib/logic/game/game_def.part.dart
index 70315df..5d53ab3 100644
--- a/lib/logic/game/game_def.part.dart
+++ b/lib/logic/game/game_def.part.dart
@@ -69,11 +69,26 @@
   }
 }
 
+// GameArrangeData details what a game needs before beginning.
+class GameArrangeData {
+  final bool needsArrangement;
+  final Set<int> requiredPlayerNumbers;
+  GameArrangeData(this.needsArrangement, this.requiredPlayerNumbers);
+  bool canStart(Iterable<int> actualPlayerNumbers) {
+    // None of the required player numbers can be missing from the actual ones.
+    return !needsArrangement ||
+        !requiredPlayerNumbers.any((int i) {
+          return !actualPlayerNumbers.contains(i);
+        });
+  }
+}
+
 typedef void NoArgCb();
 
 /// A game consists of multiple decks and tracks a single deck of cards.
 /// It also handles events; when cards are dragged to and from decks.
 abstract class Game {
+  GameArrangeData get gameArrangeData;
   final GameType gameType;
   String get gameTypeName; // abstract
   final bool isCreator;
@@ -97,8 +112,7 @@
   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.
 
   // A super constructor, don't call this unless you're a subclass.
-  Game.create(
-      this.gameType, this.gamelog, this._playerNumber, int numCollections,
+  Game.create(this.gameType, this.gamelog, int numCollections,
       {int gameID, bool isCreator})
       : gameID = gameID ?? new math.Random().nextInt(0x00FFFFFF),
         isCreator = isCreator ?? false {
diff --git a/lib/logic/hearts/hearts.dart b/lib/logic/hearts/hearts.dart
index 14e7f5c..3f25c0a 100644
--- a/lib/logic/hearts/hearts.dart
+++ b/lib/logic/hearts/hearts.dart
@@ -7,7 +7,8 @@
 import 'dart:math' as math;
 
 import '../card.dart' show Card;
-import '../game/game.dart' show Game, GameType, GameCommand, GameLog;
+import '../game/game.dart'
+    show Game, GameArrangeData, GameType, GameCommand, GameLog;
 import '../../src/syncbase/log_writer.dart' show LogWriter, SimulLevel;
 
 part 'hearts_command.part.dart';
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index 63e5e1a..9461a46 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -66,8 +66,8 @@
   List<int> deltaScores = [0, 0, 0, 0];
   List<bool> ready;
 
-  HeartsGame(int playerNumber, {int gameID, bool isCreator})
-      : super.create(GameType.Hearts, new HeartsLog(), playerNumber, 16,
+  HeartsGame({int gameID, bool isCreator})
+      : super.create(GameType.Hearts, new HeartsLog(), 16,
             gameID: gameID, isCreator: isCreator) {
     resetGame();
     unsetReady();
@@ -95,6 +95,8 @@
     deal(PLAYER_D, forD);
   }
 
+  bool get isPlayer => this.playerNumber >= 0 && this.playerNumber < 4;
+
   int get passTarget {
     switch (roundNumber % 4) {
       // is a 4-cycle
@@ -254,14 +256,29 @@
   // 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);
-    if (playerNumber >= 0 && playerNumber < 4) {
+    if (this.debugMode) {
+      // Debug Mode should pretend this device is all players.
+      for (int i = 0; i < 4; i++) {
+        gamelog.add(new HeartsCommand.ready(i));
+      }
+    } else if (this.isPlayer) {
       gamelog.add(new HeartsCommand.ready(playerNumber));
     }
   }
 
+  static final GameArrangeData _arrangeData =
+      new GameArrangeData(true, new Set.from([0, 1, 2, 3]));
+  GameArrangeData get gameArrangeData => _arrangeData;
+
   @override
   void startGameSignal() {
+    if (this.debugMode && this.playerNumber < 0) {
+      this.playerNumber = 0;
+    }
     setReadyUI();
+    if (!this.isPlayer) {
+      this.viewType = HeartsType.Board;
+    }
   }
 
   // Note that this will be called by the UI.
diff --git a/lib/logic/proto/proto.dart b/lib/logic/proto/proto.dart
index f12361f..314046f 100644
--- a/lib/logic/proto/proto.dart
+++ b/lib/logic/proto/proto.dart
@@ -5,7 +5,8 @@
 library proto;
 
 import '../card.dart' show Card;
-import '../game/game.dart' show Game, GameType, GameCommand, GameLog;
+import '../game/game.dart'
+    show Game, GameArrangeData, GameType, GameCommand, GameLog;
 
 part 'proto_command.part.dart';
 part 'proto_game.part.dart';
diff --git a/lib/logic/proto/proto_game.part.dart b/lib/logic/proto/proto_game.part.dart
index 623d062..cb76c20 100644
--- a/lib/logic/proto/proto_game.part.dart
+++ b/lib/logic/proto/proto_game.part.dart
@@ -8,8 +8,12 @@
   @override
   String get gameTypeName => "Proto";
 
-  ProtoGame(int playerNumber, {int gameID, bool isCreator})
-      : super.create(GameType.Proto, new ProtoLog(), playerNumber, 6,
+  static final GameArrangeData _arrangeData =
+      new GameArrangeData(false, new Set());
+  GameArrangeData get gameArrangeData => _arrangeData;
+
+  ProtoGame({int gameID, bool isCreator})
+      : super.create(GameType.Proto, new ProtoLog(), 6,
             gameID: gameID, isCreator: isCreator) {
     // playerNumber would be used in a real game, but I have to ignore it for debugging.
     // It would determine faceUp/faceDown status.faceDown
diff --git a/lib/logic/solitaire/solitaire.dart b/lib/logic/solitaire/solitaire.dart
index 6d38b61..aa4a63a 100644
--- a/lib/logic/solitaire/solitaire.dart
+++ b/lib/logic/solitaire/solitaire.dart
@@ -5,7 +5,8 @@
 library solitaire;
 
 import '../card.dart' show Card;
-import '../game/game.dart' show Game, GameType, GameCommand, GameLog;
+import '../game/game.dart'
+    show Game, GameArrangeData, GameType, GameCommand, GameLog;
 import '../../src/syncbase/log_writer.dart' show LogWriter, SimulLevel;
 
 part 'solitaire_command.part.dart';
diff --git a/lib/logic/solitaire/solitaire_game.part.dart b/lib/logic/solitaire/solitaire_game.part.dart
index dd0e31a..b90df65 100644
--- a/lib/logic/solitaire/solitaire_game.part.dart
+++ b/lib/logic/solitaire/solitaire_game.part.dart
@@ -19,6 +19,10 @@
   static const OFFSET_DOWN = 6;
   static const OFFSET_UP = 13;
 
+  static final GameArrangeData _arrangeData =
+      new GameArrangeData(false, new Set());
+  GameArrangeData get gameArrangeData => _arrangeData;
+
   SolitairePhase _phase = SolitairePhase.Deal;
   SolitairePhase get phase => _phase;
   void set phase(SolitairePhase other) {
@@ -26,9 +30,8 @@
     _phase = other;
   }
 
-  SolitaireGame(int playerNumber, {int gameID, bool isCreator})
-      : super.create(
-            GameType.Solitaire, new SolitaireLog(), playerNumber, NUM_PILES,
+  SolitaireGame({int gameID, bool isCreator})
+      : super.create(GameType.Solitaire, new SolitaireLog(), NUM_PILES,
             gameID: gameID, isCreator: isCreator) {
     resetGame();
   }
@@ -342,7 +345,10 @@
   // TODO(alexfandrianto): Maybe wanted for debug; if not, remove.
   void jumpToScorePhaseDebug() {}
 
-  // TODO(alexfandrianto): This needs to be a signal to trigger a deal.
   @override
-  void startGameSignal() {}
+  void startGameSignal() {
+    if (this.isCreator) {
+      this.dealCardsUI();
+    }
+  }
 }
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
index 9231d1a..f96ab8c 100644
--- a/lib/src/mocks/settings_manager.dart
+++ b/lib/src/mocks/settings_manager.dart
@@ -64,4 +64,8 @@
   Future joinGameSyncgroup(String sgName, int gameID) {
     return new Future(() => null);
   }
+
+  Future setPlayerNumber(int gameID, int playerNumber) async {
+    return new Future(() => null);
+  }
 }
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 8a5b88b..64b419b 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -181,6 +181,9 @@
           case "settings_sg":
             // Join this player's settings syncgroup.
             _cc.joinSyncgroup(value);
+
+            // Also, signal that this player has been found.
+            this.updatePlayerFoundCallback(playerID, null);
             break;
           default:
             print("Unexpected key: ${key} with value ${value}");
@@ -208,10 +211,7 @@
     int id = await _getUserID();
     await gameTable.row("${gameID}/owner").put(UTF8.encode("${id}"));
     await gameTable
-        .row("${gameID}/players/${id}/player_number")
-        .put(UTF8.encode("0"));
-    await gameTable
-        .row("${gameID}/players/{$id}/settings_sg")
+        .row("${gameID}/players/${id}/settings_sg")
         .put(UTF8.encode(await _mySettingsSyncgroupName()));
 
     logic_game.GameStartData gsd =
@@ -227,8 +227,8 @@
 
   Future joinGameSyncgroup(String sgName, int gameID) async {
     print("Now joining game syncgroup at ${sgName} and ${gameID}");
-    sc.SyncbaseSyncgroup sg = await _cc.joinSyncgroup(sgName);
 
+    await _cc.joinSyncgroup(sgName);
     sc.SyncbaseDatabase db = await _cc.createDatabase();
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
@@ -237,18 +237,20 @@
         util.syncgamePrefix(gameID) + "/players", UTF8.encode("now"));
     _startWatchPlayers(watchStream); // Don't wait for this future.
 
-    // Also write yourself to the table as player |NUM_PLAYERS - 1|
-    Map<String, sc.SyncgroupMemberInfo> fellowPlayers = await sg.getMembers();
-    print("I have found! ${fellowPlayers} ${fellowPlayers.length}");
+    int id = await _getUserID();
+    await gameTable
+        .row("${gameID}/players/${id}/settings_sg")
+        .put(UTF8.encode(await _mySettingsSyncgroupName()));
+  }
+
+  Future setPlayerNumber(int gameID, int playerNumber) async {
+    sc.SyncbaseDatabase db = await _cc.createDatabase();
+    sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
     int id = await _getUserID();
-    int playerNumber = fellowPlayers.length - 1;
     await gameTable
         .row("${gameID}/players/${id}/player_number")
         .put(UTF8.encode("${playerNumber}"));
-    await gameTable
-        .row("${gameID}/players/{$id}/settings_sg")
-        .put(UTF8.encode(await _mySettingsSyncgroupName()));
   }
 
   // When starting the settings manager, there may be settings already in the
diff --git a/lib/styles/common.dart b/lib/styles/common.dart
index 5dffee2..b5473c1 100644
--- a/lib/styles/common.dart
+++ b/lib/styles/common.dart
@@ -40,6 +40,8 @@
 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);
 }
 
 ThemeData theme = new ThemeData(
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
index 09160db..0a3de71 100644
--- a/test/hearts_test.dart
+++ b/test/hearts_test.dart
@@ -10,7 +10,7 @@
 
 void main() {
   group("Initialization", () {
-    HeartsGame game = new HeartsGame(0);
+    HeartsGame game = new HeartsGame()..playerNumber = 0;
     game.phase = HeartsPhase.Deal;
     test("Dealing", () {
       game.dealCards(); // What the dealer actually runs to get cards to everybody.
@@ -101,7 +101,7 @@
 
   // For this test, the cards may end up being duplicate or inconsistent.
   group("Scoring", () {
-    HeartsGame game = new HeartsGame(0);
+    HeartsGame game = new HeartsGame()..playerNumber = 0;
     test("Compute/Prepare Score", () {
       // In this situation, what's the score?
       game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[
@@ -163,7 +163,7 @@
   });
 
   group("Game Over", () {
-    HeartsGame game = new HeartsGame(0);
+    HeartsGame game = new HeartsGame()..playerNumber = 0;
 
     test("Has the game ended? Yes", () {
       // Check if the game has ended. Should be yes.
@@ -181,7 +181,7 @@
   // performing a single action or set of actions.
   // Reads from a log, so we will go through logical game mechanics.
   group("Card Manipulation", () {
-    HeartsGame game = new HeartsGame(0);
+    HeartsGame game = new HeartsGame()..playerNumber = 0;
 
     // Note: This could have been a non-file (in-memory), but it's fine to use a file too.
     File file = new File("test/game_log_hearts_test.txt");
@@ -462,7 +462,7 @@
 
   group("Card Manipulation - Error Cases", () {
     test("Dealing - too soon", () {
-      HeartsGame game = new HeartsGame(0);
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
       expect(game.phase, HeartsPhase.StartGame);
       expect(() {
         game.gamelog.add(new HeartsCommand.deal(
@@ -470,7 +470,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Dealing - wrong phase", () {
-      HeartsGame game = new HeartsGame(0);
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Score;
       expect(() {
         game.gamelog.add(new HeartsCommand.deal(
@@ -478,7 +478,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Dealing - missing card", () {
-      HeartsGame game = new HeartsGame(0);
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
       expect(() {
         game.gamelog.add(
@@ -486,14 +486,14 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Dealing - too many cards dealt", () {
-      HeartsGame game = new HeartsGame(0);
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
       expect(() {
         game.gamelog.add(new HeartsCommand.deal(
             0, new List<Card>.from(Card.All.getRange(0, 15))));
       }, throwsA(new isInstanceOf<StateError>()));
 
-      game = new HeartsGame(0);
+      game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
       expect(() {
         game.gamelog.add(new HeartsCommand.deal(
@@ -503,7 +503,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Passing - wrong phase", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -513,7 +513,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Passing - missing card", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -524,7 +524,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Passing - wrong number of cards", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -534,7 +534,7 @@
             0, new List<Card>.from(Card.All.getRange(0, 2))));
       }, throwsA(new isInstanceOf<StateError>()));
 
-      game = new HeartsGame(0);
+      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))));
@@ -545,14 +545,14 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Taking - wrong phase", () {
-      HeartsGame game = new HeartsGame(0);
+      HeartsGame game = new HeartsGame()..playerNumber = 0;
       game.phase = HeartsPhase.Deal;
       expect(() {
         game.gamelog.add(new HeartsCommand.take(3));
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - wrong phase", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -561,7 +561,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - missing card", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -571,7 +571,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - invalid card (not 2 of clubs as first card)", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -584,7 +584,7 @@
     test("Playing - invalid card (no penalty on first round)", () {
       // NOTE: It is actually possible to be forced to play a penalty card on round 1.
       // But the odds are miniscule, so this rule will be enforced.
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -603,7 +603,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - wrong turn", () {
-      HeartsGame game = new HeartsGame(0);
+      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))));
@@ -621,7 +621,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - invalid card (suit mismatch)", () {
-      HeartsGame game = new HeartsGame(0);
+      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, 12))..add(Card.All[25])));
@@ -640,7 +640,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
     });
     test("Playing - invalid card (hearts not broken yet)", () {
-      HeartsGame game = new HeartsGame(0);
+      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, 12))..add(Card.All[38])));
diff --git a/test/solitaire_test.dart b/test/solitaire_test.dart
index a5597f6..f41e5b9 100644
--- a/test/solitaire_test.dart
+++ b/test/solitaire_test.dart
@@ -37,7 +37,7 @@
 
 void main() {
   group("Initialization", () {
-    SolitaireGame game = new SolitaireGame(0);
+    SolitaireGame game = new SolitaireGame();
     test("Dealing", () {
       game.dealCardsUI(); // What we run when starting the game.
 
@@ -71,7 +71,7 @@
   // We have a debug cheat button that lets you advance in the game.
   // After calling it once, it is forced to place cards up into the aces area.
   group("Cheat Command", () {
-    SolitaireGame game = new SolitaireGame(0);
+    SolitaireGame game = new SolitaireGame();
     game.dealCardsUI(); // Get the cards out there.
 
     test("Cheat Functionality", () {
@@ -91,7 +91,7 @@
   });
 
   group("Check Endgame", () {
-    SolitaireGame game = new SolitaireGame(0);
+    SolitaireGame game = new SolitaireGame();
 
     test("Has not won pre-deal", () {
       expect(game.phase, equals(SolitairePhase.Deal));
@@ -135,7 +135,7 @@
 
   // Run through a canonical game of Solitaire where the player doesn't win.
   group("Solitaire - Loss", () {
-    SolitaireGame game = new SolitaireGame(0);
+    SolitaireGame game = new SolitaireGame();
 
     // Note: This could have been a non-file (in-memory), but it's fine to use a file too.
     KeepGoingCb runCommand =
@@ -166,7 +166,7 @@
 
   // Run through a canonical game of Solitaire where the player does win.
   group("Solitaire - Win", () {
-    SolitaireGame game = new SolitaireGame(0);
+    SolitaireGame game = new SolitaireGame();
 
     // Note: This could have been a non-file (in-memory), but it's fine to use a file too.
     KeepGoingCb runCommand =
@@ -198,7 +198,7 @@
   group("Card Manipulation - Error Cases", () {
     test("Dealing - wrong phase", () {
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.phase = SolitairePhase.Score;
         game.gamelog
             .add(new SolitaireCommand.deal(new List<Card>.from(Card.All)));
@@ -206,7 +206,7 @@
     });
     test("Dealing - fake cards", () {
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.gamelog.add(
             new SolitaireCommand.deal(<Card>[new Card("fake", "not real")]));
       }, throwsA(new isInstanceOf<StateError>()));
@@ -214,13 +214,13 @@
     test("Dealing - wrong number of cards dealt", () {
       // 2x as many cards
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.gamelog.add(new SolitaireCommand.deal(
             new List<Card>.from(Card.All)..addAll(Card.All)));
       }, throwsA(new isInstanceOf<StateError>()));
       // missing cards
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.gamelog.add(new SolitaireCommand.deal(
             new List<Card>.from(Card.All.getRange(0, 40))));
       }, throwsA(new isInstanceOf<StateError>()));
@@ -245,7 +245,7 @@
       Card s13 = Card.All[39 + 12];
 
       SolitaireGame _makeArbitrarySolitaireGame() {
-        SolitaireGame g = new SolitaireGame(0);
+        SolitaireGame g = new SolitaireGame();
 
         // Top row
         g.cardCollections[SolitaireGame.OFFSET_ACES].add(s2);
@@ -378,13 +378,13 @@
     test("Playing - cannot flip", () {
       // Wrong phase.
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.gamelog.add(new SolitaireCommand.flip(3));
       }, throwsA(new isInstanceOf<StateError>()));
 
       // Bad index (low)
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
 
         // Deal first.
         game.dealCardsUI();
@@ -398,7 +398,7 @@
 
       // Bad index (high)
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
 
         // Deal first.
         game.dealCardsUI();
@@ -412,7 +412,7 @@
 
       // No down card to flip.
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
 
         // Deal first.
         game.dealCardsUI();
@@ -427,7 +427,7 @@
 
       // Up card is in the way.
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
 
         // Deal first.
         game.dealCardsUI();
@@ -438,7 +438,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
 
       // This scenario should work though.
-      SolitaireGame game = new SolitaireGame(0);
+      SolitaireGame game = new SolitaireGame();
 
       // Deal. Clear away pile 1. Flip pile 1.
       game.dealCardsUI();
@@ -450,13 +450,13 @@
     test("Playing - cannot draw", () {
       // Wrong phase.
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.gamelog.add(new SolitaireCommand.draw());
       }, throwsA(new isInstanceOf<StateError>()));
 
       // No draw cards remain.
       expect(() {
-        SolitaireGame game = new SolitaireGame(0);
+        SolitaireGame game = new SolitaireGame();
         game.dealCardsUI();
 
         // Remove all draw cards.
@@ -466,7 +466,7 @@
       }, throwsA(new isInstanceOf<StateError>()));
 
       // But it should be fine to draw just after dealing.
-      SolitaireGame game = new SolitaireGame(0);
+      SolitaireGame game = new SolitaireGame();
 
       // Deal first.
       game.dealCardsUI();