codelion commited on
Commit
f5a33e6
·
verified ·
1 Parent(s): e26350a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +267 -149
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
- 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:
@@ -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" # 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()
 
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