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 with thicker lines. | |
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 (hoshi). | |
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 position indicator. | |
if hover_pos: | |
hover_row, hover_col = hover_pos | |
center = (margin + hover_col * cell_size, margin + hover_row * cell_size) | |
overlay = image.copy() | |
# Draw a semi-transparent preview. | |
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 with enhanced 3D effect. | |
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. | |
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) | |
# Highlight for 3D effect. | |
if board[i, j] == -1: # White stone highlight. | |
cv2.circle(image, | |
(center[0] - 5, center[1] - 5), | |
8, | |
(240, 240, 240), | |
-1, | |
cv2.LINE_AA) | |
else: # Black stone highlight. | |
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, difficulty): | |
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" | |
self.difficulty = Difficulty(difficulty) | |
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 | |
# Convert pixel position to board coordinates. | |
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") | |
# Use .select() for click events on the interactive image. | |
board_output.select( | |
game.process_click, | |
inputs=[board_output, difficulty], | |
outputs=[board_output, msg_output] | |
) | |
# Use .mousemove() for hover updates (if supported by your Gradio version). | |
board_output.mousemove( | |
game.update_hover, | |
inputs=board_output, | |
outputs=board_output | |
) | |
reset_btn.click( | |
game.reset_game, | |
outputs=[board_output, msg_output] | |
) | |
return interface | |
if __name__ == "__main__": | |
interface = create_interface() | |
interface.launch() | |