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(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()
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]
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]