croupier: Pivot to Join Game model and game-based Syncgroup
This CL pivots from the game creation + invitation model to the
game advertisement and join game model. (It merely swaps scanners/advertisers.)
It's not super clean yet, but you can create a game and have people
join its syncgroup to share player number, game id, game type, and who's
in the game.
Tested by playing Hearts with 4 devices at once. However, it's starting to
look like integration tests are necessary for Discovery + Syncgroup
creation. The fact that Mojo is required is worrying too. Luckily, the UI
can be ignored when testing this behavior.
Issues
- must have 4 devices to play Hearts (or else it gets stuck on Deal)
- Devices must be added in order or else they'll be assigned the wrong
player number. You can use the debug buttons to switch the player, but
this is going to be solved by "arrange players" anyway.
- It may be possible to crash adb for one of the devices if you conflict
during Deal
- Deal is super slow
- Code is not cleaned up much.
Change-Id: I78bda98170ce59c251d0c9c18ee37165bb0c15b7
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 26e5e99..a139efe 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -68,9 +68,9 @@
onPressed: makeSetStateCallback(
logic_croupier.CroupierState.ChooseGame)),
new FlatButton(
- child: new Text('Await Game'),
+ child: new Text('Join Game'),
onPressed: makeSetStateCallback(
- logic_croupier.CroupierState.AwaitGame)),
+ logic_croupier.CroupierState.JoinGame)),
new FlatButton(
child: new Text('Settings'),
onPressed: makeSetStateCallback(
@@ -110,33 +110,50 @@
onPressed: makeSetStateCallback(
logic_croupier.CroupierState.Welcome))
], direction: FlexDirection.vertical));
- case logic_croupier.CroupierState.AwaitGame:
+ case logic_croupier.CroupierState.JoinGame:
+ // A stateful view, first showing the players that can be seen creating a game.
+ List<Widget> profileWidgets = new List<Widget>();
+ config.croupier.games_found.forEach((String _, logic_game.GameStartData gsd) {
+ CroupierSettings cs = config.croupier.settings_everyone[gsd.ownerID];
+ // cs could be null if this settings data hasn't synced yet.
+ if (cs != null) {
+ profileWidgets.add(new FlatButton(
+ child: new CroupierProfileComponent(cs),
+ onPressed: makeSetStateCallback(
+ logic_croupier.CroupierState.ArrangePlayers, gsd)
+ ));
+ }
+ });
// in which players wait for game invitations to arrive.
return new Container(
padding: new EdgeDims.only(top: ui.window.padding.top),
child: new Column([
- new Text("Waiting for invitations..."),
+ new Text("Can join these games..."),
+ new Grid(profileWidgets, maxChildExtent: 150.0),
new FlatButton(
child: new Text('Back'),
onPressed: makeSetStateCallback(
logic_croupier.CroupierState.Welcome))
]));
case logic_croupier.CroupierState.ArrangePlayers:
- // A stateful view, first showing the players that can be invited.
List<Widget> profileWidgets = new List<Widget>();
- config.croupier.settings_everyone.forEach((_, CroupierSettings cs) {
- profileWidgets.add(new CroupierProfileComponent(cs));
+ config.croupier.players_found.forEach((int userID, _) {
+ CroupierSettings cs = config.croupier.settings_everyone[userID];
+ // cs could be null if this settings data hasn't synced yet.
+ if (cs != null) {
+ profileWidgets.add(new CroupierProfileComponent(cs));
+ }
});
// TODO(alexfandrianto): You can only start the game once there are enough players.
return new Container(
padding: new EdgeDims.only(top: ui.window.padding.top),
child: new Column([
- new Grid(profileWidgets, maxChildExtent: 150.0),
new FlatButton(
child: new Text('Start Game'),
onPressed: makeSetStateCallback(
logic_croupier.CroupierState.PlayGame)),
+ new Grid(profileWidgets, maxChildExtent: 150.0),
new FlatButton(
child: new Text('Back'),
onPressed: makeSetStateCallback(
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 966b3b3..c4ed7d2 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -54,7 +54,7 @@
width: config.width,
height: config.height,
child: heartsWidget));
- if (game.phase == HeartsPhase.Deal || game.phase != HeartsPhase.Score) {
+ if (game.phase != HeartsPhase.Deal && game.phase != HeartsPhase.Score) {
List<int> visibleCardCollections = new List<int>();
int playerNum = game.playerNumber;
if (game.viewType == HeartsType.Player) {
diff --git a/lib/logic/create_game.dart b/lib/logic/create_game.dart
index aeae319..d92e848 100644
--- a/lib/logic/create_game.dart
+++ b/lib/logic/create_game.dart
@@ -7,14 +7,14 @@
import 'proto/proto.dart' as proto_impl;
import 'solitaire/solitaire.dart' as solitaire_impl;
-game_impl.Game createGame(game_impl.GameType gt, int pn) {
+game_impl.Game createGame(game_impl.GameType gt, int pn, {int gameID}) {
switch (gt) {
case game_impl.GameType.Proto:
- return new proto_impl.ProtoGame(pn);
+ return new proto_impl.ProtoGame(pn, gameID: gameID);
case game_impl.GameType.Hearts:
- return new hearts_impl.HeartsGame(pn);
+ return new hearts_impl.HeartsGame(pn, gameID: gameID);
case game_impl.GameType.Solitaire:
- return new solitaire_impl.SolitaireGame(pn);
+ return new solitaire_impl.SolitaireGame(pn, gameID: gameID);
default:
assert(false);
return null;
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 63818cc..47357cc 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-import 'game/game.dart' show Game, GameType;
+import 'game/game.dart' show Game, GameType, GameStartData, stringToGameType, gameTypeToString;
import 'create_game.dart' as cg;
import 'croupier_settings.dart' show CroupierSettings;
import '../src/syncbase/settings_manager.dart' show SettingsManager;
@@ -11,7 +11,7 @@
Welcome,
Settings,
ChooseGame,
- AwaitGame,
+ JoinGame,
ArrangePlayers,
PlayGame
}
@@ -22,26 +22,60 @@
CroupierState state;
SettingsManager settings_manager;
CroupierSettings settings; // null, but loaded asynchronously.
- Map<String,
+ Map<int,
CroupierSettings> settings_everyone; // empty, but loaded asynchronously
+ Map<String, GameStartData> games_found; // empty, but loads asynchronously
+ Map<int, int> players_found; // empty, but loads asynchronously
Game game; // null until chosen
NoArgCb informUICb;
Croupier() {
state = CroupierState.Welcome;
- settings_everyone = new Map<String, CroupierSettings>();
- settings_manager = new SettingsManager(_updateSettingsEveryoneCb);
+ settings_everyone = new Map<int, CroupierSettings>();
+ games_found = new Map<String, GameStartData>();
+ players_found = new Map<int, int>();
+ settings_manager = new SettingsManager(_updateSettingsEveryoneCb, _updateGamesFoundCb, _updatePlayerFoundCb);
settings_manager.load().then((String csString) {
settings = new CroupierSettings.fromJSONString(csString);
- settings_manager.createSyncgroup(); // don't wait for this future.
+ settings_manager.createSettingsSyncgroup(); // don't wait for this future.
});
}
// Updates the settings_everyone map as people join the main Croupier syncgroup
// and change their settings.
void _updateSettingsEveryoneCb(String key, String json) {
- settings_everyone[key] = new CroupierSettings.fromJSONString(json);
+ settings_everyone[int.parse(key)] = new CroupierSettings.fromJSONString(json);
+ if (this.informUICb != null) {
+ this.informUICb();
+ }
+ }
+
+ void _updateGamesFoundCb(String gameAddr, String jsonData) {
+ if (jsonData == null) {
+ games_found.remove(gameAddr);
+ } else {
+ GameStartData gsd = new GameStartData.fromJSONString(jsonData);
+ games_found[gameAddr] = gsd;
+ }
+ if (this.informUICb != null) {
+ this.informUICb();
+ }
+ }
+
+ void _updatePlayerFoundCb(String playerID, String playerNum) {
+ int id = int.parse(playerID);
+ if (playerNum == null) {
+ games_found.remove(id);
+ } else {
+ int playerNumber = int.parse(playerNum);
+ players_found[id] = playerNumber;
+
+ // If the player number changed was ours, then set it on our game.
+ if (id == settings.userID) {
+ game.playerNumber = playerNumber;
+ }
+ }
if (this.informUICb != null) {
this.informUICb();
}
@@ -54,6 +88,12 @@
case CroupierState.Welcome:
// data should be empty.
assert(data == null);
+
+ // Start scanning for games if that's what's next for you.
+ if (nextState == CroupierState.JoinGame) {
+ settings_manager.scanSettings(); // don't wait for this future.
+ }
+
break;
case CroupierState.Settings:
// data should be empty.
@@ -65,13 +105,21 @@
// Back button pressed.
break;
}
+ assert(nextState == CroupierState.ArrangePlayers);
+
// data should be the game id here.
GameType gt = data as GameType;
game = cg.createGame(gt, 0); // Start as player 0 of whatever game type.
+
+ settings_manager.createGameSyncgroup(gameTypeToString(gt), game.gameID).then((GameStartData gsd) {
+ // Only the game chooser should be advertising the game.
+ settings_manager.advertiseSettings(gsd); // don't wait for this future.
+ });
+
break;
- case CroupierState.AwaitGame:
- // Note that if we were in await game, we must have been advertising.
- settings_manager.stopAdvertiseSettings();
+ case CroupierState.JoinGame:
+ // Note that if we were in join game, we must have been scanning.
+ settings_manager.stopScanSettings();
if (data == null) {
// Back button pressed.
@@ -79,12 +127,21 @@
}
// data would probably be the game id again.
- GameType gt = data as GameType;
- game = cg.createGame(gt, 0); // Start as player 0 of whatever game type.
+ GameStartData gsd = data as GameStartData;
+ game = cg.createGame(stringToGameType(gsd.type), gsd.playerNumber, gameID: gsd.gameID); // Start as player 0 of whatever game type.
+ String sgName;
+ games_found.forEach((String name, GameStartData g) {
+ if (g == gsd) {
+ sgName = name;
+ }
+ });
+ assert(sgName != null);
+
+ settings_manager.joinGameSyncgroup(sgName, gsd.gameID);
break;
case CroupierState.ArrangePlayers:
- // Note that if we were arranging players, we must have been scanning.
- settings_manager.stopScanSettings();
+ // Note that if we were arranging players, we might have been advertising.
+ settings_manager.stopAdvertiseSettings();
// data should be empty.
// All rearrangements affect the Game's player number without changing app state.
@@ -104,18 +161,13 @@
return; // you can't switch till the settings are present.
}
- // The nextState you are switching to may require some behind-the-scenes
- // work.
- switch (nextState) {
- case CroupierState.ArrangePlayers:
- settings_manager.scanSettings(); // don't wait for this future.
- break;
- case CroupierState.AwaitGame:
- settings_manager.advertiseSettings(); // don't wait for this future.
- break;
- default:
+ // A simplified way of clearing out the games and players found.
+ // They will need to be re-discovered in the future.
+ if (nextState == CroupierState.Welcome) {
+ games_found = new Map<String, GameStartData>();
+ players_found = new Map<int, int>();
}
state = nextState;
}
-}
+}
\ No newline at end of file
diff --git a/lib/logic/game/game.dart b/lib/logic/game/game.dart
index ab97e00..1e139a3 100644
--- a/lib/logic/game/game.dart
+++ b/lib/logic/game/game.dart
@@ -4,8 +4,10 @@
library game;
-import '../card.dart' show Card;
+import 'dart:convert' show JSON;
import 'dart:math' as math;
+
+import '../card.dart' show Card;
import '../../src/syncbase/log_writer.dart' show SimulLevel;
part 'game_def.part.dart';
diff --git a/lib/logic/game/game_def.part.dart b/lib/logic/game/game_def.part.dart
index cbaf11d..ebf206d 100644
--- a/lib/logic/game/game_def.part.dart
+++ b/lib/logic/game/game_def.part.dart
@@ -9,16 +9,71 @@
// Board is meant to show how one _could_ layout a game of Hearts. This one is not hooked up very well yet.
enum GameType { Proto, Hearts, Poker, Solitaire, Board }
+Map<GameType, String> _gameTypeMap = <GameType, String>{
+ GameType.Proto: "Proto",
+ GameType.Hearts: "Hearts",
+ GameType.Poker: "Poker",
+ GameType.Solitaire: "Solitaire",
+};
+String gameTypeToString(GameType t) {
+ return _gameTypeMap[t];
+}
+
+GameType stringToGameType(String t) {
+ GameType gt;
+ _gameTypeMap.forEach((GameType type, String name) {
+ if (name == t) {
+ gt = type;
+ }
+ });
+ return gt;
+}
+
+// You should share information like this if you want to setup a game for someone else.
+class GameStartData {
+ String type;
+ int playerNumber;
+ int gameID;
+ int ownerID;
+
+ GameStartData(this.type, this.playerNumber, this.gameID, this.ownerID);
+
+ GameStartData.fromJSONString(String json) {
+ var data = JSON.decode(json);
+ type = data["type"];
+ playerNumber = data["playerNumber"];
+ gameID = data["gameID"];
+ ownerID = data["ownerID"];
+ }
+
+ String toJSONString() {
+ return JSON.encode(
+ {"type": type, "playerNumber": playerNumber, "gameID": gameID, "ownerID": ownerID});
+ }
+
+ GameType get gameType => stringToGameType(type);
+
+ bool operator ==(Object other) {
+ if (other is! GameStartData) {
+ return false;
+ }
+ GameStartData gsd = other;
+ return gsd.type == type && gsd.playerNumber == playerNumber && gsd.gameID == gameID && gsd.ownerID == ownerID;
+ }
+}
+
typedef void NoArgCb();
/// A game consists of multiple decks and tracks a single deck of cards.
/// It also handles events; when cards are dragged to and from decks.
abstract class Game {
final GameType gameType;
+ String get gameTypeName; // abstract
+
final List<List<Card>> cardCollections = new List<List<Card>>();
final List<Card> deck = new List<Card>.from(Card.All);
+ final int gameID;
- final math.Random random = new math.Random();
final GameLog gamelog;
int _playerNumber;
@@ -32,13 +87,12 @@
NoArgCb updateCallback; // Used to inform components of when a change has occurred. This is especially important when something non-UI related changes what should be drawn.
- // A public super constructor that doesn't really do anything.
- // Don't call this unless you're a subclass.
- Game.dummy(this.gameType, this.gamelog) {}
-
// A super constructor, don't call this unless you're a subclass.
Game.create(
- this.gameType, this.gamelog, this._playerNumber, int numCollections) {
+ this.gameType, this.gamelog, this._playerNumber, int numCollections, {
+ int gameID
+ }) : gameID = gameID ?? new math.Random().nextInt(0x00FFFFFF) {
+ print("The gameID is ${gameID}");
gamelog.setGame(this);
for (int i = 0; i < numCollections; i++) {
cardCollections.add(new List<Card>());
diff --git a/lib/logic/hearts/hearts.dart b/lib/logic/hearts/hearts.dart
index aa162d7..14e7f5c 100644
--- a/lib/logic/hearts/hearts.dart
+++ b/lib/logic/hearts/hearts.dart
@@ -4,8 +4,9 @@
library hearts;
-import '../card.dart' show Card;
import 'dart:math' as math;
+
+import '../card.dart' show Card;
import '../game/game.dart' show Game, GameType, GameCommand, GameLog;
import '../../src/syncbase/log_writer.dart' show LogWriter, SimulLevel;
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index 4593066..7174f35 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -34,7 +34,10 @@
final Card TWO_OF_CLUBS = new Card("classic", "c2");
final Card QUEEN_OF_SPADES = new Card("classic", "sq");
- HeartsType viewType;
+ @override
+ String get gameTypeName => "Hearts";
+
+ HeartsType viewType = HeartsType.Player;
HeartsPhase _phase = HeartsPhase.Deal;
HeartsPhase get phase => _phase;
@@ -62,8 +65,8 @@
List<int> scores = [0, 0, 0, 0];
List<bool> ready;
- HeartsGame(int playerNumber, [this.viewType = HeartsType.Player])
- : super.create(GameType.Hearts, new HeartsLog(), playerNumber, 16) {
+ HeartsGame(int playerNumber, {int gameID})
+ : super.create(GameType.Hearts, new HeartsLog(), playerNumber, 16, gameID: gameID) {
resetGame();
}
diff --git a/lib/logic/hearts/hearts_log.part.dart b/lib/logic/hearts/hearts_log.part.dart
index 52685e4..769dd42 100644
--- a/lib/logic/hearts/hearts_log.part.dart
+++ b/lib/logic/hearts/hearts_log.part.dart
@@ -6,24 +6,31 @@
class HeartsLog extends GameLog {
LogWriter logWriter;
+ Set<String> seenKeys; // the seen ones can be ignored.
HeartsLog() {
// TODO(alexfandrianto): The Game ID needs to be part of this constructor.
- logWriter = new LogWriter(handleSyncUpdate, [0, 1, 2, 3], "<game_id>/log");
+ logWriter = new LogWriter(handleSyncUpdate, [0, 1, 2, 3]);
+ seenKeys = new Set<String>();
}
@override
void setGame(Game g) {
this.game = g;
logWriter.associatedUser = this.game.playerNumber;
+ logWriter.logPrefix = "${game.gameID}/log";
}
void handleSyncUpdate(String key, String cmd) {
- // In Hearts, we can ignore the key. Our in-memory log does not need to
- // guarantee the event order of the INDEPENDENT phase, which can reference
- // keys from the "earlier" actions of other players.
- HeartsCommand hc = new HeartsCommand.fromCommand(cmd);
- this.update(hc);
+ // In this game, we can execute commands in any order.
+ // However, we must avoid repeated keys.
+ if (!seenKeys.contains(key)) {
+ HeartsCommand hc = new HeartsCommand.fromCommand(cmd);
+ this.update(hc);
+ seenKeys.add(key);
+ } else {
+ print("The log is ignoring repeated key: ${key}");
+ }
}
@override
diff --git a/lib/logic/proto/proto_game.part.dart b/lib/logic/proto/proto_game.part.dart
index 2172750..d3ad7ff 100644
--- a/lib/logic/proto/proto_game.part.dart
+++ b/lib/logic/proto/proto_game.part.dart
@@ -5,8 +5,11 @@
part of proto;
class ProtoGame extends Game {
- ProtoGame(int playerNumber)
- : super.create(GameType.Proto, new ProtoLog(), playerNumber, 6) {
+ @override
+ String get gameTypeName => "Proto";
+
+ ProtoGame(int playerNumber, {int gameID})
+ : super.create(GameType.Proto, new ProtoLog(), playerNumber, 6, gameID: gameID) {
// playerNumber would be used in a real game, but I have to ignore it for debugging.
// It would determine faceUp/faceDown status.faceDown
diff --git a/lib/logic/solitaire/solitaire_game.part.dart b/lib/logic/solitaire/solitaire_game.part.dart
index ef0916e..9427250 100644
--- a/lib/logic/solitaire/solitaire_game.part.dart
+++ b/lib/logic/solitaire/solitaire_game.part.dart
@@ -7,6 +7,9 @@
enum SolitairePileType { ACES, DISCARD, DRAW, DOWN, UP }
class SolitaireGame extends Game {
+ @override
+ String get gameTypeName => "Solitaire";
+
// Constants for the index-based offsets of the Solitaire Game's card collection.
// There are 20 piles to track (4 aces, 1 discard, 1 draw, 7 down, 7 up).
static const NUM_PILES = 20;
@@ -23,9 +26,9 @@
_phase = other;
}
- SolitaireGame(int playerNumber)
+ SolitaireGame(int playerNumber, {int gameID})
: super.create(
- GameType.Solitaire, new SolitaireLog(), playerNumber, NUM_PILES) {
+ GameType.Solitaire, new SolitaireLog(), playerNumber, NUM_PILES, gameID: gameID) {
resetGame();
}
diff --git a/lib/logic/solitaire/solitaire_log.part.dart b/lib/logic/solitaire/solitaire_log.part.dart
index 973941c..10da814 100644
--- a/lib/logic/solitaire/solitaire_log.part.dart
+++ b/lib/logic/solitaire/solitaire_log.part.dart
@@ -6,23 +6,31 @@
class SolitaireLog extends GameLog {
LogWriter logWriter;
+ Set<String> seenKeys; // the seen ones can be ignored.
SolitaireLog() {
// TODO(alexfandrianto): The Game ID needs to be part of this constructor.
- logWriter = new LogWriter(handleSyncUpdate, [0], "<game_id>/log");
+ logWriter = new LogWriter(handleSyncUpdate, [0]);
+ seenKeys = new Set<String>();
}
@override
void setGame(Game g) {
this.game = g;
logWriter.associatedUser = this.game.playerNumber;
+ logWriter.logPrefix = "${game.gameID}/log";
}
void handleSyncUpdate(String key, String cmd) {
- // In Solitaire, we can ignore the key because this is a single player game,
- // and the Solitaire schema only has TURN_BASED moves.
- SolitaireCommand sc = new SolitaireCommand.fromCommand(cmd);
- this.update(sc);
+ // In this game, we can execute commands in any order.
+ // However, we must avoid repeated keys.
+ if (!seenKeys.contains(key)) {
+ SolitaireCommand sc = new SolitaireCommand.fromCommand(cmd);
+ this.update(sc);
+ seenKeys.add(key);
+ } else {
+ print("The log is ignoring repeated key: ${key}");
+ }
}
@override
diff --git a/lib/src/mocks/log_writer.dart b/lib/src/mocks/log_writer.dart
index c48fc1c..26e9075 100644
--- a/lib/src/mocks/log_writer.dart
+++ b/lib/src/mocks/log_writer.dart
@@ -11,12 +11,18 @@
class LogWriter {
final updateCallbackT updateCallback;
final List<int> users;
- final String logPrefix; // This can be completely ignored.
+ String logPrefix; // This can be completely ignored.
bool inProposalMode = false;
int associatedUser;
- LogWriter(this.updateCallback, this.users, this.logPrefix);
+ int _fakeTime = 0;
+ int _getNextTime() {
+ _fakeTime++;
+ return _fakeTime;
+ }
+
+ LogWriter(this.updateCallback, this.users);
Map<String, String> _data = new Map<String, String>();
@@ -46,7 +52,7 @@
// Helper that returns the log key using a mixture of timestamp + user.
String _logKey(int user) {
- int ms = new DateTime.now().millisecondsSinceEpoch;
+ int ms = _getNextTime();
String key = "${ms}-${user}";
return key;
}
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
index d465d0f..63c9acc 100644
--- a/lib/src/mocks/settings_manager.dart
+++ b/lib/src/mocks/settings_manager.dart
@@ -4,12 +4,15 @@
import 'dart:async';
-typedef void updateCallbackT(String key, String value);
+import 'util.dart' as util;
+import '../../logic/game/game.dart' as logic_game;
class SettingsManager {
- final updateCallbackT updateCallback;
+ final util.updateCallbackT updateCallback;
+ final util.updateCallbackT updateGamesCallback;
+ final util.updateCallbackT updatePlayerFoundCallback;
- SettingsManager(this.updateCallback);
+ SettingsManager(this.updateCallback, this.updateGamesCallback, this.updatePlayerFoundCallback);
Map<String, String> _data = new Map<String, String>();
@@ -26,7 +29,7 @@
return new Future(() => null);
}
- Future createSyncgroup() {
+ Future createSettingsSyncgroup() {
return new Future(() => null);
}
@@ -36,9 +39,17 @@
void stopScanSettings() {}
- Future advertiseSettings() {
+ Future advertiseSettings(logic_game.GameStartData gsd) {
return new Future(() => null);
}
void stopAdvertiseSettings() {}
+
+ Future createGameSyncgroup(String type, int gameID) {
+ return new Future(() => null);
+ }
+
+ Future joinGameSyncgroup(String sgName, int gameID) {
+ return new Future(() => null);
+ }
}
diff --git a/lib/src/syncbase/croupier_client.dart b/lib/src/syncbase/croupier_client.dart
index 1bc1ece..3fddb72 100644
--- a/lib/src/syncbase/croupier_client.dart
+++ b/lib/src/syncbase/croupier_client.dart
@@ -61,15 +61,22 @@
return db;
}
+ Completer _tableLock;
+
// TODO(alexfandrianto): Try not to call this twice at the same time.
// That would lead to very race-y behavior.
Future<sc.SyncbaseTable> createTable(
sc.SyncbaseNoSqlDatabase db, String tableName) async {
+ if (_tableLock != null) {
+ await _tableLock.future;
+ }
+ _tableLock = new Completer();
var table = db.table(tableName);
if (!(await table.exists())) {
await table.create(util.openPerms);
}
util.log('CroupierClient: ${tableName} is ready');
+ _tableLock.complete();
return table;
}
diff --git a/lib/src/syncbase/discovery_client.dart b/lib/src/syncbase/discovery_client.dart
index b78e098..450545d 100644
--- a/lib/src/syncbase/discovery_client.dart
+++ b/lib/src/syncbase/discovery_client.dart
@@ -73,10 +73,12 @@
// This sends a stop signal to the scanner. Since it is non-blocking, the
// scan handle may not stop instantaneously.
void stopScan(String key) {
- print("Stopping scan for ${key}.");
- scanners[key].proxy.ptr.stop(scanners[key].handle);
- scanners[key].proxy.close(); // don't wait for this future.
- scanners.remove(key);
+ if (scanners[key] != null) {
+ print("Stopping scan for ${key}.");
+ scanners[key].proxy.ptr.stop(scanners[key].handle);
+ scanners[key].proxy.close(); // don't wait for this future.
+ scanners.remove(key);
+ }
}
// Advertises the given service information. Keeps track of the advertiser
@@ -108,9 +110,11 @@
// This sends a stop signal to the advertiser. Since it is non-blocking, the
// advertise handle may not stop instantaneously.
void stopAdvertise(String key) {
- print("Stopping advertise for ${key}.");
- advertisers[key].proxy.ptr.stop(advertisers[key].handle);
- advertisers[key].proxy.close(); // don't wait for this future.
- advertisers.remove(key);
+ if (advertisers[key] != null) {
+ print("Stopping advertise for ${key}.");
+ advertisers[key].proxy.ptr.stop(advertisers[key].handle);
+ advertisers[key].proxy.close(); // don't wait for this future.
+ advertisers.remove(key);
+ }
}
}
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
index 4940fe8..f1760bd 100644
--- a/lib/src/syncbase/log_writer.dart
+++ b/lib/src/syncbase/log_writer.dart
@@ -39,7 +39,7 @@
final CroupierClient _cc;
// Affects read/write/watch locations of the log writer.
- final String logPrefix;
+ String logPrefix = ''; // This is usually set to <game_id>/log
// An internal boolean that should be set to true when watching and reset to
// false once watch should be turned off.
@@ -49,6 +49,7 @@
// Once a consensus has been reached, this is set to false again.
bool inProposalMode = false;
Map<String, String> proposalsKnown; // Only updated via watch.
+ Set<String> _acceptedProposals = new Set<String>(); // Add accepted proposals so that we can ignore them.
// The associated user helps in the production of unique keys.
int _associatedUser;
@@ -65,7 +66,7 @@
// The LogWriter takes a callback for watch updates, the list of users, and
// the logPrefix to write at on table.
- LogWriter(this.updateCallback, this.users, this.logPrefix)
+ LogWriter(this.updateCallback, this.users)
: _cc = new CroupierClient() {
_prepareLog();
}
@@ -115,7 +116,7 @@
}
if (_isProposalKey(key)) {
- if (value != null) {
+ if (value != null && !_acceptedProposals.contains(key)) {
await _receiveProposal(key, value);
}
} else {
@@ -148,12 +149,12 @@
// 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++) {
+ /*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;
}
@@ -192,31 +193,52 @@
return key;
}
+ bool _ownsProposal(String key, String proposalData) {
+ return _proposalSayer(key) == _proposalOwner(proposalData);
+ }
+ int _proposalSayer(String key) {
+ return int.parse(key.split("/").last);
+ }
+ int _proposalOwner(String proposalData) {
+ Map<String, String> pp = JSON.decode(proposalData);
+ String keyP = pp["key"];
+ return int.parse(keyP.split("-").last);
+ }
+
// Helper that handles a proposal update for the associatedUser.
Future _receiveProposal(String key, String proposalData) async {
// If this is a separate device, it may not be in proposal mode yet.
// Set to be in proposal mode now.
- inProposalMode = true;
+ if (!inProposalMode) {
+ inProposalMode = true;
+ proposalsKnown = new Map<String, String>();
+ }
// Let us update our proposal map.
proposalsKnown[key] = proposalData;
- // First check if something is already in data.
+ // Let's obtain our own proposal data.
var pKey = _proposalKey(associatedUser);
var pData = proposalsKnown[pKey];
- if (pData != null) {
- // Potentially change your proposal, if that person has higher priority.
- Map<String, String> pp = JSON.decode(pData);
- Map<String, String> op = JSON.decode(proposalData);
- String keyP = pp["key"];
- String keyO = op["key"];
- if (keyO.compareTo(keyP) < 0) {
- // Then switch proposals.
+
+ // We only have to update our proposal if the updating person is the owner.
+ // This avoids repeated and potentially race-y watch updates.
+ // By sharing the bare minimum, sync/watch should avoid races.
+ if (_ownsProposal(key, proposalData)) {
+ if (pData != null) {
+ // Potentially change your proposal, if that person has higher priority.
+ Map<String, String> pp = JSON.decode(pData);
+ Map<String, String> op = JSON.decode(proposalData);
+ String keyP = pp["key"];
+ String keyO = op["key"];
+ if (keyO.compareTo(keyP) < 0) {
+ // Then switch proposals.
+ await _writeData(pKey, proposalData);
+ }
+ } else {
+ // Otherwise, you have no proposal, so take theirs.
await _writeData(pKey, proposalData);
}
- } else {
- // Otherwise, you have no proposal, so take theirs.
- await _writeData(pKey, proposalData);
}
// Given these changes, check if you can commit the full batch.
@@ -225,11 +247,16 @@
String key = pp["key"];
String value = pp["value"];
+ _acceptedProposals.add(key);
print("All proposals accepted. Proceeding with ${key} ${value}");
// WOULD DO A BATCH!
for (int i = 0; i < users.length; i++) {
await _deleteData(_proposalKey(users[i]));
}
+ // TODO(alexfandrianto): It seems that this will trigger multiple watch
+ // updates even though the data written is the same value to the same key.
+ // I think this is intended, so to work around it, the layer above will
+ // be sure to ignore updates to repeated keys.
await _writeData(key, value);
proposalsKnown = null;
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index c7ef239..e12994b 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -14,6 +14,7 @@
/// In the background, these values will be synced.
/// When setting up a syncgroup, the userIDs are very important.
+import '../../logic/game/game.dart' as logic_game;
import '../../logic/croupier_settings.dart' show CroupierSettings;
import 'croupier_client.dart' show CroupierClient;
import 'discovery_client.dart' show DiscoveryClient;
@@ -26,7 +27,9 @@
import 'package:syncbase/syncbase_client.dart' as sc;
class SettingsManager {
- final util.updateCallbackT updateCallback;
+ final util.updateCallbackT updateSettingsCallback;
+ final util.updateCallbackT updateGamesCallback;
+ final util.updateCallbackT updatePlayerFoundCallback;
final CroupierClient _cc;
sc.SyncbaseTable tb;
@@ -34,11 +37,15 @@
static const String _personalKey = "personal";
static const String _settingsWatchSyncPrefix = "users";
- SettingsManager([this.updateCallback]) : _cc = new CroupierClient();
+ SettingsManager(this.updateSettingsCallback, this.updateGamesCallback, this.updatePlayerFoundCallback) : _cc = new CroupierClient();
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) {
@@ -51,7 +58,7 @@
// Start to watch the stream for the shared settings table.
Stream<sc.WatchChange> watchStream = db.watch(util.tableNameSettings,
_settingsWatchSyncPrefix, await db.getResumeMarker());
- _startWatch(watchStream); // Don't wait for this future.
+ _startWatchSettings(watchStream); // Don't wait for this future.
_loadSettings(tb); // Don't wait for this future.
}
@@ -95,7 +102,7 @@
// This watch method ensures that any changes are propagated to the caller.
// In the case of the settings manager, we're checking for any changes to
// any person's Croupier Settings.
- Future _startWatch(Stream<sc.WatchChange> watchStream) async {
+ Future _startWatchSettings(Stream<sc.WatchChange> watchStream) async {
util.log('Settings watching for changes...');
// This stream never really ends, so I guess we'll watch forever.
await for (sc.WatchChange wc in watchStream) {
@@ -115,14 +122,14 @@
assert(false);
}
- if (this.updateCallback != null) {
- this.updateCallback(key, value);
+ if (this.updateSettingsCallback != null) {
+ this.updateSettingsCallback(_settingsDataKeyUserID(key), value);
}
}
}
// Best called after load(), to ensure that there are settings in the table.
- Future createSyncgroup() async {
+ Future createSettingsSyncgroup() async {
int id = await _getUserID();
_cc.createSyncgroup(
@@ -130,6 +137,87 @@
prefix: this._settingsDataKey(id));
}
+
+ // 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...');
+ // This stream never really ends, so I guess we'll watch forever.
+ await for (sc.WatchChange wc in watchStream) {
+ assert(wc.tableName == util.tableNameGames);
+ util.log('Watch Key: ${wc.rowKey}');
+ util.log('Watch Value ${UTF8.decode(wc.valueBytes)}');
+ String key = wc.rowKey;
+ String value;
+ switch (wc.changeType) {
+ case sc.WatchChangeTypes.put:
+ value = UTF8.decode(wc.valueBytes);
+ break;
+ case sc.WatchChangeTypes.delete:
+ value = null;
+ break;
+ default:
+ assert(false);
+ }
+
+ if (this.updatePlayerFoundCallback != null) {
+ String playerID = _getPartFromBack(key, "/", 1);
+ this.updatePlayerFoundCallback(playerID, value);
+
+ // Also, you should be sure to join this person's syncgroup.
+ _cc.joinSyncgroup(_cc.makeSyncgroupName(await _syncSuffix(int.parse(playerID))));
+ }
+ }
+ }
+
+ Future<logic_game.GameStartData> createGameSyncgroup(String type, int gameID) async {
+ print("Creating game syncgroup for ${type} and ${gameID}");
+ sc.SyncbaseNoSqlDatabase 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", await db.getResumeMarker());
+ _startWatchPlayers(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.
+ await gameTable.row("${gameID}/type").put(UTF8.encode("${type}"));
+
+ int id = await _getUserID();
+ await gameTable.row("${gameID}/owner").put(UTF8.encode("${id}"));
+ await gameTable.row("${gameID}/players/${id}/player_number").put(UTF8.encode("0"));
+
+ logic_game.GameStartData gsd = new logic_game.GameStartData(type, 0, gameID, id);
+
+ await _cc.createSyncgroup(
+ _cc.makeSyncgroupName(util.syncgameSuffix(gsd.toJSONString())), util.tableNameGames,
+ prefix: util.syncgamePrefix(gameID));
+
+ return gsd;
+ }
+
+ Future joinGameSyncgroup(String sgName, int gameID) async {
+ print("Now joining game syncgroup at ${sgName} and ${gameID}");
+ sc.SyncbaseSyncgroup sg = await _cc.joinSyncgroup(sgName);
+
+ sc.SyncbaseNoSqlDatabase 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", await db.getResumeMarker());
+ _startWatchPlayers(watchStream); // Don't wait for this future.
+
+ // Also write yourself to the table as player |NUM_PLAYERS - 1|
+ Map<String, sc.SyncgroupMemberInfo> fellowPlayers = await sg.getMembers();
+ print("I have found! ${fellowPlayers} ${fellowPlayers.length}");
+
+ int id = await _getUserID();
+ int playerNumber = fellowPlayers.length - 1;
+ gameTable.row("${gameID}/players/${id}/player_number").put(UTF8.encode("${playerNumber}"));
+ }
+
// When starting the settings manager, there may be settings already in the
// store. Make sure to load those.
Future _loadSettings(sc.SyncbaseTable tb) async {
@@ -138,7 +226,7 @@
.forEach((sc.KeyValue kv) {
if (kv.key.endsWith("/settings")) {
// Then we can process the value as if it were settings data.
- this.updateCallback(kv.key, UTF8.decode(kv.value));
+ this.updateSettingsCallback(_settingsDataKeyUserID(kv.key), UTF8.decode(kv.value));
}
});
}
@@ -149,8 +237,8 @@
// Someone who is creating a game should scan for players who wish to join.
Future scanSettings() async {
- SettingsScanHandler ssh = new SettingsScanHandler(_cc);
- _cc.discoveryClient.scan(_discoverySettingsKey, "CroupierSettings", ssh);
+ SettingsScanHandler ssh = new SettingsScanHandler(_cc, this.updateGamesCallback);
+ _cc.discoveryClient.scan(_discoverySettingsKey, "CroupierSettingsAndGame", ssh);
}
void stopScanSettings() {
@@ -158,13 +246,16 @@
}
// Someone who wants to join a game should advertise their presence.
- Future advertiseSettings() async {
+ Future advertiseSettings(logic_game.GameStartData gsd) async {
String suffix = await _syncSuffix();
+ String gameSuffix = util.syncgameSuffix(gsd.toJSONString());
_cc.discoveryClient.advertise(
_discoverySettingsKey,
DiscoveryClient.serviceMaker(
- interfaceName: "CroupierSettings",
- addrs: <String>[_cc.makeSyncgroupName(suffix)]));
+ interfaceName: "CroupierSettingsAndGame",
+ addrs: <String>[
+ _cc.makeSyncgroupName(suffix),
+ _cc.makeSyncgroupName(gameSuffix)]));
}
void stopAdvertiseSettings() {
@@ -179,27 +270,48 @@
return int.parse(result);
}
- Future<String> _syncSuffix() async {
- int id = await _getUserID();
+ Future<String> _syncSuffix([int userID]) async {
+ int id = userID;
+ if (id == null) {
+ id = await _getUserID();
+ }
- return "${util.sgSuffix}${id}";
+ return "${util.sgSuffix}-${id}";
}
}
+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.
class SettingsScanHandler extends discovery.ScanHandler {
CroupierClient _cc;
+ Map<List<int>, String> settingsAddrs;
+ Map<List<int>, String> gameAddrs;
+ util.updateCallbackT updateGamesCallback;
- SettingsScanHandler(this._cc);
+ SettingsScanHandler(this._cc, this.updateGamesCallback) {
+ settingsAddrs = new Map<List<int>, String>();
+ gameAddrs = new Map<List<int>, String>();
+ }
void found(discovery.Service s) {
util.log(
"SettingsScanHandler Found ${s.instanceUuid} ${s.instanceName} ${s.addrs}");
- // TODO(alexfandrianto): Filter based on instanceName?
- if (s.addrs.length > 0) {
+ if (s.addrs.length == 2) {
+ // Note: Assumes 2 addresses.
+ settingsAddrs[s.instanceUuid] = s.addrs[0];
+ gameAddrs[s.instanceUuid] = s.addrs[1];
+
+ String json = _getPartFromBack(s.addrs[1], "-", 0);
+ updateGamesCallback(s.addrs[1], json);
+
+
_cc.joinSyncgroup(s.addrs[0]);
} else {
// An unexpected service was found. Who is advertising it?
@@ -213,5 +325,13 @@
// TODO(alexfandrianto): Leave the syncgroup?
// Looks like leave isn't actually implemented, so we can't do this.
+ String addr = gameAddrs[instanceId];
+ if (addr != null) {
+ List<String> parts = addr.split("-");
+ String gameID = parts[parts.length - 1];
+ updateGamesCallback(gameID, null);
+ }
+ settingsAddrs.remove(instanceId);
+ gameAddrs.remove(instanceId);
}
}
diff --git a/lib/src/syncbase/util.dart b/lib/src/syncbase/util.dart
index 456e083..595c6fd 100644
--- a/lib/src/syncbase/util.dart
+++ b/lib/src/syncbase/util.dart
@@ -15,14 +15,25 @@
const mtAddr = '/192.168.86.254:8101';
const sgPrefix = 'croupier/%%sync';
const sgSuffix = 'discovery';
+const sgSuffixGame = 'gaming';
+typedef void NoArgCb();
typedef void updateCallbackT(String key, String value);
String openPermsJson =
'{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}';
Perms openPerms = SyncbaseClient.perms(openPermsJson);
-log(String msg) {
+void log(String msg) {
DateTime now = new DateTime.now();
print('$now $msg');
}
+
+
+// data should contain a JSON-encoded logic_game.GameStartData
+String syncgameSuffix(String data) {
+ return "${sgSuffixGame}-${data}";
+}
+String syncgamePrefix(int gameID) {
+ return "${gameID}";
+}
\ No newline at end of file