def testAddConnectionsAfterMsgSrc(self): # message source with no messages msgSrcEmpty = MsgSrc(self.conns) # pylint: disable=unused-variable msgSrc = MsgSrc(self.conns) msgSrc.setMsgs([ Jmai([1], initiatorWs=clientWs3), Jmai([2], initiatorWs=clientWs3) ]) self.assertGiTxQueueMsgs(self.txq, []) # Add websockets afterwards conns1 = Connections(self.txq) conns2 = Connections(self.txq) conns2.addConn(clientWs2) self.conns.addConnections(conns1) self.assertGiTxQueueMsgs(self.txq, []) self.conns.addConnections(conns2) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs2}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs2}, initiatorWs=clientWs3) ]) conns1.addConn(clientWs1) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs1}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs1}, initiatorWs=clientWs3) ])
def __init__(self, turnId, wordId, secret, disallowed, player, otherTeams, allConns, utcTimeout, state=WordState.IN_PLAY, score=None): self._turnId = turnId self._wordId = wordId self._secret = secret self._disallowed = disallowed self._player = player self._otherTeams = otherTeams self._utcTimeout = utcTimeout self._state = state self._score = score or [] # list of teamNumbers that should be awarded points trace(Level.rnd, "New Word", str(self)) # Messages are broadcast to everyone connected to the room self._publicMsgSrc = MsgSrc(allConns) # If we are in play, we have to send private messages revealing # The word in play to some people self._privateMsgSrc = None if state == WordState.IN_PLAY: privateConnsGrp = ConnectionsGroup() privateConnsGrp.addConnections(self._player.playerConns) for team in self._otherTeams: privateConnsGrp.addConnections(team.conns) self._privateMsgSrc = MsgSrc(privateConnsGrp) self.updateMsgs()
class TabooPlayer: def __init__(self, txQueue, allConns, name, team, turnsPlayed=0): """ Creates a taboo player associated to a team. Self-registers to the team txQueue: tx queue allConns: Connections with all clients in the room name: player name team: team that this player should belong to """ self.name = name self.team = team self.turnsPlayed = turnsPlayed self.__ready = False self.playerConns = Connections(txQueue) team.addPlayer(self) self.playerStatusMsgSrc = MsgSrc(allConns) def __str__(self): return "TabooPlayer({}) teamId={}, turnsPlayed={}".format( self.name, self.team.teamNumber, self.turnsPlayed) def addConn(self, ws): self.playerConns.addConn(ws) self.team.conns.addConn(ws) self._updateMsgSrc() def delConn(self, ws): self.playerConns.delConn(ws) self.team.conns.delConn(ws) self._updateMsgSrc() @property def ready(self): return self.__ready @ready.setter def ready(self, flag): self.__ready = flag self._updateMsgSrc() def incTurnsPlayed(self): self.turnsPlayed += 1 def numConns(self): return self.playerConns.count() def _updateMsgSrc(self): self.playerStatusMsgSrc.setMsgs([ Jmai([ "PLAYER-STATUS", self.name, { "numConns": self.numConns(), "ready": self.ready, "turnsPlayed": self.turnsPlayed, } ], None), ])
def __init__(self, txQueue, allConns, teamNumber): self.txQueue = txQueue self.teamNumber = teamNumber self.teamStatusMsgSrc = MsgSrc(allConns) self.members = {} self.conns = Connections(self.txQueue) self._updateMsgSrc()
def initGame(self): self.hostParametersMsgSrc = HostParametersMsgSrc(self.conns, self.hostParameters) self.gameOverMsgSrc = MsgSrc(self.conns) self.teams = {n: TabooTeam(self.txQueue, self.conns, n) for n in range(1, self.hostParameters.numTeams + 1)} wordSet = SupportedWordSets[self.hostParameters.wordSets[0]] self.turnMgr = TurnManager(self.path, self.txQueue, wordSet, self.teams, self.hostParameters, self.conns, self._gameOver)
class TabooTeam: """ Creates a team with a specified teamNumber """ def __init__(self, txQueue, allConns, teamNumber): self.txQueue = txQueue self.teamNumber = teamNumber self.teamStatusMsgSrc = MsgSrc(allConns) self.members = {} self.conns = Connections(self.txQueue) self._updateMsgSrc() def _updateMsgSrc(self): self.teamStatusMsgSrc.setMsgs([ Jmai(["TEAM-STATUS", self.teamNumber, list(self.members)], None), ]) def addPlayer(self, player): """ Players need to be unique. Reset not allowed player: the player """ if self.getPlayer(player.name): assert self.members[player.name] == player trace(Level.info, "Player={} rejoined the team".format(player.name)) return self.members[player.name] = player self._updateMsgSrc() def getPlayer(self, playerName): """ Gets the player assocaited with that name. Return None if no such player found """ return self.members.get(playerName, None) def ready(self): """ Returns true if team has enough ready players to start the game""" if len(self.members) < 2: trace(Level.info, "Team", self.teamNumber, "has", len(self.members), "(less than two) players") return False if all(plyr.ready for plyr in self.members.values()): return True trace(Level.info, "Team", self.teamNumber, "some players are not ready") return False
def __init__(self, txQueue, allConns, name, team, turnsPlayed=0): """ Creates a taboo player associated to a team. Self-registers to the team txQueue: tx queue allConns: Connections with all clients in the room name: player name team: team that this player should belong to """ self.name = name self.team = team self.turnsPlayed = turnsPlayed self.__ready = False self.playerConns = Connections(txQueue) team.addPlayer(self) self.playerStatusMsgSrc = MsgSrc(allConns)
class CardGroupBase: """ How cards are stored is not captured in CardGroupBase """ def __init__(self, conns, playerConns): self.connsMsgSrc = MsgSrc(conns) if conns else None self.playerConnsMsgSrc = MsgSrc(playerConns) if playerConns else None self.refresh() def refresh(self): if self.connsMsgSrc: jmsgs = self._connsJmsgs() if jmsgs is not None: self.connsMsgSrc.setMsgs(jmsgs) if self.playerConnsMsgSrc: jmsgs = self._playerConnsJmsgs() if jmsgs is not None: self.playerConnsMsgSrc.setMsgs(jmsgs) def _connsJmsgs(self): raise NotImplementedError def _playerConnsJmsgs(self): raise NotImplementedError @staticmethod def contains(cards1, cards2): """Returns cards1 contains cards2""" cards1_ = cards1[:] for card in cards2: try: idx = cards1_.index(card) except ValueError: return False cards1_.pop(idx) return True
def __init__(self, path, txQueue, wordSet, teams, hostParameters, allConns, gameOverCb): """ Arguments --------- gameOverCb : asyncio.Queue wordSet : WordSet teams : dict[int: TabooTeam] hostParameters : HostParameters allConns : Connections gameOverCb : function (no args) Normally, the return value from TurnManager can signal to the game room if the game gets over. However, when the timer expires (timer handler is invoked from the Bari core), we need a way to call just the gameOver function in the Room. An ugly way is to pass the whole room here which I am avoiding by passing in gameOverCb instead. """ self._path = path self._txQueue = txQueue self._wordSet = wordSet self._teams = teams self._hostParameters = hostParameters # Stop the game when every live player # has played hostParameters.numTurns self._allConns = allConns self._gameOverCb = gameOverCb self._wordsByTurnId = defaultdict(list) self._curTurnId = 0 self._curTurn = None # Points to the current turn in play self._activePlayer = None self._waitForKickoffMsgSrc = MsgSrc(self._allConns) self._utcTimeout = None # UTC epoch of when this turn expires self._state = TurnMgrState.GAME_START_WAIT self._scoreMsgSrc = ScoreMsgSrc(self._allConns, self._wordsByTurnId, set(self._teams))
def testAddDelConnections(self): conns1 = Connections(self.txq) conns1.addConn(clientWs1) conns2 = Connections(self.txq) conns2.addConn(clientWs2) self.conns.addConnections(conns1) self.conns.addConnections(conns2) msgSrc = MsgSrc(self.conns) msgSrc.setMsgs([ Jmai([1], initiatorWs=clientWs3), Jmai([2], initiatorWs=clientWs3) ]) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs1}, initiatorWs=clientWs3), ClientTxMsg([1], {clientWs2}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs1}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs2}, initiatorWs=clientWs3) ], anyOrder=True) self.conns.delConnections(conns1) clientWs4 = 104 conns3 = Connections(self.txq) conns3.addConn(clientWs3) conns3.addConn(clientWs4) self.conns.addConnections(conns3) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs3, clientWs4}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs3, clientWs4}, initiatorWs=clientWs3) ], anyOrder=True)
def __init__(self, conns, playerConns): self.connsMsgSrc = MsgSrc(conns) if conns else None self.playerConnsMsgSrc = MsgSrc(playerConns) if playerConns else None self.refresh()
class TabooRoom(GamePlugin): def __init__(self, path, name, hostParameters, state=GameState.WAITING_TO_START): super(TabooRoom, self).__init__(path, name) self.hostParameters = hostParameters self.state = state self.playerByWs = {} #<ws:player> self.teams = None # int -> Team self._winnerTeamIds = [] # Initialized after queues are set up self.hostParametersMsgSrc = None self.gameOverMsgSrc = None self.turnMgr = None def initGame(self): self.hostParametersMsgSrc = HostParametersMsgSrc( self.conns, self.hostParameters) self.gameOverMsgSrc = MsgSrc(self.conns) self.teams = { n: TabooTeam(self.txQueue, self.conns, n) for n in range(1, self.hostParameters.numTeams + 1) } wordSet = SupportedWordSets[self.hostParameters.wordSets[0]] self.turnMgr = TurnManager(self.path, self.txQueue, wordSet, self.teams, self.hostParameters, self.conns, self._gameOver) def publishGiStatus(self): """Invoked to update the lobby of the game instance (room) status ["GAME-STATUS", <path:str>, {"gameState": <str>, "clientCount": { teamId<int>:{plyrName<str>:clientCount<int>} }, "hostParams": <dict>, "winners":[winnerTeam<int>,...]}] """ jmsg = [{ "hostParameters": self.hostParameters.toJmsg()[0], "gameState": self.state.name, "clientCount": self._clientInfo(), "winners": self._winnerTeamIds }] self.txQueue.put_nowait(InternalGiStatus(jmsg, self.path)) def postQueueSetup(self): """Invoked when the RX+TX queues are set up to the room and when the self.conns object is setup to track all clients in the room """ self.initGame() self.publishGiStatus() def postProcessConnect(self, ws): """Invoked when a new client (websocket) connects from the room. Note that no messages have been exchanged yet """ self.playerByWs[ws] = None self.publishGiStatus() def postProcessDisconnect(self, ws): """Invoked when a client disconnects from the room""" assert ws in self.playerByWs, "Invalid disconnect on a non-existant connection" player = self.playerByWs[ws] if player: player.delConn(ws) del self.playerByWs[ws] self.publishGiStatus() def _allPlayersReady(self): """Called when all players have sent the ready message Start the first turn (pick the player) and wait for a KICKOFF message from that player """ trace(Level.game, "All players are ready, starting game") assert self.state == GameState.WAITING_TO_START self.state = GameState.RUNNING self.turnMgr.startNewTurn() def processMsg(self, qmsg): """Handle messages from the queue coming to this room message-type1: ["JOIN", ...] """ if super(TabooRoom, self).processMsg(qmsg): return True if qmsg.jmsg[0] == "JOIN": return self.__processJoin(qmsg) if qmsg.jmsg[0] == "READY": return self.__processReady(qmsg) if qmsg.jmsg[0] == "KICKOFF": return self.__processKickoff(qmsg) if qmsg.jmsg[0] == "DISCARD" or qmsg.jmsg[0] == "COMPLETED": return self.__processCompletedOrDiscard(qmsg) return False def __validateCompletedOrDiscard(self, qmsg): """ Validates [COMPLETED|DISCARD, turn<int>, wordIdx<int>] Replies a DISCARD-BAD or COMPLETED-BAD if the message is incorrect, or if the message is received at wrong game state Returns True iff message format is valid """ msgType = qmsg.jmsg[0] assert msgType in ("DISCARD", "COMPLETED") badReplyType = "{}-BAD".format(msgType) ws = qmsg.initiatorWs if len(qmsg.jmsg) != 3: self.txQueue.put_nowait( ClientTxMsg([badReplyType, "Invalid message length"], {ws}, initiatorWs=ws)) return False if (not isinstance(qmsg.jmsg[1], int)) or (not isinstance( qmsg.jmsg[2], int)): self.txQueue.put_nowait( ClientTxMsg([badReplyType, "Invalid message type"], {ws}, initiatorWs=ws)) return False if self.state != GameState.RUNNING: trace(Level.play, "_process{} current state".format(msgType), self.state.name) self.txQueue.put_nowait( ClientTxMsg([badReplyType, "Game not running"], {ws}, initiatorWs=ws)) return False player = self.playerByWs[ws] if player != self.turnMgr.activePlayer: trace( Level.play, "_process{} msg rcvd from".format(msgType), player.name if player else None, "activePlayer", self.turnMgr.activePlayer.name if self.turnMgr.activePlayer else None) self.txQueue.put_nowait( ClientTxMsg([badReplyType, "It is not your turn"], {ws}, initiatorWs=ws)) return False return True def __processCompletedOrDiscard(self, qmsg): """ ["DISCARD|COMPLETED", turn<int>, wordIdx<int>] """ if not self.__validateCompletedOrDiscard(qmsg): return True return self.turnMgr.processCompletedOrDiscard(qmsg) def __processKickoff(self, qmsg): """ ["KICKOFF"] """ ws = qmsg.initiatorWs if len(qmsg.jmsg) != 1: self.txQueue.put_nowait( ClientTxMsg(["KICKOFF-BAD", "Invalid message length"], {ws}, initiatorWs=ws)) return True if self.state != GameState.RUNNING: trace(Level.play, "_processDiscard current state", self.state.name) self.txQueue.put_nowait( ClientTxMsg(["KICKOFF-BAD", "Game not running"], {ws}, initiatorWs=ws)) return True player = self.playerByWs[ws] if player != self.turnMgr.activePlayer: trace( Level.play, "_processDiscard msg rcvd from", player.name if player else None, "activePlayer", self.turnMgr.activePlayer.name if self.turnMgr.activePlayer else None) self.txQueue.put_nowait( ClientTxMsg(["KICKOFF-BAD", "It is not your turn"], {ws}, initiatorWs=ws)) return True return self.turnMgr.processKickoff(qmsg) def __processReady(self, qmsg): """ ["READY"] """ ws = qmsg.initiatorWs if len(qmsg.jmsg) != 1: self.txQueue.put_nowait( ClientTxMsg(["READY-BAD", "Invalid message length"], {ws}, initiatorWs=ws)) return True #if self.state != GameState.WAITING_TO_START: # self.txQueue.put_nowait(ClientTxMsg(["READY-BAD", "Game already started/ended"], # {ws}, initiatorWs=ws)) # return True player = self.playerByWs.get(ws, None) if not player: self.txQueue.put_nowait( ClientTxMsg(["READY-BAD", "Join first"], {ws}, initiatorWs=ws)) return True if player.ready: self.txQueue.put_nowait( ClientTxMsg(["READY-BAD", "Already ready"], {ws}, initiatorWs=ws)) return True player.ready = True if all(t.ready() for t in self.teams.values()): self.turnMgr.startNewTurn() self.state = GameState.RUNNING trace(Level.info, "Game started") return True def __processJoin(self, qmsg): """ ["JOIN", playerName, team:int={0..T}] """ ws = qmsg.initiatorWs assert ws in self.playerByWs, "Join request from an unrecognized connection" if self.playerByWs[ws]: self.txQueue.put_nowait( ClientTxMsg([ "JOIN-BAD", "Unexpected JOIN message from client that " "has already joined" ], {ws}, initiatorWs=ws)) return True if len(qmsg.jmsg) != 3: self.txQueue.put_nowait( ClientTxMsg(["JOIN-BAD", "Invalid message length"], {ws}, initiatorWs=ws)) return True _, playerName, teamNumber = qmsg.jmsg if not isinstance(playerName, str) or not validPlayerNameRe.match(playerName): self.txQueue.put_nowait( ClientTxMsg(["JOIN-BAD", "Invalid player name", playerName], {ws}, initiatorWs=ws)) return True if (not isinstance(teamNumber, int) or teamNumber < 0 or teamNumber > self.hostParameters.numTeams): self.txQueue.put_nowait( ClientTxMsg(["JOIN-BAD", "Invalid team number", teamNumber], {ws}, initiatorWs=ws)) return True return self.joinPlayer(ws, playerName, teamNumber) def joinPlayer(self, ws, playerName, teamNumber): player = self.getPlayer(playerName) # A new player joins if not player: team = self.__getTeam(teamNumber) player = TabooPlayer(self.txQueue, self.conns, playerName, team) # A late-joinee joins in ready state player.ready = self.state != GameState.WAITING_TO_START else: # New join-request for an existing player will be accepted # but we ignore the requested team and associate the player # to the original team if teamNumber != player.team.teamNumber: trace( Level.warn, "Player {} forced to join their " "original team {}".format(player.name, player.team.teamNumber)) self.__finalizeJoin(ws, player) self.publishGiStatus() return True def getPlayer(self, playerName): """ Returns player player: player matching the name None if no player exists by that name """ for t in self.teams.values(): v = t.getPlayer(playerName) if v is not None: return v return None def __finalizeJoin(self, ws, player): player.addConn(ws) self.playerByWs[ws] = player self.txQueue.put_nowait( ClientTxMsg(["JOIN-OKAY", player.name, player.team.teamNumber], {ws}, initiatorWs=ws)) def spectatorCount(self): return sum(1 for plyr in self.playerByWs.values() if not plyr) def __getTeam(self, teamNumber): team = self.teams.get(teamNumber) if not team: return min(self.teams.values(), key=lambda t: len(t.members)) return team def _gameOver(self): trace(Level.game, "Game Over") self.state = GameState.GAME_OVER # Update self._winnerTeamIds scoreByTeamId = self.turnMgr.totalScore maxScore = max(scoreByTeamId.values()) self._winnerTeamIds = [ teamId for teamId, score in scoreByTeamId.items() if score == maxScore ] self.gameOverMsgSrc.setMsgs([ Jmai(["GAME-OVER", self._winnerTeamIds], None), ]) self.publishGiStatus() def _clientInfo(self): return { tmNr: {plyr.name: plyr.numConns() for plyr in tm.members.values()} for (tmNr, tm) in self.teams.items() }
def test1MsgSrc1Websocket(self): """ Try various triggers of adding/removing MsgSrcs and Websockets """ self.assertGiTxQueueMsgs(self.txq, []) # Adding connections doesn't create a message self.conns.addConn(clientWs1) self.assertGiTxQueueMsgs(self.txq, []) # Adding msgSrc with no messages msgSrc1 = MsgSrc(self.conns) self.assertGiTxQueueMsgs(self.txq, []) # Set messages in msgSrc msgSrc1.setMsgs([ Jmai([1], initiatorWs=clientWs3), Jmai([2], initiatorWs=clientWs3) ]) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs1}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs1}, initiatorWs=clientWs3) ]) # Adding msgSrc with previous messages msgSrc2 = MsgSrc(self.conns) msgSrc2.setMsgs( [Jmai([True], initiatorWs=None), Jmai([False], initiatorWs=None)]) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([True], {clientWs1}), ClientTxMsg([False], {clientWs1}) ]) # Adding a second connection self.conns.addConn(clientWs2) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs2}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs2}, initiatorWs=clientWs3), ClientTxMsg([True], {clientWs2}, initiatorWs=None), ClientTxMsg([False], {clientWs2}, initiatorWs=None) ], anyOrder=True) # Adding msgSrc with state preset msgSrc2.setMsgs([Jmai(["yes"], initiatorWs=None)]) self.assertGiTxQueueMsgs( self.txq, [ClientTxMsg(["yes"], {clientWs1, clientWs2})]) # Delete msgSrc self.conns.delMsgSrc(msgSrc2) # Add another client self.conns.addConn(clientWs3) self.assertSetEqual(self.conns.msgSrcs, {msgSrc1}) self.assertGiTxQueueMsgs(self.txq, [ ClientTxMsg([1], {clientWs3}, initiatorWs=clientWs3), ClientTxMsg([2], {clientWs3}, initiatorWs=clientWs3) ], anyOrder=True) # Remove a client self.conns.delConn(clientWs2) self.assertGiTxQueueMsgs(self.txq, []) # Add msgSrc2 again self.conns.addMsgSrc(msgSrc2) self.assertGiTxQueueMsgs( self.txq, [ClientTxMsg(["yes"], {clientWs1, clientWs3}, initiatorWs=None)], anyOrder=True)
def postQueueSetup(self): self.publishGiStatus() self.winners = MsgSrc(self.conns)
def setPostInitParams(self, conns): assert not self.msgSrc self.msgSrc = MsgSrc(conns) # Set the message to be sent to all clients in conns self.msgSrc.setMsgs( [Jmai(["HOST-PARAMETERS", dict(self.state)], None)])
class HostParameters: """Tracks host parameters for a game The actual data is stored in self.state (a Map -- a glorified dictionary) Since this object is usually constructed from client side, validation is explicitly done and InvalidDataException is raised on error which is passed back to the client trying to host the game. The object embeds a MsgSrc (self.msgSrc) which is used to emit host parameters to every client of the hosted room (once the room is created). This is initialized by calling setPostInitParams() after the game room is created. """ ctrArgs = ( "numTeams", "turnDurationSec", "wordSets", "numTurns", ) def __init__(self, numTeams, turnDurationSec, wordSets, numTurns): self.msgSrc = None if not isinstance(numTeams, int) or numTeams < MIN_TEAMS or numTeams > MAX_TEAMS: raise InvalidDataException("Invalid type or numTeams", numTeams) if (not isinstance(turnDurationSec, int) or turnDurationSec < MIN_TURN_DURATION or turnDurationSec > MAX_TURN_DURATION): raise InvalidDataException("Invalid type or turnDurationSec", turnDurationSec) if (not isinstance(wordSets, list) or not wordSets or not set(wordSets).issubset(SupportedWordSets)): raise InvalidDataException("Invalid type or wordSets", wordSets) if (not isinstance(numTurns, int) or numTurns < 1 or numTurns > MAX_TURNS): raise InvalidDataException("Invalid type or numTurns", numTurns) vals = locals() self.state = Map( **{argName: vals[argName] for argName in self.ctrArgs}) def __getattr__(self, name): """When access hostParameters.numTeams, fetch it from self.state instead""" return getattr(self.state, name) def setPostInitParams(self, conns): assert not self.msgSrc self.msgSrc = MsgSrc(conns) # Set the message to be sent to all clients in conns self.msgSrc.setMsgs( [Jmai(["HOST-PARAMETERS", dict(self.state)], None)]) @staticmethod def fromJmsg(jmsg): if not isinstance(jmsg, list) or len(jmsg) != 1 or not isinstance( jmsg[0], dict): raise InvalidDataException( "Invalid host parameters type or length", jmsg) if set(jmsg[0]) != set(HostParameters.ctrArgs): raise InvalidDataException( "Invalid or unexpected host parameters keys", jmsg) return HostParameters(**jmsg[0]) def toJmsg(self): return [dict(self.state)]
class Word: def __init__(self, turnId, wordId, secret, disallowed, player, otherTeams, allConns, utcTimeout, state=WordState.IN_PLAY, score=None): self._turnId = turnId self._wordId = wordId self._secret = secret self._disallowed = disallowed self._player = player self._otherTeams = otherTeams self._utcTimeout = utcTimeout self._state = state self._score = score or [] # list of teamNumbers that should be awarded points trace(Level.rnd, "New Word", str(self)) # Messages are broadcast to everyone connected to the room self._publicMsgSrc = MsgSrc(allConns) # If we are in play, we have to send private messages revealing # The word in play to some people self._privateMsgSrc = None if state == WordState.IN_PLAY: privateConnsGrp = ConnectionsGroup() privateConnsGrp.addConnections(self._player.playerConns) for team in self._otherTeams: privateConnsGrp.addConnections(team.conns) self._privateMsgSrc = MsgSrc(privateConnsGrp) self.updateMsgs() def __str__(self): return "Word({},{}) player={} secret={} state={} score={}".format( self._turnId, self._wordId, self._player.name, self._secret, self._state, self._score) @property def player(self): return self._player @property def state(self): return self._state @property def wordId(self): return self._wordId @property def score(self): return self._score def updateMsgs(self): """Figures out what messages to send to who based on internal state""" if self._state == WordState.IN_PLAY: assert self._privateMsgSrc msg1 = ["TURN", self._turnId, self._wordId, {"team": self._player.team.teamNumber, "player": self._player.name, "state": self._state.name, "utcTimeout": self._utcTimeout}] self._publicMsgSrc.setMsgs([Jmai(msg1, None)]) msg2 = msg1[:] # make a copy of msg1 msg2[3] = dict(msg1[3]) # Make a copy of the dict (and don't update # the dictionary in msg1) msg2[3].update({ "secret": self._secret, "disallowed": self._disallowed, }) self._privateMsgSrc.setMsgs([Jmai(msg2, None)]) return # If the turn isn't in play, there is no private messaging needed. if self._privateMsgSrc: self._privateMsgSrc.setMsgs([]) # Reset previous messages self._privateMsgSrc = None msg = ["TURN", self._turnId, self._wordId, {"team": self._player.team.teamNumber, "player": self._player.name, "state": self._state.name, "secret": self._secret, "disallowed": self._disallowed, "score": self._score}] self._publicMsgSrc.setMsgs([Jmai(msg, None)]) def resolve(self, state): """Resolve the word as DISCARDED, COMPLETED, or TIMED_OUT""" trace(Level.play, "state", state.name) if state == WordState.COMPLETED: self._score = [self._player.team.teamNumber] else: self._score = [team.teamNumber for team in self._otherTeams] self._state = state self.updateMsgs()
class TurnManager: def __init__(self, path, txQueue, wordSet, teams, hostParameters, allConns, gameOverCb): """ Arguments --------- gameOverCb : asyncio.Queue wordSet : WordSet teams : dict[int: TabooTeam] hostParameters : HostParameters allConns : Connections gameOverCb : function (no args) Normally, the return value from TurnManager can signal to the game room if the game gets over. However, when the timer expires (timer handler is invoked from the Bari core), we need a way to call just the gameOver function in the Room. An ugly way is to pass the whole room here which I am avoiding by passing in gameOverCb instead. """ self._path = path self._txQueue = txQueue self._wordSet = wordSet self._teams = teams self._hostParameters = hostParameters # Stop the game when every live player # has played hostParameters.numTurns self._allConns = allConns self._gameOverCb = gameOverCb self._wordsByTurnId = defaultdict(list) self._curTurnId = 0 self._curTurn = None # Points to the current turn in play self._activePlayer = None self._waitForKickoffMsgSrc = MsgSrc(self._allConns) self._utcTimeout = None # UTC epoch of when this turn expires self._state = TurnMgrState.GAME_START_WAIT self._scoreMsgSrc = ScoreMsgSrc(self._allConns, self._wordsByTurnId, set(self._teams)) @property def activePlayer(self): return self._activePlayer @property def numTurns(self): return self._hostParameters.numTurns @property def turnDurationSec(self): return self._hostParameters.turnDurationSec @property def totalScore(self): return self._scoreMsgSrc.score def updateState(self, newState): if self._state == newState: return self._state = newState if newState == TurnMgrState.KICKOFF_WAIT: self._waitForKickoffMsgSrc.setMsgs([ Jmai([ "WAIT-FOR-KICKOFF", self._curTurnId, self._activePlayer.name ], None), ]) else: self._waitForKickoffMsgSrc.setMsgs([]) # --------------------------------- # Message handlers def __validateCompletedOrDiscard(self, qmsg): """ Validates [COMPLETED|DISCARD, turn<int>, wordIdx<int>] Replies a DISCARD-BAD or COMPLETED-BAD if the message is received at wrong turn state Returns True iff message is valid """ msgType = qmsg.jmsg[0] assert msgType in ("DISCARD", "COMPLETED") badReplyType = "{}-BAD".format(msgType) ws = qmsg.initiatorWs if self._state != TurnMgrState.RUNNING: trace(Level.play, "process{} turn state".format(msgType), self._state.name) self._txQueue.put_nowait( ClientTxMsg( [badReplyType, "Can't {} right now".format(msgType)], {ws}, initiatorWs=ws)) return False if qmsg.jmsg[1] != self._curTurnId: self._txQueue.put_nowait( ClientTxMsg([badReplyType, "Invalid turn"], {ws}, initiatorWs=ws)) return False assert self._curTurnId in self._wordsByTurnId, ( "Since the turn is in running state, {} must exist in {}".format( self._curTurnId, self._wordsByTurnId.keys())) assert self._wordsByTurnId[self._curTurnId], ( "Since the turn is in running state, there must be at least 1 word in {}" .format(self._wordsByTurnId[self._curTurnId])) lastWord = self._wordsByTurnId[self._curTurnId][-1] if qmsg.jmsg[2] != lastWord.wordId: self._txQueue.put_nowait( ClientTxMsg([badReplyType, "Invalid word"], {ws}, initiatorWs=ws)) return False if lastWord.state != WordState.IN_PLAY: self._txQueue.put_nowait( ClientTxMsg([badReplyType, "The word is no longer in play"], {ws}, initiatorWs=ws)) return False return True def processCompletedOrDiscard(self, qmsg): """ Guarantees from the caller 1. qmsg.jmsg is of right length and right type 2. Correct player is invoking this 3. Game is not over yet Always returns True (message is ingested) """ if not self.__validateCompletedOrDiscard(qmsg): return True lastWord = self._wordsByTurnId[self._curTurnId][-1] wordState = (WordState.COMPLETED if qmsg.jmsg[0] == "COMPLETED" else WordState.DISCARDED) lastWord.resolve(wordState) self._scoreMsgSrc.updateTotal() if not self.startNextWord(): # game over trace(Level.play, "Last word discarded/completed, no more words. Game over") self.activePlayer.incTurnsPlayed() self._gameOverCb() return True def processKickoff(self, qmsg): """Always returns True (implies message ingested)""" ws = qmsg.initiatorWs if self._state != TurnMgrState.KICKOFF_WAIT: self._txQueue.put_nowait( ClientTxMsg(["KICKOFF-BAD", "Can't kickoff a turn"], {ws}, initiatorWs=ws)) return True # Start with a new word for the activePlayer ctx = {"turnId": self._curTurnId} self._txQueue.put_nowait( TimerRequest(self.turnDurationSec, self.timerExpiredCb, ctx)) self._utcTimeout = expiryEpoch(self.turnDurationSec) assert self.startNextWord( ) is True, "Must always be able to start a new word" return True # --------------------------------- # Turn and word management def timerExpiredCb(self, ctx): """This method is invoked when the timer fires""" trace(Level.rnd, "Timer fired", ctx) if self._state == TurnMgrState.GAME_OVER: trace(Level.info, "Timer fired but game is already over. Nothing to do") return assert isinstance(ctx, dict) assert "turnId" in ctx if self._curTurnId != ctx["turnId"]: trace(Level.warn, "Timer fired for turn", ctx["turnId"], "but turnMgr is on turn", self._curTurnId) return assert self._state == TurnMgrState.RUNNING, \ "Turn must be running when the timer fires" lastWord = self._wordsByTurnId[self._curTurnId][-1] if lastWord.state != WordState.IN_PLAY: trace(Level.error, "The last word should be in play. This is unexpected") return self.activePlayer.incTurnsPlayed() lastWord.resolve(WordState.TIMED_OUT) self._scoreMsgSrc.updateTotal() turnStarted = self.startNewTurn() if not turnStarted: # Game over trace(Level.play, "Couldn't start a new turn. Game over") self.updateState(TurnMgrState.GAME_OVER) self._gameOverCb() def _findNextPlayer(self): """ Returns TabooPlayer or None If a player should play next, return TabooPlayer else return None """ candidatePlayersByTeam = {} for team in self._teams.values(): candidatePlayers = [ plyr for plyr in team.members.values() if plyr.playerConns.count() > 0 ] if candidatePlayers: candidatePlayersByTeam[team.teamNumber] = \ sorted(candidatePlayers, key=lambda plyr: plyr.turnsPlayed) if not candidatePlayersByTeam: trace(Level.info, "no live players") return None if not any( any(True for plyr in plyrs if plyr.turnsPlayed < self.numTurns) for plyrs in candidatePlayersByTeam.values()): trace(Level.info, "All players have played atleast", self.numTurns, "turns") return None # Identify current team that's playing if self._wordsByTurnId: currentTeam = self._wordsByTurnId[self._curTurnId][-1].player.team else: currentTeam = random.choice(list(self._teams.values())) # Identify nextPlayer (preferring a player from the next team) teamCount = len(self._teams) for teamIdx in [ 1 + ((ti + currentTeam.teamNumber) % teamCount) for ti in range(teamCount) ]: if teamIdx not in candidatePlayersByTeam: # No live clients in this team continue nextPlayer = candidatePlayersByTeam[teamIdx][0] trace(Level.rnd, "next player", nextPlayer.name) return nextPlayer assert False, "Can't find a candidate in " + str( candidatePlayersByTeam) return None def startNewTurn(self): """ Returns bool : true if new turn was started; false if the game is over """ # 1. Find next player to go. If none is found, declare end of game self._activePlayer = self._findNextPlayer() if not self._activePlayer: trace(Level.rnd, "Can't start a new turn as no next player available") self.updateState(TurnMgrState.GAME_OVER) return False # 2. Start a new turn self._curTurnId += 1 self._curTurn = None # No word selected to start if not self._wordSet.areWordsAvailable(self._path): trace(Level.rnd, "Words exhausted") self.updateState(TurnMgrState.GAME_OVER) return False self.updateState(TurnMgrState.KICKOFF_WAIT) return True def startNextWord(self): """ Returns bool : True if new word started; false if the game is over (no more words available) """ assert self.activePlayer self.updateState(TurnMgrState.RUNNING) if self._wordsByTurnId[self._curTurnId]: trace(Level.debug, "curTurnId", self._curTurnId, "lastWord", self._wordsByTurnId[self._curTurnId][-1], "state", self._wordsByTurnId[self._curTurnId][-1].state) assert self._wordsByTurnId[ self._curTurnId][-1].state != WordState.IN_PLAY nextWordId = len(self._wordsByTurnId[self._curTurnId]) + 1 # Fetch a new word found = self._wordSet.nextWord(self._path) if not found: trace(Level.rnd, "Can't start a new turn as no word available") # Ran out of words return False secret = found.word disallowed = found.disallowed trace(Level.rnd, "player", self.activePlayer.name, "word", secret) # Create a new Word turn = Word(self._curTurnId, nextWordId, secret, disallowed, self.activePlayer, [ team for team in self._teams.values() if team != self.activePlayer.team ], self._allConns, self._utcTimeout) self._wordsByTurnId[self._curTurnId].append(turn) self._curTurn = turn return True