| """ 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 """ |
| import gradio as gr |
| import copy |
| import json |
| import math |
|
|
| BOARD_SIZE = 8 |
|
|
| |
| |
| |
| |
| |
| |
|
|
| def initial_board(): |
| b = [[0]*BOARD_SIZE for _ in range(BOARD_SIZE)] |
| |
| for r in range(2): |
| for c in range(BOARD_SIZE): |
| b[r][c] = -1 |
| for r in range(BOARD_SIZE-2, BOARD_SIZE): |
| for c in range(BOARD_SIZE): |
| b[r][c] = 1 |
| return b |
|
|
| def inside(r,c): |
| return 0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE |
|
|
| def clone(board): |
| return copy.deepcopy(board) |
|
|
| class Game: |
| def __init__(self): |
| self.board = initial_board() |
| self.turn = 1 |
| self.selected = None |
| self.must_capture_moves = None |
| self.history = [] |
|
|
| def to_json(self): |
| return json.dumps({ |
| 'board': self.board, |
| 'turn': self.turn, |
| 'history': self.history |
| }) |
|
|
| @staticmethod |
| def from_json(s): |
| d = json.loads(s) |
| g = Game() |
| g.board = d['board'] |
| g.turn = d['turn'] |
| g.history = d.get('history', []) |
| return g |
|
|
| def push(self): |
| self.history.append(json.dumps({'board': clone(self.board), 'turn': self.turn})) |
|
|
| def undo(self): |
| if not self.history: |
| return |
| last = json.loads(self.history.pop()) |
| self.board = last['board'] |
| self.turn = last['turn'] |
|
|
| def is_king(self, val): |
| return abs(val) == 2 |
|
|
| def generate_moves(self, color): |
| |
| |
| captures = [] |
| noncaps = [] |
| for r in range(BOARD_SIZE): |
| for c in range(BOARD_SIZE): |
| val = self.board[r][c] |
| if val*color <= 0: |
| continue |
|
|
| if abs(val) == 1: |
| |
| |
| |
| dirs = [(-1,0),(0,-1),(0,1)] if val==1 else [(1,0),(0,-1),(0,1)] |
| |
| |
| for dr,dc in dirs: |
| r1,c1 = r+dr, c+dc |
| r2,c2 = r+2*dr, c+2*dc |
| if inside(r2,c2) and self.board[r1][c1]*color < 0 and self.board[r2][c2]==0: |
| |
| captures.append((r,c,r2,c2,[(r1,c1)])) |
|
|
| |
| for dr,dc in dirs: |
| r1,c1 = r+dr,c+dc |
| if inside(r1,c1) and self.board[r1][c1]==0: |
| noncaps.append((r,c,r1,c1,[])) |
|
|
| else: |
| |
| for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]: |
| step = 1 |
| while True: |
| r1,c1 = r+dr*step, c+dc*step |
| if not inside(r1,c1): |
| break |
| if self.board[r1][c1]==0: |
| |
| noncaps.append((r,c,r1,c1,[])) |
| step+=1 |
| continue |
| if self.board[r1][c1]*color < 0: |
| |
| step2 = 1 |
| while True: |
| r_land = r1+dr*step2 |
| c_land = c1+dc*step2 |
| if not inside(r_land,c_land): |
| break |
| if self.board[r_land][c_land]==0: |
| captures.append((r,c,r_land,c_land,[(r1,c1)])) |
| step2+=1 |
| continue |
| else: |
| break |
| break |
| else: |
| break |
| |
| |
| if captures: |
| full_caps = [] |
| for cap in captures: |
| seqs = self._expand_capture_sequence(clone(self.board), cap) |
| full_caps.extend(seqs) |
| |
| |
| max_captured = max(len(m[4]) for m in full_caps) if full_caps else 0 |
| full_caps = [m for m in full_caps if len(m[4])==max_captured] |
|
|
| return full_caps |
| else: |
| return noncaps |
|
|
| def _expand_capture_sequence(self, board_state, move): |
| |
| |
| r0,c0,r1,c1,caps = move |
| b = copy.deepcopy(board_state) |
| piece = b[r0][c0] |
| b[r0][c0]=0 |
| b[r1][c1]=piece |
| |
| |
| for (cr,cc) in caps: |
| b[cr][cc]=0 |
| |
| |
| if abs(piece)==1: |
| if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1): |
| b[r1][c1] = 2*piece |
| |
| |
| |
| further = [] |
| color = 1 if b[r1][c1]>0 else -1 |
| val = b[r1][c1] |
| |
| if abs(val)==1: |
| dirs = [(-1,0),(0,-1),(0,1)] if val==1 else [(1,0),(0,-1),(0,1)] |
| for dr,dc in dirs: |
| rA,cA = r1+dr,c1+dc |
| rB,cB = r1+2*dr,c1+2*dc |
| if inside(rB,cB) and b[rA][cA]*color<0 and b[rB][cB]==0: |
| |
| new_caps = caps+[(rA,cA)] |
| further.append((r1,c1,rB,cB,new_caps)) |
| else: |
| for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]: |
| step = 1 |
| while True: |
| rA,cA = r1+dr*step, c1+dc*step |
| if not inside(rA,cA): |
| break |
| if b[rA][cA]==0: |
| step+=1 |
| continue |
| if b[rA][cA]*color<0: |
| |
| step2=1 |
| while True: |
| rLand = rA+dr*step2 |
| cLand = cA+dc*step2 |
| if not inside(rLand,cLand): |
| break |
| if b[rLand][cLand]==0: |
| new_caps = caps+[(rA,cA)] |
| further.append((r1,c1,rLand,cLand,new_caps)) |
| step2+=1 |
| continue |
| else: |
| break |
| break |
| else: |
| break |
| |
| if not further: |
| return [(r0,c0,r1,c1,caps)] |
| |
| results = [] |
| for f in further: |
| results.extend(self._expand_capture_sequence(b, f)) |
| final = [] |
| for seq in results: |
| final.append((r0,c0,seq[2],seq[3], seq[4])) |
| return final |
|
|
| def apply_move(self, move): |
| |
| r0,c0,r1,c1,caps = move |
| piece = self.board[r0][c0] |
| self.push() |
| self.board[r0][c0]=0 |
| self.board[r1][c1]=piece |
| |
| for (cr,cc) in caps: |
| self.board[cr][cc]=0 |
| |
| |
| if abs(piece)==1: |
| if (piece==1 and r1==0) or (piece==-1 and r1==BOARD_SIZE-1): |
| self.board[r1][c1] = 2*piece |
| |
| self.turn *= -1 |
|
|
| def game_over(self): |
| |
| white_exists = any(self.board[r][c]>0 for r in range(BOARD_SIZE) for c in range(BOARD_SIZE)) |
| black_exists = any(self.board[r][c]<0 for r in range(BOARD_SIZE) for c in range(BOARD_SIZE)) |
| if not white_exists: |
| return True, -1 |
| if not black_exists: |
| return True, 1 |
| |
| white_moves = self.generate_moves(1) |
| black_moves = self.generate_moves(-1) |
| |
| if not white_moves: |
| return True, -1 |
| if not black_moves: |
| return True, 1 |
|
|
| return False, None |
|
|
| |
| 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">'] |
| |
| svg.append(f'<rect width="100%" height="100%" fill="#f0d9b5"/>') |
| |
| for r in range(BOARD_SIZE): |
| for c in range(BOARD_SIZE): |
| x = c*cell |
| y = r*cell |
| svg.append(f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" fill="none" stroke="#000" stroke-width="1"/>') |
| |
| for r in range(BOARD_SIZE): |
| for c in range(BOARD_SIZE): |
| val = board[r][c] |
| if val==0: |
| continue |
| cx = c*cell + cell/2 |
| cy = r*cell + cell/2 |
| radius = cell*0.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: |
| 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>') |
| |
| |
| if selected: |
| r,c = selected |
| x = c*cell |
| 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) |
|
|
| |
| 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 |
|
|
| def ai_best_move(game: Game, depth=3): |
| |
| 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 |
| |
| |
| 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]=2*piece |
| 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) |
|
|
| mv, _ = minimax(game, depth, -99999, 99999) |
| return mv |
|
|
| |
| STATE = Game() |
|
|
| 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 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: |
| |
| 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 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: |
| |
| 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(), "" |
|
|
| def new_game(): |
| global STATE |
| STATE = Game() |
| return render_svg(STATE.board, None), STATE.turn, "New game started.", STATE.to_json(), "" |
|
|
| def undo_action(): |
| global STATE |
| STATE.undo() |
| return render_svg(STATE.board, None), STATE.turn, "Undo.", STATE.to_json(), "" |
|
|
| 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(), "" |
|
|
| def export_state(): |
| return STATE.to_json() |
|
|
|
|
| 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') |
|
|
| |
| state_json = gr.State(value=STATE.to_json()) |
|
|
| |
| btns = [] |
| for r in range(BOARD_SIZE): |
| row = [] |
| with gr.Row(): |
| for c in range(BOARD_SIZE): |
| b = gr.Button(f'{r},{c}', elem_id=f'cell_{r}_{c}', visible=True) |
| row.append(b) |
| btns.append(row) |
|
|
| |
| for r in range(BOARD_SIZE): |
| for c in range(BOARD_SIZE): |
| 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=[gr.Textbox(visible=False), mode, ai_side, ai_depth], |
| outputs=[board_svg, status, state_json, export_out]) |
|
|
| new_btn.click(new_game, inputs=[], outputs=[board_svg, status, state_json, export_out]) |
| undo_btn.click(undo_action, inputs=[], outputs=[board_svg, status, state_json, export_out]) |
| export_btn.click(export_state, inputs=[], outputs=[export_out]) |
| import_btn.click(load_state, inputs=[import_in], outputs=[board_svg, status, state_json, export_out]) |
|
|
| gr.Markdown("---") |
| 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.") |
|
|
| demo.launch() |
|
|