Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import numpy as np | |
| import random | |
| import cv2 | |
| from enum import Enum | |
| from PIL import Image | |
| class Difficulty(Enum): | |
| EASY = "Easy" | |
| MEDIUM = "Medium" | |
| HARD = "Hard" | |
| class GoGame: | |
| def __init__(self, size=9): | |
| self.size = size | |
| self.board = np.zeros((size, size), dtype=int) | |
| self.current_player = 1 # 1 for black, -1 for white | |
| self.pass_count = 0 | |
| self.last_move = None | |
| self.captured_black = 0 | |
| self.captured_white = 0 | |
| def get_neighbors(self, x, y): | |
| neighbors = [] | |
| for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: | |
| nx, ny = x + dx, y + dy | |
| if 0 <= nx < self.size and 0 <= ny < self.size: | |
| neighbors.append((nx, ny)) | |
| return neighbors | |
| def get_group(self, x, y): | |
| color = self.board[x, y] | |
| if color == 0: | |
| return set() | |
| group = {(x, y)} | |
| frontier = [(x, y)] | |
| while frontier: | |
| current = frontier.pop() | |
| for nx, ny in self.get_neighbors(*current): | |
| if self.board[nx, ny] == color and (nx, ny) not in group: | |
| group.add((nx, ny)) | |
| frontier.append((nx, ny)) | |
| return group | |
| def has_liberties(self, x, y): | |
| group = self.get_group(x, y) | |
| for stone_x, stone_y in group: | |
| for nx, ny in self.get_neighbors(stone_x, stone_y): | |
| if self.board[nx, ny] == 0: | |
| return True | |
| return False | |
| def capture_stones(self): | |
| captured = 0 | |
| for x in range(self.size): | |
| for y in range(self.size): | |
| if self.board[x, y] == -self.current_player and not self.has_liberties(x, y): | |
| group = self.get_group(x, y) | |
| captured += len(group) | |
| for gx, gy in group: | |
| self.board[gx, gy] = 0 | |
| return captured | |
| def is_valid_move(self, x, y): | |
| if self.board[x, y] != 0: | |
| return False | |
| # Make a temporary move. | |
| self.board[x, y] = self.current_player | |
| valid = self.has_liberties(x, y) | |
| captures_enemy = self.capture_stones() > 0 | |
| # Undo temporary move. | |
| self.board[x, y] = 0 | |
| return valid or captures_enemy | |
| def make_move(self, x, y): | |
| if not self.is_valid_move(x, y): | |
| return False | |
| self.board[x, y] = self.current_player | |
| captured = self.capture_stones() | |
| if self.current_player == 1: | |
| self.captured_white += captured | |
| else: | |
| self.captured_black += captured | |
| self.last_move = (x, y) | |
| self.current_player *= -1 | |
| return True | |
| def ai_move(self, difficulty): | |
| empty_positions = list(zip(*np.where(self.board == 0))) | |
| if not empty_positions: | |
| return None | |
| valid_moves = [pos for pos in empty_positions if self.is_valid_move(*pos)] | |
| if not valid_moves: | |
| return None | |
| if difficulty == Difficulty.EASY: | |
| move = random.choice(valid_moves) | |
| elif difficulty == Difficulty.MEDIUM: | |
| move = self.medium_ai(valid_moves) | |
| else: | |
| move = self.hard_ai(valid_moves) | |
| self.make_move(*move) | |
| return move | |
| def medium_ai(self, valid_moves): | |
| if self.last_move: | |
| lx, ly = self.last_move | |
| valid_moves.sort(key=lambda p: abs(p[0] - lx) + abs(p[1] - ly)) | |
| return valid_moves[0] | |
| return self.center_based_move(valid_moves) | |
| def center_based_move(self, valid_moves): | |
| center = self.size // 2 | |
| valid_moves.sort(key=lambda p: abs(p[0] - center) + abs(p[1] - center)) | |
| return valid_moves[0] | |
| def hard_ai(self, valid_moves): | |
| for move in valid_moves: | |
| self.board[move] = self.current_player | |
| if self.capture_stones() > 0: | |
| self.board[move] = 0 | |
| return move | |
| self.board[move] = 0 | |
| opponent = -self.current_player | |
| self.current_player = opponent | |
| for move in valid_moves: | |
| if self.is_valid_move(*move): | |
| self.board[move] = opponent | |
| if self.capture_stones() > 0: | |
| self.board[move] = 0 | |
| self.current_player = -opponent | |
| return move | |
| self.board[move] = 0 | |
| self.current_player = -opponent | |
| return self.medium_ai(valid_moves) | |
| def create_board_image(board, last_move=None, hover_pos=None): | |
| cell_size = 60 | |
| margin = 40 | |
| total_size = board.shape[0] * cell_size + 2 * margin | |
| # Create wooden background. | |
| image = np.full((total_size, total_size, 3), [219, 179, 119], dtype=np.uint8) | |
| # Draw grid. | |
| for i in range(board.shape[0]): | |
| cv2.line(image, (margin + i * cell_size, margin), (margin + i * cell_size, total_size - margin), (0, 0, 0), 2) | |
| cv2.line(image, (margin, margin + i * cell_size), (total_size - margin, margin + i * cell_size), (0, 0, 0), 2) | |
| # Draw star points. | |
| star_points = [(2, 2), (2, 6), (4, 4), (6, 2), (6, 6)] | |
| for point in star_points: | |
| cv2.circle(image, (margin + point[1] * cell_size, margin + point[0] * cell_size), 4, (0, 0, 0), -1, cv2.LINE_AA) | |
| # Draw hover indicator, if any. | |
| if hover_pos: | |
| hover_row, hover_col = hover_pos | |
| center = (margin + hover_col * cell_size, margin + hover_row * cell_size) | |
| overlay = image.copy() | |
| cv2.circle(overlay, center, 23, (0, 0, 0), -1, cv2.LINE_AA) | |
| image = cv2.addWeighted(overlay, 0.5, image, 0.5, 0) | |
| cv2.circle(image, center, 23, (255, 255, 255), 1, cv2.LINE_AA) | |
| # Draw stones. | |
| for i in range(board.shape[0]): | |
| for j in range(board.shape[1]): | |
| if board[i, j] != 0: | |
| center = (margin + j * cell_size, margin + i * cell_size) | |
| # Shadow effect. | |
| cv2.circle(image, (center[0] + 2, center[1] + 2), 23, (0, 0, 0), -1, cv2.LINE_AA) | |
| stone_color = (0, 0, 0) if board[i, j] == 1 else (255, 255, 255) | |
| cv2.circle(image, center, 23, stone_color, -1, cv2.LINE_AA) | |
| # 3D highlight. | |
| if board[i, j] == -1: | |
| cv2.circle(image, (center[0] - 5, center[1] - 5), 8, (240, 240, 240), -1, cv2.LINE_AA) | |
| else: | |
| cv2.circle(image, (center[0] - 5, center[1] - 5), 8, (40, 40, 40), -1, cv2.LINE_AA) | |
| # Highlight last move. | |
| if last_move: | |
| row, col = last_move | |
| center = (margin + col * cell_size, margin + row * cell_size) | |
| cv2.circle(image, center, 5, (255, 0, 0), -1, cv2.LINE_AA) | |
| return Image.fromarray(image) | |
| class GradioGoGame: | |
| def __init__(self): | |
| self.game = GoGame() | |
| self.difficulty = Difficulty.EASY | |
| self.hover_pos = None | |
| def process_click(self, evt: gr.SelectData): | |
| cell_size = 60 | |
| margin = 40 | |
| # evt.index returns pixel coordinates as (x, y). Convert to board coordinates. | |
| col = round((evt.index[0] - margin) / cell_size) | |
| row = round((evt.index[1] - margin) / cell_size) | |
| if not (0 <= row < self.game.size and 0 <= col < self.game.size): | |
| return create_board_image(self.game.board, self.game.last_move), "Invalid click position" | |
| if not self.game.make_move(row, col): | |
| return create_board_image(self.game.board, self.game.last_move), "Invalid move (occupied or suicide)" | |
| # AI Move. | |
| ai_move = self.game.ai_move(self.difficulty) | |
| if ai_move is None: | |
| return create_board_image(self.game.board, self.game.last_move), "Game Over" | |
| status = f"Black captures: {self.game.captured_white} | White captures: {self.game.captured_black}" | |
| return create_board_image(self.game.board, self.game.last_move), f"AI moved to: {ai_move}\n{status}" | |
| def update_hover(self, evt: gr.SelectData): | |
| cell_size = 60 | |
| margin = 40 | |
| col = round((evt.index[0] - margin) / cell_size) | |
| row = round((evt.index[1] - margin) / cell_size) | |
| if 0 <= row < self.game.size and 0 <= col < self.game.size: | |
| self.hover_pos = (row, col) | |
| else: | |
| self.hover_pos = None | |
| return create_board_image(self.game.board, self.game.last_move, self.hover_pos) | |
| def reset_game(self): | |
| self.game = GoGame() | |
| self.hover_pos = None | |
| return create_board_image(self.game.board), "Game reset" | |
| def create_interface(): | |
| game = GradioGoGame() | |
| with gr.Blocks(theme=gr.themes.Soft()) as interface: | |
| gr.Markdown(""" | |
| # Go Game vs AI | |
| Play the ancient game of Go against an AI opponent. Black plays first. | |
| **Rules:** | |
| - Click on any intersection to place a stone. | |
| - Capture enemy stones by surrounding them. | |
| - Stones must have liberties (empty adjacent points) to survive. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| board_output = gr.Image( | |
| value=create_board_image(game.game.board), | |
| label="Go Board", | |
| type="pil", | |
| height=600, | |
| width=600, | |
| interactive=True | |
| ) | |
| msg_output = gr.Textbox( | |
| label="Game Status", | |
| value="Click on an intersection to place a stone", | |
| lines=2 | |
| ) | |
| with gr.Column(scale=1): | |
| difficulty = gr.Radio( | |
| choices=[d.value for d in Difficulty], | |
| value=Difficulty.EASY.value, | |
| label="AI Difficulty" | |
| ) | |
| reset_btn = gr.Button("Reset Game", variant="secondary") | |
| # Update difficulty when the radio value changes. | |
| def update_difficulty(value): | |
| game.difficulty = Difficulty(value) | |
| return value | |
| difficulty.change(update_difficulty, inputs=[difficulty], outputs=[difficulty]) | |
| # Use .select() for click events on the interactive image. | |
| board_output.select( | |
| game.process_click, | |
| inputs=[], # No additional inputs: the event data is passed automatically. | |
| outputs=[board_output, msg_output] | |
| ) | |
| # (Removed mousemove event since gr.Image doesn't support it in your version.) | |
| reset_btn.click( | |
| game.reset_game, | |
| outputs=[board_output, msg_output] | |
| ) | |
| return interface | |
| if __name__ == "__main__": | |
| interface = create_interface() | |
| interface.launch() | |