Merge "croupier: Add Player Split View"
diff --git a/go/src/hearts/img/reposition/reposition.go b/go/src/hearts/img/reposition/reposition.go
index 926cf13..cd72e9c 100644
--- a/go/src/hearts/img/reposition/reposition.go
+++ b/go/src/hearts/img/reposition/reposition.go
@@ -278,6 +278,7 @@
 }
 
 func AnimateInSplit(u *uistate.UIState) {
+	ResetAnims(u)
 	topOfBanner := u.WindowSize.Y - 4*u.CardDim.Y - 5*u.Padding - u.BottomPadding - 40
 	tableImgs := make([]*staticimg.StaticImg, 0)
 	bannerImgs := make([]*staticimg.StaticImg, 0)
@@ -322,6 +323,7 @@
 }
 
 func AnimateOutSplit(ch chan bool, u *uistate.UIState) {
+	ResetAnims(u)
 	topOfBanner := u.WindowSize.Y - 4*u.CardDim.Y - 5*u.Padding - u.BottomPadding - 40
 	tableImgs := make([]*staticimg.StaticImg, 0)
 	bannerImgs := make([]*staticimg.StaticImg, 0)
@@ -570,3 +572,10 @@
 		return
 	}
 }
+
+func ResetAnims(u *uistate.UIState) {
+	for _, ch := range u.AnimChans {
+		ch <- true
+	}
+	u.AnimChans = make([]chan bool, 0)
+}
diff --git a/go/src/hearts/img/uistate/uistate.go b/go/src/hearts/img/uistate/uistate.go
index 75fff63..392a00f 100644
--- a/go/src/hearts/img/uistate/uistate.go
+++ b/go/src/hearts/img/uistate/uistate.go
@@ -84,6 +84,7 @@
 	Overlap          *coords.Vec
 	Padding          float32
 	CurView          View                     // the screen currently being shown to the user
+	ViewOnTouch      View                     // the view at the beginning of a touch action
 	CurTable         *table.Table             // the table of the current game
 	Done             bool                     // true if the app has been quit
 	Texs             map[string]sprite.SubTex // map of all loaded images
diff --git a/go/src/hearts/img/view/view.go b/go/src/hearts/img/view/view.go
index 87a4a2b..a325a4c 100644
--- a/go/src/hearts/img/view/view.go
+++ b/go/src/hearts/img/view/view.go
@@ -29,7 +29,7 @@
 func LoadArrangeView(u *uistate.UIState) {
 	u.CurView = uistate.Arrange
 	<-time.After(1 * time.Second)
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	addHeader(u)
@@ -63,7 +63,7 @@
 // Waiting view: Displays the word "Waiting". To be displayed when players are waiting for a new round to be dealt
 // TODO(emshack): Integrate this with Arrange view and Score view so that a separate screen is not necessary
 func LoadWaitingView(u *uistate.UIState) {
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	center := u.WindowSize.DividedBy(2)
@@ -78,7 +78,7 @@
 // Discovery view: Displays a menu of possible games to join
 func LoadDiscoveryView(discChan chan []string, u *uistate.UIState) {
 	u.CurView = uistate.Discovery
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	newGameImg := u.Texs["NewGame.png"]
@@ -101,7 +101,7 @@
 // Table View: Displays the table. Intended for public devices
 func LoadTableView(u *uistate.UIState) {
 	u.CurView = uistate.Table
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	scaler := float32(6)
@@ -124,6 +124,7 @@
 	dropCard := u.CurTable.GetTrick()[0]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.Cards = append(u.Cards, dropCard)
 	}
@@ -141,6 +142,7 @@
 	dropCard = u.CurTable.GetTrick()[1]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.Cards = append(u.Cards, dropCard)
 	}
@@ -158,6 +160,7 @@
 	dropCard = u.CurTable.GetTrick()[2]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.Cards = append(u.Cards, dropCard)
 	}
@@ -175,6 +178,7 @@
 	dropCard = u.CurTable.GetTrick()[3]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.Cards = append(u.Cards, dropCard)
 	}
@@ -313,6 +317,7 @@
 				texture.PopulateCardImage(c, u)
 				c.SetBackDisplay(u.Eng)
 				pos := reposition.DetermineTablePassPosition(c, i, p.GetPlayerIndex(), u)
+				c.SetInitial(pos)
 				c.Move(pos, u.TableCardDim, u.Eng)
 				u.TableCards = append(u.TableCards, c)
 			}
@@ -340,7 +345,7 @@
 // Score View: Shows current player standings at the end of every round, including the end of the game
 func LoadScoreView(roundScores, winners []int, u *uistate.UIState) {
 	u.CurView = uistate.Score
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	addHeader(u)
@@ -352,7 +357,7 @@
 // Pass View: Shows player's hand and allows them to pass cards
 func LoadPassView(u *uistate.UIState) {
 	u.CurView = uistate.Pass
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	addHeader(u)
@@ -367,7 +372,7 @@
 // Take View: Shows player's hand and allows them to take the cards that have been passed to them
 func LoadTakeView(u *uistate.UIState) {
 	u.CurView = uistate.Take
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	addHeader(u)
@@ -384,7 +389,7 @@
 // Play View: Shows player's hand and allows them to play cards
 func LoadPlayView(u *uistate.UIState) {
 	u.CurView = uistate.Play
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	addPlaySlot(u)
@@ -408,7 +413,7 @@
 
 func LoadSplitView(reloading bool, u *uistate.UIState) {
 	u.CurView = uistate.Split
-	resetAnims(u)
+	reposition.ResetAnims(u)
 	resetImgs(u)
 	resetScene(u)
 	addPlayHeader(getTurnText(u), !reloading, u)
@@ -503,6 +508,7 @@
 	dropCard := u.CurTable.GetTrick()[u.CurPlayerIndex]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.TableCards = append(u.TableCards, dropCard)
 	}
@@ -523,6 +529,7 @@
 	dropCard = u.CurTable.GetTrick()[(u.CurPlayerIndex+1)%len(u.CurTable.GetPlayers())]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.TableCards = append(u.TableCards, dropCard)
 	}
@@ -543,6 +550,7 @@
 	dropCard = u.CurTable.GetTrick()[(u.CurPlayerIndex+2)%len(u.CurTable.GetPlayers())]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.TableCards = append(u.TableCards, dropCard)
 	}
@@ -563,6 +571,7 @@
 	dropCard = u.CurTable.GetTrick()[(u.CurPlayerIndex+3)%len(u.CurTable.GetPlayers())]
 	if dropCard != nil {
 		texture.PopulateCardImage(dropCard, u)
+		dropCard.SetInitial(dropTargetPos)
 		dropCard.Move(dropTargetPos, dropTargetDimensions, u.Eng)
 		u.TableCards = append(u.TableCards, dropCard)
 	}
@@ -1081,13 +1090,6 @@
 	})
 }
 
-func resetAnims(u *uistate.UIState) {
-	for _, ch := range u.AnimChans {
-		ch <- true
-	}
-	u.AnimChans = make([]chan bool, 0)
-}
-
 func addDebugBar(u *uistate.UIState) {
 	buttonDim := u.CardDim
 	debugTableImage := u.Texs["BakuSquare.png"]
diff --git a/go/src/hearts/sync/client.go b/go/src/hearts/sync/client.go
index f7bc1f4..c126c7c 100644
--- a/go/src/hearts/sync/client.go
+++ b/go/src/hearts/sync/client.go
@@ -84,6 +84,15 @@
 	return db.Watch(u.Ctx, tableName, prefix, resumeMarker)
 }
 
+// Returns a scanstream of the data in the table
+func ScanData(tableName, prefix string, u *uistate.UIState) nosql.ScanStream {
+	app := u.Service.App(AppName)
+	db := app.NoSQLDatabase(DbName, nil)
+	table := db.Table(tableName)
+	rowRange := nosql.Range(prefix, "")
+	return table.Scan(u.Ctx, rowRange)
+}
+
 // Joins gamelog syncgroup
 func JoinLogSyncgroup(ch chan bool, logName string, u *uistate.UIState) {
 	fmt.Println("Joining gamelog syncgroup")
diff --git a/go/src/hearts/sync/server.go b/go/src/hearts/sync/server.go
index 9acb8c7..155830d 100644
--- a/go/src/hearts/sync/server.go
+++ b/go/src/hearts/sync/server.go
@@ -119,6 +119,7 @@
 	u.IsOwner = true
 	// Generate random gameID information to advertise this game
 	gameID := rand.Intn(1000000)
+	u.GameID = gameID
 	gameMap := make(map[string]interface{})
 	gameMap["type"] = "Hearts"
 	gameMap["playerNumber"] = 0
@@ -139,7 +140,7 @@
 		"Resolve": allAccess,
 		"Debug":   allAccess,
 	}
-	logPref := wire.TableRow{LogName, ""}
+	logPref := wire.TableRow{LogName, fmt.Sprintf("%d", u.GameID)}
 	logPrefs := []wire.TableRow{logPref}
 	tables := []string{MountPoint + "/croupier"}
 	logSpec := wire.SyncgroupSpec{
@@ -156,10 +157,16 @@
 	err = logSG.Create(u.Ctx, logSpec, myInfoCreator)
 	if err != nil {
 		fmt.Println("SYNCGROUP CREATE ERROR: ", err)
-		ch <- ""
+		fmt.Println("JOINING INSTEAD...")
+		_, err2 := logSG.Join(u.Ctx, myInfoCreator)
+		if err2 != nil {
+			fmt.Println("SYNCGROUP JOIN ERROR: ", err2)
+			ch <- ""
+		} else {
+			ch <- logSGName
+		}
 	} else {
 		fmt.Println("Syncgroup created")
-		u.GameID = gameID
 		go UpdateGame(u)
 		ch <- logSGName
 	}
@@ -194,7 +201,14 @@
 	err := settingsSG.Create(u.Ctx, settingsSpec, myInfoCreator)
 	if err != nil {
 		fmt.Println("SYNCGROUP CREATE ERROR: ", err)
-		ch <- ""
+		fmt.Println("JOINING INSTEAD...")
+		_, err2 := settingsSG.Join(u.Ctx, myInfoCreator)
+		if err2 != nil {
+			fmt.Println("SYNCGROUP JOIN ERROR: ", err2)
+			ch <- ""
+		} else {
+			ch <- settingsSGName
+		}
 	} else {
 		fmt.Println("Syncgroup created")
 		ch <- settingsSGName
diff --git a/go/src/hearts/sync/util.go b/go/src/hearts/sync/util.go
index 5e1ebcc..cf72edd 100644
--- a/go/src/hearts/sync/util.go
+++ b/go/src/hearts/sync/util.go
@@ -8,16 +8,16 @@
 
 const (
 	// switch back to my mountpoint with the following code:
-	//MountPoint = "users/emshack@google.com"
-	MountPoint        = "/192.168.86.254:8101"
-	UserID            = 2222
+	MountPoint = "users/emshack@google.com"
+	//MountPoint        = "/192.168.86.254:8101"
+	UserID            = 4444
 	UserColor         = 16777215
-	UserAvatar        = "player1.jpeg"
-	UserName          = "Bob"
-	SBName            = "syncbase1"
+	UserAvatar        = "player3.jpeg"
+	UserName          = "Dan"
+	SBName            = "syncbase3"
 	AppName           = "app"
 	DbName            = "db"
 	LogName           = "games"
 	SettingsName      = "table_settings"
-	CroupierInterface = "CroupierSettingsAndGame"
+	CroupierInterface = "CroupierSettingsAndGameEmily"
 )
diff --git a/go/src/hearts/sync/watch.go b/go/src/hearts/sync/watch.go
index cb4effe..9cc2e87 100644
--- a/go/src/hearts/sync/watch.go
+++ b/go/src/hearts/sync/watch.go
@@ -24,6 +24,19 @@
 )
 
 func UpdateSettings(u *uistate.UIState) {
+	scanner := ScanData(SettingsName, "users", u)
+	for {
+		if updateExists := scanner.Advance(); updateExists {
+			key := scanner.Key()
+			var value []byte
+			if err := scanner.Value(&value); err != nil {
+				fmt.Println("Value error:", err)
+			}
+			handleSettingsUpdate(key, value, u)
+		} else {
+			break
+		}
+	}
 	stream, err := WatchData(SettingsName, "users", u)
 	if err != nil {
 		fmt.Println("WatchData error:", err)
@@ -32,42 +45,46 @@
 		if updateExists := stream.Advance(); updateExists {
 			c := stream.Change()
 			if c.ChangeType == nosql.PutChange {
+				key := c.Row
 				var value []byte
 				if err := c.Value(&value); err != nil {
 					fmt.Println("Value error:", err)
 				}
-				var valueMap map[string]interface{}
-				err := json.Unmarshal(value, &valueMap)
-				if err != nil {
-					fmt.Println("Unmarshal error:", err)
-				}
-				key := c.Row
-				userID, _ := strconv.Atoi(strings.Split(key, "/")[1])
-				u.UserData[userID] = valueMap
-				for _, v := range u.PlayerData {
-					if v == userID {
-						switch u.CurView {
-						case uistate.Arrange:
-							view.LoadArrangeView(u)
-						case uistate.Table:
-							view.LoadTableView(u)
-						case uistate.Pass:
-							view.LoadPassView(u)
-						case uistate.Take:
-							view.LoadTakeView(u)
-						case uistate.Play:
-							view.LoadPlayView(u)
-						case uistate.Split:
-							view.LoadSplitView(true, u)
-						}
-					}
-				}
+				handleSettingsUpdate(key, value, u)
 			} else {
 				fmt.Println("Unexpected ChangeType: ", c.ChangeType)
 			}
 		}
 	}
-} 
+}
+
+func handleSettingsUpdate(key string, value []byte, u *uistate.UIState) {
+	var valueMap map[string]interface{}
+	err := json.Unmarshal(value, &valueMap)
+	if err != nil {
+		fmt.Println("Unmarshal error:", err)
+	}
+	userID, _ := strconv.Atoi(strings.Split(key, "/")[1])
+	u.UserData[userID] = valueMap
+	for _, v := range u.PlayerData {
+		if v == userID {
+			switch u.CurView {
+			case uistate.Arrange:
+				view.LoadArrangeView(u)
+			case uistate.Table:
+				view.LoadTableView(u)
+			case uistate.Pass:
+				view.LoadPassView(u)
+			case uistate.Take:
+				view.LoadTakeView(u)
+			case uistate.Play:
+				view.LoadPlayView(u)
+			case uistate.Split:
+				view.LoadSplitView(true, u)
+			}
+		}
+	}
+}
 
 func UpdateGame(u *uistate.UIState) {
 	stream, err := WatchData(LogName, fmt.Sprintf("%d", u.GameID), u)
@@ -87,8 +104,6 @@
 				valueStr := string(value)
 				fmt.Println(key, valueStr)
 				keyType := strings.Split(key, "/")[1]
-				gameID := strings.Split(key, "/")[0]
-				fmt.Println("GAME ID:", gameID)
 				switch keyType {
 				case "log":
 					updateType := strings.Split(valueStr, "|")[0]
diff --git a/go/src/hearts/touchhandler/touchhandler.go b/go/src/hearts/touchhandler/touchhandler.go
index 0b4dc27..9283e23 100644
--- a/go/src/hearts/touchhandler/touchhandler.go
+++ b/go/src/hearts/touchhandler/touchhandler.go
@@ -22,6 +22,11 @@
 )
 
 func OnTouch(t touch.Event, u *uistate.UIState) {
+	if t.Type == touch.TypeBegin {
+		u.ViewOnTouch = u.CurView
+	} else if u.CurView != u.ViewOnTouch {
+		return
+	}
 	switch u.CurView {
 	case uistate.Discovery:
 		switch t.Type {
@@ -459,16 +464,10 @@
 			removeCardFromTarget(c, u)
 			// add card back to hand
 			reposition.ResetCardPosition(c, u.Eng)
-			reposition.RealignSuit(c.GetSuit(), c.GetInitial().Y, u)
-		} else {
-			reposition.RealignSuit(c.GetSuit(), c.GetInitial().Y, u)
 		}
 	} else {
-		// check to see if card was removed from a drop target
-		removeCardFromTarget(c, u)
 		// add card back to hand
 		reposition.ResetCardPosition(c, u.Eng)
-		reposition.RealignSuit(c.GetSuit(), c.GetInitial().Y, u)
 	}
 }
 
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 72fb6c3..a635060 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];
+}