Merge "Fixing bug with unset card.Initial(). Allowing for restart without restarting syncbase"
diff --git a/lib/components/card.dart b/lib/components/card.dart
index a0bf42c..c222020 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 'dart:async';
-
 import 'package:flutter/animation.dart';
 import 'package:flutter/material.dart' as widgets;
 import 'package:flutter/rendering.dart';
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 70747ac..e3de247 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -6,6 +6,7 @@
 
 import '../settings/client.dart' show AppSettings;
 import '../src/syncbase/settings_manager.dart' show SettingsManager;
+import '../src/syncbase/util.dart' as sync_util;
 import 'create_game.dart' as cg;
 import 'croupier_settings.dart' show CroupierSettings;
 import 'game/game.dart'
@@ -90,7 +91,12 @@
     return null;
   }
 
-  void _updatePlayerFoundCb(String playerID, String playerNum) {
+  void _updatePlayerFoundCb(String playerKey, String playerNum) {
+    String gameIDStr = sync_util.gameIDFromGameKey(playerKey);
+    if (game == null || game.gameID != int.parse(gameIDStr)) {
+      return; // ignore
+    }
+    String playerID = sync_util.playerIDFromPlayerKey(playerKey);
     int id = int.parse(playerID);
     if (playerNum == null) {
       if (!players_found.containsKey(id)) {
@@ -112,11 +118,15 @@
   }
 
   void _updateGameStatusCb(String statusKey, String newStatus) {
+    String gameIDStr = sync_util.gameIDFromGameKey(statusKey);
+    if (game == null || game.gameID != int.parse(gameIDStr)) {
+      return; // ignore
+    }
     switch (newStatus) {
       case "RUNNING":
         if (state == CroupierState.ArrangePlayers) {
           game.startGameSignal();
-          state = CroupierState.PlayGame;
+          setState(CroupierState.PlayGame, null);
         }
         break;
       default:
@@ -194,12 +204,12 @@
           });
         }
 
+        // The signal to start or quit is not anything special.
         // data should be empty.
-        // All rearrangements affect the Game's player number without changing app state.
+        assert(data == null);
         break;
       case CroupierState.PlayGame:
         // data should be empty.
-        // The signal to start really isn't anything special.
         break;
       default:
         assert(false);
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index b33606e..477de00 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -278,6 +278,10 @@
     if (!this.isPlayer) {
       this.viewType = HeartsType.Board;
     }
+    // Only the creator should deal the cards once everyone is ready.
+    if (this.isCreator) {
+      this.dealCards();
+    }
   }
 
   // Note that this will be called by the UI.
@@ -327,10 +331,6 @@
           this.resetGame();
 
           print('we are all ready. ${isCreator}');
-          // Only the creator should deal the cards once everyone is ready.
-          if (this.isCreator) {
-            this.dealCards();
-          }
         }
         return;
       case HeartsPhase.Deal:
diff --git a/lib/src/mocks/util.dart b/lib/src/mocks/util.dart
index e466b8d..0413ea1 100644
--- a/lib/src/mocks/util.dart
+++ b/lib/src/mocks/util.dart
@@ -3,3 +3,11 @@
 // license that can be found in the LICENSE file.
 
 typedef void keyValueCallback(String key, String value);
+
+String gameIDFromGameKey(String gameKey) {
+  return null;
+}
+
+String playerIDFromPlayerKey(String playerKey) {
+  return null;
+}
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
index 3e58094..bfb278e 100644
--- a/lib/src/syncbase/log_writer.dart
+++ b/lib/src/syncbase/log_writer.dart
@@ -176,17 +176,6 @@
       await _writeData(propKey, proposalData);
       proposalsKnown[propKey] = proposalData;
 
-      // TODO(alexfandrianto): Remove when we have 4 players going at once.
-      // For quick development purposes, we may wish to keep this block.
-      // FAKE: Do some bonus work. Where "everyone else" accepts the proposal.
-      // Normally, one would rely on watch and the syncgroup peers to do this.
-      /*for (int i = 0; i < users.length; i++) {
-        if (users[i] != associatedUser) {
-          // DO NOT AWAIT HERE. It must be done "asynchronously".
-          _writeData(_proposalKey(users[i]), proposalData);
-        }
-      }*/
-
       return;
     }
     await _writeData(key, value);
@@ -194,26 +183,16 @@
 
   // Helper that writes data to the "store" and calls the update callback.
   Future _writeData(String key, String value) async {
-    var row = tb.row("${this.logPrefix}/${key}");
+    var row = tb.row(_rowKey(key));
     await row.put(UTF8.encode(value));
   }
 
-  /*
-  // _readData could be helpful eventually, but it's not needed yet.
-  Future<String> _readData(String key) async {
-    var row = tb.row("${this.logPrefix}/${key}");
-    if (!(await row.exists())) {
-      print("${key} did not exist");
-      return null;
-    }
-    var getBytes = await row.get();
-
-    return UTF8.decode(getBytes);
+  String _rowKey(String key) {
+    return "${this.logPrefix}/${key}";
   }
-  */
 
   Future _deleteData(String key) async {
-    var row = tb.row(key);
+    var row = tb.row(_rowKey(key));
     await row.delete();
   }
 
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 790b43c..32f63a0 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -35,9 +35,7 @@
   final CroupierClient _cc;
   sc.SyncbaseTable tb;
 
-  static const String _discoverySettingsKey = "settings";
-  static const String _personalKey = "personal";
-  static const String _settingsWatchSyncPrefix = "users";
+  static const String _discoveryGameAdKey = "discovery-game-ad";
 
   SettingsManager(
       settings_client.AppSettings appSettings,
@@ -47,15 +45,6 @@
       this.updateGameStatusCallback)
       : _cc = new CroupierClient(appSettings);
 
-  String _settingsDataKey(int userID) {
-    return "${_settingsWatchSyncPrefix}/${userID}/settings";
-  }
-
-  String _settingsDataKeyUserID(String dataKey) {
-    List<String> parts = dataKey.split("/");
-    return parts[parts.length - 2];
-  }
-
   Future _prepareSettingsTable() async {
     if (tb != null) {
       return; // Then we're already prepared.
@@ -65,8 +54,8 @@
     tb = await _cc.createTable(db, util.tableNameSettings);
 
     // Start to watch the stream for the shared settings table.
-    Stream<sc.WatchChange> watchStream = db.watch(
-        util.tableNameSettings, _settingsWatchSyncPrefix, UTF8.encode("now"));
+    Stream<sc.WatchChange> watchStream = db.watch(util.tableNameSettings,
+        util.settingsWatchSyncPrefix, UTF8.encode("now"));
     _startWatchSettings(watchStream); // Don't wait for this future.
     _loadSettings(tb); // Don't wait for this future.
   }
@@ -84,7 +73,7 @@
       await this.save(settings.userID, jsonStr);
       return jsonStr;
     } else {
-      return await _tryReadData(tb, this._settingsDataKey(userID));
+      return await _tryReadData(tb, util.settingsDataKeyFromUserID(userID));
     }
   }
 
@@ -104,8 +93,10 @@
     util.log('SettingsManager.save');
     await _prepareSettingsTable();
 
-    await tb.row(_personalKey).put(UTF8.encode("${userID}"));
-    await tb.row(this._settingsDataKey(userID)).put(UTF8.encode(jsonString));
+    await tb.row(util.settingsPersonalKey).put(UTF8.encode("${userID}"));
+    await tb
+        .row(util.settingsDataKeyFromUserID(userID))
+        .put(UTF8.encode(jsonString));
   }
 
   // This watch method ensures that any changes are propagated to the caller.
@@ -132,7 +123,7 @@
       }
 
       if (this.updateSettingsCallback != null) {
-        this.updateSettingsCallback(_settingsDataKeyUserID(key), value);
+        this.updateSettingsCallback(util.userIDFromSettingsDataKey(key), value);
       }
     }
   }
@@ -143,7 +134,7 @@
 
     _cc.createSyncgroup(
         await _mySettingsSyncgroupName(), util.tableNameSettings,
-        prefix: this._settingsDataKey(id));
+        prefix: util.settingsDataKeyFromUserID(id));
   }
 
   Future<String> _mySettingsSyncgroupName() async {
@@ -175,19 +166,18 @@
 
       if (key.indexOf("/players") != -1) {
         if (this.updatePlayerFoundCallback != null) {
-          String playerID = _getPartFromBack(key, "/", 1);
-          String type = _getPartFromBack(key, "/", 0);
+          String type = util.playerUpdateTypeFromPlayerKey(key);
           switch (type) {
             case "player_number":
               // Update the player number for this player.
-              this.updatePlayerFoundCallback(playerID, value);
+              this.updatePlayerFoundCallback(key, 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);
+              this.updatePlayerFoundCallback(key, null);
               break;
             default:
               print("Unexpected key: ${key} with value ${value}");
@@ -215,12 +205,12 @@
 
     print("Now writing to some rows of ${gameID}");
     // Start up the table and write yourself as player 0.
-    await gameTable.row("${gameID}/type").put(UTF8.encode("${type}"));
+    await gameTable.row(util.gameTypeKey(gameID)).put(UTF8.encode("${type}"));
 
     int id = await _getUserID();
-    await gameTable.row("${gameID}/owner").put(UTF8.encode("${id}"));
+    await gameTable.row(util.gameOwnerKey(gameID)).put(UTF8.encode("${id}"));
     await gameTable
-        .row("${gameID}/players/${id}/settings_sg")
+        .row(util.playerSettingsKeyFromData(gameID, id))
         .put(UTF8.encode(await _mySettingsSyncgroupName()));
 
     logic_game.GameStartData gsd =
@@ -249,7 +239,7 @@
 
     int id = await _getUserID();
     await gameTable
-        .row("${gameID}/players/${id}/settings_sg")
+        .row(util.playerSettingsKeyFromData(gameID, id))
         .put(UTF8.encode(await _mySettingsSyncgroupName()));
   }
 
@@ -259,7 +249,7 @@
 
     int id = await _getUserID();
     await gameTable
-        .row("${gameID}/players/${id}/player_number")
+        .row(util.playerNumberKeyFromData(gameID, id))
         .put(UTF8.encode("${playerNumber}"));
   }
 
@@ -267,19 +257,19 @@
     sc.SyncbaseDatabase db = await _cc.createDatabase();
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
-    await gameTable.row("${gameID}/status").put(UTF8.encode(status));
+    await gameTable.row(util.gameStatusKey(gameID)).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 {
     tb
-        .scan(new sc.RowRange.prefix(_settingsWatchSyncPrefix))
+        .scan(new sc.RowRange.prefix(util.settingsWatchSyncPrefix))
         .forEach((sc.KeyValue kv) {
-      if (kv.key.endsWith("/settings")) {
+      if (util.isSettingsKey(kv.key)) {
         // Then we can process the value as if it were settings data.
         this.updateSettingsCallback(
-            _settingsDataKeyUserID(kv.key), UTF8.decode(kv.value));
+            util.userIDFromSettingsDataKey(kv.key), UTF8.decode(kv.value));
       }
     });
   }
@@ -292,12 +282,12 @@
   Future scanSettings() async {
     SettingsScanHandler ssh =
         new SettingsScanHandler(_cc, this.updateGamesCallback);
-    return _cc.discoveryClient.scan(_discoverySettingsKey,
+    return _cc.discoveryClient.scan(_discoveryGameAdKey,
         'v.InterfaceName="${util.discoveryInterfaceName}"', ssh);
   }
 
   Future stopScanSettings() {
-    return _cc.discoveryClient.stopScan(_discoverySettingsKey);
+    return _cc.discoveryClient.stopScan(_discoveryGameAdKey);
   }
 
   // Someone who wants to join a game should advertise their presence.
@@ -305,7 +295,7 @@
     String settingsSuffix = await _syncSettingsSuffix();
     String gameSuffix = util.syncgameSuffix("${gsd.gameID}");
     return _cc.discoveryClient.advertise(
-        _discoverySettingsKey,
+        _discoveryGameAdKey,
         DiscoveryClient.serviceMaker(
             interfaceName: util.discoveryInterfaceName,
             attrs: <String, String>{
@@ -316,11 +306,11 @@
   }
 
   Future stopAdvertiseSettings() {
-    return _cc.discoveryClient.stopAdvertise(_discoverySettingsKey);
+    return _cc.discoveryClient.stopAdvertise(_discoveryGameAdKey);
   }
 
   Future<int> _getUserID() async {
-    String result = await _tryReadData(tb, _personalKey);
+    String result = await _tryReadData(tb, util.settingsPersonalKey);
     if (result == null) {
       return null;
     }
@@ -337,11 +327,6 @@
   }
 }
 
-String _getPartFromBack(String input, String separator, int indexFromLast) {
-  List<String> parts = input.split(separator);
-  return parts[parts.length - 1 - indexFromLast];
-}
-
 // Implementation of the ScanHandler for Settings information.
 // Upon finding a settings advertiser, you want to join the syncgroup that
 // they're advertising.
diff --git a/lib/src/syncbase/util.dart b/lib/src/syncbase/util.dart
index 749e0cd..9523947 100644
--- a/lib/src/syncbase/util.dart
+++ b/lib/src/syncbase/util.dart
@@ -21,6 +21,9 @@
 
 const String discoveryInterfaceName = 'CroupierSettingsAndGame';
 
+const String settingsPersonalKey = "personal";
+const String settingsWatchSyncPrefix = "users";
+
 typedef void NoArgCb();
 typedef void keyValueCallback(String key, String value);
 
@@ -44,3 +47,56 @@
 
 const String syncgameSettingsAttr = "settings_sgname";
 const String syncgameGameStartDataAttr = "game_start_data";
+
+const String separator = "/";
+
+String gameIDFromGameKey(String gameKey) {
+  List<String> parts = gameKey.split(separator);
+  return parts[0];
+}
+
+String playerUpdateTypeFromPlayerKey(String playerKey) {
+  return _getPartFromBack(playerKey, 0);
+}
+
+String playerIDFromPlayerKey(String playerKey) {
+  return _getPartFromBack(playerKey, 1);
+}
+
+String gameOwnerKey(int gameID) {
+  return "${gameID}/owner";
+}
+
+String gameTypeKey(int gameID) {
+  return "${gameID}/type";
+}
+
+String gameStatusKey(int gameID) {
+  return "${gameID}/status";
+}
+
+String playerSettingsKeyFromData(int gameID, int userID) {
+  return "${gameID}/players/${userID}/settings_sg";
+}
+
+String playerNumberKeyFromData(int gameID, int userID) {
+  return "${gameID}/players/${userID}/player_number";
+}
+
+bool isSettingsKey(String key) {
+  return key.indexOf(settingsWatchSyncPrefix) == 0 && key.endsWith("/settings");
+}
+
+String settingsDataKeyFromUserID(int userID) {
+  return "${settingsWatchSyncPrefix}/${userID}/settings";
+}
+
+String userIDFromSettingsDataKey(String dataKey) {
+  List<String> parts = dataKey.split("/");
+  return parts[parts.length - 2];
+}
+
+String _getPartFromBack(String input, int indexFromLast) {
+  List<String> parts = input.split(separator);
+  return parts[parts.length - 1 - indexFromLast];
+}
diff --git a/pubspec.lock b/pubspec.lock
index 5610040..93350e9 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -154,7 +154,7 @@
   path:
     description: path
     source: hosted
-    version: "1.3.8"
+    version: "1.3.9"
   petitparser:
     description: petitparser
     source: hosted
@@ -214,7 +214,7 @@
   syncbase:
     description: syncbase
     source: hosted
-    version: "0.0.18"
+    version: "0.0.20"
   test:
     description: test
     source: hosted