def test_game(players, silent): """ Go through a whole game by pitting two AutoPlayers against each other """ # The players parameter is a list of tuples: (playername, constructorfunc) # where constructorfunc accepts a State parameter and returns a freshly # created AutoPlayer (or subclass thereof) that will generate moves # on behalf of the player. # Initial, empty game state state = State(tileset = NewTileSet, drawtiles = True) 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, players[ix][0]) if not silent: print(state.__str__()) # This works in Python 2 and 3 # Generate a sequence of moves, switching player sides automatically t0 = time.time() while not state.is_game_over(): # Call the appropriate player creation function apl = players[state.player_to_move()][1](state) g0 = time.time() move = apl.generate_move() g1 = time.time() legal = state.check_legality(move) if legal != Error.LEGAL: # Oops: the autoplayer generated an illegal move print(u"Play is not legal, code {0}".format(Error.errortext(legal))) return if not silent: print(u"Play {0} scores {1} points ({2:.2f} seconds)".format(move, state.score(move), g1 - g0)) # Apply the move to the state and switch players state.apply_move(move) if not silent: print(state.__str__()) # Tally the tiles left and calculate the final score state.finalize_score() p0, p1 = state.scores() t1 = time.time() if not silent: print(u"Game over, final score {4} {0} : {5} {1} after {2} moves ({3:.2f} seconds)".format(p0, p1, state.num_moves(), t1 - t0, state.player_name(0), state.player_name(1))) return state.scores()
def test_manual_game(): """ Manual game test """ # Initial, empty game state state = State(tileset=NewTileSet, manual_wordcheck=True, drawtiles=True) print("Manual game") print("After initial draw, bag contains {0} tiles".format( state.bag().num_tiles())) print("Bag contents are:\n{0}".format(state.bag().contents())) print("Rack 0 is {0}".format(state.rack(0))) print("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("stuðinn") test_move(state, "H4 stuði") state.player_rack().set_tiles("dettsfj") test_move(state, "5E detts") test_exchange(state, 3) state.player_rack().set_tiles("dýsturi") test_move(state, "I3 dýs") state.player_rack().set_tiles("?xalmen") # The question mark indicates a blank tile for the subsequent cover test_move(state, "6E ?óx") state.player_rack().set_tiles("eiðarps") test_move(state, "9F eipar") test_challenge(state) test_response(state) state.player_rack().set_tiles("sóbetis") test_move(state, "J3 ós") test_move(state, "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( "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) ) )
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)))
def state_after_move(self, move_number): """ Return a game state after the indicated move, 0=beginning state """ # Initialize a fresh state object s = State(drawtiles = False) # Set up the initial state for ix in range(2): s.set_player_name(ix, self.state.player_name(ix)) if self.initial_racks[ix] is None: # Load the current rack rather than nothing s.set_rack(ix, self.state.rack(ix)) else: # Load the initial rack s.set_rack(ix, self.initial_racks[ix]) # Apply the moves up to the state point for m in self.moves[0 : move_number]: s.apply_move(m.move, shallow = True) # Shallow apply if m.rack is not None: s.set_rack(m.player, m.rack) s.recalc_bag() return s
def state_after_move(self, move_number): """ Return a game state after the indicated move, 0=beginning state """ # Initialize a fresh state object s = State(drawtiles=False, tileset=self.tileset) # Set up the initial state for ix in range(2): s.set_player_name(ix, self.state.player_name(ix)) if self.initial_racks[ix] is None: # Load the current rack rather than nothing s.set_rack(ix, self.state.rack(ix)) else: # Load the initial rack s.set_rack(ix, self.initial_racks[ix]) # Apply the moves up to the state point for m in self.moves[0:move_number]: s.apply_move(m.move, shallow=True) # Shallow apply if m.rack is not None: s.set_rack(m.player, m.rack) s.recalc_bag() return s
def test_game(players, silent): """ Go through a whole game by pitting two AutoPlayers against each other """ # The players parameter is a list of tuples: (playername, constructorfunc) # where constructorfunc accepts a State parameter and returns a freshly # created AutoPlayer (or subclass thereof) that will generate moves # on behalf of the player. # Initial, empty game state state = State(tileset=NewTileSet, drawtiles=True) 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, players[ix][0]) if not silent: print(state.__str__()) # This works in Python 2 and 3 # Generate a sequence of moves, switching player sides automatically t0 = time.time() while not state.is_game_over(): # Call the appropriate player creation function apl = players[state.player_to_move()][1](state) g0 = time.time() move = apl.generate_move() g1 = time.time() legal = state.check_legality(move) if legal != Error.LEGAL: # Oops: the autoplayer generated an illegal move print(u"Play is not legal, code {0}".format( Error.errortext(legal))) return if not silent: print(u"Play {0} scores {1} points ({2:.2f} seconds)".format( move, state.score(move), g1 - g0)) # Apply the move to the state and switch players state.apply_move(move) if not silent: print(state.__str__()) # Tally the tiles left and calculate the final score state.finalize_score() p0, p1 = state.scores() t1 = time.time() if not silent: print( u"Game over, final score {4} {0} : {5} {1} after {2} moves ({3:.2f} seconds)" .format(p0, p1, state.num_moves(), t1 - t0, state.player_name(0), state.player_name(1))) return state.scores()
def test_game(players, silent): """ Go through a whole game by pitting two AutoPlayers against each other """ # The players parameter is a list of tuples: (playername, constructorfunc) # where constructorfunc accepts a State parameter and returns a freshly # created AutoPlayer (or subclass thereof) that will generate moves # on behalf of the player. # Initial, empty game state state = State(drawtiles=True) # Set player names for ix in range(2): state.set_player_name(ix, players[ix][0]) if not silent: print(state.__str__()) # This works in Python 2 and 3 # test_move(state, u"H4 stuði") # test_move(state, u"5E detts") # test_exchange(state, 3) # test_move(state, u"I3 dýs") # test_move(state, u"6E ?óx") # The question mark indicates a blank tile for the subsequent cover # state.player_rack().set_tiles(u"ðhknnmn") # Generate a sequence of moves, switching player sides automatically t0 = time.time() while not state.is_game_over(): # Call the appropriate player creation function apl = players[state.player_to_move()][1](state) g0 = time.time() move = apl.generate_move() g1 = time.time() # legal = state.check_legality(move) # if legal != Error.LEGAL: # # Oops: the autoplayer generated an illegal move # print(u"Play is not legal, code {0}".format(Error.errortext(legal))) # return if not silent: print(u"Play {0} scores {1} points ({2:.2f} seconds)".format(move, state.score(move), g1 - g0)) # Apply the move to the state and switch players state.apply_move(move) if not silent: print(state.__str__()) # Tally the tiles left and calculate the final score state.finalize_score() p0, p1 = state.scores() t1 = time.time() if not silent: print( u"Game over, final score {4} {0} : {5} {1} after {2} moves ({3:.2f} seconds)".format( p0, p1, state.num_moves(), t1 - t0, state.player_name(0), state.player_name(1) ) ) return state.scores()
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