blob: 77661a526a06a509d41c68d425b19378037fc3c3 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'dart:async';
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'
show Game, GameType, GameStartData, stringToGameType, gameTypeToString;
enum CroupierState {
welcome,
chooseGame,
joinGame,
arrangePlayers,
playGame,
resumeGame
}
typedef void VoidCallback();
class Croupier {
AppSettings appSettings;
CroupierState state;
SettingsManager settingsManager;
CroupierSettings settings; // null, but loaded asynchronously.
Map<int, CroupierSettings>
settingsEveryone; // empty, but loaded asynchronously
Map<String, GameStartData> gamesFound; // empty, but loads asynchronously
Map<int, int> playersFound; // empty, but loads asynchronously
Game game; // null until chosen
VoidCallback informUICb;
// Futures to use in order to cancel scans and advertisements.
Future _scanFuture;
Future _advertiseFuture;
bool debugMode = false; // whether to show debug buttons or not
Croupier(this.appSettings) {
state = CroupierState.welcome;
settingsEveryone = new Map<int, CroupierSettings>();
gamesFound = new Map<String, GameStartData>();
playersFound = new Map<int, int>();
settingsManager = new SettingsManager(
appSettings,
_updateSettingsEveryoneCb,
_updateGamesFoundCb,
_updatePlayerFoundCb,
_updateGameStatusCb,
_gameLogUpdateCb);
settingsManager.load().then((String csString) {
settings = new CroupierSettings.fromJSONString(csString);
if (this.informUICb != null) {
this.informUICb();
}
settingsManager.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) {
settingsEveryone[int.parse(key)] =
new CroupierSettings.fromJSONString(json);
if (this.informUICb != null) {
this.informUICb();
}
}
void _updateGamesFoundCb(String gameAddr, String jsonData) {
if (jsonData == null) {
gamesFound.remove(gameAddr);
} else {
GameStartData gsd = new GameStartData.fromJSONString(jsonData);
gamesFound[gameAddr] = gsd;
}
if (this.informUICb != null) {
this.informUICb();
}
}
Future _gameLogUpdateCb(String key, String value, bool duringScan) async {
if (game != null && game.gamelog.watchUpdateCb != null) {
await game.gamelog.watchUpdateCb(key, value, duringScan);
}
}
int userIDFromPlayerNumber(int playerNumber) {
return playersFound.keys.firstWhere(
(int user) => playersFound[user] == playerNumber,
orElse: () => null);
}
void _setCurrentGame(Game g) {
game = g;
settings.lastGameID = g.gameID;
settingsManager.save(settings.userID, settings.toJSONString()); // async
}
Game _createNewGame(GameType gt) {
return cg.createGame(gt, this.debugMode, isCreator: true);
}
Game _createExistingGame(GameStartData gsd) {
return cg.createGame(stringToGameType(gsd.type), this.debugMode,
gameID: gsd.gameID, playerNumber: gsd.playerNumber);
}
void _quitGame() {
if (game != null) {
settingsManager.quitGame();
game = null;
}
}
CroupierSettings settingsFromPlayerNumber(int playerNumber) {
int userID = userIDFromPlayerNumber(playerNumber);
if (userID != null) {
return settingsEveryone[userID];
}
return null;
}
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 (!playersFound.containsKey(id)) {
// The player exists but has not sat down yet.
playersFound[id] = null;
}
} else {
int playerNumber = int.parse(playerNum);
playersFound[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();
}
}
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();
setState(CroupierState.playGame, null);
} else if (state == CroupierState.resumeGame) {
game.startGameSignal();
}
break;
default:
print("Ignoring new status: $newStatus");
}
if (this.informUICb != null) {
this.informUICb();
}
}
// Sets the next part of croupier state.
// Depending on the originating state, data can contain extra information that we need.
void setState(CroupierState nextState, var data) {
switch (state) {
case CroupierState.welcome:
// data should be empty unless nextState is ResumeGame.
if (nextState != CroupierState.resumeGame) {
assert(data == null);
}
break;
case CroupierState.chooseGame:
if (data == null) {
// Back button pressed.
break;
}
assert(nextState == CroupierState.arrangePlayers);
// data should be the game id here.
GameType gt = data as GameType;
_setCurrentGame(_createNewGame(gt));
_advertiseFuture = settingsManager
.createGameSyncgroup(gameTypeToString(gt), game.gameID)
.then((GameStartData gsd) {
// Only the game chooser should be advertising the game.
return settingsManager.advertiseSettings(gsd);
}); // don't wait for this future.
break;
case CroupierState.joinGame:
// Note that if we were in join game, we must have been scanning.
_scanFuture.then((_) {
settingsManager.stopScanSettings();
gamesFound.clear();
_scanFuture = null;
});
if (data == null) {
// Back button pressed.
break;
}
// data would probably be the game id again.
GameStartData gsd = data as GameStartData;
gsd.playerNumber = null; // At first, there is no player number.
_setCurrentGame(_createExistingGame(gsd));
String sgName;
gamesFound.forEach((String name, GameStartData g) {
if (g == gsd) {
sgName = name;
}
});
assert(sgName != null);
playersFound[gsd.ownerID] = null;
settingsManager.joinGameSyncgroup(sgName, gsd.gameID);
break;
case CroupierState.arrangePlayers:
// Note that if we were arranging players, we might have been advertising.
if (_advertiseFuture != null) {
_advertiseFuture.then((_) {
settingsManager.stopAdvertiseSettings();
_advertiseFuture = null;
});
}
// The signal to start or quit is not anything special.
// data should be empty.
assert(data == null);
break;
case CroupierState.playGame:
break;
case CroupierState.resumeGame:
// Data might be GameStartData. If so, then we must advertise it.
GameStartData gsd = data;
if (gsd != null) {
_advertiseFuture = settingsManager.advertiseSettings(gsd);
}
break;
default:
assert(false);
}
// A simplified way of clearing out the games and players found.
// They will need to be re-discovered in the future.
switch (nextState) {
case CroupierState.welcome:
gamesFound.clear();
playersFound.clear();
_quitGame();
break;
case CroupierState.joinGame:
// Start scanning for games since that's what's next for you.
_scanFuture =
settingsManager.scanSettings(); // don't wait for this future.
break;
case CroupierState.resumeGame:
// We need to create the game again.
int gameIDData = data;
_resumeGameAsynchronously(gameIDData);
break;
default:
break;
}
state = nextState;
}
// Resumes the game from the given gameID.
Future _resumeGameAsynchronously(int gameIDData) async {
GameStartData gsd = await settingsManager.getGameStartData(gameIDData);
bool wasOwner = (gsd.ownerID == settings?.userID);
print("The game was ${gsd.toJSONString()}, and was I the owner? $wasOwner");
_setCurrentGame(_createExistingGame(gsd));
String sgName = await settingsManager.getGameSyncgroup(gameIDData);
print("The sg name was $sgName");
await settingsManager.joinGameSyncgroup(sgName, gameIDData);
// Since initial scan processing is done, we can now set isCreator
game.isCreator = wasOwner;
String gameStatus = await settingsManager.getGameStatus(gameIDData);
print("The game's status was $gameStatus");
// Depending on the game state, we should go to a different screen.
switch (gameStatus) {
case "RUNNING":
// The game is running, so let's play it!
setState(CroupierState.playGame, null);
break;
default:
// We are still arranging players, so we need to advertise our game
// start data.
setState(CroupierState.arrangePlayers, gsd);
break;
}
// And we can ask the UI to redraw
if (this.informUICb != null) {
this.informUICb();
}
}
}