|
import gradio as gr |
|
from PIL import Image, ImageDraw |
|
import numpy as np |
|
import math |
|
|
|
|
|
BOARD_SIZE = 9 |
|
CELL_SIZE = 50 |
|
PIECE_RADIUS = 20 |
|
EMPTY = 0 |
|
WHITE_SOLDIER = 1 |
|
BLACK_SOLDIER = 2 |
|
KING = 3 |
|
CASTLE = (4, 4) |
|
CAMPS = [ |
|
(0,3), (0,4), (0,5), (1,4), |
|
(8,3), (8,4), (8,5), (7,4), |
|
(3,0), (4,0), (5,0), (4,1), |
|
(3,8), (4,8), (5,8), (4,7) |
|
] |
|
ESCAPES = [(i,j) for i in [0,8] for j in range(BOARD_SIZE)] + [(i,j) for j in [0,8] for i in range(BOARD_SIZE) if (i,j) not in CAMPS] |
|
COLORS = { |
|
'empty': (255, 255, 255), |
|
'castle': (128, 128, 128), |
|
'camp': (139, 69, 19), |
|
'escape': (0, 255, 0), |
|
'white': (255, 255, 255), |
|
'black': (0, 0, 0), |
|
'king': (255, 215, 0), |
|
'highlight': (255, 255, 0) |
|
} |
|
|
|
|
|
class TablutState: |
|
def __init__(self): |
|
self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int) |
|
self.turn = 'WHITE' |
|
self.black_in_camps = set(CAMPS) |
|
self.setup_initial_position() |
|
self.move_history = [] |
|
|
|
def setup_initial_position(self): |
|
self.board[4, 4] = KING |
|
white_positions = [(3,4), (4,3), (4,5), (5,4), (2,4), (4,2), (4,6), (6,4)] |
|
for pos in white_positions: |
|
self.board[pos] = WHITE_SOLDIER |
|
for pos in CAMPS: |
|
self.board[pos] = BLACK_SOLDIER |
|
|
|
def copy(self): |
|
new_state = TablutState() |
|
new_state.board = self.board.copy() |
|
new_state.turn = self.turn |
|
new_state.black_in_camps = self.black_in_camps.copy() |
|
new_state.move_history = self.move_history.copy() |
|
return new_state |
|
|
|
|
|
def pos_to_coord(pos): |
|
row, col = pos |
|
return f"{chr(ord('A') + col)}{row + 1}" |
|
|
|
def is_adjacent_to_castle(pos): |
|
x, y = pos |
|
cx, cy = CASTLE |
|
return (abs(x - cx) == 1 and y == cy) or (abs(y - cy) == 1 and x == cx) |
|
|
|
def get_friendly_pieces(turn): |
|
return [WHITE_SOLDIER, KING] if turn == 'WHITE' else [BLACK_SOLDIER] |
|
|
|
def manhattan_distance(pos1, pos2): |
|
return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1]) |
|
|
|
|
|
def is_valid_move(state, from_pos, to_pos): |
|
if from_pos == to_pos: |
|
return False |
|
piece = state.board[from_pos] |
|
if state.turn == 'WHITE' and piece not in [WHITE_SOLDIER, KING]: |
|
return False |
|
if state.turn == 'BLACK' and piece != BLACK_SOLDIER: |
|
return False |
|
if state.board[to_pos] != EMPTY: |
|
return False |
|
from_row, from_col = from_pos |
|
to_row, to_col = to_pos |
|
if from_row != to_row and from_col != to_col: |
|
return False |
|
if from_row == to_row: |
|
step = 1 if to_col > from_col else -1 |
|
for col in range(from_col + step, to_col, step): |
|
if state.board[from_row, col] != EMPTY: |
|
return False |
|
else: |
|
step = 1 if to_row > from_row else -1 |
|
for row in range(from_row + step, to_row, step): |
|
if state.board[row, from_col] != EMPTY: |
|
return False |
|
if to_pos == CASTLE and piece != KING: |
|
return False |
|
if to_pos in CAMPS: |
|
if state.turn == 'WHITE' or (state.turn == 'BLACK' and from_pos not in state.black_in_camps): |
|
return False |
|
return True |
|
|
|
def get_legal_moves(state, from_pos): |
|
piece = state.board[from_pos] |
|
if not piece or (state.turn == 'WHITE' and piece not in [WHITE_SOLDIER, KING]) or \ |
|
(state.turn == 'BLACK' and piece != BLACK_SOLDIER): |
|
return [] |
|
row, col = from_pos |
|
moves = [] |
|
for r in range(BOARD_SIZE): |
|
if r != row: |
|
to_pos = (r, col) |
|
if is_valid_move(state, from_pos, to_pos): |
|
moves.append(to_pos) |
|
for c in range(BOARD_SIZE): |
|
if c != col: |
|
to_pos = (row, c) |
|
if is_valid_move(state, from_pos, to_pos): |
|
moves.append(to_pos) |
|
return moves |
|
|
|
def is_soldier_captured(state, pos, friendly): |
|
x, y = pos |
|
friendly_pieces = get_friendly_pieces(friendly) |
|
if y > 0 and y < BOARD_SIZE - 1: |
|
if state.board[x, y-1] in friendly_pieces and state.board[x, y+1] in friendly_pieces: |
|
return True |
|
if x > 0 and x < BOARD_SIZE - 1: |
|
if state.board[x-1, y] in friendly_pieces and state.board[x+1, y] in friendly_pieces: |
|
return True |
|
if is_adjacent_to_castle(pos): |
|
cx, cy = CASTLE |
|
if x == cx: |
|
if y < cy and y > 0 and state.board[x, y-1] in friendly_pieces: |
|
return True |
|
elif y > cy and y < BOARD_SIZE - 1 and state.board[x, y+1] in friendly_pieces: |
|
return True |
|
elif y == cy: |
|
if x < cx and x > 0 and state.board[x-1, y] in friendly_pieces: |
|
return True |
|
elif x > cx and x < BOARD_SIZE - 1 and state.board[x+1, y] in friendly_pieces: |
|
return True |
|
if pos in CAMPS: |
|
return False |
|
for camp in CAMPS: |
|
cx, cy = camp |
|
if (x, y) == (cx + 1, cy) and 0 <= cx < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]: |
|
return True |
|
elif (x, y) == (cx - 1, cy) and 0 <= cx < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]: |
|
return True |
|
elif (x, y) == (cx, cy + 1) and 0 <= cy < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]: |
|
return True |
|
elif (x, y) == (cx, cy - 1) and 0 <= cy < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]: |
|
return True |
|
return False |
|
|
|
def is_king_captured(state, pos): |
|
x, y = pos |
|
if pos == CASTLE: |
|
return all(state.board[x + dx, y + dy] == BLACK_SOLDIER for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)] |
|
if 0 <= x + dx < BOARD_SIZE and 0 <= y + dy < BOARD_SIZE) |
|
elif is_adjacent_to_castle(pos): |
|
cx, cy = CASTLE |
|
dx = cx - x |
|
dy = cy - y |
|
free_directions = [d for d in [(-1,0), (1,0), (0,-1), (0,1)] if d != (dx, dy)] |
|
return all(state.board[x + d[0], y + d[1]] == BLACK_SOLDIER for d in free_directions |
|
if 0 <= x + d[0] < BOARD_SIZE and 0 <= y + d[1] < BOARD_SIZE) |
|
else: |
|
return is_soldier_captured(state, pos, 'BLACK') |
|
|
|
def apply_move(state, from_pos, to_pos): |
|
new_state = state.copy() |
|
piece = new_state.board[from_pos] |
|
new_state.board[to_pos] = piece |
|
new_state.board[from_pos] = EMPTY |
|
if new_state.turn == 'BLACK' and from_pos in new_state.black_in_camps and to_pos not in CAMPS: |
|
new_state.black_in_camps.discard(from_pos) |
|
captures = [] |
|
opponent = 'BLACK' if new_state.turn == 'WHITE' else 'WHITE' |
|
for x in range(BOARD_SIZE): |
|
for y in range(BOARD_SIZE): |
|
if new_state.board[x, y] == (WHITE_SOLDIER if opponent == 'WHITE' else BLACK_SOLDIER): |
|
if is_soldier_captured(new_state, (x, y), new_state.turn): |
|
captures.append((x, y)) |
|
if opponent == 'WHITE': |
|
king_pos = find_king_position(new_state) |
|
if king_pos and is_king_captured(new_state, king_pos): |
|
captures.append(king_pos) |
|
for pos in captures: |
|
new_state.board[pos] = EMPTY |
|
new_state.turn = 'BLACK' if new_state.turn == 'WHITE' else 'WHITE' |
|
board_tuple = tuple(new_state.board.flatten()) |
|
new_state.move_history.append(board_tuple) |
|
return new_state |
|
|
|
def find_king_position(state): |
|
for x in range(BOARD_SIZE): |
|
for y in range(BOARD_SIZE): |
|
if state.board[x, y] == KING: |
|
return (x, y) |
|
return None |
|
|
|
def check_game_status(state): |
|
king_pos = find_king_position(state) |
|
if king_pos is None: |
|
return "BLACK WINS" |
|
if king_pos in ESCAPES: |
|
return "WHITE WINS" |
|
pieces = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if |
|
(state.turn == 'WHITE' and state.board[x, y] in [WHITE_SOLDIER, KING]) or |
|
(state.turn == 'BLACK' and state.board[x, y] == BLACK_SOLDIER)] |
|
has_moves = any(get_legal_moves(state, pos) for pos in pieces) |
|
if not has_moves: |
|
return "BLACK WINS" if state.turn == 'WHITE' else "WHITE WINS" |
|
if state.move_history.count(tuple(state.board.flatten())) >= 2: |
|
return "DRAW" |
|
return "CONTINUE" |
|
|
|
|
|
def evaluate_state(state): |
|
status = check_game_status(state) |
|
if status == "WHITE WINS": |
|
return 1000 |
|
elif status == "BLACK WINS": |
|
return -1000 |
|
elif status == "DRAW": |
|
return 0 |
|
king_pos = find_king_position(state) |
|
if not king_pos: |
|
return -1000 |
|
min_escape_dist = min(manhattan_distance(king_pos, e) for e in ESCAPES) |
|
white_pieces = sum(1 for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == WHITE_SOLDIER) |
|
black_pieces = sum(1 for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == BLACK_SOLDIER) |
|
if state.turn == 'WHITE': |
|
return -min_escape_dist * 10 + white_pieces * 5 - black_pieces * 3 |
|
else: |
|
return min_escape_dist * 10 - white_pieces * 3 + black_pieces * 5 |
|
|
|
def minimax(state, depth, alpha, beta, maximizing_player): |
|
if depth == 0 or check_game_status(state) != "CONTINUE": |
|
return evaluate_state(state), None |
|
if maximizing_player: |
|
max_eval = -math.inf |
|
best_move = None |
|
pieces = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == BLACK_SOLDIER] |
|
for from_pos in pieces: |
|
for to_pos in get_legal_moves(state, from_pos): |
|
new_state = apply_move(state, from_pos, to_pos) |
|
eval_score, _ = minimax(new_state, depth - 1, alpha, beta, False) |
|
if eval_score > max_eval: |
|
max_eval = eval_score |
|
best_move = (from_pos, to_pos) |
|
alpha = max(alpha, eval_score) |
|
if beta <= alpha: |
|
break |
|
return max_eval, best_move |
|
else: |
|
min_eval = math.inf |
|
best_move = None |
|
pieces = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] in [WHITE_SOLDIER, KING]] |
|
for from_pos in pieces: |
|
for to_pos in get_legal_moves(state, from_pos): |
|
new_state = apply_move(state, from_pos, to_pos) |
|
eval_score, _ = minimax(new_state, depth - 1, alpha, beta, True) |
|
if eval_score < min_eval: |
|
min_eval = eval_score |
|
best_move = (from_pos, to_pos) |
|
beta = min(beta, eval_score) |
|
if beta <= alpha: |
|
break |
|
return min_eval, best_move |
|
|
|
def ai_move(state): |
|
if state.turn != 'BLACK': |
|
return state, "Not AI's turn" |
|
depth = 3 |
|
_, move = minimax(state, depth, -math.inf, math.inf, True) |
|
if move: |
|
from_pos, to_pos = move |
|
new_state = apply_move(state, from_pos, to_pos) |
|
return new_state, f"AI moved from {pos_to_coord(from_pos)} to {pos_to_coord(to_pos)}" |
|
return state, "AI has no moves" |
|
|
|
|
|
def generate_board_image(state, selected_pos=None): |
|
img = Image.new('RGB', (BOARD_SIZE * CELL_SIZE, BOARD_SIZE * CELL_SIZE), color=COLORS['empty']) |
|
draw = ImageDraw.Draw(img) |
|
for i in range(BOARD_SIZE + 1): |
|
draw.line([(i * CELL_SIZE, 0), (i * CELL_SIZE, BOARD_SIZE * CELL_SIZE)], fill=(0,0,0), width=1) |
|
draw.line([(0, i * CELL_SIZE), (BOARD_SIZE * CELL_SIZE, i * CELL_SIZE)], fill=(0,0,0), width=1) |
|
for x in range(BOARD_SIZE): |
|
for y in range(BOARD_SIZE): |
|
pos = (x, y) |
|
fill = COLORS['empty'] |
|
if pos == CASTLE: |
|
fill = COLORS['castle'] |
|
elif pos in CAMPS: |
|
fill = COLORS['camp'] |
|
elif pos in ESCAPES: |
|
fill = COLORS['escape'] |
|
if pos == selected_pos: |
|
fill = COLORS['highlight'] |
|
draw.rectangle([(y * CELL_SIZE, x * CELL_SIZE), ((y + 1) * CELL_SIZE, (x + 1) * CELL_SIZE)], fill=fill) |
|
for x in range(BOARD_SIZE): |
|
for y in range(BOARD_SIZE): |
|
piece = state.board[x, y] |
|
if piece != EMPTY: |
|
center = (y * CELL_SIZE + CELL_SIZE // 2, x * CELL_SIZE + CELL_SIZE // 2) |
|
color = COLORS['white'] if piece == WHITE_SOLDIER else COLORS['black'] if piece == BLACK_SOLDIER else COLORS['king'] |
|
draw.ellipse([(center[0] - PIECE_RADIUS, center[1] - PIECE_RADIUS), |
|
(center[0] + PIECE_RADIUS, center[1] + PIECE_RADIUS)], fill=color, outline=(0,0,0)) |
|
for i in range(BOARD_SIZE): |
|
draw.text((5, i * CELL_SIZE + CELL_SIZE // 2 - 5), str(BOARD_SIZE - i), fill=(0,0,0)) |
|
draw.text((i * CELL_SIZE + CELL_SIZE // 2 - 5, BOARD_SIZE * CELL_SIZE - 15), chr(ord('A') + i), fill=(0,0,0)) |
|
return img |
|
|
|
|
|
def click_board(state, selected_pos, evt: gr.SelectData): |
|
if state.turn != 'WHITE': |
|
return state, "It's the AI's turn", generate_board_image(state), selected_pos |
|
y = evt.index[0] // CELL_SIZE |
|
x = evt.index[1] // CELL_SIZE |
|
pos = (y, x) |
|
if selected_pos is None: |
|
if state.board[pos] in [WHITE_SOLDIER, KING]: |
|
return state, f"Selected {pos_to_coord(pos)}", generate_board_image(state, pos), pos |
|
else: |
|
return state, "Select a White piece or King", generate_board_image(state), None |
|
else: |
|
if is_valid_move(state, selected_pos, pos): |
|
new_state = apply_move(state, selected_pos, pos) |
|
status = check_game_status(new_state) |
|
if status != "CONTINUE": |
|
return new_state, status, generate_board_image(new_state), None |
|
ai_state, ai_message = ai_move(new_state) |
|
final_status = check_game_status(ai_state) |
|
message = f"Your move to {pos_to_coord(pos)}. {ai_message}. {final_status if final_status != 'CONTINUE' else ''}" |
|
return ai_state, message, generate_board_image(ai_state), None |
|
else: |
|
return state, "Invalid move", generate_board_image(state), None |
|
|
|
def new_game(): |
|
state = TablutState() |
|
return state, "New game started. Your turn (White).", generate_board_image(state), None |
|
|
|
|
|
demo = gr.Blocks(title="Tablut Game") |
|
with demo: |
|
state = gr.State() |
|
selected_pos = gr.State(value=None) |
|
board_image = gr.Image(label="Board", type="pil") |
|
message_label = gr.Label(label="Message") |
|
new_game_button = gr.Button("New Game") |
|
board_image.select(fn=click_board, inputs=[state, selected_pos], outputs=[state, message_label, board_image, selected_pos]) |
|
new_game_button.click(fn=new_game, outputs=[state, message_label, board_image, selected_pos]) |
|
demo.load(fn=new_game, outputs=[state, message_label, board_image, selected_pos]) |
|
|
|
demo.launch() |