Beispiel #1
0
    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)
        ])
Beispiel #2
0
    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()
Beispiel #3
0
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),
        ])
Beispiel #4
0
    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()
Beispiel #5
0
    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)
Beispiel #6
0
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
Beispiel #7
0
    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)
Beispiel #8
0
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
Beispiel #9
0
    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))
Beispiel #10
0
    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)
Beispiel #11
0
 def __init__(self, conns, playerConns):
     self.connsMsgSrc = MsgSrc(conns) if conns else None
     self.playerConnsMsgSrc = MsgSrc(playerConns) if playerConns else None
     self.refresh()
Beispiel #12
0
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()
        }
Beispiel #13
0
    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)
Beispiel #14
0
 def postQueueSetup(self):
     self.publishGiStatus()
     self.winners = MsgSrc(self.conns)
Beispiel #15
0
 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)])
Beispiel #16
0
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)]
Beispiel #17
0
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()
Beispiel #18
0
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