Esempio n. 1
0
def test_manual_game():
    """ Manual game test """

    # Initial, empty game state
    state = State(tileset = NewTileSet, manual_wordcheck = True, drawtiles = True)

    print(u"Manual game")
    print(u"After initial draw, bag contains {0} tiles".format(state.bag().num_tiles()))
    print(u"Bag contents are:\n{0}".format(state.bag().contents()))
    print(u"Rack 0 is {0}".format(state.rack(0)))
    print(u"Rack 1 is {0}".format(state.rack(1)))

    # Set player names
    for ix in range(2):
        state.set_player_name(ix, "Player " + ("A", "B")[ix])

    # print(state.__str__()) # This works in Python 2 and 3

    state.player_rack().set_tiles(u"stuðinn")
    test_move(state, u"H4 stuði")
    state.player_rack().set_tiles(u"dettsfj")
    test_move(state, u"5E detts")
    test_exchange(state, 3)
    state.player_rack().set_tiles(u"dýsturi")
    test_move(state, u"I3 dýs")
    state.player_rack().set_tiles(u"?xalmen")
    test_move(state, u"6E ?óx") # The question mark indicates a blank tile for the subsequent cover
    state.player_rack().set_tiles(u"eiðarps")

    test_move(state, u"9F eipar")
    test_challenge(state)
    test_response(state)

    state.player_rack().set_tiles(u"sóbetis")
    test_move(state, u"J3 ós")

    test_move(state, u"9F eiðar")
    test_challenge(state)
    test_response(state)

    # Tally the tiles left and calculate the final score
    state.finalize_score()
    p0, p1 = state.scores()

    print(u"Manual game over, final score {3} {0} : {4} {1} after {2} moves".format(p0, p1,
        state.num_moves(), state.player_name(0), state.player_name(1)))
Esempio n. 2
0
def test_manual_game():
    """ Manual game test """

    # Initial, empty game state
    state = State(tileset=NewTileSet, manual_wordcheck=True, drawtiles=True)

    print(u"Manual game")
    print(u"After initial draw, bag contains {0} tiles".format(
        state.bag().num_tiles()))
    print(u"Bag contents are:\n{0}".format(state.bag().contents()))
    print(u"Rack 0 is {0}".format(state.rack(0)))
    print(u"Rack 1 is {0}".format(state.rack(1)))

    # Set player names
    for ix in range(2):
        state.set_player_name(ix, "Player " + ("A", "B")[ix])

    # print(state.__str__()) # This works in Python 2 and 3

    state.player_rack().set_tiles(u"stuðinn")
    test_move(state, u"H4 stuði")
    state.player_rack().set_tiles(u"dettsfj")
    test_move(state, u"5E detts")
    test_exchange(state, 3)
    state.player_rack().set_tiles(u"dýsturi")
    test_move(state, u"I3 dýs")
    state.player_rack().set_tiles(u"?xalmen")
    test_move(
        state, u"6E ?óx"
    )  # The question mark indicates a blank tile for the subsequent cover
    state.player_rack().set_tiles(u"eiðarps")

    test_move(state, u"9F eipar")
    test_challenge(state)
    test_response(state)

    state.player_rack().set_tiles(u"sóbetis")
    test_move(state, u"J3 ós")

    test_move(state, u"9F eiðar")
    test_challenge(state)
    test_response(state)

    # Tally the tiles left and calculate the final score
    state.finalize_score()
    p0, p1 = state.scores()

    print(u"Manual game over, final score {3} {0} : {4} {1} after {2} moves".
          format(p0, p1, state.num_moves(), state.player_name(0),
                 state.player_name(1)))
Esempio n. 3
0
class Game:

    """ A wrapper class for a particular game that is in process
        or completed. Contains inter alia a State instance.
    """

    # The human-readable name of the computer player
    AUTOPLAYER_NAME = u"Netskrafl"

    def __init__(self, uuid = None):
        # Unique id of the game
        self.uuid = uuid
        # The nickname of the human (local) player
        self.username = None
        # The current game state
        self.state = None
        # Is the human player 0 or 1, where player 0 begins the game?
        self.player_index = 0
        # The last move made by the autoplayer
        self.last_move = None
        # Was the game finished by resigning?
        self.resigned = False
        # History of moves in this game so far
        self.moves = []

    # The current game state held in memory for different users
    # !!! TODO: limit the size of the cache and make it LRU
    _cache = dict()

    def _make_new(self, username):
        # Initialize a new, fresh game
        self.username = username
        self.state = State(drawtiles = True)
        self.player_index = randint(0, 1)
        self.state.set_player_name(self.player_index, username)
        self.state.set_player_name(1 - self.player_index, Game.AUTOPLAYER_NAME)

    @classmethod
    def current(cls):
        """ Obtain the current game state """
        user = User.current()
        user_id = None if user is None else user.id()
        if not user_id:
            # No game state found
            return None
        if user_id in Game._cache:
            return Game._cache[user_id]
        # No game in cache: attempt to find one in the database
        uuid = GameModel.find_live_game(user_id)
        if uuid is None:
            # Not found in persistent storage: create a new game
            return cls.new(user.nickname())
        # Load from persistent storage
        return cls.load(uuid, user.nickname())

    @classmethod
    def new(cls, username):
        """ Start and initialize a new game """
        game = cls(Unique.id()) # Assign a new unique id to the game
        game._make_new(username)
        # Cache the game so it can be looked up by user id
        user = User.current()
        if user is not None:
            Game._cache[user.id()] = game
        # If AutoPlayer is first to move, generate the first move
        if game.player_index == 1:
            game.autoplayer_move()
        # Store the new game in persistent storage
        game.store()
        return game

    @classmethod
    def load(cls, uuid, username):

        """ Load an already existing game from persistent storage """

        gm = GameModel.fetch(uuid)
        if gm is None:
            # A game with this uuid is not found in the database: give up
            return None

        # Initialize a new Game instance with a pre-existing uuid
        game = cls(uuid)

        game.username = username
        game.state = State(drawtiles = False)

        if gm.player0 is None:
            # Player 0 is an Autoplayer
            game.player_index = 1 # Human (local) player is 1
        else:
            assert gm.player1 is None
            game.player_index = 0 # Human (local) player is 0

        game.state.set_player_name(game.player_index, username)
        game.state.set_player_name(1 - game.player_index, u"Netskrafl")

        # Load the current racks
        game.state._racks[0].set_tiles(gm.rack0)
        game.state._racks[1].set_tiles(gm.rack1)

        # Process the moves
        player = 0
        for mm in gm.moves:

            m = None
            if mm.coord:

                # Normal tile move
                # Decode the coordinate: A15 = horizontal, 15A = vertical
                if mm.coord[0] in Board.ROWIDS:
                    row = Board.ROWIDS.index(mm.coord[0])
                    col = int(mm.coord[1:]) - 1
                    horiz = True
                else:
                    row = Board.ROWIDS.index(mm.coord[-1])
                    col = int(mm.coord[0:-1]) - 1
                    horiz = False
                # The tiles string may contain wildcards followed by their meaning
                # Remove the ? marks to get the "plain" word formed
                m = Move(mm.tiles.replace(u'?', u''), row, col, horiz)
                m.make_covers(game.state.board(), mm.tiles)

            elif mm.tiles[0:4] == u"EXCH":

                # Exchange move
                m = ExchangeMove(mm.tiles[5:])

            elif mm.tiles == u"PASS":

                # Pass move
                m = PassMove()

            elif mm.tiles == u"RSGN":

                # Game resigned
                m = ResignMove(- mm.score)

            assert m is not None
            if m:
                # Do a "shallow apply" of the move, which updates
                # the board and internal state variables but does
                # not modify the bag or the racks
                game.state.apply_move(m, True)
                # Append to the move history
                game.moves.append((player, m))
                player = 1 - player

        # If the moves were correctly applied, the scores should match
        assert game.state._scores[0] == gm.score0
        assert game.state._scores[1] == gm.score1

        # Find out what tiles are now in the bag
        game.state.recalc_bag()

        # Cache the game so it can be looked up by user id
        user = User.current()
        if user is not None:
            Game._cache[user.id()] = game
        return game

    def store(self):
        """ Store the game state in persistent storage """
        assert self.uuid is not None
        user = User.current()
        if user is None:
            # No current user: can't store game
            assert False
            return
        gm = GameModel(id = self.uuid)
        gm.set_player(self.player_index, user.id())
        gm.set_player(1 - self.player_index, None)
        gm.rack0 = self.state._racks[0].contents()
        gm.rack1 = self.state._racks[1].contents()
        gm.score0 = self.state.scores()[0]
        gm.score1 = self.state.scores()[1]
        gm.to_move = len(self.moves) % 2
        gm.over = self.state.is_game_over()
        movelist = []
        for player, m in self.moves:
            mm = MoveModel()
            coord, tiles, score = m.summary(self.state.board())
            mm.coord = coord
            mm.tiles = tiles
            mm.score = score
            movelist.append(mm)
        gm.moves = movelist
        gm.put()

    def set_human_name(self, nickname):
        """ Set the nickname of the human player """
        self.state.set_player_name(self.player_index, nickname)

    def resign(self):
        """ The human player is resigning the game """
        self.resigned = True

    def autoplayer_move(self):
        """ Let the AutoPlayer make its move """
        # !!! DEBUG for testing various move types
        # rnd = randint(0,3)
        # if rnd == 0:
        #     print(u"Generating ExchangeMove")
        #     move = ExchangeMove(self.state.player_rack().contents()[0:randint(1,7)])
        # else:
        apl = AutoPlayer(self.state)
        move = apl.generate_move()
        self.state.apply_move(move)
        self.moves.append((1 - self.player_index, move))
        self.last_move = move

    def human_move(self, move):
        """ Register the human move, update the score and move list """
        self.state.apply_move(move)
        self.moves.append((self.player_index, move))
        self.last_move = None # No autoplayer move yet

    def enum_tiles(self):
        """ Enumerate all tiles on the board in a convenient form """
        for x, y, tile, letter in self.state.board().enum_tiles():
            yield (Board.ROWIDS[x] + str(y + 1), tile, letter,
                0 if tile == u'?' else Alphabet.scores[tile])

    BAG_SORT_ORDER = Alphabet.order + u'?'

    def display_bag(self):
        """ Returns the bag as it should be displayed, i.e. including the autoplayer's rack """
        displaybag = self.state.display_bag(1 - self.player_index)
        return u''.join(sorted(displaybag, key=lambda ch: Game.BAG_SORT_ORDER.index(ch)))

    def num_moves(self):
        """ Returns the number of moves in the game so far """
        return len(self.moves)

    def client_state(self):
        """ Create a package of information for the client about the current state """
        reply = dict()
        if self.state.is_game_over():
            # The game is now over - one of the players finished it
            reply["result"] = Error.GAME_OVER # Not really an error
            num_moves = 1
            if self.last_move is not None:
                # Show the autoplayer move if it was the last move in the game
                reply["lastmove"] = self.last_move.details()
                num_moves = 2 # One new move to be added to move list
            newmoves = [(player, m.summary(self.state.board())) for player, m in self.moves[-num_moves:]]
            # Lastplayer is the player who finished the game
            lastplayer = self.moves[-1][0]
            if not self.resigned:
                # If the game did not end by resignation,
                # account for the losing rack
                rack = self.state._racks[1 - lastplayer].contents()
                # Subtract the score of the losing rack from the losing player
                newmoves.append((1 - lastplayer, (u"", rack, -1 * Alphabet.score(rack))))
                # Add the score of the losing rack to the winning player
                newmoves.append((lastplayer, (u"", rack, 1 * Alphabet.score(rack))))
            # Add a synthetic "game over" move
            newmoves.append((1 - lastplayer, (u"", u"OVER", 0)))
            reply["newmoves"] = newmoves
            reply["bag"] = "" # Bag is now empty, by definition
            reply["xchg"] = False # Exchange move not allowed
        else:
            # Game is still in progress
            reply["result"] = 0 # Indicate no error
            reply["rack"] = self.state.player_rack().details()
            reply["lastmove"] = self.last_move.details()
            reply["newmoves"] = [(player, m.summary(self.state.board())) for player, m in self.moves[-2:]]
            reply["bag"] = self.display_bag()
            reply["xchg"] = self.state.is_exchange_allowed()
        reply["scores"] = self.state.scores()
        return reply
Esempio n. 4
0
    def _find_best_move(self, depth):
        """ Analyze the list of candidate moves and pick the best one """

        # assert depth >= 0

        if not self._candidates:
            # No moves: must exchange or pass instead
            return None

        if len(self._candidates) == 1:
            # Only one legal move: play it
            return self._candidates[0]

        # !!! TODO: Consider looking at exchange moves if there are
        # few and weak candidates

        # Calculate the score of each candidate
        scored_candidates = [(m, self._state.score(m)) for m in self._candidates]

        def keyfunc(x):
            # Sort moves first by descending score;
            # in case of ties prefer shorter words
            # !!! TODO: Insert more sophisticated logic here,
            # including whether triple-word-score opportunities
            # are being opened for the opponent, minimal use
            # of blank tiles, leaving a good vowel/consonant
            # balance on the rack, etc.
            return (- x[1], x[0].num_covers())

        def keyfunc_firstmove(x):
            # Special case for first move:
            # Sort moves first by descending score, and in case of ties,
            # try to go to the upper half of the board for a more open game
            return (- x[1], x[0]._row)

        # Sort the candidate moves using the appropriate key function
        if self._board.is_empty():
            # First move
            scored_candidates.sort(key=keyfunc_firstmove)
        else:
            # Subsequent moves
            scored_candidates.sort(key=keyfunc)

        # If we're not going deeper into the minimax analysis,
        # cut the crap and simply return the top scoring move
        if depth == 0:
            return scored_candidates[0][0]

        # Weigh top candidates by alpha-beta testing of potential
        # moves and counter-moves

        # !!! TODO: In endgame, if we have moves that complete the game (use all rack tiles)
        # we need not consider opponent countermoves

        NUM_TEST_RACKS = 20 # How many random test racks to try for statistical average
        NUM_CANDIDATES = 12 # How many top candidates do we look at with MiniMax?

        weighted_candidates = []
        min_score = None

        print(u"Looking at {0} top scoring candidate moves".format(NUM_CANDIDATES))
        # Look at the top scoring candidates
        for m, score in scored_candidates[0:NUM_CANDIDATES]:

            print(u"Candidate move {0} with raw score {1}".format(m, score))

            # Create a game state where the candidate move has been played
            teststate = State(tileset = None, copy = self._state) # Copy constructor
            teststate.apply_move(m)

            countermoves = list()

            if teststate.is_game_over():
                # This move finishes the game. The opponent then scores nothing
                # !!! TODO: (and in fact we get her tile score, but leave that aside here)
                avg_score = 0.0
                countermoves.append(0)
            else:
                # Loop over NUM_TEST_RACKS random racks to find the average countermove score
                sum_score = 0
                rackscores = dict()
                for _ in range(NUM_TEST_RACKS):
                    # Make sure we test this for a random opponent rack
                    teststate.randomize_and_sort_rack()
                    rack = teststate.player_rack().contents()
                    if rack in rackscores:
                        # We have seen this rack before: fetch its score
                        sc = rackscores[rack]
                    else:
                        # New rack: see how well it would score
                        apl = AutoPlayer_MiniMax(teststate)
                        # Go one level deeper into move generation
                        move = apl._generate_move(depth = depth - 1)
                        # Calculate the score of this random rack based move
                        # but do not apply it to the teststate
                        sc = teststate.score(move)
                        if sc > 100:
                            print(u"Countermove rack '{0}' generated move {1} scoring {2}".format(rack, move, sc))
                        # Cache the score
                        rackscores[rack] = sc
                    sum_score += sc
                    countermoves.append(sc)
                # Calculate the average score of the countermoves to this candidate
                # !!! TODO: Maybe a median score is better than average?
                avg_score = float(sum_score) / NUM_TEST_RACKS

            print(u"Average score of {0} countermove racks is {1:.2f}".format(NUM_TEST_RACKS, avg_score))
            print(countermoves)

            # Keep track of the lowest countermove score across all candidates as a baseline
            min_score = avg_score if (min_score is None) or (avg_score < min_score) else min_score
            # Keep track of the weighted candidate moves
            weighted_candidates.append((m, score, avg_score))

        print(u"Lowest score of countermove to all evaluated candidates is {0:.2f}".format(min_score))
        # Sort the candidates by the plain score after subtracting the effect of
        # potential countermoves, measured as the countermove score in excess of
        # the lowest countermove score found
        weighted_candidates.sort(key = lambda x: float(x[1]) - (x[2] - min_score), reverse = True)

        print(u"AutoPlayer_MinMax: Rack '{0}' generated {1} candidate moves:".format(self._rack, len(scored_candidates)))
        # Show top 20 candidates
        for m, sc, wsc in weighted_candidates:
            print(u"Move {0} score {1} weighted {2:.2f}".format(m, sc, float(sc) - (wsc - min_score)))
        # Return the highest-scoring candidate
        return weighted_candidates[0][0]
Esempio n. 5
0
    def _find_best_move(self, depth):
        """ Analyze the list of candidate moves and pick the best one """

        # assert depth >= 0

        if not self._candidates:
            # No moves: must exchange or pass instead
            return None

        if len(self._candidates) == 1:
            # Only one legal move: play it
            return self._candidates[0]

        # !!! TODO: Consider looking at exchange moves if there are
        # few and weak candidates

        # Calculate the score of each candidate
        scored_candidates = [(m, self._state.score(m))
                             for m in self._candidates]

        def keyfunc(x):
            # Sort moves first by descending score;
            # in case of ties prefer shorter words
            # !!! TODO: Insert more sophisticated logic here,
            # including whether triple-word-score opportunities
            # are being opened for the opponent, minimal use
            # of blank tiles, leaving a good vowel/consonant
            # balance on the rack, etc.
            return (-x[1], x[0].num_covers())

        def keyfunc_firstmove(x):
            # Special case for first move:
            # Sort moves first by descending score, and in case of ties,
            # try to go to the upper half of the board for a more open game
            return (-x[1], x[0]._row)

        # Sort the candidate moves using the appropriate key function
        if self._board.is_empty():
            # First move
            scored_candidates.sort(key=keyfunc_firstmove)
        else:
            # Subsequent moves
            scored_candidates.sort(key=keyfunc)

        # If we're not going deeper into the minimax analysis,
        # cut the crap and simply return the top scoring move
        if depth == 0:
            return scored_candidates[0][0]

        # Weigh top candidates by alpha-beta testing of potential
        # moves and counter-moves

        # !!! TODO: In endgame, if we have moves that complete the game (use all rack tiles)
        # we need not consider opponent countermoves

        NUM_TEST_RACKS = 20  # How many random test racks to try for statistical average
        NUM_CANDIDATES = 12  # How many top candidates do we look at with MiniMax?

        weighted_candidates = []
        min_score = None

        print(u"Looking at {0} top scoring candidate moves".format(
            NUM_CANDIDATES))
        # Look at the top scoring candidates
        for m, score in scored_candidates[0:NUM_CANDIDATES]:

            print(u"Candidate move {0} with raw score {1}".format(m, score))

            # Create a game state where the candidate move has been played
            teststate = State(tileset=None,
                              copy=self._state)  # Copy constructor
            teststate.apply_move(m)

            countermoves = list()

            if teststate.is_game_over():
                # This move finishes the game. The opponent then scores nothing
                # !!! TODO: (and in fact we get her tile score, but leave that aside here)
                avg_score = 0.0
                countermoves.append(0)
            else:
                # Loop over NUM_TEST_RACKS random racks to find the average countermove score
                sum_score = 0
                rackscores = dict()
                for _ in range(NUM_TEST_RACKS):
                    # Make sure we test this for a random opponent rack
                    teststate.randomize_and_sort_rack()
                    rack = teststate.player_rack().contents()
                    if rack in rackscores:
                        # We have seen this rack before: fetch its score
                        sc = rackscores[rack]
                    else:
                        # New rack: see how well it would score
                        apl = AutoPlayer_MiniMax(teststate)
                        # Go one level deeper into move generation
                        move = apl._generate_move(depth=depth - 1)
                        # Calculate the score of this random rack based move
                        # but do not apply it to the teststate
                        sc = teststate.score(move)
                        if sc > 100:
                            print(
                                u"Countermove rack '{0}' generated move {1} scoring {2}"
                                .format(rack, move, sc))
                        # Cache the score
                        rackscores[rack] = sc
                    sum_score += sc
                    countermoves.append(sc)
                # Calculate the average score of the countermoves to this candidate
                # !!! TODO: Maybe a median score is better than average?
                avg_score = float(sum_score) / NUM_TEST_RACKS

            print(u"Average score of {0} countermove racks is {1:.2f}".format(
                NUM_TEST_RACKS, avg_score))
            print(countermoves)

            # Keep track of the lowest countermove score across all candidates as a baseline
            min_score = avg_score if (min_score is None) or (
                avg_score < min_score) else min_score
            # Keep track of the weighted candidate moves
            weighted_candidates.append((m, score, avg_score))

        print(
            u"Lowest score of countermove to all evaluated candidates is {0:.2f}"
            .format(min_score))
        # Sort the candidates by the plain score after subtracting the effect of
        # potential countermoves, measured as the countermove score in excess of
        # the lowest countermove score found
        weighted_candidates.sort(key=lambda x: float(x[1]) -
                                 (x[2] - min_score),
                                 reverse=True)

        print(u"AutoPlayer_MinMax: Rack '{0}' generated {1} candidate moves:".
              format(self._rack, len(scored_candidates)))
        # Show top 20 candidates
        for m, sc, wsc in weighted_candidates:
            print(u"Move {0} score {1} weighted {2:.2f}".format(
                m, sc,
                float(sc) - (wsc - min_score)))
        # Return the highest-scoring candidate
        return weighted_candidates[0][0]