Nayefleb commited on
Commit
4347edc
·
verified ·
1 Parent(s): 0f7c724

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +497 -304
app.py CHANGED
@@ -1,324 +1,517 @@
1
- """ Turkish Dama (Dama) — Gradio Space
2
-
3
- Single-file Gradio app implementing Turkish Draughts (Dama) playable as:
4
-
5
- Local 2-player (same device)
6
-
7
- Single-player vs AI (minimax)
8
-
9
-
10
- How to run locally:
11
-
12
- 1. pip install gradio
13
-
14
-
15
- 2. python turkish_dama_gradio.py
16
-
17
-
18
- 3. Open the local Gradio URL or deploy to Hugging Face Spaces (create a new Space, upload this file, set runtime to "gradio").
19
-
20
-
21
-
22
- Notes / limitations:
23
-
24
- This implementation focuses on correct Turkish Dama rules: orthogonal moves, mandatory captures, immediate removal during multi-jump, kings move like rooks and capture from distance.
25
-
26
- Multiplayer over the network (real-time matchmaking) is not implemented here. I can add a turn-based room system (requires a small backend or use of external persistence) if you want.
27
-
28
-
29
- Author: ChatGPT — example implementation for Hugging Face Spaces """
30
-
31
- import gradio as gr
32
- import copy
33
- import json
34
  import math
35
 
36
  BOARD_SIZE = 8
37
 
38
- #Piece encoding:
39
-
40
- 0 = empty
41
-
42
- 1 = white man
43
-
44
- 2 = white king
45
-
46
- -1 = black man
47
-
48
- -2 = black king
49
-
50
- def initial_board(): b = [[0]*BOARD_SIZE for _ in range(BOARD_SIZE)] # Place white at bottom two rows, black at top two rows for r in range(2): for c in range(BOARD_SIZE): b[r][c] = -1 # black for r in range(BOARD_SIZE-2, BOARD_SIZE): for c in range(BOARD_SIZE): b[r][c] = 1 # white return b
51
-
52
- def inside(r,c): return 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE
53
-
54
- def clone(board): return copy.deepcopy(board)
55
-
56
- class Game: def init(self): self.board = initial_board() self.turn = 1 # 1 = white, -1 = black self.selected = None self.must_capture_moves = None self.history = []
57
-
58
- def to_json(self):
59
- return json.dumps({
60
- 'board': self.board,
61
- 'turn': self.turn,
62
- 'history': self.history
63
- })
64
-
65
- @staticmethod
66
- def from_json(s):
67
- d = json.loads(s)
68
- g = Game()
69
- g.board = d['board']
70
- g.turn = d['turn']
71
- g.history = d.get('history', [])
72
- return g
73
-
74
- def push(self):
75
- self.history.append(json.dumps({'board': clone(self.board), 'turn': self.turn}))
76
-
77
- def undo(self):
78
- if not self.history:
79
- return
80
- last = json.loads(self.history.pop())
81
- self.board = last['board']
82
- self.turn = last['turn']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- def is_king(self, val):
85
- return abs(val) == 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- def generate_moves(self, color):
88
- # Returns list of moves. Move: tuple (r_from,c_from, r_to,c_to, [list of captures positions])
89
- # We must enforce mandatory capture: if any capture exists, only captures allowed.
90
- captures = []
91
- noncaps = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  for r in range(BOARD_SIZE):
93
  for c in range(BOARD_SIZE):
94
- val = self.board[r][c]
95
- if val*color <= 0:
96
  continue
97
- if abs(val) == 1: # man
98
- # moves: orthogonal forward (toward enemy) or sideways
99
- # For white (1): forward is -1 row (up)
100
- # But men can move forward or sideways one square
101
- dirs = [(-1,0),(0,-1),(0,1)] if val==1 else [(1,0),(0,-1),(0,1)]
102
- # For capturing, men capture orthogonally by jumping over adjacent enemy into empty square
103
- for dr,dc in dirs:
104
- r1,c1 = r+dr, c+dc
105
- r2,c2 = r+2*dr, c+2*dc
106
- if inside(r2,c2) and self.board[r1][c1]*color < 0 and self.board[r2][c2]==0:
107
- # capture
108
- captures.append((r,c,r2,c2,[(r1,c1)]))
109
- # non-capture moves
110
- for dr,dc in dirs:
111
- r1,c1 = r+dr,c+dc
112
- if inside(r1,c1) and self.board[r1][c1]==0:
113
- noncaps.append((r,c,r1,c1,[]))
114
- else: # king
115
- # slide any distance orthogonally for moves; for captures can capture from distance
116
- for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]:
117
- step = 1
118
- while True:
119
- r1,c1 = r+dr*step, c+dc*step
120
- if not inside(r1,c1):
121
- break
122
- if self.board[r1][c1]==0:
123
- # candidate non-capture move
124
- noncaps.append((r,c,r1,c1,[]))
125
- step+=1
126
- continue
127
- if self.board[r1][c1]*color < 0:
128
- # possible capture: there must be an empty square beyond
129
- step2 = 1
130
- while True:
131
- r_land = r1+dr*step2
132
- c_land = c1+dc*step2
133
- if not inside(r_land,c_land):
134
- break
135
- if self.board[r_land][c_land]==0:
136
- captures.append((r,c,r_land,c_land,[(r1,c1)]))
137
- # kings can capture landing any square beyond captured piece; we record all
138
- step2+=1
139
- continue
140
- else:
141
- break
142
- break
143
- else:
144
- break
145
- # If captures exist, we must expand multi-captures (since immediate removal can open more captures)
146
- if captures:
147
- full_caps = []
148
- for cap in captures:
149
- seqs = self._expand_capture_sequence(clone(self.board), cap)
150
- full_caps.extend(seqs)
151
- # choose only moves that capture maximum number of pieces per rule
152
- max_captured = max(len(m[4]) for m in full_caps) if full_caps else 0
153
- full_caps = [m for m in full_caps if len(m[4])==max_captured]
154
- return full_caps
155
- else:
156
- return noncaps
157
-
158
- def _expand_capture_sequence(self, board_state, move):
159
- # move is (r_from,c_from,r_to,c_to, [captures_so_far])
160
- # perform move on board_state, remove captured pieces immediately, then see if further captures possible from landing square
161
- r0,c0,r1,c1,caps = move
162
- b = copy.deepcopy(board_state)
163
- piece = b[r0][c0]
164
- b[r0][c0]=0
165
- b[r1][c1]=piece
166
- # remove captured pieces
167
- for (cr,cc) in caps:
168
- b[cr][cc]=0
169
- # check promotion: if man reaches back row -> becomes king immediately
170
- if abs(piece)==1:
171
- if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1):
172
- b[r1][c1] = 2*piece
173
- # When promoted, possible capturing abilities change; continue expansion with new board
174
- # find further captures from r1,c1
175
- further = []
176
- color = 1 if b[r1][c1]>0 else -1
177
- val = b[r1][c1]
178
- if abs(val)==1:
179
- dirs = [(-1,0),(0,-1),(0,1)] if val==1 else [(1,0),(0,-1),(0,1)]
180
- for dr,dc in dirs:
181
- rA,cA = r1+dr,c1+dc
182
- rB,cB = r1+2*dr,c1+2*dc
183
- if inside(rB,cB) and b[rA][cA]*color<0 and b[rB][cB]==0:
184
- # can capture
185
- new_caps = caps+[(rA,cA)]
186
- further.append((r1,c1,rB,cB,new_caps))
187
- else: # king
188
- for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]:
189
- step = 1
190
- while True:
191
- rA,cA = r1+dr*step, c1+dc*step
192
- if not inside(rA,cA):
193
- break
194
- if b[rA][cA]==0:
195
- step+=1
196
- continue
197
- if b[rA][cA]*color<0:
198
- # try landing squares beyond
199
- step2=1
200
- while True:
201
- rLand = rA+dr*step2
202
- cLand = cA+dc*step2
203
- if not inside(rLand,cLand):
204
- break
205
- if b[rLand][cLand]==0:
206
- new_caps = caps+[(rA,cA)]
207
- further.append((r1,c1,rLand,cLand,new_caps))
208
- step2+=1
209
- continue
210
- else:
211
- break
212
  break
213
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  break
215
- if not further:
216
- return [(r0,c0,r1,c1,caps)]
217
- results = []
218
- for f in further:
219
- results.extend(self._expand_capture_sequence(b, f))
220
- # prepend original source coordinates
221
- final = []
222
- for seq in results:
223
- final.append((r0,c0,seq[2],seq[3], seq[4]))
224
- return final
225
-
226
- def apply_move(self, move):
227
- # move: (r0,c0,r1,c1,captures)
228
- r0,c0,r1,c1,caps = move
229
- piece = self.board[r0][c0]
230
- self.push()
231
- self.board[r0][c0]=0
232
- self.board[r1][c1]=piece
233
- for (cr,cc) in caps:
234
- self.board[cr][cc]=0
235
- # promotion immediate
236
- if abs(piece)==1:
237
- if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1):
238
- self.board[r1][c1] = 2*piece
239
- self.turn *= -1
240
-
241
- def game_over(self):
242
- # game over if one side has no pieces or no legal moves
243
- white_exists = any(self.board[r][c]>0 for r in range(BOARD_SIZE) for c in range(BOARD_SIZE))
244
- black_exists = any(self.board[r][c]<0 for r in range(BOARD_SIZE) for c in range(BOARD_SIZE))
245
- if not white_exists:
246
- return True, -1
247
- if not black_exists:
248
- return True, 1
249
- white_moves = self.generate_moves(1)
250
- black_moves = self.generate_moves(-1)
251
- if not white_moves:
252
- return True, -1
253
- if not black_moves:
254
- return True, 1
255
- return False, None
256
-
257
- Rendering helper: produce an SVG of the current board
258
-
259
- def render_svg(board, selected=None): size = 480 cell = size // BOARD_SIZE svg = [f'<svg width="{size}" height="{size}" viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">'] # background svg.append(f'<rect width="100%" height="100%" fill="#f0d9b5"/>') # grid lines for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): x = ccell y = rcell svg.append(f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" fill="" stroke="#000" stroke-width="1"/>') # pieces for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): val = board[r][c] if val==0: continue cx = ccell + cell/2 cy = rcell + cell/2 radius = cell0.36 if val>0: fill = '#fff' stroke = '#000' else: fill = '#000' stroke = '#fff' svg.append(f'<circle cx="{cx}" cy="{cy}" r="{radius}" fill="{fill}" stroke="{stroke}" stroke-width="3"/>') if abs(val)==2: # king crown — simple crown path svg.append(f'<text x="{cx}" y="{cy+6}" font-size="{int(cell*0.4)}" text-anchor="middle" fill="{stroke}" font-family="Arial" font-weight="700">♛</text>') # highlight selected if selected: r,c = selected x = ccell y = r*cell svg.append(f'<rect x="{x+2}" y="{y+2}" width="{cell-4}" height="{cell-4}" fill="none" stroke="#ff0000" stroke-width="3"/>') svg.append('</svg>') return '\n'.join(svg)
260
-
261
- Basic AI: minimax with simple evaluation
262
-
263
- def evaluate(board): score = 0 for r in range(BOARD_SIZE): for c in range(BOARD_SIZE): v = board[r][c] if v==0: continue if abs(v)==1: score += 3 * (1 if v>0 else -1) else: score += 8 * (1 if v>0 else -1) return score
264
-
265
- def ai_best_move(game: Game, depth=3): # returns a move for black (-1) or white depending on game.turn color = game.turn def minimax(g: Game, d, alpha, beta): ov, winner = g.game_over() if ov: if winner==color: return (None, 10000) else: return (None, -10000) if d==0: return (None, evaluate(g.board)) moves = g.generate_moves(g.turn) if not moves: return (None, -10000 if g.turn==color else 10000) best_move = None if g.turn==color: max_eval = -99999 for m in moves: ng = Game() ng.board = clone(g.board) ng.turn = g.turn ng.apply_move = Game.apply_move.get(ng) # apply move manually since apply_move references history r0,c0,r1,c1,caps = m piece = ng.board[r0][c0] ng.board[r0][c0]=0 ng.board[r1][c1]=piece for (cr,cc) in caps: ng.board[cr][cc]=0 if abs(piece)==1: if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1): ng.board[r1][c1]=2*piece ng.turn = -1 _, val = minimax(ng, d-1, alpha, beta) if val>max_eval: max_eval = val best_move = m alpha = max(alpha, val) if beta<=alpha: break return (best_move, max_eval) else: min_eval = 99999 for m in moves: ng = Game() ng.board = clone(g.board) ng.turn = g.turn r0,c0,r1,c1,caps = m piece = ng.board[r0][c0] ng.board[r0][c0]=0 ng.board[r1][c1]=piece for (cr,cc) in caps: ng.board[cr][cc]=0 if abs(piece)==1: if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1): ng.board[r1][c1]=2piece ng.turn *= -1 _, val = minimax(ng, d-1, alpha, beta) if val<min_eval: min_eval = val best_move = m beta = min(beta, val) if beta<=alpha: break return (best_move, min_eval)
266
-
267
- mv, _ = minimax(game, depth, -99999, 99999)
268
- return mv
269
-
270
- Gradio app state
271
-
272
- STATE = Game()
273
-
274
- def click_cell(r_c, mode, ai_side, ai_depth): """ r_c: string like 'r,c' when user clicks a cell from the SVG. mode: 'local' or 'ai' ai_side: 'black' or 'white' (AI plays which color when mode=='ai') """ global STATE if r_c is None or r_c=="": return render_svg(STATE.board, STATE.selected), STATE.turn, "", STATE.to_json(), "" r,c = map(int, r_c.split(',')) # If nothing selected, select piece if it belongs to player if STATE.selected is None: if STATE.board[r][c]*STATE.turn>0: STATE.selected = (r,c) return render_svg(STATE.board, STATE.selected), STATE.turn, "", STATE.to_json(), "" else: # attempt to make move from selected to r,c moves = STATE.generate_moves(STATE.turn) chosen = None for m in moves: if m[0]==STATE.selected[0] and m[1]==STATE.selected[1] and m[2]==r and m[3]==c: chosen = m break if chosen: STATE.apply_move(chosen) STATE.selected = None ov, winner = STATE.game_over() if ov: return render_svg(STATE.board, None), STATE.turn, f'Game over. Winner: {"White" if winner==1 else "Black"}', STATE.to_json(), "" # If AI mode and it's AI's turn, compute AI move if mode=='ai': side = -1 if ai_side=='black' else 1 if STATE.turn==side: mv = ai_best_move(STATE, depth=ai_depth) if mv: STATE.apply_move(mv) ov,winner = STATE.game_over() if ov: return render_svg(STATE.board, None), STATE.turn, f'Game over. Winner: {"White" if winner==1 else "Black"}', STATE.to_json(), "" return render_svg(STATE.board, None), STATE.turn, "", STATE.to_json(), "" else: # clicked invalid target => change selection if clicking own piece if STATE.board[r][c]*STATE.turn>0: STATE.selected = (r,c) else: STATE.selected = None return render_svg(STATE.board, STATE.selected), STATE.turn, "Invalid move", STATE.to_json(), ""
275
-
276
- def new_game(): global STATE STATE = Game() return render_svg(STATE.board, None), STATE.turn, "New game started.", STATE.to_json(), ""
277
-
278
- def undo_action(): global STATE STATE.undo() return render_svg(STATE.board, None), STATE.turn, "Undo.", STATE.to_json(), ""
279
-
280
- def load_state(s): global STATE try: STATE = Game.from_json(s) return render_svg(STATE.board, None), STATE.turn, "Loaded.", STATE.to_json(), "" except Exception as e: return render_svg(STATE.board, None), STATE.turn, f'Load failed: {e}', STATE.to_json(), ""
281
-
282
- def export_state(): return STATE.to_json()
283
-
284
- with gr.Blocks() as demo: gr.Markdown("# Turkish Dama (Draughts) — Gradio Space\nPlay local 2-player or single-player vs AI. Click a piece, then click destination.") with gr.Row(): board_svg = gr.HTML(render_svg(STATE.board)) with gr.Column(): mode = gr.Radio(['local','ai'], value='local', label='Mode') ai_side = gr.Radio(['black','white'], value='black', label='AI plays') ai_depth = gr.Slider(minimum=1, maximum=4, value=3, step=1, label='AI depth (strength)') new_btn = gr.Button('New Game') undo_btn = gr.Button('Undo') status = gr.Textbox(label='Status', value='') export_btn = gr.Button('Export Game (JSON)') export_out = gr.Textbox(label='Exported JSON') import_in = gr.Textbox(label='Import Game (paste JSON)') import_btn = gr.Button('Load') # Hidden state exchange state_json = gr.State(value=STATE.to_json())
285
-
286
- # We'll capture clicks by overlaying an invisible grid of buttons in HTML using simple JS that sends coordinates to Python.
287
- # For simplicity, provide a small JS snippet that calls a Gradio function via URL - but Gradio's native event passing is simpler by having gradio.Buttons per cell.
288
- # Simpler solution: produce an HTML SVG with clickable rects that call the Gradio endpoint by sending their "r,c" as input. We'll use gradio's extra JS handler.
289
 
290
- # Create a helper that updates board HTML
291
- def update_board_html(svg, turn, msg, jsstate, _unused=""):
292
- # svg is actual string; replace board
293
- return gr.update(value=svg), gr.update(value=msg), jsstate
294
 
295
- # Connect UI actions
296
- # Click handler: we cheat by using a text input that the frontend fills via small JS in the HTML. But to keep this single-file and simple, we'll use buttons overlay.
297
-
298
- # Create 8x8 invisible buttons grid: we'll render an HTML block with embedded JS that forwards clicks to Python via the Gradio api.
299
- # Build clickable SVG where each cell has an onclick that sets a hidden text input value then triggers a python call via the "submit" button. We'll instead wire via gradio's `js` - but simple approach: use prebuilt `click_cell` via gr.Buttons per cell.
300
 
301
- # Create 8x8 buttons below for clicks (works but not pretty)
302
- btns = []
303
- for r in range(BOARD_SIZE):
304
- row = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  for c in range(BOARD_SIZE):
307
- b = gr.Button(f'{r},{c}', elem_id=f'cell_{r}_{c}', visible=True)
308
- row.append(b)
309
- btns.append(row)
310
-
311
- # Wire buttons
312
- for r in range(BOARD_SIZE):
313
- for c in range(BOARD_SIZE):
314
- btns[r][c].click(fn=lambda rc=f"{r},{c}", mode_comp=mode, ai_side_comp=ai_side, depth_comp=ai_depth: click_cell(rc, mode_comp, ai_side_comp, int(depth_comp)), inputs=[], outputs=[board_svg, gr.Textbox.update, status, state_json, export_out])
315
 
316
- new_btn.click(new_game, inputs=[], outputs=[board_svg, gr.Textbox.update, status, state_json, export_out])
317
- undo_btn.click(undo_action, inputs=[], outputs=[board_svg, gr.Textbox.update, status, state_json, export_out])
318
- export_btn.click(lambda: STATE.to_json(), inputs=[], outputs=[export_out])
319
- import_btn.click(lambda s=import_in: load_state(s), inputs=[import_in], outputs=[board_svg, gr.Textbox.update, status, state_json, export_out])
320
 
321
- gr.Markdown("---\nControls: Click a piece then click a destination. Use 'Export Game' to get JSON that you can save, and 'Load' to restore a saved game.")
 
322
 
323
  demo.launch()
324
-
 
1
+ """ Turkish Dama (Dama) — Gradio SpaceSingle-file Gradio app implementing Turkish Draughts (Dama) playable as:Local 2-player (same device)Single-player vs AI (minimax)How to run locally:1. pip install gradio2. python turkish_dama_gradio.py3. Open the local Gradio URL or deploy to Hugging Face Spaces (create a new Space, upload this file, set runtime to "gradio").Notes / limitations:This implementation focuses on correct Turkish Dama rules: orthogonal moves, mandatory captures, immediate removal during multi-jump, kings move like rooks and capture from distance.Multiplayer over the network (real-time matchmaking) is not implemented here. I can add a turn-based room system (requires a small backend or use of external persistence) if you want.Author: ChatGPT — example implementation for Hugging Face Spaces """
2
+ import gradio as gr
3
+ import copy
4
+ import json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import math
6
 
7
  BOARD_SIZE = 8
8
 
9
+ # Piece encoding:
10
+ # 0 = empty
11
+ # 1 = white man
12
+ # 2 = white king
13
+ # -1 = black man
14
+ # -2 = black king
15
+
16
+ def initial_board():
17
+ b = [[0]*BOARD_SIZE for _ in range(BOARD_SIZE)]
18
+ # Place white at bottom two rows, black at top two rows
19
+ for r in range(2):
20
+ for c in range(BOARD_SIZE):
21
+ b[r][c] = -1 # black
22
+ for r in range(BOARD_SIZE-2, BOARD_SIZE):
23
+ for c in range(BOARD_SIZE):
24
+ b[r][c] = 1 # white
25
+ return b
26
+
27
+ def inside(r,c):
28
+ return 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE
29
+
30
+ def clone(board):
31
+ return copy.deepcopy(board)
32
+
33
+ class Game:
34
+ def __init__(self):
35
+ self.board = initial_board()
36
+ self.turn = 1 # 1 = white, -1 = black
37
+ self.selected = None
38
+ self.must_capture_moves = None
39
+ self.history = []
40
+
41
+ def to_json(self):
42
+ return json.dumps({
43
+ 'board': self.board,
44
+ 'turn': self.turn,
45
+ 'history': self.history
46
+ })
47
+
48
+ @staticmethod
49
+ def from_json(s):
50
+ d = json.loads(s)
51
+ g = Game()
52
+ g.board = d['board']
53
+ g.turn = d['turn']
54
+ g.history = d.get('history', [])
55
+ return g
56
+
57
+ def push(self):
58
+ self.history.append(json.dumps({'board': clone(self.board), 'turn': self.turn}))
59
+
60
+ def undo(self):
61
+ if not self.history:
62
+ return
63
+ last = json.loads(self.history.pop())
64
+ self.board = last['board']
65
+ self.turn = last['turn']
66
+
67
+ def is_king(self, val):
68
+ return abs(val) == 2
69
+
70
+ def generate_moves(self, color):
71
+ # Returns list of moves. Move: tuple (r_from,c_from, r_to,c_to, [list of captures positions])
72
+ # We must enforce mandatory capture: if any capture exists, only captures allowed.
73
+ captures = []
74
+ noncaps = []
75
+ for r in range(BOARD_SIZE):
76
+ for c in range(BOARD_SIZE):
77
+ val = self.board[r][c]
78
+ if val*color <= 0:
79
+ continue
80
 
81
+ if abs(val) == 1: # man
82
+ # moves: orthogonal forward (toward enemy) or sideways
83
+ # For white (1): forward is -1 row (up)
84
+ # But men can move forward or sideways one square
85
+ dirs = [(-1,0),(0,-1),(0,1)] if val==1 else [(1,0),(0,-1),(0,1)]
86
+
87
+ # For capturing, men capture orthogonally by jumping over adjacent enemy into empty square
88
+ for dr,dc in dirs:
89
+ r1,c1 = r+dr, c+dc
90
+ r2,c2 = r+2*dr, c+2*dc
91
+ if inside(r2,c2) and self.board[r1][c1]*color < 0 and self.board[r2][c2]==0:
92
+ # capture
93
+ captures.append((r,c,r2,c2,[(r1,c1)]))
94
+
95
+ # non-capture moves
96
+ for dr,dc in dirs:
97
+ r1,c1 = r+dr,c+dc
98
+ if inside(r1,c1) and self.board[r1][c1]==0:
99
+ noncaps.append((r,c,r1,c1,[]))
100
 
101
+ else: # king
102
+ # slide any distance orthogonally for moves; for captures can capture from distance
103
+ for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]:
104
+ step = 1
105
+ while True:
106
+ r1,c1 = r+dr*step, c+dc*step
107
+ if not inside(r1,c1):
108
+ break
109
+ if self.board[r1][c1]==0:
110
+ # candidate non-capture move
111
+ noncaps.append((r,c,r1,c1,[]))
112
+ step+=1
113
+ continue
114
+ if self.board[r1][c1]*color < 0:
115
+ # possible capture: there must be an empty square beyond
116
+ step2 = 1
117
+ while True:
118
+ r_land = r1+dr*step2
119
+ c_land = c1+dc*step2
120
+ if not inside(r_land,c_land):
121
+ break
122
+ if self.board[r_land][c_land]==0:
123
+ captures.append((r,c,r_land,c_land,[(r1,c1)])) # kings can capture landing any square beyond captured piece; we record all
124
+ step2+=1
125
+ continue
126
+ else:
127
+ break
128
+ break
129
+ else:
130
+ break
131
+
132
+ # If captures exist, we must expand multi-captures (since immediate removal can open more captures)
133
+ if captures:
134
+ full_caps = []
135
+ for cap in captures:
136
+ seqs = self._expand_capture_sequence(clone(self.board), cap)
137
+ full_caps.extend(seqs)
138
+
139
+ # choose only moves that capture maximum number of pieces per rule
140
+ max_captured = max(len(m[4]) for m in full_caps) if full_caps else 0
141
+ full_caps = [m for m in full_caps if len(m[4])==max_captured]
142
+
143
+ return full_caps
144
+ else:
145
+ return noncaps
146
+
147
+ def _expand_capture_sequence(self, board_state, move):
148
+ # move is (r_from,c_from,r_to,c_to, [captures_so_far])
149
+ # perform move on board_state, remove captured pieces immediately, then see if further captures possible from landing square
150
+ r0,c0,r1,c1,caps = move
151
+ b = copy.deepcopy(board_state)
152
+ piece = b[r0][c0]
153
+ b[r0][c0]=0
154
+ b[r1][c1]=piece
155
+
156
+ # remove captured pieces
157
+ for (cr,cc) in caps:
158
+ b[cr][cc]=0
159
+
160
+ # check promotion: if man reaches back row -> becomes king immediately
161
+ if abs(piece)==1:
162
+ if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1):
163
+ b[r1][c1] = 2*piece
164
+
165
+ # When promoted, possible capturing abilities change; continue expansion with new board
166
+ # find further captures from r1,c1
167
+ further = []
168
+ color = 1 if b[r1][c1]>0 else -1
169
+ val = b[r1][c1]
170
+
171
+ if abs(val)==1:
172
+ dirs = [(-1,0),(0,-1),(0,1)] if val==1 else [(1,0),(0,-1),(0,1)]
173
+ for dr,dc in dirs:
174
+ rA,cA = r1+dr,c1+dc
175
+ rB,cB = r1+2*dr,c1+2*dc
176
+ if inside(rB,cB) and b[rA][cA]*color<0 and b[rB][cB]==0:
177
+ # can capture
178
+ new_caps = caps+[(rA,cA)]
179
+ further.append((r1,c1,rB,cB,new_caps))
180
+ else: # king
181
+ for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]:
182
+ step = 1
183
+ while True:
184
+ rA,cA = r1+dr*step, c1+dc*step
185
+ if not inside(rA,cA):
186
+ break
187
+ if b[rA][cA]==0:
188
+ step+=1
189
+ continue
190
+ if b[rA][cA]*color<0:
191
+ # try landing squares beyond
192
+ step2=1
193
+ while True:
194
+ rLand = rA+dr*step2
195
+ cLand = cA+dc*step2
196
+ if not inside(rLand,cLand):
197
+ break
198
+ if b[rLand][cLand]==0:
199
+ new_caps = caps+[(rA,cA)]
200
+ further.append((r1,c1,rLand,cLand,new_caps))
201
+ step2+=1
202
+ continue
203
+ else:
204
+ break
205
+ break
206
+ else:
207
+ break
208
+
209
+ if not further:
210
+ return [(r0,c0,r1,c1,caps)]
211
+
212
+ results = []
213
+ for f in further:
214
+ results.extend(self._expand_capture_sequence(b, f)) # prepend original source coordinates
215
+ final = []
216
+ for seq in results:
217
+ final.append((r0,c0,seq[2],seq[3], seq[4]))
218
+ return final
219
+
220
+ def apply_move(self, move):
221
+ # move: (r0,c0,r1,c1,captures)
222
+ r0,c0,r1,c1,caps = move
223
+ piece = self.board[r0][c0]
224
+ self.push()
225
+ self.board[r0][c0]=0
226
+ self.board[r1][c1]=piece
227
+
228
+ for (cr,cc) in caps:
229
+ self.board[cr][cc]=0
230
+
231
+ # promotion immediate
232
+ if abs(piece)==1:
233
+ if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1):
234
+ self.board[r1][c1] = 2*piece
235
+
236
+ self.turn *= -1
237
+
238
+ def game_over(self):
239
+ # game over if one side has no pieces or no legal moves
240
+ white_exists = any(self.board[r][c]>0 for r in range(BOARD_SIZE) for c in range(BOARD_SIZE))
241
+ black_exists = any(self.board[r][c]<0 for r in range(BOARD_SIZE) for c in range(BOARD_SIZE))
242
+ if not white_exists:
243
+ return True, -1
244
+ if not black_exists:
245
+ return True, 1
246
+
247
+ white_moves = self.generate_moves(1)
248
+ black_moves = self.generate_moves(-1)
249
+
250
+ if not white_moves:
251
+ return True, -1
252
+ if not black_moves:
253
+ return True, 1
254
+
255
+ return False, None
256
+
257
+ # Rendering helper: produce an SVG of the current board
258
+ def render_svg(board, selected=None):
259
+ size = 480
260
+ cell = size // BOARD_SIZE
261
+ svg = [f'<svg width="{size}" height="{size}" viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">']
262
+ # background
263
+ svg.append(f'<rect width="100%" height="100%" fill="#f0d9b5"/>')
264
+ # grid lines
265
+ for r in range(BOARD_SIZE):
266
+ for c in range(BOARD_SIZE):
267
+ x = c*cell
268
+ y = r*cell
269
+ svg.append(f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" fill="none" stroke="#000" stroke-width="1"/>')
270
+ # pieces
271
  for r in range(BOARD_SIZE):
272
  for c in range(BOARD_SIZE):
273
+ val = board[r][c]
274
+ if val==0:
275
  continue
276
+ cx = c*cell + cell/2
277
+ cy = r*cell + cell/2
278
+ radius = cell*0.36
279
+
280
+ if val>0:
281
+ fill = '#fff'
282
+ stroke = '#000'
283
+ else:
284
+ fill = '#000'
285
+ stroke = '#fff'
286
+
287
+ svg.append(f'<circle cx="{cx}" cy="{cy}" r="{radius}" fill="{fill}" stroke="{stroke}" stroke-width="3"/>')
288
+
289
+ if abs(val)==2: # king crown — simple crown path
290
+ svg.append(f'<text x="{cx}" y="{cy+6}" font-size="{int(cell*0.4)}" text-anchor="middle" fill="{stroke}" font-family="Arial" font-weight="700">♛</text>')
291
+
292
+ # highlight selected
293
+ if selected:
294
+ r,c = selected
295
+ x = c*cell
296
+ y = r*cell
297
+ svg.append(f'<rect x="{x+2}" y="{y+2}" width="{cell-4}" height="{cell-4}" fill="none" stroke="#ff0000" stroke-width="3"/>')
298
+
299
+ svg.append('</svg>')
300
+ return '\n'.join(svg)
301
+
302
+ # Basic AI: minimax with simple evaluation
303
+ def evaluate(board):
304
+ score = 0
305
+ for r in range(BOARD_SIZE):
306
+ for c in range(BOARD_SIZE):
307
+ v = board[r][c]
308
+ if v==0:
309
+ continue
310
+ if abs(v)==1:
311
+ score += 3 * (1 if v>0 else -1)
312
+ else:
313
+ score += 8 * (1 if v>0 else -1)
314
+ return score
315
+
316
+ def ai_best_move(game: Game, depth=3):
317
+ # returns a move for black (-1) or white depending on game.turn
318
+ color = game.turn
319
+
320
+ def minimax(g: Game, d, alpha, beta):
321
+ ov, winner = g.game_over()
322
+ if ov:
323
+ if winner==color:
324
+ return (None, 10000)
325
+ else:
326
+ return (None, -10000)
327
+
328
+ if d==0:
329
+ return (None, evaluate(g.board))
330
+
331
+ moves = g.generate_moves(g.turn)
332
+ if not moves:
333
+ return (None, -10000 if g.turn==color else 10000)
334
+
335
+ best_move = None
336
+ if g.turn==color:
337
+ max_eval = -99999
338
+ for m in moves:
339
+ ng = Game()
340
+ ng.board = clone(g.board)
341
+ ng.turn = g.turn
342
+
343
+ # apply move manually since apply_move references history
344
+ r0,c0,r1,c1,caps = m
345
+ piece = ng.board[r0][c0]
346
+ ng.board[r0][c0]=0
347
+ ng.board[r1][c1]=piece
348
+ for (cr,cc) in caps:
349
+ ng.board[cr][cc]=0
350
+ if abs(piece)==1:
351
+ if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1):
352
+ ng.board[r1][c1]=2*piece
353
+ ng.turn = -1
354
+
355
+ _, val = minimax(ng, d-1, alpha, beta)
356
+
357
+ if val>max_eval:
358
+ max_eval = val
359
+ best_move = m
360
+ alpha = max(alpha, val)
361
+ if beta<=alpha:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  break
363
+ return (best_move, max_eval)
364
+ else:
365
+ min_eval = 99999
366
+ for m in moves:
367
+ ng = Game()
368
+ ng.board = clone(g.board)
369
+ ng.turn = g.turn
370
+
371
+ r0,c0,r1,c1,caps = m
372
+ piece = ng.board[r0][c0]
373
+ ng.board[r0][c0]=0
374
+ ng.board[r1][c1]=piece
375
+ for (cr,cc) in caps:
376
+ ng.board[cr][cc]=0
377
+ if abs(piece)==1:
378
+ if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1):
379
+ ng.board[r1][c1]=2*piece
380
+ ng.turn *= -1
381
+
382
+ _, val = minimax(ng, d-1, alpha, beta)
383
+
384
+ if val<min_eval:
385
+ min_eval = val
386
+ best_move = m
387
+ beta = min(beta, val)
388
+ if beta<=alpha:
389
  break
390
+ return (best_move, min_eval)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
392
+ mv, _ = minimax(game, depth, -99999, 99999)
393
+ return mv
 
 
394
 
395
+ # Gradio app state
396
+ STATE = Game()
 
 
 
397
 
398
+ def click_cell(r_c, mode, ai_side, ai_depth):
399
+ """
400
+ r_c: string like 'r,c' when user clicks a cell from the SVG.
401
+ mode: 'local' or 'ai'
402
+ ai_side: 'black' or 'white' (AI plays which color when mode=='ai')
403
+ """
404
+ global STATE
405
+ if r_c is None or r_c=="":
406
+ return render_svg(STATE.board, STATE.selected), STATE.turn, "", STATE.to_json(), ""
407
+
408
+ r,c = map(int, r_c.split(','))
409
+
410
+ # If nothing selected, select piece if it belongs to player
411
+ if STATE.selected is None:
412
+ if STATE.board[r][c]*STATE.turn>0:
413
+ STATE.selected = (r,c)
414
+ return render_svg(STATE.board, STATE.selected), STATE.turn, "", STATE.to_json(), ""
415
+ else:
416
+ # attempt to make move from selected to r,c
417
+ moves = STATE.generate_moves(STATE.turn)
418
+ chosen = None
419
+ for m in moves:
420
+ if m[0]==STATE.selected[0] and m[1]==STATE.selected[1] and m[2]==r and m[3]==c:
421
+ chosen = m
422
+ break
423
+
424
+ if chosen:
425
+ STATE.apply_move(chosen)
426
+ STATE.selected = None
427
+ ov, winner = STATE.game_over()
428
+ if ov:
429
+ return render_svg(STATE.board, None), STATE.turn, f'Game over. Winner: {"White" if winner==1 else "Black"}', STATE.to_json(), ""
430
+
431
+ # If AI mode and it's AI's turn, compute AI move
432
+ if mode=='ai':
433
+ side = -1 if ai_side=='black' else 1
434
+ if STATE.turn==side:
435
+ mv = ai_best_move(STATE, depth=ai_depth)
436
+ if mv:
437
+ STATE.apply_move(mv)
438
+ ov,winner = STATE.game_over()
439
+ if ov:
440
+ return render_svg(STATE.board, None), STATE.turn, f'Game over. Winner: {"White" if winner==1 else "Black"}', STATE.to_json(), ""
441
+
442
+ return render_svg(STATE.board, None), STATE.turn, "", STATE.to_json(), ""
443
+ else:
444
+ # clicked invalid target => change selection if clicking own piece
445
+ if STATE.board[r][c]*STATE.turn>0:
446
+ STATE.selected = (r,c)
447
+ else:
448
+ STATE.selected = None
449
+ return render_svg(STATE.board, STATE.selected), STATE.turn, "Invalid move", STATE.to_json(), ""
450
+
451
+ def new_game():
452
+ global STATE
453
+ STATE = Game()
454
+ return render_svg(STATE.board, None), STATE.turn, "New game started.", STATE.to_json(), ""
455
+
456
+ def undo_action():
457
+ global STATE
458
+ STATE.undo()
459
+ return render_svg(STATE.board, None), STATE.turn, "Undo.", STATE.to_json(), ""
460
+
461
+ def load_state(s):
462
+ global STATE
463
+ try:
464
+ STATE = Game.from_json(s)
465
+ return render_svg(STATE.board, None), STATE.turn, "Loaded.", STATE.to_json(), ""
466
+ except Exception as e:
467
+ return render_svg(STATE.board, None), STATE.turn, f'Load failed: {e}', STATE.to_json(), ""
468
+
469
+ def export_state():
470
+ return STATE.to_json()
471
+
472
+
473
+ with gr.Blocks() as demo:
474
+ gr.Markdown("# Turkish Dama (Draughts) — Gradio Space\nPlay local 2-player or single-player vs AI. Click a piece, then click destination.")
475
  with gr.Row():
476
+ board_svg = gr.HTML(render_svg(STATE.board))
477
+ with gr.Column():
478
+ mode = gr.Radio(['local','ai'], value='local', label='Mode')
479
+ ai_side = gr.Radio(['black','white'], value='black', label='AI plays')
480
+ ai_depth = gr.Slider(minimum=1, maximum=4, value=3, step=1, label='AI depth (strength)')
481
+ new_btn = gr.Button('New Game')
482
+ undo_btn = gr.Button('Undo')
483
+ status = gr.Textbox(label='Status', value='')
484
+ export_btn = gr.Button('Export Game (JSON)')
485
+ export_out = gr.Textbox(label='Exported JSON')
486
+ import_in = gr.Textbox(label='Import Game (paste JSON)')
487
+ import_btn = gr.Button('Load')
488
+
489
+ # Hidden state exchange
490
+ state_json = gr.State(value=STATE.to_json())
491
+
492
+ # Create 8x8 invisible buttons grid for clicks (works but not pretty)
493
+ btns = []
494
+ for r in range(BOARD_SIZE):
495
+ row = []
496
+ with gr.Row():
497
+ for c in range(BOARD_SIZE):
498
+ b = gr.Button(f'{r},{c}', elem_id=f'cell_{r}_{c}', visible=True)
499
+ row.append(b)
500
+ btns.append(row)
501
+
502
+ # Wire buttons
503
+ for r in range(BOARD_SIZE):
504
  for c in range(BOARD_SIZE):
505
+ btns[r][c].click(fn=lambda rc=f"{r},{c}", mode_comp=mode, ai_side_comp=ai_side, depth_comp=ai_depth: click_cell(rc, mode_comp, ai_side_comp, int(depth_comp)),
506
+ inputs=[gr.Textbox(visible=False), mode, ai_side, ai_depth],
507
+ outputs=[board_svg, status, state_json, export_out])
 
 
 
 
 
508
 
509
+ new_btn.click(new_game, inputs=[], outputs=[board_svg, status, state_json, export_out])
510
+ undo_btn.click(undo_action, inputs=[], outputs=[board_svg, status, state_json, export_out])
511
+ export_btn.click(export_state, inputs=[], outputs=[export_out])
512
+ import_btn.click(load_state, inputs=[import_in], outputs=[board_svg, status, state_json, export_out])
513
 
514
+ gr.Markdown("---")
515
+ gr.Markdown("Controls: Click a piece then click a destination. Use 'Export Game' to get JSON that you can save, and 'Load' to restore a saved game.")
516
 
517
  demo.launch()