Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
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()
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
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()
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
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")
Ejemplo n.º 10
0
 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()
Ejemplo n.º 11
0
    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()
Ejemplo n.º 12
0
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()
Ejemplo n.º 13
0
    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
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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()
Ejemplo n.º 17
0
    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_)
Ejemplo n.º 18
0
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
Ejemplo n.º 19
0
    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()
Ejemplo n.º 20
0
    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
Ejemplo n.º 21
0
    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
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    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)
Ejemplo n.º 24
0
 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
Ejemplo n.º 25
0
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)
Ejemplo n.º 26
0
    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()
Ejemplo n.º 27
0
    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
Ejemplo n.º 28
0
    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
Ejemplo n.º 29
0
    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))
Ejemplo n.º 30
0
    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