tablut / app.py
codelion's picture
Update app.py
070fae0 verified
import gradio as gr
from PIL import Image, ImageDraw
import numpy as np
import math
# Constants
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), # Top camp
(8,3), (8,4), (8,5), (7,4), # Bottom camp
(3,0), (4,0), (5,0), (4,1), # Left camp
(3,8), (4,8), (5,8), (4,7) # Right camp
]
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)
}
# Game state class
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
# Utility functions
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])
# Game logic functions
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"
# AI implementation
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"
# Board visualization
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
# Gradio interface functions
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 # Image coordinates (x, y) map to board (row, col)
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
# Define Gradio interface
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()