def test_add_tiles(): bag = Bag() starting_num_of_tiles = bag.remaining_tiles() tiles = bag.get_tiles(10) assert bag.remaining_tiles() == starting_num_of_tiles - 10 bag.add_tiles(tiles) assert bag.remaining_tiles() == starting_num_of_tiles
def test_get_tiles(): bag = Bag() starting_num_of_tiles = len(bag.bag_tiles) assert starting_num_of_tiles == sum(LETTER_DISTRIBUTIONS) tiles = bag.get_tiles(10) assert len(tiles) == 10 assert len(bag.bag_tiles) == starting_num_of_tiles - 10
def test_contents(): rack = Rack(Bag()) rack.rack_tiles = ['A', 'A', 'B', 'B', 'C', 'D', 'E'] assert 'ABC' in rack assert 'AAB' in rack assert 'AAABBB' not in rack assert ['D', 'A', 'B'] in rack assert ['D', 'A', 'B', 'D'] not in rack
def test_get_a_tile_in_rack(): rack = Rack(Bag()) letter = rack.rack_tiles[0] assert len(rack.rack_tiles) == RACK_SIZE tile = rack.get_tile(letter) assert len(rack.rack_tiles) == (RACK_SIZE - 1) assert tile is not None assert tile == letter
def test_init(): players = [None, None] bag = Bag() game = GameController(players, bag) gui = ConsoleGui(game) player1 = HumanPlayer(bag, gui, "Player 1") player2 = HumanPlayer(bag, gui, "Player 2") game.players = [player1, player2] assert game.game_state == GameState.PENDING
def test_replenish(): rack = Rack(Bag()) for i in range(13): rack.get_tiles(str(rack)) assert len(rack.rack_tiles) == 0 rack.replenish_tiles() assert len(rack.rack_tiles) == RACK_SIZE rack.get_tiles(str(rack)) assert len(rack.rack_tiles) == 0 rack.replenish_tiles() assert len(rack.rack_tiles) < RACK_SIZE
def test_change_active_player(): players = [None, None] bag = Bag() game = GameController(players, bag) gui = ConsoleGui(game) player1 = HumanPlayer(bag, gui, "Player 1") player2 = HumanPlayer(bag, gui, "Player 2") game.players = [player1, player2] game.active_player = player1 assert game.active_player == player1 game.change_active_player() assert game.active_player == player2
def test_fill(): bag = Bag() bag.bag_tiles.clear() assert bag.bag_tiles == [] bag.fill() assert bag.remaining_tiles() == sum(LETTER_DISTRIBUTIONS) temp_distributions = copy.deepcopy(LETTER_DISTRIBUTIONS) for i in range(bag.remaining_tiles()): tile = bag.get_tiles(1)[0] temp_distributions[ord(tile) - 64] -= 1 # check off each letter as it is drawn assert temp_distributions.count(0) == len( temp_distributions) # all elements should now be zero
def test_parse_move_string(): bag = Bag() game = GameController([None, None], bag) gui = ConsoleGui(game) player1 = HumanPlayer(bag, gui, "Player 1") player2 = HumanPlayer(bag, gui, "Player 2") game.players = [player1, player2] game.active_player = player1 player1.rack.rack_tiles[0] = 'A' player1.rack.rack_tiles[1] = 'C' player1.rack.rack_tiles[2] = 'T' move = gui.parse_move_string('F8HCAT') assert move.row.direction == Direction.HORIZONTAL assert move.row.rank == 8 assert len(move.played_squares) == 3 assert all([a == b for a, b in zip(move.played_squares, [6, 7, 8])]) assert len(move.tiles) == 3 assert all([a == b for a, b in zip(move.tiles, ['C', 'A', 'T'])])
def __init__(self, event_manager): super().__init__(event_manager) self.game_finished = False self.playing = False self.turn = 0 self.clock = pygame.time.Clock() self.std_font = pygame.font.SysFont("sans-serif", 22) self.button_font = pygame.font.SysFont("monospace", 24) self.button_font.set_bold(True) self.board = Board(self.event_manager, 50, 100, 1, 7) self.options = [ Button(self.button_font, config.END_ROUND, (235, 200), self.on_end_round_click), Button(self.button_font, config.CLEAR, (150, 250), self.on_clear_click), Button(self.button_font, config.BACKSPACE, (350, 250), self.on_backspace_click) ] # init players self.player_a = Player('Player 1') self.player_b = Player('PC') self.render_sleep = 0 # todo remove self.player_a.is_playing = True self.score_board = ScoreBoard(self.player_a, self.player_b, 20, 20) self.deck = Deck(self.event_manager, 50, 350) self.bag = Bag() self.set_deck() for option in self.options: self.event_manager.register(ClickEvent, option)
def test_overflow_rack(): rack = Rack(Bag()) with pytest.raises(ValueError) as e_info: rack.add_tiles(['A', 'B']) assert len(rack.rack_tiles) == RACK_SIZE
def test_init(): bag = Bag()
def process_game(gcg_lines, game_number): players = [None, None] game = GameController(players, Bag()) gui = ConsoleGui(game) movelist = [line.split() for line in gcg_lines] player1 = AiPlayer(game, gui, movelist.pop(0)[1]) player2 = AiPlayer(game, gui, movelist.pop(0)[1]) game.players = [player1, player2] game.active_player = player1 if player1.name == movelist[0][0][1:-1] else player2 current_move = 0 # current move while movelist: current_move += 1 current_move_info = movelist.pop(0) # if there are things in brackets, they are score adjustments so we've # finished all the moves: if '(' in current_move_info[1]: return # we use '@' for blank for pragmatic reasons (it's the ASCII character before 'A') # GCG uses '?', so let's fix that: game.active_player.rack.rack_tiles = list(current_move_info[1].replace('?', '@')) # Now we have the correct tiles in the rack and the board is advanced to the correct position, # let's get a list of all the moves ALexIS can come up with: alexis_moves = game.active_player.generate_all_moves() alexis_moves.sort(key=lambda x: x.score, reverse=True) # Sort if with highest scores first # put any blanks back to having letter un-assigned, ready for parsing Quackle move game.active_player.rack.reset_blanks() # Now let's move onto getting our 'good' move from the GCG file: # GCG has digit(s) followed by letter for horizontal move, or vice versa for vertical, # wheras we are expecting digit, then letter, then 'H' or 'V', so let's fix that: start_square = current_move_info[2] if start_square.startswith('-'): tiles = '' if len(start_square)>1: tiles = start_square[1] start_square = '' else: if start_square[0].isdigit(): start_square = start_square[-1] + start_square[:-1] + 'H' else: start_square += 'V' # lowercase letters in the GCG represent blanks. We're expecting a question mark for a blank, # followed by the desired letter, so replace 'a' with '?A', etc: tiles = ''.join(['?' + letter.upper() if letter.islower() else letter for letter in current_move_info[3]]) # GCG gives the start square as the first letter in the word, # and uses '.' as a placeholder for any tile already on the board, # whereas we list the first square we're actually playing on, and # just the tiles actually played, so let's strip out '.' and adjust the # starting square if necessary: if '.' in tiles: start_square = list(start_square) # treat string as char list x = 0 while tiles[x] == '.': # whilst there's a dot at the start if start_square[-1] == 'V': start_square[-2] = str(int(start_square[-2]) + 1) # increase the row if playing vertical else: start_square[0] = chr(ord(start_square[0]) + 1) # or the column if horizontal x += 1 start_square = ''.join(start_square) # make a string again tiles = tiles.replace('.', '') # strip out any dot in the middle of the word # save the rack for later: cached_rack = copy.deepcopy(game.active_player.rack) # This will get a move and remove tiles from rack: quackle_move = gui.parse_move_string(start_square + tiles) if quackle_move.direction is not Direction.NOT_APPLICABLE: # validator will place tiles onto row in course of validation, # so first we'll copy the row so as not to mess up the board: quackle_move.row = copy.deepcopy(quackle_move.row) game.validator.is_valid(quackle_move) # this will calculate score (but only if tiles are already played on row): quackle_move.calculate_score() # now choose some moves. If we do data augmentation by transposing the 'correct' Quackle-derived moves, # we'll have 2 correct moves, so picking six of these would give us multiples of 8 moves. # Picking the top couple and randomly picking the rest would analyse a couple of 'good' moves # but still allow a little exploration (c.f. Q learning) if quackle_move in alexis_moves: alexis_moves.remove(quackle_move) # don't process the quackle move as one of the wrong ones # just in case it's the end game: while len(alexis_moves) < 6: # add a pass: alexis_moves.append(Move(None, None, None)) wrong_moves = [] wrong_moves.append(alexis_moves.pop(0)) wrong_moves.append(alexis_moves.pop(0)) # add 4 randomly chosen moves: wrong_moves.extend(choices(alexis_moves, k=4)) for option in range(len(wrong_moves)): base_layer = copy.deepcopy(game.board.existing_letters[:-1, :-1]) # slice off last sentinel # set first sentinel squares to zero instead of sentinel value: base_layer[0, :] = 0 base_layer[:, 0] = 0 move_layer = np.zeros([16, 16], dtype='int') # put rack tiles we're playing from in first row: move_layer[0][0:len(cached_rack)] = [ord(t) - 64 for t in cached_rack.rack_tiles] # set first sentinel squares to zero instead of sentinel value: word_mult = np.where(game.board.word_multipliers > 1, game.board.word_multipliers * 8, 0) letter_mult = np.where(game.board.letter_multipliers > 1, game.board.letter_multipliers * 2, 0) score_layer = np.where(game.board.existing_letter_scores > 0, game.board.existing_letter_scores * 2, word_mult + letter_mult) score_layer = score_layer[:-1, :-1] # slice off last sentinel # set first sentinel squares to zero instead of sentinel value: score_layer[0, :] = 0 score_layer[:, 0] = 0 move = wrong_moves[option] move_tiles = move.tiles if move.tiles else [] if move.direction == Direction.NOT_APPLICABLE: # pass or exchange # put rack tiles we're exchanging in first row: if move.tiles: # unless it's a pass with no tiles move_layer[0][0:len(move_tiles)] = [ord(t) - 64 for t in move.tiles] else: # regular move row = move_layer[move.row.rank, :] if move.direction == Direction.HORIZONTAL else move_layer[:, move.row.rank] # put rack tiles we're playing on board: row[move.played_squares] = [ord(t) - 64 for t in move.tiles] # change score of square containing blank to zero: score_layer = np.where(move_layer <= 26, score_layer, 0) # change blanks to normal letter ordinal (by subtracting 32) move_layer = np.where(move_layer <= 26, move_layer, move_layer - 32) # flatten arrays and convert int8 to int so values aren't clipped at 128: rgb = zip((base_layer.astype(int)).flatten() * 9, (score_layer.astype(int)).flatten() * 9, (move_layer.astype(int)).flatten() * 9) # put in a list: rgb = [pixel for pixel in rgb] # convert to an image, and resize so things like # max pooling layers won't lose all the information in the image: img = Image.new('RGB', (16, 16)) img.putdata(rgb) img = img.resize((256, 256), Image.NEAREST) # save the image img.save(img_path + 'a_g' + str(game_number).zfill(4) + '_m' + str(current_move).zfill(2) + '_option' + str(option + 1) + '.png') # add a little feedback to the console: print(":" + str((game_number, current_move, option))) # now process the Quackle move: base_layer = copy.deepcopy(game.board.existing_letters[:-1, :-1]) # slice off last sentinel # set first sentinel squares to zero instead of sentinel value: base_layer[0, :] = 0 base_layer[:, 0] = 0 move_layer = np.zeros([16, 16], dtype='int') # put rack tiles we're playing from in first row: move_layer[0][0:len(cached_rack)] = [ord(t) - 64 for t in cached_rack.rack_tiles] # set first sentinel squares to zero instead of sentinel value: word_mult = np.where(game.board.word_multipliers > 1, game.board.word_multipliers * 8, 0) letter_mult = np.where(game.board.letter_multipliers > 1, game.board.letter_multipliers * 2, 0) score_layer = np.where(game.board.existing_letter_scores > 0, game.board.existing_letter_scores * 2, word_mult + letter_mult) score_layer = score_layer[:-1, :-1] # slice off last sentinel # set first sentinel squares to zero instead of sentinel value: score_layer[0, :] = 0 score_layer[:, 0] = 0 move = quackle_move move_tiles = move.tiles if move.tiles else [] if move.direction == Direction.NOT_APPLICABLE: # pass or exchange # put rack tiles we're exchanging in first row: if move_tiles: move_layer[0][0:len(move_tiles)] = [ord(t) - 64 for t in move.tiles] else: # regular move row = move_layer[move.row.rank, :] if move.direction == Direction.HORIZONTAL else move_layer[:, move.row.rank] # put rack tiles we're playing on board: row[move.played_squares] = [ord(t) - 64 for t in move.tiles] # change score of square containing blank to zero: score_layer = np.where(move_layer <= 26, score_layer, 0) # change blanks to normal letter ordinal (by subtracting 32) move_layer = np.where(move_layer <= 26, move_layer, move_layer - 32) # flatten arrays and convert int8 to int so values aren't clipped at 128: rgb = zip((base_layer.astype(int)).flatten() * 9, (score_layer.astype(int)).flatten() * 9, (move_layer.astype(int)).flatten() * 9) # put in a list: rgb = [pixel for pixel in rgb] # convert to an image, and resize so things like # max pooling layers won't lose all the information in the image: img = Image.new('RGB', (16, 16)) img.putdata(rgb) img = img.resize((256, 256), Image.NEAREST) # save the image img.save(img_path + 'q_g' + str(game_number).zfill(4) + '_m' + str(current_move).zfill(2) + '_option1.png') # now do data augmentation by transposing the board: base_layer[1:16, 1:16] = base_layer[1:16, 1:16].T move_layer[1:16, 1:16] = move_layer[1:16, 1:16].T score_layer[1:16, 1:16] = score_layer[1:16, 1:16].T # save the transposed version: rgb = zip((base_layer.astype(int)).flatten() * 9, (score_layer.astype(int)).flatten() * 9, (move_layer.astype(int)).flatten() * 9) rgb = [pixel for pixel in rgb] img = Image.new('RGB', (16, 16)) img.putdata(rgb) # img = img.resize((256, 256), Image.NEAREST) img.save(img_path + 'q_q' + str(game_number).zfill(4) + '_m' + str(current_move).zfill(2) + '_option2.png') # now actually execute the move to prepare the board for the next move: # we've probably fake-played all the tiles so put them back in the rack: game.active_player.rack = cached_rack # only bother playing the move if it actually changes the board, # since we're not tracking what's in the bag, or what the current scores are: if quackle_move.direction is not Direction.NOT_APPLICABLE: # ensure the row we're using is a slice of the board, not a copy: if quackle_move.row: quackle_move.row = game.board.get_row(quackle_move.row.rank, quackle_move.row.direction) # clear move validation and re-validate the move, in doing so play it onto the correct row: quackle_move.is_valid = None game.validator.is_valid(quackle_move) game.execute_move(quackle_move)
def test_remaining_tiles(): bag = Bag() starting_num_of_tiles = sum(LETTER_DISTRIBUTIONS) assert bag.remaining_tiles() == starting_num_of_tiles bag.get_tiles(10) assert bag.remaining_tiles() == starting_num_of_tiles - 10
def test_create_rack(): rack = Rack(Bag())
def test_rack_size(): rack = Rack(Bag()) assert len(rack.rack_tiles) == RACK_SIZE
def test_add_too_many_tiles(): rack = Rack(Bag()) with pytest.raises(IndexError) as e_info: rack.add_tile('A') # adds tile to a new (hence full) rack
def test_create_gui(): players = [None, None] bag = Bag() game = GameController(players, bag) gui = ConsoleGui(game)