def get_move(self, board: Board, player_id: int, rules: Rules) -> int: # find out which moves are valid possible_moves = [] for x in range(board.width): if rules.check_move(board.get_board(), board.height, x): possible_moves += [x] # this simple bot checks if it can win by playing a certain column first for x in possible_moves: board.do_move(x, player_id) if rules.check_win(board.get_board(), board.width, board.height, board.get_last_move(), player_id): # even though a winning move has been detected, undo it - "game" will handle further process board.undo_move() return x # if bot cannot win with this move, undo it board.undo_move() # if bot cannot win in this round, it checks if opponent can win and blocks his move opponent_player_id = 1 if player_id == 2 else 2 for x in possible_moves: board.do_move(x, opponent_player_id) if rules.check_win(board.get_board(), board.width, board.height, board.get_last_move(), opponent_player_id): # make sure to undo the latest move - "game" will handle further process board.undo_move() return x # if bot cannot prevent a win by the opponent with this move, undo it board.undo_move() # if no victory or defeat can be detected with this very limited search depth, play randomly return choice(possible_moves)
class Gomoku: def __init__(self, size, connection_to_win, players): self.size = size self.win = connection_to_win self.clear(players) self.rules = Rules(size, self.win) def clear(self, players): self.fst_board = np.zeros((self.size, self.size), dtype=np.bool_) self.snd_board = np.zeros((self.size, self.size), dtype=np.bool_) self.fst_str, self.snd_str = players self.moves = 0 def occupied(self, position): x, y = position return self.fst_board[x, y] or self.snd_board[x, y] def move(self, position): x, y = position assert not self.fst_board[x, y] and not self.snd_board[x, y] self.moves += 1 self.fst_board[x, y] = True win, connections = self.rules.check_win(position, self.fst_board) if win: return Result.WIN, connections if self.moves == self.size ** 2: return Result.TIE, [] self.fst_board, self.snd_board = self.snd_board, self.fst_board self.fst_str, self.snd_str = self.snd_str, self.fst_str return Result.CONTINUE, None def print(self): print(" ", end="") for x in range(1, self.size + 1): print("{:2}".format(chr(ord('A') + x - 1)), end="") print() for x in range(1, self.size + 1): print("{:2}".format(chr(ord('a') + x - 1)), end="") for y in range(1, self.size + 1): if self.fst_board[x - 1, y - 1]: c = self.fst_str elif self.snd_board[x - 1, y - 1]: c = self.snd_str else: c = ' ' print("{:2}".format(c), end='') print() print()
class Game: def __init__(self) -> None: # create instances of needed classes self.__board = Board() self.__gui = GUI() self.__rules = Rules() # player objects will be set in "initialize_match" self.__player_1 = None self.__player_2 = None # players will have these well distinguishable symbols by default self.__player_1_default_symbol = 'x' self.__player_2_default_symbol = 'o' # "id_to_symbol" translates field states from database value to output symbol; player symbols will be set later self.__id_to_symbol = {0: ' '} # start game session self.__play() def __welcome(self) -> None: self.__gui.text_blue("Ready to play CONNECT FOUR?\nHere we go!") # wait for user to press any key input() self.__gui.clear_display() input_change_board = input( "Press 'B' to change default board dimensions or hit ENTER to play." ).lower() self.__gui.clear_display() if input_change_board == 'b': self.__change_board_dimensions() def __human_player_wanted(self, player_number: int) -> bool: # inform user about options self.__gui.text("Player {} shall be:".format(player_number)) self.__gui.text("-> human (press 'H')") self.__gui.text("-> a bot (press 'B')") # ask for input until valid decision has been made while True: # let user choose type of player (human or bot) by pressing the corresponding key player_type = input().lower() self.__gui.clear_display() if player_type == 'h': return True elif player_type == 'b': return False self.__gui.text_red("ERROR: Press 'H' or 'B'.") def __initialize_session(self) -> None: # create either objects of "Player" or "Bot" for both players - this is decided in "human_player_wanted" # initialization of player names and symbols for representation on board is done in "Player" or "Bot" if self.__human_player_wanted(1): self.__player_1 = Player(1, self.__player_1_default_symbol, self.__gui) else: self.__player_1 = Bot(1, self.__player_1_default_symbol) if self.__human_player_wanted(2): self.__player_2 = Player(2, self.__player_2_default_symbol, self.__gui) else: self.__player_2 = Bot(2, self.__player_2_default_symbol) # ensure that players have unique names and symbols if self.__player_1.name == self.__player_2.name or self.__player_1.symbol == self.__player_2.symbol: if type(self.__player_1) == Player and type( self.__player_2) == Player: # if both players are human, restart initialization self.__gui.text_red( "ERROR: Both players have the same name or symbol! Try again." ) # wait for user to press any key input() self.__gui.clear_display() self.__initialize_session() return # abort current call of "initialize_match" since new one has been created else: # if at least one player is a bot, resolve ambiguity automatically # by creating a new Bot instance, a new random name is given while self.__player_1.name == self.__player_2.name: if type(self.__player_1) == Bot: self.__player_1 = Bot(1, self.__player_1_default_symbol) else: self.__player_2 = Bot(2, self.__player_2_default_symbol) # if both players have the same symbol, the human player must have chosen the default symbol that was # intended for the other player - to resolve this, the bot simply picks 'o' instead of 'x' or vice versa if type(self.__player_1) == Bot: self.__player_1.symbol = self.__player_2_default_symbol else: self.__player_2.symbol = self.__player_1_default_symbol # map ids to symbols self.__id_to_symbol[self.__player_1.id] = self.__player_1.symbol self.__id_to_symbol[self.__player_2.id] = self.__player_2.symbol # summarize player information before exiting initialization self.__gui.text_blue("Great. Let's start the game!") self.__gui.text("Player 1 ({}): {} is '{}'.".format( "HUMAN" if type(self.__player_1) == Player else "BOT", self.__player_1.name, self.__player_1.symbol)) self.__gui.text("Player 2 ({}): {} is '{}'.".format( "HUMAN" if type(self.__player_2) == Player else "BOT", self.__player_2.name, self.__player_2.symbol)) # wait for user to press any key input() self.__gui.clear_display() # with "typing.Union", the function annotation can be expanded so that player can be of different Classes def __move(self, player: Union[Player, Bot]) -> None: # a move of a bot will always be valid - thus, only for a human player further checking is needed if type(player) == Bot: self.__gui.text("{} 'thinks' about the next move.".format( player.name)) self.__board.do_move( player.get_move(self.__board, player.id, self.__rules), player.id) else: while True: # "player.get_move" asks user for valid input (when width is 7: integer between 0 and 6) desired_move = player.get_move(self.__board, self.__id_to_symbol, self.__gui) # "rules.is_move_possible" checks if desired move is not against the rules if self.__rules.check_move(self.__board.get_board(), self.__board.height, desired_move): break else: self.__gui.clear_display() print(self.__board.width, self.__board.height) self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol) # let user know the move is not possible # "desired_move" is zero-based and needs to be increased by one for display self.__gui.text_red( "ERROR: Column {} is already full! Try again.".format( desired_move + 1)) # when a legal move is given, "board.do_move" organizes actually playing it self.__board.do_move(desired_move, player.id) def __play_match(self) -> None: # "match_round" keeps track of number of played moves so far match_round = 0 while True: match_round += 1 # before asking a player what to do, show the board self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol) # depending on who's turn it is, let player do a move if match_round % 2 == 1: # prompt player to do a move with method "move" self.__move(self.__player_1) # check if player 1 has won with his latest move winning_line = self.__rules.check_win( self.__board.get_board(), self.__board.width, self.__board.height, self.__board.get_last_move(), self.__player_1.id) # "winning_line" is either None or a list of tuples that store positions of tokens which led to win if winning_line is not None: self.__gui.clear_display() # player 1 has won; show board - emphasizing the winning line - one last time before exiting method self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol, winning_line) self.__gui.text_blue("{} has won. Good game!".format( self.__player_1.name)) # increase the score of player 1 by one before exiting method self.__player_1.score += 1 # wait for user to press any key input() self.__gui.clear_display() return else: # prompt player to do a move with method "move" self.__move(self.__player_2) # check if player 2 has won with his latest move winning_line = self.__rules.check_win( self.__board.get_board(), self.__board.width, self.__board.height, self.__board.get_last_move(), self.__player_2.id) # "winning_line" is either None or a list of tuples that store positions of tokens which led to win if winning_line is not None: self.__gui.clear_display() # player 2 has won; show board - emphasizing the winning line - one last time before exiting method self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol, winning_line) self.__gui.text_blue("{} has won. Good game!".format( self.__player_2.name)) # increase the score of player 2 by one before exiting method self.__player_2.score += 1 # wait for user to press any key input() self.__gui.clear_display() return self.__gui.clear_display() # if board is full, show it one last time and let players know that match is a draw before exiting method if self.__rules.check_game_over(self.__board.get_board(), self.__board.width, self.__board.height): self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol) self.__gui.text_blue("It's a draw!") # wait for user to press any key input() self.__gui.clear_display() return def __replay_match(self): self.__gui.text( "You are about to watch the latest match again. Press Enter to see the next move." ) # get game history from board as a list history = self.__board.get_history() # in some situations (like changing the board dimensions and then wanting a replay), history might be corrupted if len(history) == 0: self.__gui.text_red( "ERROR: Game history is no longer available after setting changes." ) input() self.__gui.clear_display() return # reset board and history before "playing it again" self.__board.clear_board() self.__board.clear_history() player_id = 1 # show all moves except the last one highlighting the current move as "winning_line" for move in history[:-1]: # only x value is needed to play the move self.__board.do_move(move[0], player_id) # display current board self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol, [move]) player_id = 1 if player_id == 2 else 2 input() self.__gui.clear_display() # play the final move self.__board.do_move(history[-1][0], player_id) # emphasize the winning line (if it exists) winning_line = self.__rules.check_win(self.__board.get_board(), self.__board.width, self.__board.height, history[-1], player_id) self.__gui.show_board(self.__board.get_board(), self.__board.width, self.__board.height, self.__id_to_symbol, winning_line) def __change_board_dimensions(self): while True: self.__gui.clear_display() input_width = input("What width should the board have?") self.__gui.clear_display() input_height = input("What height should the board have?") self.__gui.clear_display() try: input_width_int = int(input_width) input_height_int = int(input_height) if 0 < input_width_int < 10 and 0 < input_height_int <= 20: self.__board = Board(input_width_int, input_height_int) self.__board.clear_history() self.__gui.text_blue("Alright, changes have been saved.") return self.__gui.text_red( "ERROR: Width cannot exceed 9 and height cannot exceed 20! Try again." ) # wait for user to press any key input() except ValueError: self.__gui.text_red( "ERROR: Only integers are permissible input! Try again.") # wait for user to press any key input() def __settings(self) -> None: while True: self.__gui.text("Welcome to the settings. Here's what you can do") self.__gui.text("-> change board dimensions (D)") self.__gui.text("-> initialize a new session and reset scores (R)") input_change_player_settings = input().lower() if input_change_player_settings == 'd': self.__change_board_dimensions() return elif input_change_player_settings == 'r': self.__initialize_session() return else: self.__gui.text_red("ERROR: Your input is not an option.") def __goodbye(self) -> None: self.__gui.clear_display() self.__gui.text_blue("Thank you for playing. Bye for now!") def __keep_playing(self) -> bool: # show users current score and display options to continue self.__gui.text("What a match!") self.__gui.text("Your score now is: {} ({}) - {} ({})".format( self.__player_1.score, self.__player_1.name, self.__player_2.score, self.__player_2.name)) # stay in this "menu" until user decides to exit it while True: self.__gui.text("What would you like to do next?") self.__gui.text("-> watch a replay of the match (R)") self.__gui.text("-> change game settings (S)") self.__gui.text("-> start a new match (M)") self.__gui.text("-> quit game (Q)") input_decision = input().lower() self.__gui.clear_display() if input_decision == 'r': self.__replay_match() elif input_decision == 's': self.__settings() # clear history to prevent player from replaying a previous game after fiddling around in settingsd self.__board.clear_board() self.__board.clear_history() elif input_decision == 'm': self.__gui.text_blue("Cool, next match starts now!") # wait for user to press any key input() self.__gui.clear_display() self.__board.clear_board() self.__board.clear_history() return True elif input_decision == 'q': self.__goodbye() return False else: self.__gui.text_red("ERROR: Your input is not an option.") input() self.__gui.clear_display() def __play(self) -> None: self.__gui.clear_display() # welcome players once in the beginning self.__welcome() # before playing, players need to be set etc.; this is done in "initialize_match" self.__initialize_session() # until breaking out of an infinite loop, matches are played while True: # "play_match" will carry out an entire match until a player wins or there are no moves left self.__play_match() # find out if another match is wanted and react accordingly with "keep_playing" if self.__keep_playing(): self.__board.clear_board() else: break
class MonteCarloExploer: state_init = 0 state_select = 1 state_simulation = 2 state_backpropagation = 3 state_waitfor_move = 4 def __init__(self, size, board, tree, network): self.network = network self.size = size self.tree = tree self.orig = board self.rules = Rules(size, FLAGS.connections) self.nr_moves = 0 self.reinitialize() def reinitialize(self): self.tree.total += 1.0 self.nr_moves = 0 a, b = self.orig self.board = a.copy(), b.copy(), a + b self.state = MonteCarloExploer.state_select self.trace = [self.tree] self.termination = False self.reward = 0.0 self.pending_state = None self.move = None self.node = self.tree def step(self): return_value = 0 if self.state == MonteCarloExploer.state_select: fst, snd, m = self.board if self.node.prior is None: state = np.stack(self.board, axis=-1) moves, Q = DqnAgent.select(np.expand_dims(state, axis=0), self.network) Q = Q.reshape(-1) Q = Q - Q.min() Q /= np.max(np.abs(Q)) self.node.prior = Q mask = m.ravel() move = self.node.select(mask) x, y = move // self.size, move % self.size assert (fst[(x, y)] == False and m[(x, y)] == False) fst[x, y] = True m[x, y] = True self.nr_moves += 1 win_condition, _ = self.rules.check_win((x, y), fst) if win_condition: self.reward = 1.0 self.termination = True if np.all(m): self.reward = 0.5 self.termination = True expand = False if self.node.children[move] == None: # expand self.node.children[move] = MonteCarloTree(self.size) expand = True self.trace.append(self.node.children[move]) self.node = self.node.children[move] self.node.total += 1.0 if self.termination == True: self.state = MonteCarloExploer.state_backpropagation elif expand: self.state = MonteCarloExploer.state_simulation else: self.state = MonteCarloExploer.state_select self.board = snd, fst, m elif self.state == MonteCarloExploer.state_backpropagation: if self.nr_moves % 2 == 1: self.reward = 1.0 - self.reward for i in self.trace: i.reward += self.reward self.reward = 1.0 - self.reward self.state = MonteCarloExploer.state_select self.reinitialize() elif self.state == MonteCarloExploer.state_simulation: self.pending_state = np.stack(self.board, axis=-1) self.move = None self.state = MonteCarloExploer.state_waitfor_move return_value = 1 elif self.state == MonteCarloExploer.state_waitfor_move: if self.move != None: fst, snd, m = self.board if random.random() < FLAGS.mcts_epsilon: while True: m = self.board[0] + self.board[1] move = random.randint(0, self.size**2 - 1) move = move // self.size, move % self.size if m[move] == False: break else: move = self.move // self.size, self.move % self.size self.move = None fst[move] = True m[move] = True self.nr_moves += 1 termination = False win_condition, _ = self.rules.check_win(move, fst) if win_condition: self.reward = 1.0 termination = True if np.all(m): self.reward = 0.5 termination = True if termination: self.state = MonteCarloExploer.state_backpropagation else: self.state = MonteCarloExploer.state_simulation self.board = snd, fst, m return return_value
class DqnAgent(Agent): state_self = 0 state_opponent = 1 state_mask = 2 def __init__(self, size, session, scope, threads): super().__init__(size, session, scope, threads) self.autoresolve = False self.rules = Rules(size, FLAGS.connections) self.test_mode = False self.epsilon = 0.0 self.board = None def clear(self): super().clear() self.buffered_move = None self.board = np.zeros((self.size, self.size), dtype = np.int32), \ np.zeros((self.size, self.size), dtype = np.int32), \ np.zeros((self.size, self.size), dtype = np.int32) self.fst_move = self.snd_move = None def update_state_(self, position, mover): mask = self.board[2] mover_board = self.board[mover] assert (mask[position] == 0.0 and mover_board[position] == 0.0) mask[position] = 1.0 mover_board[position] = 1.0 @staticmethod def select(input, network): stacked = np.stack(input, axis=0) pred, out = network.session.run( [network.predictions, network.legal_moves], feed_dict={network.input: stacked}) m = np.argmax(out, axis=1) return m, out def self_move(self, _): fst, snd, mask = self.board move = None if self.autoresolve and self.fst_move and self.snd_move: if move == None: win_condition, set = self.rules.check_win( self.fst_move, fst, mask) if win_condition: for s in set: x, y = s // self.size, s % self.size if fst[(x, y)] == 0.0: move = x, y break if move == None: win_condition, set = self.rules.check_win( self.snd_move, snd, mask) if win_condition: for s in set: x, y = s // self.size, s % self.size move = x, y if snd[(x, y)] == 0.0: move = x, y break m, q = self.buffered_move if move == None: if random.uniform(0, 1) < self.epsilon: x, y = move = super().random_policy(mask) else: assert (self.buffered_move != None) x, y = move = m // self.size, m % self.size self.buffered_move = None self.update_state_(move, DqnAgent.state_self) self.fst_move = move return move, q def opponent_move(self, position, _): self.update_state_(position, DqnAgent.state_opponent) self.snd_move = position