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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +58 -96
app.py CHANGED
@@ -2,8 +2,6 @@ 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
@@ -13,7 +11,7 @@ EMPTY = 0
13
  WHITE_SOLDIER = 1
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
@@ -22,14 +20,14 @@ CAMPS = [
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
@@ -37,9 +35,9 @@ 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
@@ -59,16 +57,9 @@ class TablutState:
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
@@ -95,7 +86,6 @@ def is_valid_move(state, from_pos, to_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):
@@ -106,10 +96,8 @@ def is_valid_move(state, from_pos, to_pos):
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
@@ -137,14 +125,12 @@ def get_legal_moves(state, from_pos):
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:
@@ -158,16 +144,17 @@ def is_soldier_captured(state, pos, friendly):
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):
@@ -192,7 +179,6 @@ def apply_move(state, from_pos, to_pos):
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):
@@ -207,7 +193,6 @@ def apply_move(state, from_pos, to_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
@@ -225,23 +210,13 @@ def check_game_status(state):
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
 
@@ -257,11 +232,9 @@ def evaluate_state(state):
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:
@@ -303,21 +276,22 @@ def minimax(state, depth, alpha, beta, maximizing_player):
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)
@@ -330,70 +304,58 @@ def generate_board_svg(state, selected_pos=None):
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
 
2
  from PIL import Image, ImageDraw
3
  import numpy as np
4
  import math
 
 
5
 
6
  # Constants
7
  BOARD_SIZE = 9
 
11
  WHITE_SOLDIER = 1
12
  BLACK_SOLDIER = 2
13
  KING = 3
14
+ CASTLE = (4, 4)
15
  CAMPS = [
16
  (0,3), (0,4), (0,5), (1,4), # Top camp
17
  (8,3), (8,4), (8,5), (7,4), # Bottom camp
 
20
  ]
21
  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]
22
  COLORS = {
23
+ 'empty': (255, 255, 255), # White
24
+ 'castle': (128, 128, 128), # Gray
25
+ 'camp': (139, 69, 19), # Brown
26
+ 'escape': (0, 255, 0), # Green
27
+ 'white': (255, 255, 255), # White
28
+ 'black': (0, 0, 0), # Black
29
+ 'king': (255, 215, 0), # Gold
30
+ 'highlight': (255, 255, 0) # Yellow
31
  }
32
 
33
  # Game state class
 
35
  def __init__(self):
36
  self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
37
  self.turn = 'WHITE'
38
+ self.black_in_camps = set(CAMPS)
39
  self.setup_initial_position()
40
+ self.move_history = []
41
 
42
  def setup_initial_position(self):
43
  self.board[4, 4] = KING
 
57
 
58
  # Utility functions
59
  def pos_to_coord(pos):
 
60
  row, col = pos
61
  return f"{chr(ord('A') + col)}{row + 1}"
62
 
 
 
 
 
 
 
63
  def is_adjacent_to_castle(pos):
64
  x, y = pos
65
  cx, cy = CASTLE
 
86
  to_row, to_col = to_pos
87
  if from_row != to_row and from_col != to_col:
88
  return False
 
89
  if from_row == to_row:
90
  step = 1 if to_col > from_col else -1
91
  for col in range(from_col + step, to_col, step):
 
96
  for row in range(from_row + step, to_row, step):
97
  if state.board[row, from_col] != EMPTY:
98
  return False
 
99
  if to_pos == CASTLE and piece != KING:
100
  return False
 
101
  if to_pos in CAMPS:
102
  if state.turn == 'WHITE' or (state.turn == 'BLACK' and from_pos not in state.black_in_camps):
103
  return False
 
125
  def is_soldier_captured(state, pos, friendly):
126
  x, y = pos
127
  friendly_pieces = get_friendly_pieces(friendly)
 
128
  if y > 0 and y < BOARD_SIZE - 1:
129
  if state.board[x, y-1] in friendly_pieces and state.board[x, y+1] in friendly_pieces:
130
  return True
131
  if x > 0 and x < BOARD_SIZE - 1:
132
  if state.board[x-1, y] in friendly_pieces and state.board[x+1, y] in friendly_pieces:
133
  return True
 
134
  if is_adjacent_to_castle(pos):
135
  cx, cy = CASTLE
136
  if x == cx:
 
144
  elif x > cx and x < BOARD_SIZE - 1 and state.board[x+1, y] in friendly_pieces:
145
  return True
146
  if pos in CAMPS:
147
+ return False
148
  for camp in CAMPS:
149
+ cx, cy = camp
150
+ if (x, y) == (cx + 1, cy) and 0 <= cx < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]:
151
+ return True
152
+ elif (x, y) == (cx - 1, cy) and 0 <= cx < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]:
153
+ return True
154
+ elif (x, y) == (cx, cy + 1) and 0 <= cy < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]:
155
+ return True
156
+ elif (x, y) == (cx, cy - 1) and 0 <= cy < BOARD_SIZE and state.board[cx, cy] in friendly_pieces + [EMPTY]:
157
+ return True
158
  return False
159
 
160
  def is_king_captured(state, pos):
 
179
  new_state.board[from_pos] = EMPTY
180
  if new_state.turn == 'BLACK' and from_pos in new_state.black_in_camps and to_pos not in CAMPS:
181
  new_state.black_in_camps.discard(from_pos)
 
182
  captures = []
183
  opponent = 'BLACK' if new_state.turn == 'WHITE' else 'WHITE'
184
  for x in range(BOARD_SIZE):
 
193
  for pos in captures:
194
  new_state.board[pos] = EMPTY
195
  new_state.turn = 'BLACK' if new_state.turn == 'WHITE' else 'WHITE'
 
196
  board_tuple = tuple(new_state.board.flatten())
197
  new_state.move_history.append(board_tuple)
198
  return new_state
 
210
  return "BLACK WINS"
211
  if king_pos in ESCAPES:
212
  return "WHITE WINS"
213
+ pieces = [(x, y) for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if
214
+ (state.turn == 'WHITE' and state.board[x, y] in [WHITE_SOLDIER, KING]) or
215
+ (state.turn == 'BLACK' and state.board[x, y] == BLACK_SOLDIER)]
216
+ has_moves = any(get_legal_moves(state, pos) for pos in pieces)
 
 
 
 
 
 
 
 
217
  if not has_moves:
218
  return "BLACK WINS" if state.turn == 'WHITE' else "WHITE WINS"
219
+ if state.move_history.count(tuple(state.board.flatten())) >= 2:
 
 
220
  return "DRAW"
221
  return "CONTINUE"
222
 
 
232
  king_pos = find_king_position(state)
233
  if not king_pos:
234
  return -1000
 
235
  min_escape_dist = min(manhattan_distance(king_pos, e) for e in ESCAPES)
236
  white_pieces = sum(1 for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == WHITE_SOLDIER)
237
  black_pieces = sum(1 for x in range(BOARD_SIZE) for y in range(BOARD_SIZE) if state.board[x, y] == BLACK_SOLDIER)
 
238
  if state.turn == 'WHITE':
239
  return -min_escape_dist * 10 + white_pieces * 5 - black_pieces * 3
240
  else:
 
276
 
277
  def ai_move(state):
278
  if state.turn != 'BLACK':
279
+ return state, "Not AI's turn"
280
+ depth = 3
281
  _, move = minimax(state, depth, -math.inf, math.inf, True)
282
  if move:
283
  from_pos, to_pos = move
284
  new_state = apply_move(state, from_pos, to_pos)
285
+ return new_state, f"AI moved from {pos_to_coord(from_pos)} to {pos_to_coord(to_pos)}"
286
+ return state, "AI has no moves"
287
 
288
+ # Board visualization
289
+ def generate_board_image(state, selected_pos=None):
290
+ img = Image.new('RGB', (BOARD_SIZE * CELL_SIZE, BOARD_SIZE * CELL_SIZE), color=COLORS['empty'])
291
+ draw = ImageDraw.Draw(img)
292
+ for i in range(BOARD_SIZE + 1):
293
+ draw.line([(i * CELL_SIZE, 0), (i * CELL_SIZE, BOARD_SIZE * CELL_SIZE)], fill=(0,0,0), width=1)
294
+ draw.line([(0, i * CELL_SIZE), (BOARD_SIZE * CELL_SIZE, i * CELL_SIZE)], fill=(0,0,0), width=1)
295
  for x in range(BOARD_SIZE):
296
  for y in range(BOARD_SIZE):
297
  pos = (x, y)
 
304
  fill = COLORS['escape']
305
  if pos == selected_pos:
306
  fill = COLORS['highlight']
307
+ draw.rectangle([(y * CELL_SIZE, x * CELL_SIZE), ((y + 1) * CELL_SIZE, (x + 1) * CELL_SIZE)], fill=fill)
 
308
  for x in range(BOARD_SIZE):
309
  for y in range(BOARD_SIZE):
310
  piece = state.board[x, y]
311
  if piece != EMPTY:
312
+ center = (y * CELL_SIZE + CELL_SIZE // 2, x * CELL_SIZE + CELL_SIZE // 2)
 
313
  color = COLORS['white'] if piece == WHITE_SOLDIER else COLORS['black'] if piece == BLACK_SOLDIER else COLORS['king']
314
+ draw.ellipse([(center[0] - PIECE_RADIUS, center[1] - PIECE_RADIUS),
315
+ (center[0] + PIECE_RADIUS, center[1] + PIECE_RADIUS)], fill=color, outline=(0,0,0))
316
  for i in range(BOARD_SIZE):
317
+ draw.text((5, i * CELL_SIZE + CELL_SIZE // 2 - 5), str(BOARD_SIZE - i), fill=(0,0,0))
318
+ draw.text((i * CELL_SIZE + CELL_SIZE // 2 - 5, BOARD_SIZE * CELL_SIZE - 15), chr(ord('A') + i), fill=(0,0,0))
319
+ return img
 
 
 
 
 
 
 
 
320
 
321
  # Gradio interface functions
322
  def click_board(state, selected_pos, evt: gr.SelectData):
323
  if state.turn != 'WHITE':
324
+ return state, "It's the AI's turn", generate_board_image(state), selected_pos
325
+ x = evt.index[1] // CELL_SIZE # Image coordinates are (x, y), board is (row, col)
326
  y = evt.index[0] // CELL_SIZE
327
  pos = (x, y)
328
  if selected_pos is None:
 
329
  if state.board[pos] in [WHITE_SOLDIER, KING]:
330
+ return state, f"Selected {pos_to_coord(pos)}", generate_board_image(state, pos), pos
331
  else:
332
+ return state, "Select a White piece or King", generate_board_image(state), None
333
  else:
 
334
  if is_valid_move(state, selected_pos, pos):
335
  new_state = apply_move(state, selected_pos, pos)
336
  status = check_game_status(new_state)
337
  if status != "CONTINUE":
338
+ return new_state, status, generate_board_image(new_state), None
339
+ ai_state, ai_message = ai_move(new_state)
 
340
  final_status = check_game_status(ai_state)
341
+ message = f"Your move to {pos_to_coord(pos)}. {ai_message}. {final_status if final_status != 'CONTINUE' else ''}"
342
+ return ai_state, message, generate_board_image(ai_state), None
343
  else:
344
+ return state, "Invalid move", generate_board_image(state), None
345
 
346
  def new_game():
347
  state = TablutState()
348
+ return state, "New game started. Your turn (White).", generate_board_image(state), None
349
 
350
  # Gradio interface
351
  with gr.Blocks(title="Tablut Game") as demo:
352
  state = gr.State()
353
  selected_pos = gr.State(value=None)
354
+ board_image = gr.Image(label="Board", type="pil")
355
  message_label = gr.Label(label="Message")
356
  new_game_button = gr.Button("New Game")
357
+ board_image.select(fn=click_board, inputs=[state, selected_pos], outputs=[state, message_label, board_image, selected_pos])
358
+ new_game_button.click(fn=new_game, outputs=[state, message_label, board_image, selected_pos])
359
+ demo.load(fn=new_game, outputs=[state, message_label, board_image, selected_pos])
360
 
361
  # Note: demo.launch() is not needed for HF Spaces