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

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -0
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()