TBR croupier: Only Creator has Game Start button.

- use watch update to look for an updated game status
- Remove Start Game buttons from the other devices
- Style arrange players a little more flexibly.

Change-Id: Ie2dfdb510c15dc4534d663c7340c723cd4903b88
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index b164dd1..8c151fb 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -108,13 +108,7 @@
       case logic_croupier.CroupierState.ArrangePlayers:
         return new Container(
             padding: new EdgeDims.only(top: ui.window.padding.top),
-            child: new Column([
-              _buildArrangePlayers(),
-              new FlatButton(
-                  child: new Text('Back'),
-                  onPressed: makeSetStateCallback(
-                      logic_croupier.CroupierState.Welcome))
-            ]));
+            child: _buildArrangePlayers());
       case logic_croupier.CroupierState.PlayGame:
         return new Container(
             padding: new EdgeDims.only(top: ui.window.padding.top),
@@ -134,16 +128,23 @@
   // shown if the person has not sat down yet.
   Widget _buildPlayerProfiles(bool needsArrangement) {
     List<Widget> profileWidgets = new List<Widget>();
+    double size = 125.0;
     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));
+        profileWidgets.add(new CroupierProfileComponent(
+            settings: cs, height: size, width: size));
       }
     });
 
-    return new Grid(profileWidgets, maxChildExtent: 120.0);
+    if (needsArrangement) {
+      return new ScrollableViewport(
+          child: new Row(profileWidgets),
+          scrollDirection: ScrollDirection.horizontal);
+    }
+    return new Grid(profileWidgets, maxChildExtent: size);
   }
 
   Widget _buildArrangePlayers() {
@@ -152,34 +153,60 @@
     logic_game.GameArrangeData gad = config.croupier.game.gameArrangeData;
     Iterable<int> playerNumbers = config.croupier.players_found.values;
 
+    allWidgets.add(new Flexible(
+        flex: 0,
+        child: new Row([
+          new Text("${config.croupier.game.gameTypeName}",
+              style: style.Text.hugeStyle)
+        ], justifyContent: FlexJustifyContent.spaceAround)));
+
+    // Then show the profile widgets of those who have joined the game.
+    allWidgets.add(new Flexible(flex: 0, child: new Text("Player List")));
+    allWidgets.add(new Flexible(
+        flex: 1, child: _buildPlayerProfiles(gad.needsArrangement)));
+
+    if (gad.needsArrangement) {
+      // Games that need arrangement can show their game arrange component.
+      allWidgets.add(component_game.createGameArrangeComponent(config.croupier,
+          width: ui.window.size.width, height: ui.window.size.height / 2));
+    }
+
     // Allow games that can start with these players to begin.
     // Debug Mode should also go through.
-    NoArgCb onPressed;
+    NoArgCb startCb;
     if (gad.canStart(playerNumbers) || config.croupier.debugMode) {
-      onPressed = () {
-        makeSetStateCallback(logic_croupier.CroupierState.PlayGame)();
+      startCb = () {
+        config.croupier.settings_manager
+            .setGameStatus(config.croupier.game.gameID, "RUNNING");
 
         // 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));
+    if (config.croupier.game.isCreator) {
+      allWidgets.add(new Flexible(
+          flex: 0,
+          child: new Row([
+            new Container(
+                decoration: new BoxDecoration(
+                    backgroundColor: startCb != null
+                        ? style.theme.accentColor
+                        : Colors.grey[300]),
+                padding: new EdgeDims.all(10.0),
+                child: new FlatButton(
+                    child: new Text("Start Game", style: style.Text.hugeStyle),
+                    onPressed: startCb))
+          ], justifyContent: FlexJustifyContent.spaceAround)));
     }
-
-    // Then show the profile widgets of those who have joined the game.
-    allWidgets.add(_buildPlayerProfiles(gad.needsArrangement));
+    allWidgets.add(new Flexible(
+        flex: 0,
+        child: new FlatButton(
+            child: new Text('Back'),
+            onPressed:
+                makeSetStateCallback(logic_croupier.CroupierState.Welcome))));
 
     return new Column(allWidgets);
   }
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index c3f03e5..20ed05f 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -612,6 +612,9 @@
   Widget _buildSlot(String name, int index) {
     NoArgCb onTap = () {
       croupier.settings_manager.setPlayerNumber(croupier.game.gameID, index);
+      HeartsGame game = croupier.game;
+      game.playerNumber = index;
+      game.setReadyUI();
     };
     Widget slotWidget = new Text(name, style: style.Text.hugeStyle);
 
@@ -632,7 +635,7 @@
                 color: croupier.game.playerNumber == index
                     ? style.theme.accentColor
                     : null,
-                child: new Center(child: slotWidget)),
+                child: slotWidget),
             onTap: onTap));
   }
 }
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 98dc9a6..70747ac 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -38,8 +38,12 @@
     settings_everyone = new Map<int, CroupierSettings>();
     games_found = new Map<String, GameStartData>();
     players_found = new Map<int, int>();
-    settings_manager = new SettingsManager(appSettings,
-        _updateSettingsEveryoneCb, _updateGamesFoundCb, _updatePlayerFoundCb);
+    settings_manager = new SettingsManager(
+        appSettings,
+        _updateSettingsEveryoneCb,
+        _updateGamesFoundCb,
+        _updatePlayerFoundCb,
+        _updateGameStatusCb);
 
     settings_manager.load().then((String csString) {
       settings = new CroupierSettings.fromJSONString(csString);
@@ -107,6 +111,22 @@
     }
   }
 
+  void _updateGameStatusCb(String statusKey, String newStatus) {
+    switch (newStatus) {
+      case "RUNNING":
+        if (state == CroupierState.ArrangePlayers) {
+          game.startGameSignal();
+          state = CroupierState.PlayGame;
+        }
+        break;
+      default:
+        print("Ignoring new status: ${newStatus}");
+    }
+    if (this.informUICb != null) {
+      this.informUICb();
+    }
+  }
+
   // Sets the next part of croupier state.
   // Depending on the originating state, data can contain extra information that we need.
   void setState(CroupierState nextState, var data) {
@@ -190,6 +210,7 @@
     if (nextState == CroupierState.Welcome) {
       games_found.clear();
       players_found.clear();
+      game = null;
     } else if (nextState == CroupierState.JoinGame) {
       // Start scanning for games since that's what's next for you.
       _scanFuture =
diff --git a/lib/logic/croupier_settings.dart b/lib/logic/croupier_settings.dart
index 14fc3a6..c83f3c9 100644
--- a/lib/logic/croupier_settings.dart
+++ b/lib/logic/croupier_settings.dart
@@ -127,7 +127,7 @@
 
   // Return a random user id.
   static int get userID {
-    return new math.Random().nextInt(0xffffffff);
+    return new math.Random().nextInt(0xffffff);
   }
 
   // Return a random image name.
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index 9461a46..b33606e 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -275,7 +275,6 @@
     if (this.debugMode && this.playerNumber < 0) {
       this.playerNumber = 0;
     }
-    setReadyUI();
     if (!this.isPlayer) {
       this.viewType = HeartsType.Board;
     }
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
index f96ab8c..533fead 100644
--- a/lib/src/mocks/settings_manager.dart
+++ b/lib/src/mocks/settings_manager.dart
@@ -13,9 +13,14 @@
   final util.keyValueCallback updateCallback;
   final util.keyValueCallback updateGamesCallback;
   final util.keyValueCallback updatePlayerFoundCallback;
+  final util.keyValueCallback updateGameStatusCallback;
 
-  SettingsManager(settings_client.AppSettings _, this.updateCallback,
-      this.updateGamesCallback, this.updatePlayerFoundCallback);
+  SettingsManager(
+      settings_client.AppSettings _,
+      this.updateCallback,
+      this.updateGamesCallback,
+      this.updatePlayerFoundCallback,
+      this.updateGameStatusCallback);
 
   Map<String, String> _data = new Map<String, String>();
 
@@ -68,4 +73,8 @@
   Future setPlayerNumber(int gameID, int playerNumber) async {
     return new Future(() => null);
   }
+
+  Future setGameStatus(int gameID, String status) async {
+    return new Future(() => null);
+  }
 }
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 64b419b..790b43c 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -31,6 +31,7 @@
   final util.keyValueCallback updateSettingsCallback;
   final util.keyValueCallback updateGamesCallback;
   final util.keyValueCallback updatePlayerFoundCallback;
+  final util.keyValueCallback updateGameStatusCallback;
   final CroupierClient _cc;
   sc.SyncbaseTable tb;
 
@@ -42,7 +43,8 @@
       settings_client.AppSettings appSettings,
       this.updateSettingsCallback,
       this.updateGamesCallback,
-      this.updatePlayerFoundCallback)
+      this.updatePlayerFoundCallback,
+      this.updateGameStatusCallback)
       : _cc = new CroupierClient(appSettings);
 
   String _settingsDataKey(int userID) {
@@ -150,8 +152,9 @@
 
   // This watch method ensures that any changes are propagated to the caller.
   // In this case, we're forwarding any player changes to the Croupier logic.
-  Future _startWatchPlayers(Stream<sc.WatchChange> watchStream) async {
-    util.log('Players watching for changes...');
+  // We also catch the game status signals.
+  Future _startWatchGame(Stream<sc.WatchChange> watchStream) async {
+    util.log('Game watching for changes...');
     // This stream never really ends, so I guess we'll watch forever.
     await for (sc.WatchChange wc in watchStream) {
       assert(wc.tableName == util.tableNameGames);
@@ -170,24 +173,30 @@
           assert(false);
       }
 
-      if (this.updatePlayerFoundCallback != null) {
-        String playerID = _getPartFromBack(key, "/", 1);
-        String type = _getPartFromBack(key, "/", 0);
-        switch (type) {
-          case "player_number":
-            // Update the player number for this player.
-            this.updatePlayerFoundCallback(playerID, value);
-            break;
-          case "settings_sg":
-            // Join this player's settings syncgroup.
-            _cc.joinSyncgroup(value);
+      if (key.indexOf("/players") != -1) {
+        if (this.updatePlayerFoundCallback != null) {
+          String playerID = _getPartFromBack(key, "/", 1);
+          String type = _getPartFromBack(key, "/", 0);
+          switch (type) {
+            case "player_number":
+              // Update the player number for this player.
+              this.updatePlayerFoundCallback(playerID, value);
+              break;
+            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}");
-            assert(false);
+              // Also, signal that this player has been found.
+              this.updatePlayerFoundCallback(playerID, null);
+              break;
+            default:
+              print("Unexpected key: ${key} with value ${value}");
+              assert(false);
+          }
+        }
+      } else if (key.indexOf("/status") != -1) {
+        if (this.updateGameStatusCallback != null) {
+          this.updateGameStatusCallback(key, value);
         }
       }
     }
@@ -200,9 +209,9 @@
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
     // Watch for the players in the game.
-    Stream<sc.WatchChange> watchStream = db.watch(util.tableNameGames,
-        util.syncgamePrefix(gameID) + "/players", UTF8.encode("now"));
-    _startWatchPlayers(watchStream); // Don't wait for this future.
+    Stream<sc.WatchChange> watchStream = db.watch(
+        util.tableNameGames, util.syncgamePrefix(gameID), UTF8.encode("now"));
+    _startWatchGame(watchStream); // Don't wait for this future.
 
     print("Now writing to some rows of ${gameID}");
     // Start up the table and write yourself as player 0.
@@ -228,14 +237,15 @@
   Future joinGameSyncgroup(String sgName, int gameID) async {
     print("Now joining game syncgroup at ${sgName} and ${gameID}");
 
-    await _cc.joinSyncgroup(sgName);
     sc.SyncbaseDatabase db = await _cc.createDatabase();
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
     // Watch for the players in the game.
-    Stream<sc.WatchChange> watchStream = db.watch(util.tableNameGames,
-        util.syncgamePrefix(gameID) + "/players", UTF8.encode("now"));
-    _startWatchPlayers(watchStream); // Don't wait for this future.
+    Stream<sc.WatchChange> watchStream = db.watch(
+        util.tableNameGames, util.syncgamePrefix(gameID), UTF8.encode("now"));
+    _startWatchGame(watchStream); // Don't wait for this future.
+
+    await _cc.joinSyncgroup(sgName);
 
     int id = await _getUserID();
     await gameTable
@@ -253,6 +263,13 @@
         .put(UTF8.encode("${playerNumber}"));
   }
 
+  Future setGameStatus(int gameID, String status) async {
+    sc.SyncbaseDatabase db = await _cc.createDatabase();
+    sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
+
+    await gameTable.row("${gameID}/status").put(UTF8.encode(status));
+  }
+
   // When starting the settings manager, there may be settings already in the
   // store. Make sure to load those.
   Future _loadSettings(sc.SyncbaseTable tb) async {
diff --git a/manifest.yaml b/manifest.yaml
index ec47bfb..bfa1022 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -3,6 +3,7 @@
   - name: action/build
   - name: action/help
   - name: action/settings
+  - name: av/play_arrow
   - name: navigation/arrow_back
   - name: navigation/menu
 assets:
diff --git a/schema.md b/schema.md
index d5aafcd..8c01264 100644
--- a/schema.md
+++ b/schema.md
@@ -13,6 +13,7 @@
 For advertising and setting up the game:
 <game_id>/type = <game_type>
 <game_id>/owner = <user_id>
+<game_id>/status = [null|RUNNING]
 <game_id>/players/<user_id>/player_number = <player_number>
 <game_id>/players/<user_id>/settings_sg = <settings_syncgroup_name>