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 __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 __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 validatePlay(self, round_, player, dropCards, numDrawCards, pickCards): # All dropCards should have the same rank if len(set(card.rank for card in dropCards)) > 1: trace(Level.debug, "All cards must have the same rank") return False return True
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 main(wsAddr="0.0.0.0"): """Initialize websocket serving server and load plugins""" parser = ArgumentParser() parser.add_argument("-p", "--port", metavar="PORT", help="Listen port (default={})".format(WS_SERVER_PORT_DEFAULT), default=WS_SERVER_PORT_DEFAULT) parser.add_argument("--d7-storage", metavar="SQLITE3_FILE", help="Dirty7 sqlite3 storage file (default={})".format( Dirty7.Dirty7Lobby.DefaultStorageFile), default=Dirty7.Dirty7Lobby.DefaultStorageFile) parser.add_argument("--trace-file", help="Trace file (default=STDERR)") args = parser.parse_args() setTraceFile(args.trace_file) trace(Level.info, "Starting server. Listening on", wsAddr, "port", args.port) wsServer = websockets.serve(rxClient, wsAddr, args.port) # pylint: disable=no-member asyncio.get_event_loop().create_task(giTxQueue(txQueue())) plugins = [ fwk.LobbyPlugin.plugin(), Chat.ChatLobbyPlugin.plugin(), Dirty7.Dirty7Lobby.plugin(args.d7_storage), Taboo.TabooLobby.plugin(), ] for plugin in plugins: registerGameClass(plugin) asyncio.get_event_loop().run_until_complete(wsServer) asyncio.get_event_loop().run_forever()
async def clientTxMsg(msg, toWs): """Helper to queue a message to be sent to 'toWs'""" if toWs not in ClientTxQueueByWs: trace(Level.error, "clientTxMsg: unable to queue", "'%s'" % msg, "for sending to client", toWs) return trace(Level.debug, "clientTxMsg:", toWs, "'%s'" % msg) ClientTxQueueByWs[toWs].put_nowait(msg)
def validatePlay(self, round_, player, dropCards, numDrawCards, pickCards): # Cards played must have exactly 1 suit (and jokers) suitsSeen = set(card.suit for card in dropCards) - {Card.JOKER} if len(suitsSeen) > 1: trace(Level.debug, "More than 1 suit is played") return False return True
def clientTxQueueRemove(ws): """Remove a TX queue + task for a websocket""" del ClientTxQueueByWs[ws] ClientTxTaskByWs[ws].cancel() #await asyncio.gather(ClientTxTaskByWs[ws]) # When is this called? del ClientTxTaskByWs[ws] # Client disconnected trace(Level.conn, "Client", ws, "disconnected")
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()
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()
async def clientTxTask(queue, clientWs): """ Task to drain the queue with messages meant to be sent to the client's websocket. """ while True: msg = await queue.get() trace(Level.msg, "clientTxTask: sending", "'%s'" % msg, "to", clientWs) await clientWs.send(msg) queue.task_done()
def processRefresh(self, qmsg): if qmsg.jmsg[1] in SupportedWordSets: try: SupportedWordSets[qmsg.jmsg[1]].loadData() except Exception as exc: # pylint: disable=broad-except trace(Level.error, "Failed to refresh", qmsg.jmsg[1], str(exc)) self.txQueue.put_nowait( ClientTxMsg(["REFRESH-BAD"], {qmsg.initiatorWs}, initiatorWs=qmsg.initiatorWs)) return False # Pretend message wasn't understood
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 __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 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 newRound(self, startRound): roundNum = startRound.roundNum trace(Level.rnd, self.path, "starting round", roundNum) self.currRoundTurn = Turn(self.conns, roundNum, startRound.turnOrderNames, startRound.turnIdx) # Get round parameters roundParameters = self.hostParameters.roundParameters(roundNum) round_ = Round(self.path, self.conns, roundParameters, self.playerByName, self.currRoundTurn) self.rounds.append(round_)
def registerGameClass(gi): """Register game instance with the main loop""" trace(Level.game, "Registering {} ({})".format(gi.name, gi.path)) assert gi.path not in GiByPath GiByPath[gi.path] = gi giRxQueue = asyncio.Queue() gi.setRxTxQueues(giRxQueue, TxQueue) GiRxTaskByPath[gi.path] = \ asyncio.get_event_loop().create_task(gi.worker()) GiRxQueueByPath[gi.path] = giRxQueue
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 validatePlay(self, round_, player, dropCards, numDrawCards, pickCards): # Cards played must have exactly 1 card of each rank ranksSeen = [card.rank for card in dropCards] if len(ranksSeen) != len(set(ranksSeen)): trace(Level.debug, "Duplicate cards played") return False # Ensure the cards are in a sequence ranksSeen = sorted(ranksSeen) if ranksSeen != list(range(ranksSeen[0], ranksSeen[0] + len(ranksSeen))): trace(Level.debug, "Cards not in a sequence") return False return True
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 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 self.__finalizeJoin(ws, player) #self.processEvent(PlayerJoin(player)) return True #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) return True
def processDeclare(self, round_, player): """ If declareMaxPoints is specified, make sure that the person declaring has points <= declareMaxPoints. Return Declare() event if it is a valid declare, None otherwise. """ roundParams = round_.roundParams playerRoundStatus = round_.playerRoundStatus[player.name] playerHand = playerRoundStatus.hand handScore = playerHand.score() if (roundParams.declareMaxPoints[0] and handScore > roundParams.declareMaxPoints[0]): trace(Level.info, "Declaring with", handScore, "points >", roundParams.declareMaxPoints, "points.", [str(cd) for cd in playerHand.cards]) return None # Points are sufficiently low return Declare(player, handScore)
async def _job(self): trace(Level.msg, "Starting sleep for", self._qmsg) await asyncio.sleep(self._qmsg.afterSec) trace(Level.msg, "Firing callback for", self._qmsg) self._qmsg.cb(self._qmsg.ctx) trace(Level.msg, "Callback returned for", self._qmsg) del timerByQmsg[self._qmsg] # This is funky? Losing reference to self
async def giTxQueue(queue): """ Task to drain the queue with messages from game instances. These messages can be meant for other game instances (only lobby) or to client websockets. """ while True: qmsg = await queue.get() queue.task_done() if isinstance(qmsg, InternalHost): if giByPath(qmsg.path) is None: await clientTxMsg("Bad path", qmsg.initiatorWs) continue giRxMsg(qmsg.path, qmsg) continue if isinstance(qmsg, InternalRegisterGi): registerGameClass(qmsg.gi) continue if isinstance(qmsg, InternalGiStatus): giRxMsg(LOBBY_PATH, qmsg) continue if isinstance(qmsg, TimerRequest): await timerAdd(qmsg) continue try: msg = json.dumps(qmsg.jmsg) except TypeError as exc: trace(Level.error, "Error serializing as JSON:", str(qmsg.jmsg), str(exc)) continue assert isinstance(qmsg, ClientTxMsg) for toWs in qmsg.toWss: await clientTxMsg(msg, toWs)
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 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 _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
async def worker(self): """The worker task for a plugin. All messages should be processed. Any unhandled message returns a bad message to the sender""" trace(Level.game, "{}.worker() ready".format(self.path)) while True: qmsg = await self.rxQueue.get() self.rxQueue.task_done() trace(Level.msg, self.path, "received", str(qmsg)) processed = self.processMsg(qmsg) if not processed: if not isinstance(qmsg, ClientRxMsg): trace(Level.error, "Unexpected message not handled:", str(qmsg)) continue self.txQueue.put_nowait( ClientTxMsg("Bad message", {qmsg.initiatorWs}, initiatorWs=qmsg.initiatorWs))
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