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_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 cpu_play(self, characters, mode): # make sure characters is an uppercase string if isinstance(characters, list): characters = "".join(characters) characters = characters.upper() wlens = range(2, 8) if mode == CPU_MODE_MAX: wlens = reversed(wlens) selected = None for l in wlens: for word in itertools.permutations(characters, l): # convert tupple to str word = ''.join(word) # check if valid word if word in config.GREEK7_WORDS: score = Bag.count_score(word) if selected == None or selected[1] < score: selected = (word, score, l) if mode < CPU_MODE_SMART: return selected return selected
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_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_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 __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_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 test_init(): bag = Bag()
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_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_create_gui(): players = [None, None] bag = Bag() game = GameController(players, bag) gui = ConsoleGui(game)
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_rack_size(): rack = Rack(Bag()) assert len(rack.rack_tiles) == RACK_SIZE
class NewGame(View): 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 on_clear_click(self, button, event): self.board.on_press_esc(event) def on_end_round_click(self, button, event): if not self.playing: self.play() def on_backspace_click(self, button, event): self.board.on_press_backspace(event) def get_player(self): return self.player_a if self.player_a.is_playing else self.player_b def set_deck(self): player = self.get_player() deck = [] if self.bag.remaining_chars() < config.MAX_WORD_LEN: return None if len(player.unused_words) != config.MAX_WORD_LEN: for i in range(len(player.unused_words)): deck.append(player.unused_words[i]) self.deck.append_character(player.unused_words[i]) else: for i in range(len(player.unused_words)): self.bag.collection.append(player.unused_words[i]) while not self.deck.get_free_tile() == -1: word = self.bag.get_char() deck.append(word) self.deck.append_character(word) return deck def flip_players(self): self.player_a.is_playing = not self.player_a.is_playing self.player_b.is_playing = not self.player_b.is_playing def play(self): self.playing = True self.turn += 1 word, word_score = self.board.get_word() # check user word and update score if word in config.GREEK7_WORDS: self.player_a.add_score(word_score) self.player_a.append_word(word) # clear deck and board self.player_a.unused_words = self.deck.clear() self.board.clear() # computer turn self.flip_players() if self.bag.remaining_chars() < config.MAX_WORD_LEN: return self.end_game() deck_cpu = self.set_deck() # TODO add back to bag deck_cpu not used chars word_cpu = self.player_a.cpu_play([i[0] for i in deck_cpu], CPU_MODE_SMART) if word_cpu is None: return self.end_game() else: for i in range(len(word_cpu[0])): for j in range(len(deck_cpu)): if deck_cpu[j][0] == word_cpu[0][i]: deck_cpu.pop(j) break self.player_b.unused_words = deck_cpu self.player_b.append_word(word_cpu[0]) self.player_b.add_score(word_cpu[1]) self.board.set_word(word_cpu[0]) self.render_sleep = 1 def end_game(self): data = [self.player_a.score, self.player_b.score, self.turn] utils.write_to_file(config.SCORES_PATH, data) self.game_finished = True def render(self): # draw background self.render_background() # draws the score board self.score_board.render() if self.game_finished: end_font = pygame.font.SysFont('sans-serif', 100) end_font.set_bold(True) winner = self.player_a.get_name( ) if self.player_a.score >= self.player_b.score else self.player_b.get_name( ) rendered_message = end_font.render(winner + " WIN !", 1, config.WHITE) self.screen.blit( rendered_message, (config.SCREEN_W / 2 - rendered_message.get_rect().width / 2, config.SCREEN_H / 2 - 20)) pygame.display.flip() pygame.time.delay(3000) self.event_manager.post(MainMenuEvent()) else: # draws the board self.board.render() # draws the options for option in self.options: option.render(self.screen) # draws the decks self.deck.render() # draws the number of remaining chars self.render_remaining_chars() # checks if CPU is playing self.check_sleep() # limit to 60 frames per second self.clock.tick(60) # go ahead and update the screen with what we've drawn pygame.display.flip() def on_destroy(self): print("destroy new_game view") # call nested views on_destroy self.score_board.on_destroy() self.board.on_destroy() self.deck.on_destroy() for option in self.options: self.event_manager.remove(ClickEvent, option) def check_sleep(self): if self.render_sleep > 0: self.render_sleep += 1 if self.render_sleep > 3 and not self.game_finished: pygame.time.delay(1000) self.render_sleep = 0 self.board.clear() self.deck.clear() self.flip_players() self.playing = False if self.set_deck() is None: return self.end_game() def render_remaining_chars(self): rem_chars_render = self.std_font.render( "remaining: " + str(self.bag.remaining_chars()), 1, (255, 255, 255)) self.screen.blit(rem_chars_render, (config.SCREEN_W - 140, config.SCREEN_H - 100)) def render_background(self): img_path = os.path.normpath( os.path.dirname(__file__) + "/../res/images/green_fabric.jpg") img = pygame.image.load(img_path) self.screen.fill(config.BLACK) for x in range(0, config.SCREEN_W, img.get_rect().width): for y in range(0, config.SCREEN_H, img.get_rect().height): self.screen.blit(img, (x, y))
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)