Update app.py
Browse files
app.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1 |
import gradio as gr
|
2 |
from PIL import Image, ImageDraw
|
3 |
import numpy as np
|
|
|
|
|
|
|
4 |
|
5 |
# Constants
|
6 |
BOARD_SIZE = 9
|
@@ -11,173 +14,205 @@ WHITE_SOLDIER = 1
|
|
11 |
BLACK_SOLDIER = 2
|
12 |
KING = 3
|
13 |
CASTLE = (4, 4) # Center of the 9x9 board (0-based indices)
|
14 |
-
|
15 |
-
|
16 |
-
#
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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
|
48 |
-
"""
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
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(
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
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 |
-
#
|
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 |
-
|
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 |
-
#
|
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
|
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
|
121 |
if is_adjacent_to_castle(pos):
|
122 |
cx, cy = CASTLE
|
123 |
-
if x == cx:
|
124 |
-
if y < cy and y > 0 and state.board[x, y-1] in friendly_pieces:
|
125 |
return True
|
126 |
-
elif y > cy and y < BOARD_SIZE - 1 and state.board[x, y+1] in friendly_pieces:
|
127 |
return True
|
128 |
-
elif y == cy:
|
129 |
-
if x < cx and x > 0 and state.board[x-1, y] in friendly_pieces:
|
130 |
return True
|
131 |
-
elif x > cx and x < BOARD_SIZE - 1 and state.board[x+1, y] in friendly_pieces:
|
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 |
-
|
140 |
-
|
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 |
-
|
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
|
161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
captures = []
|
163 |
-
opponent = 'BLACK' if
|
164 |
-
# Check opponent soldiers
|
165 |
for x in range(BOARD_SIZE):
|
166 |
for y in range(BOARD_SIZE):
|
167 |
-
if
|
168 |
-
if is_soldier_captured(
|
169 |
captures.append((x, y))
|
170 |
-
# Check king if opponent is White
|
171 |
if opponent == 'WHITE':
|
172 |
-
king_pos = find_king_position(
|
173 |
-
if king_pos and is_king_captured(
|
174 |
captures.append(king_pos)
|
175 |
-
# Remove captured pieces
|
176 |
for pos in captures:
|
177 |
-
|
|
|
|
|
|
|
|
|
|
|
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:
|
@@ -185,97 +220,180 @@ def find_king_position(state):
|
|
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"
|
192 |
-
|
193 |
-
return "WHITE WINS"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
194 |
else:
|
195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
|
197 |
-
def
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
for x in range(BOARD_SIZE):
|
207 |
for y in range(BOARD_SIZE):
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
231 |
|
232 |
# Gradio interface functions
|
233 |
-
def
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
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 |
-
|
253 |
-
|
254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
|
256 |
def new_game():
|
257 |
-
"""Start a new game."""
|
258 |
state = TablutState()
|
259 |
-
return state, "",
|
260 |
|
261 |
-
#
|
262 |
with gr.Blocks(title="Tablut Game") as demo:
|
263 |
state = gr.State()
|
264 |
-
|
265 |
-
|
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 |
-
|
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()
|
|
|
1 |
import gradio as gr
|
2 |
from PIL import Image, ImageDraw
|
3 |
import numpy as np
|
4 |
+
import math
|
5 |
+
from io import BytesIO
|
6 |
+
import base64
|
7 |
|
8 |
# Constants
|
9 |
BOARD_SIZE = 9
|
|
|
14 |
BLACK_SOLDIER = 2
|
15 |
KING = 3
|
16 |
CASTLE = (4, 4) # Center of the 9x9 board (0-based indices)
|
17 |
+
CAMPS = [
|
18 |
+
(0,3), (0,4), (0,5), (1,4), # Top camp
|
19 |
+
(8,3), (8,4), (8,5), (7,4), # Bottom camp
|
20 |
+
(3,0), (4,0), (5,0), (4,1), # Left camp
|
21 |
+
(3,8), (4,8), (5,8), (4,7) # Right camp
|
22 |
+
]
|
23 |
+
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]
|
24 |
+
COLORS = {
|
25 |
+
'empty': '#FFFFFF', # White for regular empty cells
|
26 |
+
'castle': '#808080', # Gray for castle
|
27 |
+
'camp': '#A0522D', # Brown for camps
|
28 |
+
'escape': '#00FF00', # Green for escape tiles
|
29 |
+
'white': '#FFFFFF', # White for white soldiers
|
30 |
+
'black': '#000000', # Black for black soldiers
|
31 |
+
'king': '#FFD700', # Gold for king
|
32 |
+
'highlight': '#FFFF00' # Yellow for selected cell
|
33 |
+
}
|
34 |
|
35 |
# Game state class
|
36 |
class TablutState:
|
37 |
def __init__(self):
|
|
|
38 |
self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
|
39 |
self.turn = 'WHITE'
|
40 |
+
self.black_in_camps = set(CAMPS) # Track black pieces in camps
|
41 |
self.setup_initial_position()
|
42 |
+
self.move_history = [] # To detect draws
|
43 |
|
44 |
def setup_initial_position(self):
|
|
|
|
|
45 |
self.board[4, 4] = KING
|
|
|
46 |
white_positions = [(3,4), (4,3), (4,5), (5,4), (2,4), (4,2), (4,6), (6,4)]
|
47 |
for pos in white_positions:
|
48 |
self.board[pos] = WHITE_SOLDIER
|
49 |
+
for pos in CAMPS:
|
|
|
|
|
|
|
50 |
self.board[pos] = BLACK_SOLDIER
|
51 |
|
52 |
+
def copy(self):
|
53 |
+
new_state = TablutState()
|
54 |
+
new_state.board = self.board.copy()
|
55 |
+
new_state.turn = self.turn
|
56 |
+
new_state.black_in_camps = self.black_in_camps.copy()
|
57 |
+
new_state.move_history = self.move_history.copy()
|
58 |
+
return new_state
|
59 |
+
|
60 |
# Utility functions
|
61 |
+
def pos_to_coord(pos):
|
62 |
+
"""Convert (row, col) to board coordinate (e.g., (4,4) -> 'E5')."""
|
63 |
+
row, col = pos
|
64 |
+
return f"{chr(ord('A') + col)}{row + 1}"
|
65 |
+
|
66 |
+
def coord_to_pos(coord):
|
67 |
+
"""Convert board coordinate (e.g., 'E5') to (row, col)."""
|
68 |
+
col = ord(coord[0].upper()) - ord('A')
|
69 |
+
row = int(coord[1]) - 1
|
|
|
|
|
|
|
70 |
return (row, col)
|
71 |
|
72 |
def is_adjacent_to_castle(pos):
|
|
|
73 |
x, y = pos
|
74 |
cx, cy = CASTLE
|
75 |
return (abs(x - cx) == 1 and y == cy) or (abs(y - cy) == 1 and x == cx)
|
76 |
|
77 |
+
def get_friendly_pieces(turn):
|
78 |
+
return [WHITE_SOLDIER, KING] if turn == 'WHITE' else [BLACK_SOLDIER]
|
79 |
+
|
80 |
+
def manhattan_distance(pos1, pos2):
|
81 |
+
return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
|
|
|
82 |
|
83 |
# Game logic functions
|
84 |
def is_valid_move(state, from_pos, to_pos):
|
|
|
85 |
if from_pos == to_pos:
|
86 |
return False
|
87 |
piece = state.board[from_pos]
|
|
|
88 |
if state.turn == 'WHITE' and piece not in [WHITE_SOLDIER, KING]:
|
89 |
return False
|
90 |
if state.turn == 'BLACK' and piece != BLACK_SOLDIER:
|
91 |
return False
|
|
|
92 |
if state.board[to_pos] != EMPTY:
|
93 |
return False
|
|
|
94 |
from_row, from_col = from_pos
|
95 |
to_row, to_col = to_pos
|
96 |
if from_row != to_row and from_col != to_col:
|
97 |
return False
|
98 |
+
# Path must be clear
|
99 |
if from_row == to_row:
|
100 |
step = 1 if to_col > from_col else -1
|
101 |
for col in range(from_col + step, to_col, step):
|
102 |
if state.board[from_row, col] != EMPTY:
|
103 |
return False
|
104 |
+
else:
|
105 |
step = 1 if to_row > from_row else -1
|
106 |
for row in range(from_row + step, to_row, step):
|
107 |
if state.board[row, from_col] != EMPTY:
|
108 |
return False
|
109 |
+
# Castle is only for the king
|
110 |
if to_pos == CASTLE and piece != KING:
|
111 |
return False
|
112 |
+
# Camp restrictions
|
113 |
+
if to_pos in CAMPS:
|
114 |
+
if state.turn == 'WHITE' or (state.turn == 'BLACK' and from_pos not in state.black_in_camps):
|
115 |
+
return False
|
116 |
return True
|
117 |
|
118 |
+
def get_legal_moves(state, from_pos):
|
119 |
+
piece = state.board[from_pos]
|
120 |
+
if not piece or (state.turn == 'WHITE' and piece not in [WHITE_SOLDIER, KING]) or \
|
121 |
+
(state.turn == 'BLACK' and piece != BLACK_SOLDIER):
|
122 |
+
return []
|
123 |
+
row, col = from_pos
|
124 |
+
moves = []
|
125 |
+
for r in range(BOARD_SIZE):
|
126 |
+
if r != row:
|
127 |
+
to_pos = (r, col)
|
128 |
+
if is_valid_move(state, from_pos, to_pos):
|
129 |
+
moves.append(to_pos)
|
130 |
+
for c in range(BOARD_SIZE):
|
131 |
+
if c != col:
|
132 |
+
to_pos = (row, c)
|
133 |
+
if is_valid_move(state, from_pos, to_pos):
|
134 |
+
moves.append(to_pos)
|
135 |
+
return moves
|
136 |
+
|
137 |
def is_soldier_captured(state, pos, friendly):
|
|
|
138 |
x, y = pos
|
139 |
friendly_pieces = get_friendly_pieces(friendly)
|
140 |
+
# Standard capture
|
141 |
if y > 0 and y < BOARD_SIZE - 1:
|
142 |
if state.board[x, y-1] in friendly_pieces and state.board[x, y+1] in friendly_pieces:
|
143 |
return True
|
144 |
if x > 0 and x < BOARD_SIZE - 1:
|
145 |
if state.board[x-1, y] in friendly_pieces and state.board[x+1, y] in friendly_pieces:
|
146 |
return True
|
147 |
+
# Capture against castle or camp
|
148 |
if is_adjacent_to_castle(pos):
|
149 |
cx, cy = CASTLE
|
150 |
+
if x == cx:
|
151 |
+
if y < cy and y > 0 and state.board[x, y-1] in friendly_pieces:
|
152 |
return True
|
153 |
+
elif y > cy and y < BOARD_SIZE - 1 and state.board[x, y+1] in friendly_pieces:
|
154 |
return True
|
155 |
+
elif y == cy:
|
156 |
+
if x < cx and x > 0 and state.board[x-1, y] in friendly_pieces:
|
157 |
return True
|
158 |
+
elif x > cx and x < BOARD_SIZE - 1 and state.board[x+1, y] in friendly_pieces:
|
159 |
return True
|
160 |
+
if pos in CAMPS:
|
161 |
+
return False # Cannot capture pieces in camps
|
162 |
+
for camp in CAMPS:
|
163 |
+
if pos == (camp[0] + 1, camp[1]) and state.board[camp] in friendly_pieces + [EMPTY]:
|
164 |
+
return False
|
165 |
+
elif pos == (camp[0] - 1, camp[1]) and state.board[camp] in friendly_pieces + [EMPTY]:
|
166 |
+
return False
|
167 |
+
elif pos == (camp[0], camp[1] + 1) and state.board[camp] in friendly_pieces + [EMPTY]:
|
168 |
+
return False
|
169 |
+
elif pos == (camp[0], camp[1] - 1) and state.board[camp] in friendly_pieces + [EMPTY]:
|
170 |
+
return False
|
171 |
return False
|
172 |
|
173 |
def is_king_captured(state, pos):
|
|
|
174 |
x, y = pos
|
175 |
if pos == CASTLE:
|
176 |
+
return all(state.board[x + dx, y + dy] == BLACK_SOLDIER for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]
|
177 |
+
if 0 <= x + dx < BOARD_SIZE and 0 <= y + dy < BOARD_SIZE)
|
|
|
|
|
|
|
|
|
178 |
elif is_adjacent_to_castle(pos):
|
|
|
179 |
cx, cy = CASTLE
|
180 |
dx = cx - x
|
181 |
dy = cy - y
|
182 |
free_directions = [d for d in [(-1,0), (1,0), (0,-1), (0,1)] if d != (dx, dy)]
|
183 |
+
return all(state.board[x + d[0], y + d[1]] == BLACK_SOLDIER for d in free_directions
|
184 |
+
if 0 <= x + d[0] < BOARD_SIZE and 0 <= y + d[1] < BOARD_SIZE)
|
|
|
|
|
|
|
185 |
else:
|
|
|
186 |
return is_soldier_captured(state, pos, 'BLACK')
|
187 |
|
188 |
+
def apply_move(state, from_pos, to_pos):
|
189 |
+
new_state = state.copy()
|
190 |
+
piece = new_state.board[from_pos]
|
191 |
+
new_state.board[to_pos] = piece
|
192 |
+
new_state.board[from_pos] = EMPTY
|
193 |
+
if new_state.turn == 'BLACK' and from_pos in new_state.black_in_camps and to_pos not in CAMPS:
|
194 |
+
new_state.black_in_camps.discard(from_pos)
|
195 |
+
# Apply captures
|
196 |
captures = []
|
197 |
+
opponent = 'BLACK' if new_state.turn == 'WHITE' else 'WHITE'
|
|
|
198 |
for x in range(BOARD_SIZE):
|
199 |
for y in range(BOARD_SIZE):
|
200 |
+
if new_state.board[x, y] == (WHITE_SOLDIER if opponent == 'WHITE' else BLACK_SOLDIER):
|
201 |
+
if is_soldier_captured(new_state, (x, y), new_state.turn):
|
202 |
captures.append((x, y))
|
|
|
203 |
if opponent == 'WHITE':
|
204 |
+
king_pos = find_king_position(new_state)
|
205 |
+
if king_pos and is_king_captured(new_state, king_pos):
|
206 |
captures.append(king_pos)
|
|
|
207 |
for pos in captures:
|
208 |
+
new_state.board[pos] = EMPTY
|
209 |
+
new_state.turn = 'BLACK' if new_state.turn == 'WHITE' else 'WHITE'
|
210 |
+
# Update move history for draw detection
|
211 |
+
board_tuple = tuple(new_state.board.flatten())
|
212 |
+
new_state.move_history.append(board_tuple)
|
213 |
+
return new_state
|
214 |
|
215 |
def find_king_position(state):
|
|
|
216 |
for x in range(BOARD_SIZE):
|
217 |
for y in range(BOARD_SIZE):
|
218 |
if state.board[x, y] == KING:
|
|
|
220 |
return None
|
221 |
|
222 |
def check_game_status(state):
|
|
|
223 |
king_pos = find_king_position(state)
|
224 |
if king_pos is None:
|
225 |
+
return "BLACK WINS"
|
226 |
+
if king_pos in ESCAPES:
|
227 |
+
return "WHITE WINS"
|
228 |
+
# Check for no legal moves
|
229 |
+
pieces = []
|
230 |
+
for x in range(BOARD_SIZE):
|
231 |
+
for y in range(BOARD_SIZE):
|
232 |
+
if (state.turn == 'WHITE' and state.board[x, y] in [WHITE_SOLDIER, KING]) or \
|
233 |
+
(state.turn == 'BLACK' and state.board[x, y] == BLACK_SOLDIER):
|
234 |
+
pieces.append((x, y))
|
235 |
+
has_moves = False
|
236 |
+
for pos in pieces:
|
237 |
+
if get_legal_moves(state, pos):
|
238 |
+
has_moves = True
|
239 |
+
break
|
240 |
+
if not has_moves:
|
241 |
+
return "BLACK WINS" if state.turn == 'WHITE' else "WHITE WINS"
|
242 |
+
# Check for draw (same state twice)
|
243 |
+
board_tuple = tuple(state.board.flatten())
|
244 |
+
if state.move_history.count(board_tuple) >= 2:
|
245 |
+
return "DRAW"
|
246 |
+
return "CONTINUE"
|
247 |
+
|
248 |
+
# AI implementation
|
249 |
+
def evaluate_state(state):
|
250 |
+
status = check_game_status(state)
|
251 |
+
if status == "WHITE WINS":
|
252 |
+
return 1000
|
253 |
+
elif status == "BLACK WINS":
|
254 |
+
return -1000
|
255 |
+
elif status == "DRAW":
|
256 |
+
return 0
|
257 |
+
king_pos = find_king_position(state)
|
258 |
+
if not king_pos:
|
259 |
+
return -1000
|
260 |
+
# Heuristic: distance from king to nearest escape
|
261 |
+
min_escape_dist = min(manhattan_distance(king_pos, e) for e in ESCAPES)
|
262 |
+
white_pieces = sum(1 for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == WHITE_SOLDIER)
|
263 |
+
black_pieces = sum(1 for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == BLACK_SOLDIER)
|
264 |
+
# Encourage Black to surround king, White to escape
|
265 |
+
if state.turn == 'WHITE':
|
266 |
+
return -min_escape_dist * 10 + white_pieces * 5 - black_pieces * 3
|
267 |
+
else:
|
268 |
+
return min_escape_dist * 10 - white_pieces * 3 + black_pieces * 5
|
269 |
+
|
270 |
+
def minimax(state, depth, alpha, beta, maximizing_player):
|
271 |
+
if depth == 0 or check_game_status(state) != "CONTINUE":
|
272 |
+
return evaluate_state(state), None
|
273 |
+
if maximizing_player:
|
274 |
+
max_eval = -math.inf
|
275 |
+
best_move = None
|
276 |
+
pieces = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == BLACK_SOLDIER]
|
277 |
+
for from_pos in pieces:
|
278 |
+
for to_pos in get_legal_moves(state, from_pos):
|
279 |
+
new_state = apply_move(state, from_pos, to_pos)
|
280 |
+
eval_score, _ = minimax(new_state, depth - 1, alpha, beta, False)
|
281 |
+
if eval_score > max_eval:
|
282 |
+
max_eval = eval_score
|
283 |
+
best_move = (from_pos, to_pos)
|
284 |
+
alpha = max(alpha, eval_score)
|
285 |
+
if beta <= alpha:
|
286 |
+
break
|
287 |
+
return max_eval, best_move
|
288 |
else:
|
289 |
+
min_eval = math.inf
|
290 |
+
best_move = None
|
291 |
+
pieces = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] in [WHITE_SOLDIER, KING]]
|
292 |
+
for from_pos in pieces:
|
293 |
+
for to_pos in get_legal_moves(state, from_pos):
|
294 |
+
new_state = apply_move(state, from_pos, to_pos)
|
295 |
+
eval_score, _ = minimax(new_state, depth - 1, alpha, beta, True)
|
296 |
+
if eval_score < min_eval:
|
297 |
+
min_eval = eval_score
|
298 |
+
best_move = (from_pos, to_pos)
|
299 |
+
beta = min(beta, eval_score)
|
300 |
+
if beta <= alpha:
|
301 |
+
break
|
302 |
+
return min_eval, best_move
|
303 |
|
304 |
+
def ai_move(state):
|
305 |
+
if state.turn != 'BLACK':
|
306 |
+
return state, "Not AI's turn", None
|
307 |
+
depth = 3 # Adjustable for performance
|
308 |
+
_, move = minimax(state, depth, -math.inf, math.inf, True)
|
309 |
+
if move:
|
310 |
+
from_pos, to_pos = move
|
311 |
+
new_state = apply_move(state, from_pos, to_pos)
|
312 |
+
return new_state, f"AI moved from {pos_to_coord(from_pos)} to {pos_to_coord(to_pos)}", None
|
313 |
+
return state, "AI has no moves", None
|
314 |
+
|
315 |
+
# SVG board generation
|
316 |
+
def generate_board_svg(state, selected_pos=None):
|
317 |
+
width = BOARD_SIZE * CELL_SIZE
|
318 |
+
height = BOARD_SIZE * CELL_SIZE
|
319 |
+
svg = [f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">']
|
320 |
+
# Draw cells
|
321 |
for x in range(BOARD_SIZE):
|
322 |
for y in range(BOARD_SIZE):
|
323 |
+
pos = (x, y)
|
324 |
+
fill = COLORS['empty']
|
325 |
+
if pos == CASTLE:
|
326 |
+
fill = COLORS['castle']
|
327 |
+
elif pos in CAMPS:
|
328 |
+
fill = COLORS['camp']
|
329 |
+
elif pos in ESCAPES:
|
330 |
+
fill = COLORS['escape']
|
331 |
+
if pos == selected_pos:
|
332 |
+
fill = COLORS['highlight']
|
333 |
+
svg.append(f'<rect x="{y * CELL_SIZE}" y="{x * CELL_SIZE}" width="{CELL_SIZE}" height="{CELL_SIZE}" fill="{fill}" stroke="black" stroke-width="1"/>')
|
334 |
# Draw pieces
|
335 |
for x in range(BOARD_SIZE):
|
336 |
for y in range(BOARD_SIZE):
|
337 |
piece = state.board[x, y]
|
338 |
if piece != EMPTY:
|
339 |
+
cx = y * CELL_SIZE + CELL_SIZE // 2
|
340 |
+
cy = x * CELL_SIZE + CELL_SIZE // 2
|
341 |
+
color = COLORS['white'] if piece == WHITE_SOLDIER else COLORS['black'] if piece == BLACK_SOLDIER else COLORS['king']
|
342 |
+
svg.append(f'<circle cx="{cx}" cy="{cy}" r="{PIECE_RADIUS}" fill="{color}" stroke="black" stroke-width="1"/>')
|
343 |
+
# Draw grid labels
|
344 |
+
for i in range(BOARD_SIZE):
|
345 |
+
svg.append(f'<text x="5" y="{i * CELL_SIZE + CELL_SIZE // 2 + 5}" fill="black">{BOARD_SIZE - i}</text>')
|
346 |
+
svg.append(f'<text x="{i * CELL_SIZE + CELL_SIZE // 2 - 5}" y="{height - 10}" fill="black">{chr(ord("A") + i)}</text>')
|
347 |
+
svg.append('</svg>')
|
348 |
+
return ''.join(svg)
|
349 |
+
|
350 |
+
def svg_to_image(svg_content):
|
351 |
+
img = Image.new('RGB', (BOARD_SIZE * CELL_SIZE, BOARD_SIZE * CELL_SIZE), color='white')
|
352 |
+
# For Gradio, we encode SVG as base64 to display in HTML
|
353 |
+
svg_bytes = svg_content.encode('utf-8')
|
354 |
+
svg_base64 = base64.b64encode(svg_bytes).decode('utf-8')
|
355 |
+
return svg_base64
|
356 |
|
357 |
# Gradio interface functions
|
358 |
+
def click_board(state, selected_pos, evt: gr.SelectData):
|
359 |
+
if state.turn != 'WHITE':
|
360 |
+
return state, "It's the AI's turn", None, selected_pos
|
361 |
+
x = evt.index[1] // CELL_SIZE
|
362 |
+
y = evt.index[0] // CELL_SIZE
|
363 |
+
pos = (x, y)
|
364 |
+
if selected_pos is None:
|
365 |
+
# Select a piece
|
366 |
+
if state.board[pos] in [WHITE_SOLDIER, KING]:
|
367 |
+
return state, f"Selected {pos_to_coord(pos)}", None, pos
|
368 |
+
else:
|
369 |
+
return state, "Select a White piece or King", None, None
|
|
|
|
|
|
|
|
|
|
|
|
|
370 |
else:
|
371 |
+
# Try to move
|
372 |
+
if is_valid_move(state, selected_pos, pos):
|
373 |
+
new_state = apply_move(state, selected_pos, pos)
|
374 |
+
status = check_game_status(new_state)
|
375 |
+
if status != "CONTINUE":
|
376 |
+
return new_state, status, None, None
|
377 |
+
# Trigger AI move
|
378 |
+
ai_state, ai_message, _ = ai_move(new_state)
|
379 |
+
final_status = check_game_status(ai_state)
|
380 |
+
return ai_state, f"Your move to {pos_to_coord(pos)}. {ai_message}. {final_status if final_status != 'CONTINUE' else ''}", None, None
|
381 |
+
else:
|
382 |
+
return state, "Invalid move", None, None
|
383 |
|
384 |
def new_game():
|
|
|
385 |
state = TablutState()
|
386 |
+
return state, "New game started. Your turn (White).", generate_board_svg(state), None
|
387 |
|
388 |
+
# Gradio interface
|
389 |
with gr.Blocks(title="Tablut Game") as demo:
|
390 |
state = gr.State()
|
391 |
+
selected_pos = gr.State(value=None)
|
392 |
+
board_html = gr.HTML(label="Board")
|
393 |
message_label = gr.Label(label="Message")
|
|
|
|
|
|
|
|
|
394 |
new_game_button = gr.Button("New Game")
|
395 |
+
board_html.select(fn=click_board, inputs=[state, selected_pos], outputs=[state, message_label, board_html, selected_pos])
|
396 |
+
new_game_button.click(fn=new_game, outputs=[state, message_label, board_html, selected_pos])
|
397 |
+
demo.load(fn=new_game, outputs=[state, message_label, board_html, selected_pos])
|
398 |
|
399 |
+
# Note: demo.launch() is not needed for HF Spaces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|