def test_board_defaults(): """Test Board constructor uses default location value.""" b1 = Board(3) for row in range(b1.side_len()): for col in range(b1.side_len()): assert b1.get(row, col) == None b2 = _board_3x3_Indexes() for row in range(b2.side_len()): for col in range(b2.side_len()): assert b2.get(row, col) == row * b2.side_len() + col b3 = Board(3, []) for row in range(b3.side_len()): for col in range(b3.side_len()): assert b3.get(row, col) == []
def test_get_raises_ValueError(): """Test Board.get() validates arguments.""" b = Board(3) with pytest.raises(ValueError): b.get(-1, 0) with pytest.raises(ValueError): b.get(3, 0) with pytest.raises(ValueError): b.get(0, -1) with pytest.raises(ValueError): b.get(0, 3)
def test_set_immutable(): """Test Board.set() preserves immutable board.""" b1 = Board(3, 0) b2 = b1 for row in range(b2.side_len()): for col in range(b2.side_len()): b2 = b2.set(row, col, row*b2.side_len() + col) for row in range(b1.side_len()): for col in range(b1.side_len()): assert b1.get(row, col) == 0
def move(self, board: Board): """ Ask for the user's input until a valid one appears, and then update the board :param board: :return: """ while 1: coord = input(">") coord = coord.split(" ") row = coord[0] try: col = coord[1] except IndexError: print("col is invalid") continue try: row = int(row) except ValueError: print("row is not an int") continue try: col = int(col) except ValueError: print("col is not an int") continue if row < 0 or row >= board.rows: print("row out of bound") continue if col < 0 or col >= board.cols: print("col out of bound") continue if board.get(row, col) != 0: print(f"({row}, {col}) has been occupied") continue return row, col
class Simulation: ''' Handle IO logic for simulation. Simulation objects function as iterable finite state machines. ''' # State variables INIT = 0 PROMPT_TEAM = 1 PLAYER_MOVE = 2 CPU_MOVE = 3 PROMPT_RESTART = 4 FINISHED = 5 @staticmethod def get_input(prompt, restrictions): ''' Get input from user while applying given constraints Parameters prompt: str, message to guide user restrictions: str[], list of valid input options Return str, input from user ''' # keep requesting until valid input received while True: result = input(prompt) if result in restrictions: return result else: print(static.UTIL['input_error']) def __init__(self): ''' Initialize fieleds. ''' self._solver = Solver() self._board = Board() self._state = Simulation.INIT def __iter__(self): ''' Mark Simulation objects as iterable Return Simulation, this object ''' return self def __next__(self): ''' Continue simulation until next piece of output is available Return str, output from game since last call to next() ''' if self._state == Simulation.INIT: return self._state_init() elif self._state == Simulation.PROMPT_TEAM: return self._state_prompt_team() elif self._state == Simulation.CPU_MOVE: return self._state_cpu_move() elif self._state == Simulation.PLAYER_MOVE: return self._state_player_move() elif self._state == Simulation.PROMPT_RESTART: return self._state_prompt_restart() else: # self._state == Simulation.FINISHED raise StopIteration def _state_init(self): ''' Update state to PROMPT_TEAM Return str, rules for simulation ''' self._state = Simulation.PROMPT_TEAM return '\n%s\n' % static.INFO['man'] def _state_prompt_team(self): ''' Determine teams and update state to either CPU_MOVE or PLAYER_MOVE Return str, board representation ''' # ask user is they would like to go first choice = Simulation.get_input( static.UTIL['team_prompt'], static.BINARY) if choice in static.YES: self._state = Simulation.PLAYER_MOVE else: self._state = Simulation.CPU_MOVE return str(self._board) def _state_cpu_move(self): ''' Make cpu move and update state to either PROMPT_RESTART or PLAYER_MOVE Return str, board representation and optional end of game message ''' move = self._solver.get_next_move(self._board) turn = str(self._board.turn()) self._board = self._board.move(move) # result is cpu move and string representation of board result = ['%s >>> %d' % (turn, move), str(self._board)] # if game is over, append game over message if self._board.game_over(): result.append(static.UTIL['lose_game'] if self._board.winner() else static.UTIL['tie_game']) self._state = Simulation.PROMPT_RESTART else: self._state = Simulation.PLAYER_MOVE return '\n'.join(result) def _state_player_move(self): ''' Request player move and update state to either PROMPT_RESTART or PLAYER_MOVE Return str, board representation and optional end of game message ''' # commands include available spaces, an action, or a help command options = [str(x) for x in self._board.get(Team.NEITHER)] + \ static.ACTIONS + list(static.INFO.keys()) prompt = '%s >>> ' % str(self._board.turn()) command = Simulation.get_input(prompt, options) if command in static.INFO: # print help message return static.INFO[command] elif command == 'undo': if self._board.turn() in self._board: # check that player has a move that can be undone # undo twice to undo cpu's move as well self._board = self._board.undo().undo() return str(self._board) else: return static.UTIL['undo_error'] elif command == 'print': return str(self._board) elif command == 'quit': self._state = Simulation.PROMPT_RESTART return '' # return empty line to print else: # integer coordinate self._board = self._board.move(int(command)) result = [str(self._board)] # if game is over, append game over message if self._board.game_over(): result.append(static.UTIL['tie_game']) self._state = Simulation.PROMPT_RESTART else: self._state = Simulation.CPU_MOVE return '\n'.join(result) def _state_prompt_restart(self): ''' Determine whether to re-run simulation and update state to either PROMPT_TEAM of FINISHED Return str, board representation ''' # ask whether player wants to play again choice = Simulation.get_input( static.UTIL['retry_prompt'], static.BINARY) if choice in static.YES: self._board = Board() self._state = Simulation.PROMPT_TEAM else: self._state = Simulation.FINISHED return '' # return empty line to print def board(self): ''' Return Board, current board for this simulation ''' return self._board def state(self): ''' Return int, current Simulation state constant for this simulation ''' return self._state
def test_get(side, ival): """Test Board.get().""" b = Board(side, ival) for row in range(b.side_len()): for col in range(b.side_len()): assert b.get(row, col) == ival
class GUI: def __init__(self): self.win = Tk() self.win.title("Gomoku") self.buttons = [] self.turn = "black" self.display() self.p1 = PlayerLV4(1, "White") self.p2 = Player(2, "Black") self.board = Board(15, 15) self.win.mainloop() def display(self): f1 = Frame(self.win, padx=10, pady=10) f1.grid(row=1, column=1) row = [] for i in range(15): for j in range(15): button = Button(f1, text="", width=2, heigh=1, command=lambda i=i, j=j: self.click_button(i, j)) # button.pack() button.grid(row=i, column=j) row.append(button) self.buttons.append(row) row = [] # for i in range(len(self.buttons)): # for j in range(len(row)): # print(i, j) # button = self.buttons[i][j] # button.bind("<Button>", lambda i=i, j=j: self.click_button(i, j)) # button.bind("<ButtonRelease>", self.put_white) def click_button(self, x, y): print("click button") if self.buttons[x][y]["text"] == "": if self.turn == "black": self.buttons[x][y]["text"] = "⚫" self.board.put(2, x, y) if self.is_win(2, (x, y)): messagebox.showinfo("Black wins!", "Do you want to play again?") else: self.turn = "white" self.put_white() def put_white(self): print("put white") print(self.board.board) x, y = self.p1.move(self.board) print(f"x: {x}, y: {y}") self.board.put(1, x, y) self.buttons[x][y]["text"] = "⚪️" if self.is_win(1, (x, y)): messagebox.showinfo("White wins!", "Do you want to play again?") else: self.turn = "black" def is_win(self, stone_num: int, coord: tuple) -> bool: """ check whether the player wins the game when put a stone at the coord me: 1 for black stone, 2 for white stone coord: (row, col) return: true if wins, else false """ def is_chain(stone_num: int, coord: tuple, step: tuple): """ Check whether there is an unbreakable chain of 5 stones at coord such as the coordinates of the adjacent stone is the coordinate of the stone +/- step :return: true if there is a chain of 5 stones, else false """ total = 0 row, col = coord for i in range(5): if total >= 5: return True try: if self.board.get(row, col) == stone_num: total += 1 else: break except IndexError: break row += step[0] col += step[1] row, col = coord row -= step[0] col -= step[1] for i in range(5): if total >= 5: return True try: if self.board.get(row, col) == stone_num: total += 1 else: break except IndexError: break row -= step[0] col -= step[1] return False # row col diagonal steps = [(0, 1), (1, 0), (1, -1), (1, 1)] for step in steps: if is_chain(stone_num, coord, step): return True
class GUI: def __init__(self): self.width = 675 self.height = 700 self.screen = pygame.display.set_mode( (self.width + GRID_WIDTH, self.height + GRID_WIDTH)) pygame.display.set_caption("Gomoku") self.screen.fill(COLOR_BOARD) self.turn = "black" # black goes first self.cur_player = 2 self.ai = PlayerLV3(1, "White") self.steps = { # keep track of the steps of each stone "white": [], "black": [] } self.board = Board(15, 15) self.highlight = None def draw_board(self): # for i in range(1, 16): # pygame.font.init() # my_font = pygame.font.SysFont("Arial", 12) # text_surface = my_font.render(f"{i}", True, COLOR_BLACK) # text_rect = text_surface.get_rect(center=(self.width / 2, self.height)) # self.screen.blit(text_surface, text_rect) for i in range(1, 16): pygame.draw.line(self.screen, COLOR_BLACK, [GRID_WIDTH * i, GRID_WIDTH], [GRID_WIDTH * i, self.width], 2) pygame.draw.line(self.screen, COLOR_BLACK, [GRID_WIDTH, GRID_WIDTH * i], [self.width, GRID_WIDTH * i], 2) pygame.draw.circle(self.screen, COLOR_BLACK, [GRID_WIDTH * 8, GRID_WIDTH * 8], 8) @staticmethod def get_draw_pos(x, y): draw_x, draw_y = x - x % GRID_WIDTH, y - y % GRID_WIDTH if x % GRID_WIDTH > GRID_WIDTH / 2: # close to the right point draw_x += GRID_WIDTH if y % GRID_WIDTH > GRID_WIDTH / 2: # close to the bottom point draw_y += GRID_WIDTH return draw_x, draw_y def draw_stone(self, x: int, y: int): # Do not put stone in occupied position if (x, y) in self.steps["white"]: return if (x, y) in self.steps["black"]: return if self.turn == "white": img_path = path.abspath("imgs/white.png") img = pygame.image.load(img_path) else: img_path = path.abspath("imgs/black.png") img = pygame.image.load(img_path) scaled_img = pygame.transform.smoothscale( img, (GRID_WIDTH // 3 * 2, GRID_WIDTH // 3 * 2)) self.screen.blit(scaled_img, (x - GRID_WIDTH // 3, y - GRID_WIDTH // 3)) self.steps[self.turn].append((x, y)) i, j = x // GRID_WIDTH - 1, y // GRID_WIDTH - 1 # index of the stone in the board self.board.put(self.cur_player, i, j) # Switch the turn if self.turn == "white": self.turn = "black" else: self.turn = "white" # Switch the current player if self.cur_player == 1: self.cur_player = 2 elif self.cur_player == 2: self.cur_player = 1 def highlight_stone(self, x, y): # delete the previous highlight # if self.highlight: # self.highlight.move(x, y) # if self.highlight: # self.highlight.fill((0, 0, 0)) # self.highlight.set_alpha(255) # self.screen.blit(self.highlight, (0, 0), special_flags=(pygame.BLEND_RGBA_ADD)) # self.highlight = pygame.Surface(((GRID_WIDTH // 3 + 4) * 2, (GRID_WIDTH // 3 + 4) * 2), pygame.SRCALPHA, 32) # self.highlight = highlight.convert_alpha(highlight) # pygame.gfxdraw.aacircle(self.highlight, (GRID_WIDTH // 3 + 4), (GRID_WIDTH // 3 + 4), GRID_WIDTH // 3 + 3, COLOR_RED) # circle = pygame.gfxdraw.filled_circle(self.screen, x, y, GRID_WIDTH // 3 + 3, COLOR_RED) pass if self.highlight: self.highlight.move_ip(10, 10) # self.highlight.center = (x, y) pygame.display.update(self.highlight) else: self.highlight = pygame.draw.circle(self.screen, COLOR_RED, (x, y), GRID_WIDTH // 3 + 4) # highlight_rect = self.highlight.get_rect(center=(x, y)) # self.screen.blit(self.highlight, highlight_rect) def ai_move(self): # Assume ai is 1 by default if self.cur_player == 1: i, j = self.ai.move(self.board) x, y = (i + 1) * GRID_WIDTH, (j + 1) * GRID_WIDTH self.draw_stone(x, y) return i, j def is_win(self, stone_num: int, coord: tuple) -> bool: """ check whether the player wins the game when put a stone at the coord me: 1 for black stone, 2 for white stone coord: (row, col) return: true if wins, else false """ def is_chain(stone_num: int, coord: tuple, step: tuple): """ Check whether there is an unbreakable chain of 5 stones at coord such as the coordinates of the adjacent stone is the coordinate of the stone +/- step :return: true if there is a chain of 5 stones, else false """ total = 0 row, col = coord for i in range(5): if total >= 5: return True try: if self.board.get(row, col) == stone_num: total += 1 else: break except IndexError: break row += step[0] col += step[1] row, col = coord row -= step[0] col -= step[1] for i in range(5): if total >= 5: return True try: if self.board.get(row, col) == stone_num: total += 1 else: break except IndexError: break row -= step[0] col -= step[1] return False # row col diagonal steps = [(0, 1), (1, 0), (1, -1), (1, 1)] for step in steps: if is_chain(stone_num, coord, step): return True def show_win_msg(self, win_stone: str): pygame.font.init() my_font = pygame.font.SysFont("Arial", 30) text_surface = my_font.render(f"{win_stone} Wins!", True, COLOR_BLACK) text_rect = text_surface.get_rect(center=(self.width / 2, self.height)) self.screen.blit(text_surface, text_rect)