Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from PIL import Image, ImageDraw
|
3 |
+
import numpy as np
|
4 |
+
|
5 |
+
# Constants
|
6 |
+
BOARD_SIZE = 9
|
7 |
+
CELL_SIZE = 50
|
8 |
+
PIECE_RADIUS = 20
|
9 |
+
EMPTY = 0
|
10 |
+
WHITE_SOLDIER = 1
|
11 |
+
BLACK_SOLDIER = 2
|
12 |
+
KING = 3
|
13 |
+
CASTLE = (4, 4) # Center of the 9x9 board (0-based indices)
|
14 |
+
ESCAPES = [(i, j) for i in range(BOARD_SIZE) for j in range(BOARD_SIZE) if i == 0 or i == BOARD_SIZE-1 or j == 0 or j == BOARD_SIZE-1]
|
15 |
+
|
16 |
+
# Colors for visualization
|
17 |
+
COLOR_EMPTY = (255, 255, 255) # White for regular empty cells
|
18 |
+
COLOR_CASTLE = (150, 150, 150) # Gray for the castle
|
19 |
+
COLOR_ESCAPE = (0, 255, 0) # Green for escape tiles
|
20 |
+
COLOR_WHITE = (255, 255, 255) # White for white soldiers
|
21 |
+
COLOR_BLACK = (0, 0, 0) # Black for black soldiers
|
22 |
+
COLOR_KING = (255, 215, 0) # Gold for the king
|
23 |
+
|
24 |
+
# Game state class
|
25 |
+
class TablutState:
|
26 |
+
def __init__(self):
|
27 |
+
"""Initialize the game state with an empty board and set White as the starting player."""
|
28 |
+
self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
|
29 |
+
self.turn = 'WHITE'
|
30 |
+
self.setup_initial_position()
|
31 |
+
|
32 |
+
def setup_initial_position(self):
|
33 |
+
"""Set up the initial board configuration for Tablut."""
|
34 |
+
# Place the king at the center (castle)
|
35 |
+
self.board[4, 4] = KING
|
36 |
+
# Place white soldiers around the king in a cross pattern
|
37 |
+
white_positions = [(3,4), (4,3), (4,5), (5,4), (2,4), (4,2), (4,6), (6,4)]
|
38 |
+
for pos in white_positions:
|
39 |
+
self.board[pos] = WHITE_SOLDIER
|
40 |
+
# Place black soldiers in groups at the edges
|
41 |
+
black_positions = [(0,3), (0,4), (0,5), (1,4), (8,3), (8,4), (8,5), (7,4),
|
42 |
+
(3,0), (4,0), (5,0), (4,1), (3,8), (4,8), (5,8), (4,7)]
|
43 |
+
for pos in black_positions:
|
44 |
+
self.board[pos] = BLACK_SOLDIER
|
45 |
+
|
46 |
+
# Utility functions
|
47 |
+
def parse_position(s):
|
48 |
+
"""
|
49 |
+
Convert a position string (e.g., 'E5') to board coordinates (row, col).
|
50 |
+
A1 is top-left (0,0), I9 is bottom-right (8,8).
|
51 |
+
"""
|
52 |
+
s = s.strip().upper()
|
53 |
+
if len(s) != 2 or not s[0].isalpha() or not s[1].isdigit():
|
54 |
+
raise ValueError("Position must be a letter (A-I) followed by a number (1-9)")
|
55 |
+
col = ord(s[0]) - ord('A')
|
56 |
+
row = int(s[1]) - 1
|
57 |
+
if not (0 <= col < BOARD_SIZE and 0 <= row < BOARD_SIZE):
|
58 |
+
raise ValueError("Position out of bounds")
|
59 |
+
return (row, col)
|
60 |
+
|
61 |
+
def is_adjacent_to_castle(pos):
|
62 |
+
"""Check if a position is orthogonally adjacent to the castle."""
|
63 |
+
x, y = pos
|
64 |
+
cx, cy = CASTLE
|
65 |
+
return (abs(x - cx) == 1 and y == cy) or (abs(y - cy) == 1 and x == cx)
|
66 |
+
|
67 |
+
def get_friendly_pieces(friendly):
|
68 |
+
"""Return the piece types that can capture for the current player."""
|
69 |
+
if friendly == 'WHITE':
|
70 |
+
return [WHITE_SOLDIER, KING] # Both white soldiers and king can capture
|
71 |
+
else:
|
72 |
+
return [BLACK_SOLDIER] # Only black soldiers can capture
|
73 |
+
|
74 |
+
# Game logic functions
|
75 |
+
def is_valid_move(state, from_pos, to_pos):
|
76 |
+
"""Validate if a move from from_pos to to_pos is legal."""
|
77 |
+
if from_pos == to_pos:
|
78 |
+
return False
|
79 |
+
piece = state.board[from_pos]
|
80 |
+
# Check if the piece belongs to the current player
|
81 |
+
if state.turn == 'WHITE' and piece not in [WHITE_SOLDIER, KING]:
|
82 |
+
return False
|
83 |
+
if state.turn == 'BLACK' and piece != BLACK_SOLDIER:
|
84 |
+
return False
|
85 |
+
# Check if destination is empty
|
86 |
+
if state.board[to_pos] != EMPTY:
|
87 |
+
return False
|
88 |
+
# Check if move is orthogonal (like a rook)
|
89 |
+
from_row, from_col = from_pos
|
90 |
+
to_row, to_col = to_pos
|
91 |
+
if from_row != to_row and from_col != to_col:
|
92 |
+
return False
|
93 |
+
# Check if the path is clear
|
94 |
+
if from_row == to_row:
|
95 |
+
step = 1 if to_col > from_col else -1
|
96 |
+
for col in range(from_col + step, to_col, step):
|
97 |
+
if state.board[from_row, col] != EMPTY:
|
98 |
+
return False
|
99 |
+
elif from_col == to_col:
|
100 |
+
step = 1 if to_row > from_row else -1
|
101 |
+
for row in range(from_row + step, to_row, step):
|
102 |
+
if state.board[row, from_col] != EMPTY:
|
103 |
+
return False
|
104 |
+
# Only the king can move to the castle
|
105 |
+
if to_pos == CASTLE and piece != KING:
|
106 |
+
return False
|
107 |
+
return True
|
108 |
+
|
109 |
+
def is_soldier_captured(state, pos, friendly):
|
110 |
+
"""Check if a soldier at pos is captured by the friendly player."""
|
111 |
+
x, y = pos
|
112 |
+
friendly_pieces = get_friendly_pieces(friendly)
|
113 |
+
# Standard capture: between two friendly pieces
|
114 |
+
if y > 0 and y < BOARD_SIZE - 1:
|
115 |
+
if state.board[x, y-1] in friendly_pieces and state.board[x, y+1] in friendly_pieces:
|
116 |
+
return True
|
117 |
+
if x > 0 and x < BOARD_SIZE - 1:
|
118 |
+
if state.board[x-1, y] in friendly_pieces and state.board[x+1, y] in friendly_pieces:
|
119 |
+
return True
|
120 |
+
# Capture against the castle
|
121 |
+
if is_adjacent_to_castle(pos):
|
122 |
+
cx, cy = CASTLE
|
123 |
+
if x == cx: # Same row as castle
|
124 |
+
if y < cy and y > 0 and state.board[x, y-1] in friendly_pieces: # Left of castle
|
125 |
+
return True
|
126 |
+
elif y > cy and y < BOARD_SIZE - 1 and state.board[x, y+1] in friendly_pieces: # Right of castle
|
127 |
+
return True
|
128 |
+
elif y == cy: # Same column as castle
|
129 |
+
if x < cx and x > 0 and state.board[x-1, y] in friendly_pieces: # Above castle
|
130 |
+
return True
|
131 |
+
elif x > cx and x < BOARD_SIZE - 1 and state.board[x+1, y] in friendly_pieces: # Below castle
|
132 |
+
return True
|
133 |
+
return False
|
134 |
+
|
135 |
+
def is_king_captured(state, pos):
|
136 |
+
"""Check if the king at pos is captured by Black."""
|
137 |
+
x, y = pos
|
138 |
+
if pos == CASTLE:
|
139 |
+
# King in castle: must be surrounded on all four sides by black soldiers
|
140 |
+
adjacent = [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]
|
141 |
+
for adj in adjacent:
|
142 |
+
if not (0 <= adj[0] < BOARD_SIZE and 0 <= adj[1] < BOARD_SIZE and state.board[adj] == BLACK_SOLDIER):
|
143 |
+
return False
|
144 |
+
return True
|
145 |
+
elif is_adjacent_to_castle(pos):
|
146 |
+
# King adjacent to castle: must be surrounded on the three free sides
|
147 |
+
cx, cy = CASTLE
|
148 |
+
dx = cx - x
|
149 |
+
dy = cy - y
|
150 |
+
free_directions = [d for d in [(-1,0), (1,0), (0,-1), (0,1)] if d != (dx, dy)]
|
151 |
+
for d in free_directions:
|
152 |
+
check_pos = (x + d[0], y + d[1])
|
153 |
+
if not (0 <= check_pos[0] < BOARD_SIZE and 0 <= check_pos[1] < BOARD_SIZE and state.board[check_pos] == BLACK_SOLDIER):
|
154 |
+
return False
|
155 |
+
return True
|
156 |
+
else:
|
157 |
+
# King elsewhere: captured like a soldier by black pieces
|
158 |
+
return is_soldier_captured(state, pos, 'BLACK')
|
159 |
+
|
160 |
+
def apply_captures(state):
|
161 |
+
"""Apply capture rules after a move."""
|
162 |
+
captures = []
|
163 |
+
opponent = 'BLACK' if state.turn == 'WHITE' else 'WHITE'
|
164 |
+
# Check opponent soldiers
|
165 |
+
for x in range(BOARD_SIZE):
|
166 |
+
for y in range(BOARD_SIZE):
|
167 |
+
if state.board[x, y] == (WHITE_SOLDIER if opponent == 'WHITE' else BLACK_SOLDIER):
|
168 |
+
if is_soldier_captured(state, (x, y), state.turn):
|
169 |
+
captures.append((x, y))
|
170 |
+
# Check king if opponent is White
|
171 |
+
if opponent == 'WHITE':
|
172 |
+
king_pos = find_king_position(state)
|
173 |
+
if king_pos and is_king_captured(state, king_pos):
|
174 |
+
captures.append(king_pos)
|
175 |
+
# Remove captured pieces
|
176 |
+
for pos in captures:
|
177 |
+
state.board[pos] = EMPTY
|
178 |
+
|
179 |
+
def find_king_position(state):
|
180 |
+
"""Find the current position of the king on the board."""
|
181 |
+
for x in range(BOARD_SIZE):
|
182 |
+
for y in range(BOARD_SIZE):
|
183 |
+
if state.board[x, y] == KING:
|
184 |
+
return (x, y)
|
185 |
+
return None
|
186 |
+
|
187 |
+
def check_game_status(state):
|
188 |
+
"""Check if the game has ended and determine the winner."""
|
189 |
+
king_pos = find_king_position(state)
|
190 |
+
if king_pos is None:
|
191 |
+
return "BLACK WINS" # King captured
|
192 |
+
elif king_pos in ESCAPES:
|
193 |
+
return "WHITE WINS" # King reached an escape tile
|
194 |
+
else:
|
195 |
+
return "CONTINUE"
|
196 |
+
|
197 |
+
def generate_board_image(state):
|
198 |
+
"""Generate an image of the current board state."""
|
199 |
+
img = Image.new('RGB', (BOARD_SIZE * CELL_SIZE, BOARD_SIZE * CELL_SIZE), color=COLOR_EMPTY)
|
200 |
+
draw = ImageDraw.Draw(img)
|
201 |
+
# Draw grid lines
|
202 |
+
for i in range(BOARD_SIZE + 1):
|
203 |
+
draw.line([(i * CELL_SIZE, 0), (i * CELL_SIZE, BOARD_SIZE * CELL_SIZE)], fill=(0,0,0), width=1)
|
204 |
+
draw.line([(0, i * CELL_SIZE), (BOARD_SIZE * CELL_SIZE, i * CELL_SIZE)], fill=(0,0,0), width=1)
|
205 |
+
# Draw special cells
|
206 |
+
for x in range(BOARD_SIZE):
|
207 |
+
for y in range(BOARD_SIZE):
|
208 |
+
if (x, y) == CASTLE:
|
209 |
+
color = COLOR_CASTLE
|
210 |
+
elif (x, y) in ESCAPES:
|
211 |
+
color = COLOR_ESCAPE
|
212 |
+
else:
|
213 |
+
color = COLOR_EMPTY
|
214 |
+
draw.rectangle([(y * CELL_SIZE, x * CELL_SIZE), ((y + 1) * CELL_SIZE, (x + 1) * CELL_SIZE)], fill=color)
|
215 |
+
# Draw pieces
|
216 |
+
for x in range(BOARD_SIZE):
|
217 |
+
for y in range(BOARD_SIZE):
|
218 |
+
piece = state.board[x, y]
|
219 |
+
if piece != EMPTY:
|
220 |
+
center = (y * CELL_SIZE + CELL_SIZE // 2, x * CELL_SIZE + CELL_SIZE // 2)
|
221 |
+
if piece == WHITE_SOLDIER:
|
222 |
+
draw.ellipse([(center[0] - PIECE_RADIUS, center[1] - PIECE_RADIUS),
|
223 |
+
(center[0] + PIECE_RADIUS, center[1] + PIECE_RADIUS)], fill=COLOR_WHITE)
|
224 |
+
elif piece == BLACK_SOLDIER:
|
225 |
+
draw.ellipse([(center[0] - PIECE_RADIUS, center[1] - PIECE_RADIUS),
|
226 |
+
(center[0] + PIECE_RADIUS, center[1] + PIECE_RADIUS)], fill=COLOR_BLACK)
|
227 |
+
elif piece == KING:
|
228 |
+
draw.ellipse([(center[0] - PIECE_RADIUS, center[1] - PIECE_RADIUS),
|
229 |
+
(center[0] + PIECE_RADIUS, center[1] + PIECE_RADIUS)], fill=COLOR_KING)
|
230 |
+
return img
|
231 |
+
|
232 |
+
# Gradio interface functions
|
233 |
+
def make_move(state, from_str, to_str):
|
234 |
+
"""Process a player's move."""
|
235 |
+
try:
|
236 |
+
from_pos = parse_position(from_str)
|
237 |
+
to_pos = parse_position(to_str)
|
238 |
+
except ValueError as e:
|
239 |
+
return state, str(e), generate_board_image(state), state.turn
|
240 |
+
if not is_valid_move(state, from_pos, to_pos):
|
241 |
+
return state, "Invalid move", generate_board_image(state), state.turn
|
242 |
+
# Apply the move
|
243 |
+
state.board[to_pos] = state.board[from_pos]
|
244 |
+
state.board[from_pos] = EMPTY
|
245 |
+
# Apply captures
|
246 |
+
apply_captures(state)
|
247 |
+
# Check game status
|
248 |
+
status = check_game_status(state)
|
249 |
+
if status != "CONTINUE":
|
250 |
+
message = status
|
251 |
+
else:
|
252 |
+
message = ""
|
253 |
+
state.turn = 'BLACK' if state.turn == 'WHITE' else 'WHITE'
|
254 |
+
return state, message, generate_board_image(state), state.turn
|
255 |
+
|
256 |
+
def new_game():
|
257 |
+
"""Start a new game."""
|
258 |
+
state = TablutState()
|
259 |
+
return state, "", generate_board_image(state), state.turn
|
260 |
+
|
261 |
+
# Set up the Gradio interface
|
262 |
+
with gr.Blocks(title="Tablut Game") as demo:
|
263 |
+
state = gr.State()
|
264 |
+
board_image = gr.Image(label="Board", type="pil")
|
265 |
+
turn_label = gr.Label(label="Turn")
|
266 |
+
message_label = gr.Label(label="Message")
|
267 |
+
with gr.Row():
|
268 |
+
from_input = gr.Textbox(label="From (e.g., E5)", placeholder="Enter starting position")
|
269 |
+
to_input = gr.Textbox(label="To (e.g., E6)", placeholder="Enter destination")
|
270 |
+
submit_button = gr.Button("Make Move")
|
271 |
+
new_game_button = gr.Button("New Game")
|
272 |
+
|
273 |
+
# Connect functions to buttons
|
274 |
+
submit_button.click(fn=make_move, inputs=[state, from_input, to_input],
|
275 |
+
outputs=[state, message_label, board_image, turn_label])
|
276 |
+
new_game_button.click(fn=new_game, outputs=[state, message_label, board_image, turn_label])
|
277 |
+
# Initialize the game on load
|
278 |
+
demo.load(fn=new_game, outputs=[state, message_label, board_image, turn_label])
|
279 |
+
|
280 |
+
# Launch the app
|
281 |
+
demo.launch()
|