TBR croupier: Add GLOBAL support and new Arrange Players

(Sorry for combining 2 changes.)

GLOBAL support was added to the Makefile.
- If you say GLOBAL=1 when running, then you will mount on the global
  mount table and use the proxy.

New Arrange Players
- Now, we primarily arrange players on the Table Device
- After being prompted (player vs table), we allow the Table to place
  each player who joined the game.
- If there is no Table, the owner can still fallback to the old
  Arrange Players behavior.

NOTE: Presubmit will not pass in the short term.
https://github.com/vanadium/issues/issues/1093

So I've had to prepend TBR to the front of this CL.

Change-Id: Ic0486554444819da991a52130143749fff3dc04d
diff --git a/Makefile b/Makefile
index 2dc7152..bc066e0 100644
--- a/Makefile
+++ b/Makefile
@@ -21,13 +21,20 @@
 	DEVICE_ID := $(shell adb devices | sed -n $(ANDROID_PLUS_ONE)p | awk '{ print $$1; }')
 endif
 
+# The default mount table location. Note: May not always exist.
+MOUNTTABLE := /192.168.86.254:8101
+
+# If defined, use the proxy and global mount table.
+ifdef GLOBAL
+	PROXY_FLAG := --v23.proxy=/ns.dev.v.io:8101/proxy
+	MOUNTTABLE := /ns.dev.v.io:8101/tmp/croupier
+endif
+
 ifdef ANDROID
 	MOJO_ANDROID_FLAGS := --android
 	SYNCBASE_MOJO_BIN_DIR := packages/syncbase/mojo_services/android
 	DISCOVERY_MOJO_BIN_DIR := packages/v23discovery/mojo_services/android
 
-	# Location of mounttable on syncslides-alpha network.
-	MOUNTTABLE := /192.168.86.254:8101
 	# Name to mount under.
 	SYNCBASE_NAME_FLAG := --name=$(MOUNTTABLE)/croupier-$(DEVICE_ID)
 	SYNCBASE_FLAGS += $(SYNCBASE_NAME_FLAG)
@@ -37,13 +44,8 @@
 
 	SYNCBASE_FLAGS += --logtostderr=true \
 		--root-dir=$(APP_HOME_DIR)/syncbase_data \
-		--v23.credentials=$(ANDROID_CREDS_DIR)
-
-# If this is not the first mojo shell, then you must reuse the dev servers
-# to avoid a "port in use" error.
-ifneq ($(shell fuser 31841/tcp),)
-	REUSE_FLAG := --reuse-servers
-endif
+		--v23.credentials=$(ANDROID_CREDS_DIR) \
+		$(PROXY_FLAG)
 
 else
 	SYNCBASE_MOJO_BIN_DIR := packages/syncbase/mojo_services/linux_amd64
@@ -51,12 +53,18 @@
 	SYNCBASE_FLAGS += --root-dir=$(PWD)/tmp/syncbase_data --v23.credentials=$(PWD)/creds
 endif
 
+# If this is not the first mojo shell, then you must reuse the dev servers
+# to avoid a "port in use" error.
+ifneq ($(shell fuser 31841/tcp),)
+	REUSE_FLAG := --reuse-servers
+endif
+
 # We should consider combining these URLs and hosting our *.mojo files.
 # https://github.com/vanadium/issues/issues/834
 export SYNCBASE_SERVER_URL := https://mojo.v.io/syncbase_server.mojo
 export DISCOVERY_SERVER_URL := https://mojo2.v.io/discovery.mojo
 MOJO_SHELL_FLAGS := --enable-multiprocess \
-	--map-origin="https://mojo2.v.io=$(DISCOVERY_MOJO_BIN_DIR)" --args-for="$(DISCOVERY_SERVER_URL) host$(DEVICE_ID) mdns" \
+	--map-origin="https://mojo2.v.io=$(DISCOVERY_MOJO_BIN_DIR)" --args-for="$(DISCOVERY_SERVER_URL) host$(DEVICE_ID)" \
 	--map-origin="https://mojo.v.io=$(SYNCBASE_MOJO_BIN_DIR)" --args-for="$(SYNCBASE_SERVER_URL) $(SYNCBASE_FLAGS)"
 
 ifdef ANDROID
@@ -164,7 +172,7 @@
 # Creates a shortcut on the phone that runs the hosted version of croupier.flx
 # Does nothing if ANDROID is not defined.
 define GENERATE_SHORTCUT_FILE
-	sed -e "s;%DEVICE_ID%;$1;g" -e "s;%SYNCBASE_NAME_FLAG%;$2;g" \
+	sed -e "s;%DEVICE_ID%;$1;g" -e "s;%SYNCBASE_NAME_FLAG%;$2;g" -e "s;%PROXY_FLAG%;$3;g" \
 	shortcut_template > shortcut_commands
 endef
 
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 3ed6e9e..038e804 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -269,10 +269,8 @@
   }
 
   Widget _getProfile(int pNum, double sizeFactor) {
-    return new CroupierProfileComponent(
-        settings: config.croupier.settingsFromPlayerNumber(pNum),
-        height: config.height * sizeFactor,
-        width: config.height * sizeFactor * 1.5);
+    return new CroupierProfileComponent.horizontal(
+        settings: config.croupier.settingsFromPlayerNumber(pNum));
   }
 
   Widget _playerProfile(int pNum, double sizeFactor) {
@@ -365,12 +363,11 @@
           top: 0.0,
           left: 0.0,
           child: new IgnorePointer(
-              child: new CroupierProfileComponent(
+              child: new CroupierProfileComponent.mini(
                   settings:
                       config.croupier.settingsFromPlayerNumber(playerNumber),
                   height: config.cardHeight,
-                  width: config.cardWidth,
-                  isMini: true))));
+                  width: config.cardWidth))));
     }
 
     return new Container(
@@ -396,8 +393,16 @@
         ? config.game.determineTrickWinner()
         : config.game.whoseTurn;
 
+    // You can tap anywhere on the board to "Ask" or "Take Trick".
+    NoArgCb tapCb;
+    if (!config.game.asking && !config.game.allPlayed) {
+      tapCb = config.game.askUI;
+    } else if (config.game.allPlayed) {
+      tapCb = config.game.takeTrickUI;
+    }
+
     return new GestureDetector(
-        onTap: config.game.asking ? null : config.game.askUI,
+        onTap: tapCb,
         child: new Container(
             height: config.height,
             width: config.width,
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index ffb5664..910e776 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -19,6 +19,7 @@
 typedef void NoArgCb();
 
 GlobalObjectKey _gameKey = new GlobalObjectKey("CroupierGameKey");
+GlobalObjectKey _gameArrangeKey = new GlobalObjectKey("CroupierGameArrangeKey");
 
 class CroupierComponent extends StatefulComponent {
   final logic_croupier.Croupier croupier;
@@ -61,8 +62,8 @@
                       : null),
               new CroupierProfileComponent(
                   settings: config.croupier.settings,
-                  width: style.Size.settingsSize,
-                  height: style.Size.settingsSize)
+                  width: style.Size.settingsWidth,
+                  height: style.Size.settingsHeight)
             ]));
       case logic_croupier.CroupierState.ChooseGame:
         // in which we let them pick a game out of the many possible games... There aren't that many.
@@ -151,24 +152,23 @@
   // shown if the person has not sat down yet.
   Widget _buildPlayerProfiles(bool needsArrangement) {
     List<Widget> profileWidgets = new List<Widget>();
-    double size = style.Size.settingsSize;
     config.croupier.players_found.forEach((int userID, int playerNumber) {
-      if (!needsArrangement || playerNumber == null) {
-        // Note: Even if cs is null, a placeholder will be shown instead.
-        CroupierSettings cs = config.croupier.settings_everyone[userID];
-        bool isMe = config.croupier.settings.userID == userID;
-        Widget cpc = new Container(
-            decoration: isMe ? style.Box.liveNow : null,
-            child: new CroupierProfileComponent(
-                settings: cs, height: size, width: size));
+      // Note: Even if cs is null, a placeholder will be shown instead.
+      CroupierSettings cs = config.croupier.settings_everyone[userID];
+      bool isMe = config.croupier.settings.userID == userID;
+      Widget cpc = new Container(
+          decoration: isMe ? style.Box.liveNow : null,
+          child: new CroupierProfileComponent(
+              settings: cs,
+              height: style.Size.settingsHeight,
+              width: style.Size.settingsWidth));
 
-        // If the player profiles can be arranged, they should be draggable too.
-        if (needsArrangement) {
-          profileWidgets.add(new Draggable<CroupierSettings>(
-              child: cpc, feedback: cpc, data: cs));
-        } else {
-          profileWidgets.add(cpc);
-        }
+      // If the player profiles can be arranged, they should be draggable too.
+      if (needsArrangement) {
+        profileWidgets.add(new Draggable<CroupierSettings>(
+            child: cpc, feedback: cpc, data: cs));
+      } else {
+        profileWidgets.add(cpc);
       }
     });
 
@@ -177,7 +177,8 @@
           child: new Row(profileWidgets),
           scrollDirection: ScrollDirection.horizontal);
     }
-    return new MaxTileWidthGrid(profileWidgets, maxTileWidth: size);
+    return new MaxTileWidthGrid(profileWidgets,
+        maxTileWidth: style.Size.settingsWidth);
   }
 
   Widget _buildArrangePlayers() {
@@ -202,7 +203,8 @@
       // Games that need arrangement can show their game arrange component.
       allWidgets.add(component_game.createGameArrangeComponent(config.croupier,
           width: ui.window.size.width * 0.90,
-          height: ui.window.size.height * 0.50));
+          height: ui.window.size.height * 0.50,
+          key: _gameArrangeKey));
     }
 
     // Allow games that can start with these players to begin.
@@ -224,15 +226,10 @@
       allWidgets.add(new Flexible(
           flex: 0,
           child: new Row([
-            new Container(
-                decoration: new BoxDecoration(
-                    backgroundColor: startCb != null
-                        ? style.theme.accentColor
-                        : Colors.grey[300]),
-                padding: style.Spacing.smallPadding,
-                child: new FlatButton(
-                    child: new Text("Start Game", style: style.Text.hugeStyle),
-                    onPressed: startCb))
+            new FlatButton(
+                child: new Text("Start Game", style: style.Text.hugeStyle),
+                onPressed: startCb,
+                color: style.theme.accentColor)
           ], justifyContent: FlexJustifyContent.spaceAround)));
     }
     allWidgets.add(new Flexible(
diff --git a/lib/components/croupier_game_advertisement.dart b/lib/components/croupier_game_advertisement.dart
index 545f028..07a960c 100644
--- a/lib/components/croupier_game_advertisement.dart
+++ b/lib/components/croupier_game_advertisement.dart
@@ -24,7 +24,11 @@
     return new GestureDetector(
         child: new Card(
             child: new Row([
-          new Card(child: new CroupierProfileComponent(settings: settings)),
+          new Card(
+              child: new CroupierProfileComponent(
+                  settings: settings,
+                  height: style.Size.settingsHeight,
+                  width: style.Size.settingsWidth)),
           new Text(game.gameTypeToString(gameStartData.gameType),
               style: style.Text.hugeStyle),
         ])),
diff --git a/lib/components/croupier_profile.dart b/lib/components/croupier_profile.dart
index ffd0869..b406546 100644
--- a/lib/components/croupier_profile.dart
+++ b/lib/components/croupier_profile.dart
@@ -5,44 +5,93 @@
 import 'package:flutter/material.dart';
 
 import '../logic/croupier_settings.dart' show CroupierSettings;
+import '../styles/common.dart' as style;
+
+enum CroupierProfileComponentOrientation {
+  DEFAULT,
+  MINI,
+  HORIZONTAL,
+  TEXT_ONLY
+}
 
 class CroupierProfileComponent extends StatelessComponent {
   final CroupierSettings settings;
   final double height;
   final double width;
-  final bool isMini;
+  final CroupierProfileComponentOrientation orientation;
 
   static const double padAmount = 4.0;
 
   CroupierProfileComponent(
-      {CroupierSettings settings, this.height, this.width, this.isMini: false})
+      {CroupierSettings settings,
+      this.height,
+      this.width,
+      this.orientation: CroupierProfileComponentOrientation.DEFAULT})
       : settings = settings ?? new CroupierSettings.placeholder();
 
+  CroupierProfileComponent.mini(
+      {CroupierSettings settings, double height, double width})
+      : this(
+            settings: settings,
+            height: height,
+            width: width,
+            orientation: CroupierProfileComponentOrientation.MINI);
+
+  CroupierProfileComponent.horizontal({CroupierSettings settings})
+      : this(
+            settings: settings,
+            orientation: CroupierProfileComponentOrientation.HORIZONTAL);
+
+  CroupierProfileComponent.textOnly({CroupierSettings settings})
+      : this(
+            settings: settings,
+            orientation: CroupierProfileComponentOrientation.TEXT_ONLY);
+
   Widget build(BuildContext context) {
-    if (!isMini) {
-      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)
-              ], justifyContent: FlexJustifyContent.spaceAround)));
-    } else {
-      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),
-                    fit: ImageFit.scaleDown)
-              ], justifyContent: FlexJustifyContent.spaceAround)));
+    switch (orientation) {
+      case CroupierProfileComponentOrientation.DEFAULT:
+        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.largeStyle)
+                ], justifyContent: FlexJustifyContent.spaceAround)));
+      case CroupierProfileComponentOrientation.MINI:
+        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),
+                      fit: ImageFit.scaleDown)
+                ], justifyContent: FlexJustifyContent.spaceAround)));
+      case CroupierProfileComponentOrientation.HORIZONTAL:
+        return new Card(
+            color: new Color(settings.color),
+            child: new Container(
+                padding: const EdgeDims.all(padAmount),
+                child: new Row([
+                  new AssetImage(
+                      name: CroupierSettings.makeAvatarUrl(settings.avatar),
+                      fit: ImageFit.scaleDown),
+                  new Text(settings.name, style: style.Text.hugeStyle)
+                ], justifyContent: FlexJustifyContent.collapse)));
+      case CroupierProfileComponentOrientation.TEXT_ONLY:
+        return new Card(
+            color: new Color(settings.color),
+            child: new Container(
+                padding: const EdgeDims.all(padAmount),
+                child: new Row(
+                    [new Text(settings.name, style: style.Text.largeStyle)],
+                    justifyContent: FlexJustifyContent.collapse)));
     }
   }
 }
diff --git a/lib/components/game.dart b/lib/components/game.dart
index bbe1bcf..591c92e 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -222,18 +222,20 @@
   }
 }
 
-abstract class GameArrangeComponent extends StatelessComponent {
+abstract class GameArrangeComponent extends StatefulComponent {
   final Croupier croupier;
   final double width;
   final double height;
-  GameArrangeComponent(this.croupier, {this.width, this.height});
+  GameArrangeComponent(this.croupier, {this.width, this.height, Key key})
+      : super(key: key);
 }
 
 GameArrangeComponent createGameArrangeComponent(Croupier croupier,
-    {double width, double height}) {
+    {double width, double height, Key key}) {
   switch (croupier.game.gameType) {
     case GameType.Hearts:
-      return new HeartsArrangeComponent(croupier, width: width, height: height);
+      return new HeartsArrangeComponent(croupier,
+          width: width, height: height, key: key);
     default:
       // We can't arrange this game.
       throw new UnimplementedError(
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 066cbec..3120772 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -821,50 +821,196 @@
 }
 
 class HeartsArrangeComponent extends GameArrangeComponent {
-  HeartsArrangeComponent(Croupier croupier, {double width, double height})
-      : super(croupier, width: width, height: height);
+  HeartsArrangeComponent(Croupier croupier,
+      {double width, double height, Key key})
+      : super(croupier, width: width, height: height, key: key);
+
+  HeartsArrangeComponentState createState() =>
+      new HeartsArrangeComponentState();
+}
+
+class HeartsArrangeComponentState extends State<HeartsArrangeComponent> {
+  bool isTable = null; // Must first ask what kind of device this is.
+  bool fallback = false; // Set to true to switch to the old arrange players.
+
+  static final String personIcon = "social/person_outline";
+  static final String tableIcon = "hardware/tablet";
 
   Widget build(BuildContext context) {
-    int numAtTable = croupier.players_found.values
-        .where((int playerNumber) => playerNumber == 4)
-        .length;
+    if (isTable == null) {
+      return _buildAskDeviceType();
+    }
+    if (isTable) {
+      return _buildTableArrangePlayers();
+    }
+    if (fallback) {
+      return _buildFallbackArrangePlayers();
+    }
+    return _buildPlayerArrangePlayers();
+  }
+
+  Widget _buildAskDeviceType() {
+    return new Container(
+        decoration: style.Box.liveNow,
+        height: config.height,
+        width: config.width,
+        child: new Column([
+          new Text("Play Hearts as a...", style: style.Text.hugeStyle),
+          new FlatButton(
+              child: new Row([
+                new Icon(size: IconSize.s48, icon: personIcon),
+                new Text("Player", style: style.Text.largeStyle)
+              ], justifyContent: FlexJustifyContent.collapse),
+              color: style.secondaryTextColor, onPressed: () {
+            setState(() {
+              isTable = false;
+            });
+          }),
+          new FlatButton(
+              child: new Row([
+                new Icon(size: IconSize.s48, icon: tableIcon),
+                new Text("Table", style: style.Text.largeStyle)
+              ], justifyContent: FlexJustifyContent.collapse),
+              color: style.secondaryTextColor, onPressed: () {
+            setState(() {
+              isTable = true;
+            });
+            // Also sit at the table.
+            config.croupier.settings_manager.setPlayerNumber(
+                config.croupier.game.gameID,
+                config.croupier.settings.userID,
+                4);
+          })
+        ], alignItems: FlexAlignItems.center));
+  }
+
+  int get numSitting => config.croupier.players_found.values.where((int index) {
+        return index != null && index >= 0 && index < 4;
+      }).length;
+
+  Widget _buildTableArrangePlayers() {
+    int arrangeID = null;
+    String status = "";
+    bool canStart = false;
+    if (numSitting < 4) {
+      // We still need people to sit.
+      // Only include non-table and non-sitting devices in this search.
+      // Note that this means it's possible arrangeID is null.
+      arrangeID = config.croupier.players_found.keys.firstWhere((int playerID) {
+        int index = config.croupier.players_found[playerID];
+        return index == null || index < 0;
+      }, orElse: () => null);
+      status = arrangeID != null ? "Tap to place" : "Waiting for players...";
+    } else {
+      status = "Play Hearts!";
+      canStart = true;
+    }
+
+    List<Widget> children = [new Text(status, style: style.Text.hugeStyle)];
+    // Also add the player's name (using a placeholder if that's not possible).
+    if (arrangeID != null) {
+      children.add(new CroupierProfileComponent.textOnly(
+          settings: config.croupier.settings_everyone[arrangeID]));
+    }
+
+    // You will need to show a Start Game button if the table isn't the creator.
+    // Replace the status text with a status button instead.
+    if (canStart && !config.croupier.game.isCreator) {
+      children = [
+        new FlatButton(
+            child: new Text(status, style: style.Text.hugeStyle),
+            color: style.theme.accentColor, onPressed: () {
+          config.croupier.settings_manager
+              .setGameStatus(config.croupier.game.gameID, "RUNNING");
+        })
+      ];
+    }
+    Widget firstChild =
+        new Row(children, justifyContent: FlexJustifyContent.collapse);
 
     return new Container(
         decoration: style.Box.liveNow,
-        height: height,
-        width: width,
+        height: config.height,
+        width: config.width,
         child: new Column([
-          new Flexible(
-              flex: 1,
-              child: new Row(
-                  [_buildEmptySlot(), _buildSlot("social/person_outline", 2), _buildEmptySlot()],
-                  justifyContent: FlexJustifyContent.spaceAround,
-                  alignItems: FlexAlignItems.stretch)),
-          new Flexible(
-              flex: 1,
-              child: new Row([
-                _buildSlot("social/person_outline", 1),
-                _buildSlot("hardware/tablet", 4, extra: "x${numAtTable}"),
-                _buildSlot("social/person_outline", 3)
-              ],
-                  justifyContent: FlexJustifyContent.spaceAround,
-                  alignItems: FlexAlignItems.stretch)),
-          new Flexible(
-              flex: 1,
-              child: new Row(
-                  [_buildEmptySlot(), _buildSlot("social/person_outline", 0), _buildEmptySlot()],
-                  justifyContent: FlexJustifyContent.spaceAround,
-                  alignItems: FlexAlignItems.stretch))
-        ],
-            justifyContent: FlexJustifyContent.spaceAround,
-            alignItems: FlexAlignItems.stretch));
+          firstChild,
+          new Flexible(child: _buildArrangeTable(activeID: arrangeID))
+        ], alignItems: FlexAlignItems.center));
+  }
+
+  Widget _buildPlayerArrangePlayers() {
+    List<Widget> children = <Widget>[
+      new Text("Waiting for game setup...", style: style.Text.hugeStyle)
+    ];
+    if (config.croupier.game.isCreator) {
+      children.add(new FlatButton(
+          child: new Text("Manual Setup", style: style.Text.largeStyle),
+          color: style.theme.accentColor, onPressed: () {
+        setState(() {
+          fallback = true;
+        });
+      }));
+    }
+
+    return new Container(
+        decoration: style.Box.liveNow,
+        height: config.height,
+        width: config.width,
+        child: new Column(children, alignItems: FlexAlignItems.center));
+  }
+
+  Widget _buildFallbackArrangePlayers() {
+    return new Container(
+        decoration: style.Box.liveNow,
+        height: config.height,
+        width: config.width,
+        child: _buildArrangeTable(canDragTo: true));
+  }
+
+  Widget _buildArrangeTable({int activeID: null, bool canDragTo: false}) {
+    int numAtTable = config.croupier.players_found.values
+        .where((int playerNumber) => playerNumber == 4)
+        .length;
+    return new Column([
+      new Flexible(
+          flex: 1,
+          child: new Row([
+            _buildEmptySlot(),
+            _buildSlot(personIcon, 2, activeID, canDragTo),
+            _buildEmptySlot()
+          ],
+              justifyContent: FlexJustifyContent.spaceAround,
+              alignItems: FlexAlignItems.stretch)),
+      new Flexible(
+          flex: 1,
+          child: new Row([
+            _buildSlot(personIcon, 1, activeID, canDragTo),
+            _buildSlot(tableIcon, 4, activeID, canDragTo,
+                extra: "x${numAtTable}"),
+            _buildSlot(personIcon, 3, activeID, canDragTo)
+          ],
+              justifyContent: FlexJustifyContent.spaceAround,
+              alignItems: FlexAlignItems.stretch)),
+      new Flexible(
+          flex: 1,
+          child: new Row([
+            _buildEmptySlot(),
+            _buildSlot(personIcon, 0, activeID, canDragTo),
+            _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, {String extra: ""}) {
+  Widget _buildSlot(String name, int index, int activeID, bool canDragTo,
+      {String extra: ""}) {
     Widget slotWidget = new Row([
       new Icon(size: IconSize.s48, icon: name),
       new Text(extra, style: style.Text.largeStyle)
@@ -872,29 +1018,36 @@
         alignItems: FlexAlignItems.center,
         justifyContent: FlexJustifyContent.center);
 
-    bool isMe = croupier.game.playerNumber == index;
+    bool isMe = config.croupier.game.playerNumber == index;
     bool isPlayerIndex = index >= 0 && index < 4;
     bool isTableIndex = index == 4;
     bool seatTaken = (isPlayerIndex || (isTableIndex && isMe)) &&
-        croupier.players_found.containsValue(index);
+        config.croupier.players_found.containsValue(index);
     if (seatTaken) {
       // Note: If more than 1 person is in the seat, it may no longer show you.
-      CroupierSettings cs = croupier.settingsFromPlayerNumber(index);
-      CroupierProfileComponent cpc = new CroupierProfileComponent(settings: cs);
+      CroupierSettings cs = config.croupier.settingsFromPlayerNumber(index);
+      CroupierProfileComponent cpc =
+          new CroupierProfileComponent.horizontal(settings: cs);
       slotWidget =
           new Draggable<CroupierSettings>(child: cpc, feedback: cpc, data: cs);
     }
 
     Widget dragTarget = new DragTarget<CroupierSettings>(
         builder: (BuildContext context, List<CroupierSettings> data, _) {
-      return new Container(
-          constraints: const BoxConstraints.expand(),
-          decoration: isMe ? style.Box.liveBackground : style.Box.border,
-          child: slotWidget);
+      return new GestureDetector(onTap: () {
+        if (activeID != null) {
+          config.croupier.settings_manager
+              .setPlayerNumber(config.croupier.game.gameID, activeID, index);
+        }
+      },
+          child: new Container(
+              constraints: const BoxConstraints.expand(),
+              decoration: isMe ? style.Box.liveBackground : style.Box.border,
+              child: new Center(child: slotWidget)));
     }, onAccept: (CroupierSettings cs) {
-      croupier.settings_manager
-          .setPlayerNumber(croupier.game.gameID, cs.userID, index);
-    }, onWillAccept: (_) => true);
+      config.croupier.settings_manager
+          .setPlayerNumber(config.croupier.game.gameID, cs.userID, index);
+    }, onWillAccept: (_) => canDragTo);
 
     return new Flexible(flex: 1, child: dragTarget);
   }
diff --git a/lib/components/main_route.dart b/lib/components/main_route.dart
index 0c402bd..2c4887f 100644
--- a/lib/components/main_route.dart
+++ b/lib/components/main_route.dart
@@ -81,8 +81,8 @@
           child: new BlockBody([
         new CroupierProfileComponent(
             settings: config.croupier.settings,
-            width: style.Size.settingsSize,
-            height: style.Size.settingsSize),
+            width: style.Size.settingsWidth,
+            height: style.Size.settingsHeight),
         new Text('Croupier', style: style.Text.titleStyle)
       ])),
       new DrawerItem(
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 5c33aa2..0970318 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -199,8 +199,6 @@
         _advertiseFuture = settings_manager
             .createGameSyncgroup(gameTypeToString(gt), game.gameID)
             .then((GameStartData gsd) {
-          // The game creator should always sit as player 0, at least initially.
-          settings_manager.setPlayerNumber(gsd.gameID, settings.userID, 0);
           // Only the game chooser should be advertising the game.
           return settings_manager.advertiseSettings(gsd);
         }); // don't wait for this future.
@@ -232,11 +230,7 @@
         assert(sgName != null);
 
         players_found[gsd.ownerID] = null;
-        settings_manager.joinGameSyncgroup(sgName, gsd.gameID).then((_) {
-          if (!game.gameArrangeData.needsArrangement) {
-            settings_manager.setPlayerNumber(gsd.gameID, settings.userID, 0);
-          }
-        });
+        settings_manager.joinGameSyncgroup(sgName, gsd.gameID);
 
         break;
       case CroupierState.ArrangePlayers:
diff --git a/lib/logic/croupier_settings.dart b/lib/logic/croupier_settings.dart
index 0522fed..8f18c3a 100644
--- a/lib/logic/croupier_settings.dart
+++ b/lib/logic/croupier_settings.dart
@@ -151,7 +151,7 @@
     int nameIndex = rng.nextInt(names.length);
     int appIndex = rng.nextInt(appellations.length);
 
-    return "${names[nameIndex]} the ${appellations[appIndex]}";
+    return "${names[nameIndex]} ${appellations[appIndex]}";
   }
 
   // Return something between 0x00000000 and 0xffffffff
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index a2170d1..336c38c 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -207,7 +207,10 @@
 
     // Watch for the players in the game.
     _gameSubscription = await _cc.watchEverything(
-        db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange);
+        db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange,
+        sorter: (sc.WatchChange a, sc.WatchChange b) {
+      return a.rowKey.compareTo(b.rowKey);
+    });
 
     await _cc.joinSyncgroup(sgName);
 
diff --git a/lib/src/syncbase/util.dart b/lib/src/syncbase/util.dart
index c6c8259..83ec86e 100644
--- a/lib/src/syncbase/util.dart
+++ b/lib/src/syncbase/util.dart
@@ -10,9 +10,6 @@
 const String tableNameGames = 'games';
 const String tableNameSettings = 'table_settings';
 
-// TODO(alexfandrianto): This may need to be the global mount table with a
-// proxy. Otherwise, it will be difficult for other users to run.
-// https://github.com/vanadium/issues/issues/782
 String makeSgPrefix(String mounttable, String deviceID) {
   return "${mounttable}/croupier-${deviceID}/%%sync";
 }
diff --git a/lib/styles/common.dart b/lib/styles/common.dart
index 77caba5..f3ba93e 100644
--- a/lib/styles/common.dart
+++ b/lib/styles/common.dart
@@ -23,7 +23,8 @@
 
 class Size {
   static const double splashLogo = 75.0;
-  static const double settingsSize = 125.0;
+  static const double settingsHeight = 125.0;
+  static const double settingsWidth = 175.0;
 }
 
 class Spacing {
diff --git a/pubspec.lock b/pubspec.lock
index f2e179a..0be2f9c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -12,7 +12,7 @@
   args:
     description: args
     source: hosted
-    version: "0.13.2"
+    version: "0.13.3+1"
   asn1lib:
     description: asn1lib
     source: hosted
@@ -20,7 +20,7 @@
   async:
     description: async
     source: hosted
-    version: "1.6.0"
+    version: "1.8.0"
   barback:
     description: barback
     source: hosted
@@ -50,7 +50,7 @@
   collection:
     description: collection
     source: hosted
-    version: "1.2.0"
+    version: "1.4.0"
   contrast:
     description: contrast
     source: hosted
@@ -102,11 +102,11 @@
   github:
     description: github
     source: hosted
-    version: "2.3.1"
+    version: "2.3.1+1"
   glob:
     description: glob
     source: hosted
-    version: "1.0.5"
+    version: "1.1.0"
   html:
     description: html
     source: hosted
@@ -126,7 +126,7 @@
   intl:
     description: intl
     source: hosted
-    version: "0.12.5"
+    version: "0.12.6"
   logging:
     description: logging
     source: hosted
@@ -184,7 +184,7 @@
   petitparser:
     description: petitparser
     source: hosted
-    version: "1.5.0"
+    version: "1.5.1"
   plugin:
     description: plugin
     source: hosted
@@ -240,11 +240,11 @@
   stack_trace:
     description: stack_trace
     source: hosted
-    version: "1.5.1"
+    version: "1.6.0"
   string_scanner:
     description: string_scanner
     source: hosted
-    version: "0.1.4"
+    version: "0.1.4+1"
   syncbase:
     description: syncbase
     source: hosted
@@ -268,7 +268,7 @@
   vector_math:
     description: vector_math
     source: hosted
-    version: "1.4.4"
+    version: "1.4.6"
   watcher:
     description: watcher
     source: hosted
@@ -284,8 +284,8 @@
   xml:
     description: xml
     source: hosted
-    version: "2.4.0"
+    version: "2.4.1"
   yaml:
     description: yaml
     source: hosted
-    version: "2.1.7"
+    version: "2.1.8"
diff --git a/shortcut_template b/shortcut_template
index 015b1db..cb90797 100644
--- a/shortcut_template
+++ b/shortcut_template
@@ -6,6 +6,6 @@
 --args-for=mojo:flutter --enable-checked-mode
 --enable-multiprocess
 --map-origin=https://mojo2.v.io=https://storage.googleapis.com/mojo_services/v23discovery/mojo_services/android/
---args-for=https://mojo2.v.io/discovery.mojo host%DEVICE_ID% mdns
+--args-for=https://mojo2.v.io/discovery.mojo host%DEVICE_ID%
 --map-origin=https://mojo.v.io=https://storage.googleapis.com/mojo_services/syncbase/mojo_services/android/
---args-for=https://mojo.v.io/syncbase_server.mojo --v=0 --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbase_data --v23.credentials=/sdcard/v23creds %SYNCBASE_NAME_FLAG%
+--args-for=https://mojo.v.io/syncbase_server.mojo --v=0 --logtostderr=true --root-dir=/data/data/org.chromium.mojo.shell/app_home/syncbase_data --v23.credentials=/sdcard/v23creds %SYNCBASE_NAME_FLAG% %PROXY_FLAG%