FlameF0X commited on
Commit
7464f98
Β·
verified Β·
1 Parent(s): 9633f26

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +475 -460
app.py CHANGED
@@ -1,556 +1,571 @@
1
  """
2
- ChessSLM β€” AI vs AI & Model Arena
3
- HF Space | Gradio 4
4
  """
5
 
6
- import re, random, time, torch, chess, chess.pgn, chess.svg, gradio as gr
7
- from typing import Optional
8
- from transformers import (
9
- GPT2LMHeadModel, GPT2Tokenizer,
10
- AutoModelForCausalLM, AutoTokenizer,
11
- )
 
 
 
 
 
 
 
12
 
13
- # ══════════════════════════════════════════════════════════════════════════════
14
- # CONFIG
15
- # ══════════════════════════════════════════════════════════════════════════════
16
  MODEL_ID = "FlameF0X/ChessSLM"
17
- BOARD_SZ = 460
18
-
19
- # ══════════════════════════════════════════════════════════════════════════════
20
- # MODEL LOADING (lazy β€” on first move, not at build time)
21
- # ══════════════════════════════════════════════════════════════════════════════
22
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
23
- _tok = None
24
- _mdl = None
25
- _extra: dict = {} # cache for custom models
26
-
27
- def _ensure_main_model():
28
- global _tok, _mdl
29
- if _mdl is not None:
30
- return
31
- print(f"Loading {MODEL_ID}…")
32
- _tok = GPT2Tokenizer.from_pretrained(MODEL_ID)
33
- _tok.pad_token = _tok.eos_token
34
- _mdl = GPT2LMHeadModel.from_pretrained(MODEL_ID).to(device).eval()
35
- _mdl.config.use_cache = True
36
- print(f"βœ“ {MODEL_ID} ready on {device}")
37
-
38
- def load_custom(model_id: str):
39
- if model_id in _extra:
40
- return _extra[model_id]
41
- tok = AutoTokenizer.from_pretrained(model_id)
42
- if tok.pad_token is None:
43
- tok.pad_token = tok.eos_token
44
- mdl = AutoModelForCausalLM.from_pretrained(
45
- model_id,
46
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
47
- ).to(device).eval()
48
- mdl.config.use_cache = True
49
- _extra[model_id] = (mdl, tok)
50
- return mdl, tok
51
-
52
- # ══════════════════════════════════════════════════════════════════════════════
53
- # CHESS HELPERS
54
- # ══════════════════════════════════════════════════════════════════════════════
55
 
56
  def board_to_prompt(board: chess.Board) -> str:
57
  game = chess.pgn.Game()
58
  node = game
59
- for m in board.move_stack:
60
- node = node.add_variation(m)
61
- exp = chess.pgn.StringExporter(headers=False, variations=False, comments=False)
62
- pgn = game.accept(exp).strip()
63
  pgn = re.sub(r"\s*[\*\d][-\d/]*\s*$", "", pgn).strip()
64
- n = board.fullmove_number
65
- pgn += f" {n}." if board.turn == chess.WHITE else f" {n}..."
66
  return f"<|endoftext|>{pgn}"
67
 
68
- def parse_model_move(text: str, board: chess.Board) -> Optional[chess.Move]:
 
69
  text = re.sub(r"^\s*\d+\.+\s*", "", text).strip()
70
- for tok in text.split()[:5]:
71
- t = re.sub(r"[!?+#,;]+$", "", tok)
72
  try:
73
- m = board.parse_san(t)
74
- if m in board.legal_moves: return m
75
- except: pass
 
 
76
  try:
77
- m = chess.Move.from_uci(t.lower()[:4])
78
- if m in board.legal_moves: return m
79
- except: pass
 
 
80
  return None
81
 
 
82
  @torch.no_grad()
83
- def ai_move(board: chess.Board, mdl=None, tok=None):
84
- """Ask a model for its next move. Returns (move, was_legal)."""
85
- if mdl is None:
86
- _ensure_main_model()
87
- mdl, tok = _mdl, _tok
88
- inp = tok(board_to_prompt(board), return_tensors="pt").to(device)
89
- out = mdl.generate(
90
- inp.input_ids, max_new_tokens=12, do_sample=True,
91
- temperature=0.3, top_k=40, top_p=0.9, repetition_penalty=1.1,
92
- pad_token_id=tok.eos_token_id, eos_token_id=tok.eos_token_id,
 
 
 
93
  )
94
- txt = tok.decode(out[0][inp.input_ids.shape[1]:], skip_special_tokens=True)
95
- mv = parse_model_move(txt, board)
96
- if mv: return mv, True
 
 
97
  return random.choice(list(board.legal_moves)), False
98
 
99
- def game_over_status(board: chess.Board, label_w: str, label_b: str) -> tuple[str, bool]:
100
- if board.is_checkmate():
101
- winner = label_w if board.turn == chess.BLACK else label_b
102
- return f"β™Ÿ Checkmate β€” {winner} wins!", True
103
- if board.is_stalemate(): return "Β½ Stalemate β€” Draw", True
104
- if board.is_insufficient_material(): return "Β½ Draw (material)", True
105
- if board.is_seventyfive_moves(): return "Β½ Draw (75 moves)", True
106
- if board.is_fivefold_repetition(): return "Β½ Draw (repetition)", True
107
- if board.is_check():
108
- in_check = label_w if board.turn == chess.WHITE else label_b
109
- return f"⚠ Check on {in_check}!", False
110
- to_move = label_w if board.turn == chess.WHITE else label_b
111
- return f"{to_move} to move…", False
112
-
113
- # ══════════════════════════════════════════════════════════════════════════════
114
- # HALLUCINATION CARD
115
- # ══════════════════════════════════════════════════════════════════════════════
116
-
117
- def _ratio_color(legal: int, halluc: int) -> str:
118
- if halluc == 0: return "#50e090"
119
- if legal / max(halluc, 1) >= 3: return "#50e090"
120
- if legal / max(halluc, 1) >= 1: return "#e0b840"
121
- return "#e05050"
122
-
123
- def halluc_side_html(legal: int, halluc: int, label: str) -> str:
124
- total = legal + halluc
125
- ratio = f"{legal/halluc:.2f}" if halluc > 0 else "∞"
126
- pct = (halluc / total * 100) if total > 0 else 0
127
- col = _ratio_color(legal, halluc)
128
- bar_w = max(2, int(pct)) if total > 0 else 0
129
- return f"""
130
- <div style="flex:1;min-width:0;padding:0 .5rem;">
131
- <div style="font-size:.68em;color:#6a5a38;letter-spacing:.06em;margin-bottom:.3rem;
132
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{label}</div>
133
- <div style="font-size:2em;font-weight:700;color:{col};line-height:1;
134
- font-family:'Cinzel',serif;">{ratio}</div>
135
- <div style="font-size:.75em;color:#7a6a48;margin:.2rem 0 .45rem;">
136
- <span style="color:#e8d5a3">{legal}</span>βœ“&nbsp;
137
- <span style="color:#e8d5a3">{halluc}</span>βœ—&nbsp;
138
- <span style="color:#555">{pct:.0f}% halluc</span>
139
- </div>
140
- <div style="background:#1a1408;border-radius:3px;height:5px;overflow:hidden;">
141
- <div style="height:100%;width:{bar_w}%;background:{col};border-radius:3px;
142
- transition:width .5s ease;"></div>
143
- </div>
144
- </div>"""
145
-
146
- def halluc_html(
147
- legal_w: int, halluc_w: int, label_w: str,
148
- legal_b: int, halluc_b: int, label_b: str,
149
- ) -> str:
150
- return f"""
151
- <div style="background:#0d0a05;border:1px solid #2e1f08;border-radius:8px;
152
- padding:.75rem 1rem;margin-top:.5rem;">
153
- <div style="font-size:.68em;font-family:'Cinzel',serif;letter-spacing:.09em;
154
- color:#5a4a28;margin-bottom:.6rem;">HALLUCINATION RATIO</div>
155
- <div style="display:flex;gap:.5rem;">
156
- {halluc_side_html(legal_w, halluc_w, label_w)}
157
- <div style="width:1px;background:#2e1f08;align-self:stretch;"></div>
158
- {halluc_side_html(legal_b, halluc_b, label_b)}
159
- </div>
160
- </div>"""
161
-
162
- # ══════════════════════════════════════════════════════════════════════════════
163
- # RENDERING
164
- # ══════════════════════════════════════════════════════════════════════════════
165
-
166
- _SVG_COLORS = {
167
- "square light": "#f0d9b5",
168
- "square dark": "#b58863",
169
  "square light lastmove": "#cdd16e",
170
  "square dark lastmove": "#aaa23a",
171
  }
172
 
173
- def render_board(board: chess.Board, last_move=None) -> str:
174
- check_sq = board.king(board.turn) if board.is_check() else None
 
 
 
175
  svg = chess.svg.board(
176
- board, lastmove=last_move, check=check_sq,
177
- size=BOARD_SZ, coordinates=True, colors=_SVG_COLORS,
 
 
 
 
178
  )
179
  return f"""
180
- <div style="display:flex;justify-content:center;align-items:center;
181
- padding:16px;background:radial-gradient(ellipse at center,#1a1208 0%,#0d0d0d 100%);
182
- border-radius:12px;box-shadow:0 0 60px rgba(0,0,0,.8);">
183
- <div style="border-radius:4px;overflow:hidden;
184
- box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 3px #3d2b0e,0 0 0 5px #6b4c1e;">
185
- {svg}
186
- </div>
187
- </div>"""
188
-
189
- def fmt_history(board: chess.Board) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  if not board.move_stack:
191
- return "<em style='color:#555'>No moves yet.</em>"
192
- tmp, lines, mvs, i = chess.Board(), [], list(board.move_stack), 0
193
- while i < len(mvs):
194
- n = tmp.fullmove_number
195
- w = tmp.san(mvs[i]); tmp.push(mvs[i]); i += 1
196
- if i < len(mvs):
197
- b = tmp.san(mvs[i]); tmp.push(mvs[i]); i += 1
 
 
 
 
 
 
 
 
 
 
198
  lines.append(
199
- f"<span style='color:#555;font-size:.78em'>{n}.</span> "
200
- f"<span style='color:#e8d5a3'>{w}</span> "
201
- f"<span style='color:#c4b48a'>{b}</span>")
 
202
  else:
203
  lines.append(
204
- f"<span style='color:#555;font-size:.78em'>{n}.</span> "
205
- f"<span style='color:#e8d5a3'>{w}</span>")
206
- inner = "<br>".join(lines[-16:])
207
- return (f"<div style='font-family:\"Courier New\",monospace;"
208
- f"line-height:2;font-size:.88em'>{inner}</div>")
209
-
210
- def status_html(msg: str) -> str:
211
- return f"<div id='status-bar'>{msg}</div>"
212
-
213
- def log_html(lines: list[str]) -> str:
214
- if not lines: return "<em style='color:#555'>β€”</em>"
215
- items = "".join(f"<div>{l}</div>" for l in lines[-8:])
216
- return (f"<div style='font-family:\"Crimson Text\",serif;"
217
- f"line-height:1.9;font-size:.95em;color:#c8a96e'>{items}</div>")
218
-
219
- # ══════════════════════════════════════════════════════════════════════════════
220
- # MODE 1 β€” AI vs AI (ChessSLM self-play)
221
- # ══════════════════════════════════════════════════════════════════════════════
222
-
223
- def aivsai_run(delay_val: float):
224
- board = chess.Board()
225
- lw, hw, lb, hb = 0, 0, 0, 0
226
- label_w, label_b = "ChessSLM (W)", "ChessSLM (B)"
227
- log_lines = []
228
-
229
- yield (render_board(board),
230
- status_html("Starting…"),
231
- fmt_history(board),
232
- log_html([]),
233
- halluc_html(lw, hw, label_w, lb, hb, label_b))
234
-
235
- while not board.is_game_over() and len(board.move_stack) < 200:
236
- is_white = board.turn == chess.WHITE
237
- mv, legal = ai_move(board)
238
- san = board.san(mv)
239
- board.push(mv)
240
-
241
- if is_white:
242
- if legal: lw += 1
243
- else: hw += 1
244
- tag = label_w
245
  else:
246
- if legal: lb += 1
247
- else: hb += 1
248
- tag = label_b
249
-
250
- flag = "" if legal else " <em style='color:#e05050'>(hallucinated)</em>"
251
- log_lines.append(f"<b>{tag}</b>: {san}{flag}")
252
- stxt, over = game_over_status(board, label_w, label_b)
253
-
254
- yield (render_board(board, mv),
255
- status_html(stxt),
256
- fmt_history(board),
257
- log_html(log_lines),
258
- halluc_html(lw, hw, label_w, lb, hb, label_b))
259
-
260
- if over: return
261
- time.sleep(max(0.05, float(delay_val)))
262
-
263
- if not board.is_game_over():
264
- yield (render_board(board),
265
- status_html("Β½ Draw β€” move limit reached"),
266
- fmt_history(board),
267
- log_html(log_lines),
268
- halluc_html(lw, hw, label_w, lb, hb, label_b))
269
-
270
- # ══════════════════════════════════════════════════════════════════════════════
271
- # MODE 2 β€” MODEL ARENA (ChessSLM vs any HF causal LM)
272
- # ══════════════════════════════════════════════════════════════════════════════
273
-
274
- def arena_load(model_id: str):
275
- mid = model_id.strip()
276
- if not mid:
277
- return status_html("⚠ Enter a model repo ID first"), gr.update()
278
- try:
279
- load_custom(mid)
280
- return (status_html(f"βœ“ <b>{mid}</b> loaded β€” press β–Ά Play"),
281
- gr.update(interactive=True))
282
- except Exception as e:
283
- return status_html(f"βœ— Could not load <em>{mid}</em>: {e}"), gr.update()
284
-
285
- def arena_run(model_id: str, delay_val: float):
286
- mid = model_id.strip()
287
- if not mid:
288
- yield (gr.update(), status_html("⚠ Load a model first"),
289
- gr.update(), gr.update(), gr.update())
290
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  try:
292
- mdl2, tok2 = load_custom(mid)
293
- except Exception as e:
294
- yield (gr.update(), status_html(f"βœ— {e}"),
295
- gr.update(), gr.update(), gr.update())
296
- return
297
-
298
- label_w = "ChessSLM (W)"
299
- label_b = f"{mid.split('/')[-1]} (B)"
300
- board = chess.Board()
301
- lw, hw, lb, hb = 0, 0, 0, 0
302
- log_lines = []
303
-
304
- yield (render_board(board),
305
- status_html(f"{label_w} vs {label_b}"),
306
- fmt_history(board),
307
- log_html([]),
308
- halluc_html(lw, hw, label_w, lb, hb, label_b))
309
-
310
- while not board.is_game_over() and len(board.move_stack) < 200:
311
- if board.turn == chess.WHITE:
312
- mv, legal = ai_move(board)
313
- tag = label_w
314
- if legal: lw += 1
315
- else: hw += 1
316
- else:
317
- mv, legal = ai_move(board, mdl2, tok2)
318
- tag = label_b
319
- if legal: lb += 1
320
- else: hb += 1
321
-
322
- san = board.san(mv)
323
- board.push(mv)
324
- flag = "" if legal else " <em style='color:#e05050'>(hallucinated)</em>"
325
- log_lines.append(f"<b>{tag}</b>: {san}{flag}")
326
- stxt, over = game_over_status(board, label_w, label_b)
327
-
328
- yield (render_board(board, mv),
329
- status_html(stxt),
330
- fmt_history(board),
331
- log_html(log_lines),
332
- halluc_html(lw, hw, label_w, lb, hb, label_b))
333
-
334
- if over: return
335
- time.sleep(max(0.05, float(delay_val)))
336
-
337
- if not board.is_game_over():
338
- yield (render_board(board),
339
- status_html("Β½ Draw β€” move limit reached"),
340
- fmt_history(board),
341
- log_html(log_lines),
342
- halluc_html(lw, hw, label_w, lb, hb, label_b))
343
-
344
- # ══════════════════════════════════════════════════════════════════════════════
345
  # CSS
346
- # ══════════════════════════════════════════════════════════════════════════════
347
 
348
  CSS = """
349
  @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
350
 
351
- *, *::before, *::after { box-sizing: border-box; }
352
-
353
  body, .gradio-container {
354
- background: #0a0805 !important;
355
  color: #e8d5a3 !important;
 
 
 
 
 
356
  font-family: 'Crimson Text', Georgia, serif !important;
357
  }
358
- .gradio-container { max-width: 1180px !important; margin: 0 auto !important; }
359
 
360
- #site-header {
 
 
 
 
 
 
361
  text-align: center;
362
- padding: 2rem 0 1.2rem;
363
- border-bottom: 1px solid #2e1f08;
364
- margin-bottom: 1.4rem;
365
  }
366
 
 
 
 
 
 
 
 
 
 
367
  #status-bar {
368
  text-align: center;
369
  font-family: 'Cinzel', serif;
370
- font-size: 1em;
371
  letter-spacing: 0.05em;
372
  padding: 0.6rem 1rem;
373
- border-radius: 5px;
374
- background: #110e07;
375
- border: 1px solid #3d2b0e;
376
  color: #f0c060;
377
- min-height: 2.6rem;
378
- }
379
-
380
- .tabs { border: none !important; }
381
- .tabitem { background: transparent !important; }
382
- .tab-nav button {
383
- font-family: 'Cinzel', serif !important;
384
- font-size: 0.8em !important;
385
- letter-spacing: 0.07em !important;
386
- color: #8a7a5a !important;
387
- border-bottom: 2px solid transparent !important;
388
- background: transparent !important;
389
- padding: 0.55rem 1.1rem !important;
390
- transition: color .2s, border-color .2s !important;
391
- }
392
- .tab-nav button.selected {
393
- color: #e8c060 !important;
394
- border-bottom-color: #c49020 !important;
395
  }
396
 
 
397
  button.primary {
398
- background: linear-gradient(135deg, #7a5a10, #c4922a, #7a5a10) !important;
399
- border: 1px solid #c49020 !important;
400
  color: #fff8e8 !important;
401
  font-family: 'Cinzel', serif !important;
402
- letter-spacing: 0.07em !important;
403
- font-size: 0.85em !important;
404
  border-radius: 4px !important;
405
- transition: all .2s !important;
406
  }
407
  button.primary:hover {
408
- box-shadow: 0 0 20px rgba(196,144,32,.45) !important;
409
- filter: brightness(1.12) !important;
410
  }
 
411
  button.secondary {
412
- background: #1a140a !important;
413
- border: 1px solid #4a3318 !important;
414
  color: #c8a96e !important;
415
  font-family: 'Cinzel', serif !important;
416
- font-size: 0.82em !important;
417
  border-radius: 4px !important;
418
  }
419
- button.stop {
420
- background: #2a0808 !important;
421
- border: 1px solid #6b2020 !important;
422
- color: #e07070 !important;
423
- }
424
 
425
- input, textarea, select {
426
- background: #130f08 !important;
427
- border: 1px solid #3d2b0e !important;
 
428
  color: #e8d5a3 !important;
429
  font-family: 'Crimson Text', serif !important;
430
- border-radius: 4px !important;
431
- }
432
- label span {
433
- font-family: 'Cinzel', serif !important;
434
- font-size: 0.73em !important;
435
- letter-spacing: 0.07em !important;
436
- text-transform: uppercase !important;
437
- color: #8a6a30 !important;
438
  }
439
 
440
- .panel {
441
- background: #0d0a05;
442
- border: 1px solid #2e1f08;
 
443
  border-radius: 6px;
444
  padding: 0.8rem 1rem;
 
 
 
445
  min-height: 80px;
446
- max-height: 260px;
 
 
 
 
 
 
 
 
 
 
447
  overflow-y: auto;
448
  }
449
 
450
- .div-line { height: 1px; background: #2e1f08; margin: 0.9rem 0; }
 
 
 
 
451
 
452
- ::-webkit-scrollbar { width: 5px; }
453
- ::-webkit-scrollbar-track { background: #0a0805; }
454
- ::-webkit-scrollbar-thumb { background: #4a3318; border-radius: 3px; }
 
 
 
 
 
455
 
456
- input[type=range] { accent-color: #c49020 !important; }
 
 
 
457
  """
458
 
459
- # ══════════════════════════════════════════════════════════════════════════════
460
- # LAYOUT
461
- # ══════════════════════════════════════════════════════════════════════════════
462
 
463
- _BOARD_0 = render_board(chess.Board())
464
- _HALLUC_0 = halluc_html(0, 0, "White", 0, 0, "Black")
465
 
466
- with gr.Blocks(css=CSS, title="ChessSLM Arena") as demo:
467
 
 
468
  gr.HTML("""
469
- <div id="site-header">
470
- <h1 style="font-family:'Cinzel',serif;font-size:2.3em;font-weight:700;
471
- color:#e8c060;text-shadow:0 0 30px rgba(232,180,60,.35);
472
- margin:0 0 .3rem;letter-spacing:.14em;">β™› ChessSLM Arena</h1>
473
- <p style="font-family:'Crimson Text',serif;color:#7a6a48;
474
- font-size:1.05em;font-style:italic;margin:0;">
475
- GPT-2 pre-trained on 100,000 chess games Β· watch the models battle</p>
476
- </div>""")
477
-
 
 
 
 
 
 
 
 
 
 
 
 
478
  with gr.Row():
479
 
480
- # ── Left column: board + status + hallucination card ──────────────────
481
- with gr.Column(scale=3, min_width=480):
482
- board_out = gr.HTML(value=_BOARD_0)
483
- status_out = gr.HTML(value=status_html("Choose a mode and press β–Ά Play"))
484
- halluc_out = gr.HTML(value=_HALLUC_0)
485
-
486
- # ── Right column: tabs ────────────────────────────────────────────────
487
- with gr.Column(scale=2, min_width=300):
488
-
489
- with gr.Tabs():
490
-
491
- # ── Tab 1: AI vs AI ───────────────────────────────────────────
492
- with gr.Tab("πŸ€– AI vs AI"):
493
- gr.HTML("<p style='font-family:Crimson Text,serif;color:#7a6a48;"
494
- "font-style:italic;font-size:.95em;margin:.3rem 0 .8rem;'>"
495
- "ChessSLM plays itself from both sides.</p>")
496
- delay1 = gr.Slider(minimum=0.1, maximum=3.0, value=0.6, step=0.1,
497
- label="Delay between moves (s)")
498
- with gr.Row():
499
- btn1_play = gr.Button("β–Ά Play", variant="primary")
500
- btn1_stop = gr.Button("⏹ Stop", variant="secondary",
501
- elem_classes=["stop"])
502
-
503
- # ── Tab 2: Model Arena ────────────────────────────────────────
504
- with gr.Tab("βš”οΈ Model Arena"):
505
- gr.HTML("<p style='font-family:Crimson Text,serif;color:#7a6a48;"
506
- "font-style:italic;font-size:.95em;margin:.3rem 0 .8rem;'>"
507
- "ChessSLM (White) vs any HuggingFace causal LM (Black).</p>")
508
- arena_id = gr.Textbox(
509
- placeholder="e.g. another-user/chess-gpt",
510
- label="Opponent model repo ID",
511
- )
512
- btn_load = gr.Button("⬇ Load Opponent", variant="secondary")
513
- gr.HTML("<div class='div-line'></div>")
514
- delay2 = gr.Slider(minimum=0.1, maximum=3.0, value=0.6, step=0.1,
515
- label="Delay between moves (s)")
516
- with gr.Row():
517
- btn2_play = gr.Button("β–Ά Play", variant="primary",
518
- interactive=False)
519
- btn2_stop = gr.Button("⏹ Stop", variant="secondary",
520
- elem_classes=["stop"])
521
-
522
- gr.HTML("<div class='div-line'></div>")
523
- gr.HTML("<p style='font-family:Cinzel,serif;font-size:.73em;"
524
- "color:#8a6a30;letter-spacing:.08em;margin:0 0 .4rem;'>COMMENTARY</p>")
525
- log_out = gr.HTML(
526
- value="<div class='panel'><em style='color:#555'>β€”</em></div>")
527
-
528
- gr.HTML("<div class='div-line'></div>")
529
- gr.HTML("<p style='font-family:Cinzel,serif;font-size:.73em;"
530
- "color:#8a6a30;letter-spacing:.08em;margin:0 0 .4rem;'>MOVE LIST</p>")
531
- hist_out = gr.HTML(
532
- value="<div class='panel'><em style='color:#555'>No moves yet.</em></div>")
533
 
534
- gr.HTML("""
535
- <div style="text-align:center;margin-top:1.6rem;padding-top:.9rem;
536
- border-top:1px solid #1e1508;font-family:'Crimson Text',serif;
537
- font-size:.82em;color:#4a3a20;">
538
- <a href="https://huggingface.co/FlameF0X/ChessSLM" target="_blank"
539
- style="color:#7a5a28;text-decoration:none;">FlameF0X/ChessSLM</a>
540
- &nbsp;Β·&nbsp; GPT-2 Β· 100K PGN games Β· top-k sampling (temp=0.3)
541
- </div>""")
 
 
 
 
 
 
 
 
542
 
543
- # ── Event wiring ──────────────────────────────────────────────────────────
544
- _outs = [board_out, status_out, hist_out, log_out, halluc_out]
 
 
 
 
545
 
546
- ev1 = btn1_play.click(fn=aivsai_run, inputs=[delay1], outputs=_outs)
547
- btn1_stop.click(fn=None, cancels=[ev1])
548
 
549
- btn_load.click(fn=arena_load, inputs=[arena_id],
550
- outputs=[status_out, btn2_play])
551
 
552
- ev2 = btn2_play.click(fn=arena_run, inputs=[arena_id, delay2], outputs=_outs)
553
- btn2_stop.click(fn=None, cancels=[ev2])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
 
556
  if __name__ == "__main__":
 
1
  """
2
+ ChessSLM β€” Play against FlameF0X/ChessSLM
3
+ Hugging Face Space | Gradio app
4
  """
5
 
6
+ import re
7
+ import random
8
+ import time
9
+ import chess
10
+ import chess.svg
11
+ import chess.pgn
12
+ import gradio as gr
13
+ import torch
14
+ from transformers import GPT2LMHeadModel, GPT2Tokenizer
15
+
16
+ # ──────────────────────────────────────────────────────────────────────────────
17
+ # Model loading (once at startup)
18
+ # ──────────────────────────────────────────────────────────────────────────────
19
 
 
 
 
20
  MODEL_ID = "FlameF0X/ChessSLM"
21
+
22
+ print(f"Loading {MODEL_ID}...")
23
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
24
+ tokenizer = GPT2Tokenizer.from_pretrained(MODEL_ID)
25
+ tokenizer.pad_token = tokenizer.eos_token
26
+ model = GPT2LMHeadModel.from_pretrained(MODEL_ID)
27
+ model.to(device)
28
+ model.eval()
29
+ model.config.use_cache = True
30
+ print(f"βœ“ Model ready on {device}")
31
+
32
+ # ──────────────────────────────────────────────────────────────────────────────
33
+ # Chess / model logic
34
+ # ──────────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  def board_to_prompt(board: chess.Board) -> str:
37
  game = chess.pgn.Game()
38
  node = game
39
+ for move in board.move_stack:
40
+ node = node.add_variation(move)
41
+ exporter = chess.pgn.StringExporter(headers=False, variations=False, comments=False)
42
+ pgn = game.accept(exporter).strip()
43
  pgn = re.sub(r"\s*[\*\d][-\d/]*\s*$", "", pgn).strip()
44
+ full_move = board.fullmove_number
45
+ pgn += f" {full_move}." if board.turn == chess.WHITE else f" {full_move}..."
46
  return f"<|endoftext|>{pgn}"
47
 
48
+
49
+ def extract_move(text: str, board: chess.Board):
50
  text = re.sub(r"^\s*\d+\.+\s*", "", text).strip()
51
+ for token in text.split()[:5]:
52
+ clean = re.sub(r"[!?+#,;]+$", "", token)
53
  try:
54
+ move = board.parse_san(clean)
55
+ if move in board.legal_moves:
56
+ return move
57
+ except Exception:
58
+ pass
59
  try:
60
+ move = chess.Move.from_uci(clean.lower()[:4])
61
+ if move in board.legal_moves:
62
+ return move
63
+ except Exception:
64
+ pass
65
  return None
66
 
67
+
68
  @torch.no_grad()
69
+ def get_model_move(board: chess.Board):
70
+ prompt = board_to_prompt(board)
71
+ inputs = tokenizer(prompt, return_tensors="pt").to(device)
72
+ outputs = model.generate(
73
+ inputs.input_ids,
74
+ max_new_tokens=12,
75
+ do_sample=True,
76
+ temperature=0.3,
77
+ top_k=40,
78
+ top_p=0.9,
79
+ repetition_penalty=1.1,
80
+ pad_token_id=tokenizer.eos_token_id,
81
+ eos_token_id=tokenizer.eos_token_id,
82
  )
83
+ new_tokens = outputs[0][inputs.input_ids.shape[1]:]
84
+ generated = tokenizer.decode(new_tokens, skip_special_tokens=True)
85
+ move = extract_move(generated, board)
86
+ if move:
87
+ return move, True
88
  return random.choice(list(board.legal_moves)), False
89
 
90
+ # ──────────────────────────────────────────────────────────────────────────────
91
+ # Board rendering
92
+ # ──────────────────────────────────────────────────────────────────────────────
93
+
94
+ PIECE_COLORS = {
95
+ "square light": "#f0d9b5",
96
+ "square dark": "#b58863",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  "square light lastmove": "#cdd16e",
98
  "square dark lastmove": "#aaa23a",
99
  }
100
 
101
+ def render_board_html(board: chess.Board, last_move=None, flipped=False, size=480):
102
+ check_square = None
103
+ if board.is_check():
104
+ check_square = board.king(board.turn)
105
+
106
  svg = chess.svg.board(
107
+ board,
108
+ lastmove=last_move,
109
+ check=check_square,
110
+ flipped=flipped,
111
+ size=size,
112
+ colors=PIECE_COLORS,
113
  )
114
  return f"""
115
+ <div style="
116
+ display:flex; justify-content:center; align-items:center;
117
+ padding: 16px;
118
+ background: radial-gradient(ellipse at center, #1a1208 0%, #0d0d0d 100%);
119
+ border-radius: 12px;
120
+ box-shadow: 0 0 60px rgba(0,0,0,0.8), inset 0 0 30px rgba(0,0,0,0.4);
121
+ ">
122
+ <div style="
123
+ border-radius: 4px;
124
+ overflow: hidden;
125
+ box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 3px #3d2b0e, 0 0 0 5px #6b4c1e;
126
+ ">
127
+ {svg}
128
+ </div>
129
+ </div>
130
+ """
131
+
132
+
133
+ def get_legal_moves_san(board: chess.Board):
134
+ moves = []
135
+ for move in board.legal_moves:
136
+ try:
137
+ moves.append(board.san(move))
138
+ except Exception:
139
+ pass
140
+ return sorted(moves)
141
+
142
+
143
+ def format_move_history(board: chess.Board):
144
  if not board.move_stack:
145
+ return "<em style='color:#666'>No moves yet.</em>"
146
+
147
+ temp = chess.Board()
148
+ lines = []
149
+ moves = list(board.move_stack)
150
+ i = 0
151
+
152
+ while i < len(moves):
153
+ move_num = temp.fullmove_number
154
+ white_san = temp.san(moves[i])
155
+ temp.push(moves[i])
156
+ i += 1
157
+
158
+ if i < len(moves):
159
+ black_san = temp.san(moves[i])
160
+ temp.push(moves[i])
161
+ i += 1
162
  lines.append(
163
+ f"<span style='color:#8a7a5a;font-size:0.8em'>{move_num}.</span> "
164
+ f"<span style='color:#e8d5a3'>{white_san}</span> "
165
+ f"<span style='color:#c4b48a'>{black_san}</span>"
166
+ )
167
  else:
168
  lines.append(
169
+ f"<span style='color:#8a7a5a;font-size:0.8em'>{move_num}.</span> "
170
+ f"<span style='color:#e8d5a3'>{white_san}</span>"
171
+ )
172
+
173
+ # Last 10 pairs
174
+ visible = lines[-10:]
175
+ html = "<div style='font-family:\"Courier New\",monospace; line-height:2; font-size:0.92em;'>"
176
+ html += "<br>".join(visible)
177
+ html += "</div>"
178
+ return html
179
+
180
+
181
+ def game_status(board: chess.Board, player_color: str):
182
+ if board.is_checkmate():
183
+ loser = "White" if board.turn == chess.WHITE else "Black"
184
+ winner = "Black" if board.turn == chess.WHITE else "White"
185
+ if (winner == "White") == (player_color == "white"):
186
+ return "β™Ÿ Checkmate β€” You win! πŸŽ‰", "win"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  else:
188
+ return "β™Ÿ Checkmate β€” ChessSLM wins!", "loss"
189
+ if board.is_stalemate():
190
+ return "Β½ Stalemate β€” Draw", "draw"
191
+ if board.is_insufficient_material():
192
+ return "Β½ Insufficient material β€” Draw", "draw"
193
+ if board.is_seventyfive_moves():
194
+ return "Β½ 75-move rule β€” Draw", "draw"
195
+ if board.is_fivefold_repetition():
196
+ return "Β½ Fivefold repetition β€” Draw", "draw"
197
+ if board.is_check():
198
+ return "⚠ Check!", "check"
199
+ whose = "Your turn" if (board.turn == chess.WHITE) == (player_color == "white") else "ChessSLM is thinking..."
200
+ return whose, "playing"
201
+
202
+ # ──────────────────────────────────────────────────────────────────────────────
203
+ # Gradio callbacks
204
+ # ──────────────────────────────────────────────────────────────────────────────
205
+
206
+ def new_game(player_color_choice: str):
207
+ """Reset the board and, if player chose Black, let the model move first."""
208
+ board = chess.Board()
209
+ player_color = "white" if player_color_choice == "⬜ White (move first)" else "black"
210
+ flipped = (player_color == "black")
211
+ last_move = None
212
+ log_lines = []
213
+
214
+ # If player chose Black, model plays White first
215
+ if player_color == "black":
216
+ move, legal = get_model_move(board)
217
+ san = board.san(move)
218
+ board.push(move)
219
+ last_move = move
220
+ log_lines.append(f"ChessSLM opens with **{san}**")
221
+
222
+ legal_moves = get_legal_moves_san(board)
223
+ status_text, _ = game_status(board, player_color)
224
+ board_html = render_board_html(board, last_move=last_move, flipped=flipped)
225
+ history_html = format_move_history(board)
226
+ log_html = "<br>".join(log_lines) if log_lines else "<em style='color:#666'>Game started.</em>"
227
+
228
+ # Serialise state
229
+ state = {
230
+ "fen": board.fen(),
231
+ "move_stack": [m.uci() for m in board.move_stack],
232
+ "player_color": player_color,
233
+ "last_move_uci": last_move.uci() if last_move else None,
234
+ "game_over": False,
235
+ }
236
+
237
+ return (
238
+ board_html,
239
+ gr.Dropdown(choices=legal_moves, value=None, interactive=True, label="Your move"),
240
+ status_text,
241
+ history_html,
242
+ log_html,
243
+ state,
244
+ )
245
+
246
+
247
+ def make_player_move(move_san: str, state: dict):
248
+ """Apply the player's chosen move, then let the model respond."""
249
+ if not state or state.get("game_over"):
250
+ return (
251
+ gr.update(), gr.update(), "Game is over. Start a new game.", gr.update(), gr.update(), state
252
+ )
253
+ if not move_san:
254
+ return (
255
+ gr.update(), gr.update(), "Please select a move first.", gr.update(), gr.update(), state
256
+ )
257
+
258
+ # Rebuild board from state
259
+ board = chess.Board()
260
+ for uci in state["move_stack"]:
261
+ board.push(chess.Move.from_uci(uci))
262
+
263
+ player_color = state["player_color"]
264
+ flipped = (player_color == "black")
265
+ log_lines = []
266
+
267
+ # Apply player move
268
  try:
269
+ player_move = board.parse_san(move_san)
270
+ except Exception:
271
+ return (
272
+ gr.update(), gr.update(), f"Invalid move: {move_san}", gr.update(), gr.update(), state
273
+ )
274
+
275
+ board.push(player_move)
276
+ log_lines.append(f"You played **{move_san}**")
277
+ last_move = player_move
278
+
279
+ # Check game over after player move
280
+ status_text, status_key = game_status(board, player_color)
281
+ game_over = status_key in ("win", "loss", "draw")
282
+
283
+ model_san = None
284
+ if not game_over:
285
+ # Model responds
286
+ model_move_obj, legal = get_model_move(board)
287
+ model_san = board.san(model_move_obj)
288
+ board.push(model_move_obj)
289
+ last_move = model_move_obj
290
+ flag = "" if legal else " *(random fallback)*"
291
+ log_lines.append(f"ChessSLM plays **{model_san}**{flag}")
292
+
293
+ status_text, status_key = game_status(board, player_color)
294
+ game_over = status_key in ("win", "loss", "draw")
295
+
296
+ # Update state
297
+ state = {
298
+ "fen": board.fen(),
299
+ "move_stack": [m.uci() for m in board.move_stack],
300
+ "player_color": player_color,
301
+ "last_move_uci": last_move.uci() if last_move else None,
302
+ "game_over": game_over,
303
+ }
304
+
305
+ legal_moves = [] if game_over else get_legal_moves_san(board)
306
+ board_html = render_board_html(board, last_move=last_move, flipped=flipped)
307
+ history_html = format_move_history(board)
308
+ log_html = "<br>".join(
309
+ [f"<span style='color:#c8a96e'>{l}</span>" for l in log_lines]
310
+ )
311
+
312
+ return (
313
+ board_html,
314
+ gr.Dropdown(choices=legal_moves, value=None, interactive=not game_over, label="Your move"),
315
+ status_text,
316
+ history_html,
317
+ log_html,
318
+ state,
319
+ )
320
+
321
+ # ──────────────────────────────────────────────────────────────────────────────
322
  # CSS
323
+ # ──────────────────────────────────────────────────────────────────────────────
324
 
325
  CSS = """
326
  @import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&display=swap');
327
 
 
 
328
  body, .gradio-container {
329
+ background: #0d0d0d !important;
330
  color: #e8d5a3 !important;
331
+ }
332
+
333
+ .gradio-container {
334
+ max-width: 1100px !important;
335
+ margin: 0 auto !important;
336
  font-family: 'Crimson Text', Georgia, serif !important;
337
  }
 
338
 
339
+ h1, h2, h3 {
340
+ font-family: 'Cinzel', serif !important;
341
+ letter-spacing: 0.08em;
342
+ }
343
+
344
+ /* Title */
345
+ #title-block {
346
  text-align: center;
347
+ padding: 2rem 0 1rem;
348
+ border-bottom: 1px solid #3d2b0e;
349
+ margin-bottom: 1.5rem;
350
  }
351
 
352
+ /* Panels */
353
+ .panel {
354
+ background: #141008 !important;
355
+ border: 1px solid #3d2b0e !important;
356
+ border-radius: 8px !important;
357
+ padding: 1rem !important;
358
+ }
359
+
360
+ /* Status bar */
361
  #status-bar {
362
  text-align: center;
363
  font-family: 'Cinzel', serif;
364
+ font-size: 1.1em;
365
  letter-spacing: 0.05em;
366
  padding: 0.6rem 1rem;
367
+ border-radius: 6px;
368
+ background: #1a1208;
369
+ border: 1px solid #4a3520;
370
  color: #f0c060;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
 
373
+ /* Buttons */
374
  button.primary {
375
+ background: linear-gradient(135deg, #8b6914 0%, #c4922a 50%, #8b6914 100%) !important;
376
+ border: 1px solid #d4a843 !important;
377
  color: #fff8e8 !important;
378
  font-family: 'Cinzel', serif !important;
379
+ letter-spacing: 0.06em !important;
380
+ font-size: 0.9em !important;
381
  border-radius: 4px !important;
382
+ transition: all 0.2s ease !important;
383
  }
384
  button.primary:hover {
385
+ background: linear-gradient(135deg, #a07820 0%, #d4a843 50%, #a07820 100%) !important;
386
+ box-shadow: 0 0 16px rgba(212,168,67,0.4) !important;
387
  }
388
+
389
  button.secondary {
390
+ background: #1e1810 !important;
391
+ border: 1px solid #5a4020 !important;
392
  color: #c8a96e !important;
393
  font-family: 'Cinzel', serif !important;
394
+ letter-spacing: 0.04em !important;
395
  border-radius: 4px !important;
396
  }
 
 
 
 
 
397
 
398
+ /* Dropdown */
399
+ select, .gr-dropdown select {
400
+ background: #1a1208 !important;
401
+ border: 1px solid #5a4020 !important;
402
  color: #e8d5a3 !important;
403
  font-family: 'Crimson Text', serif !important;
404
+ font-size: 1em !important;
 
 
 
 
 
 
 
405
  }
406
 
407
+ /* Move log */
408
+ #move-log {
409
+ background: #0f0c06 !important;
410
+ border: 1px solid #3d2b0e !important;
411
  border-radius: 6px;
412
  padding: 0.8rem 1rem;
413
+ font-family: 'Crimson Text', serif;
414
+ font-size: 0.95em;
415
+ line-height: 1.8;
416
  min-height: 80px;
417
+ color: #c8a96e;
418
+ }
419
+
420
+ /* History panel */
421
+ #history-panel {
422
+ background: #0f0c06 !important;
423
+ border: 1px solid #3d2b0e !important;
424
+ border-radius: 6px;
425
+ padding: 0.8rem 1rem;
426
+ min-height: 200px;
427
+ max-height: 320px;
428
  overflow-y: auto;
429
  }
430
 
431
+ /* Radio buttons */
432
+ .gr-radio label {
433
+ color: #e8d5a3 !important;
434
+ font-family: 'Crimson Text', serif !important;
435
+ }
436
 
437
+ /* Labels */
438
+ label span {
439
+ color: #a08050 !important;
440
+ font-family: 'Cinzel', serif !important;
441
+ font-size: 0.8em !important;
442
+ letter-spacing: 0.06em !important;
443
+ text-transform: uppercase !important;
444
+ }
445
 
446
+ /* Scrollbar */
447
+ ::-webkit-scrollbar { width: 6px; }
448
+ ::-webkit-scrollbar-track { background: #0d0d0d; }
449
+ ::-webkit-scrollbar-thumb { background: #5a4020; border-radius: 3px; }
450
  """
451
 
452
+ # ──────────────────────────────────────────────────────────────────────────────
453
+ # Layout
454
+ # ──────────────────────────────────────────────────────────────────────────────
455
 
456
+ with gr.Blocks(css=CSS, title="ChessSLM β€” Play vs AI") as demo:
 
457
 
458
+ state = gr.State({})
459
 
460
+ # ── Header ────────────────────────────────────────────────────────────────
461
  gr.HTML("""
462
+ <div id="title-block">
463
+ <h1 style="
464
+ font-family:'Cinzel',serif;
465
+ font-size:2.4em;
466
+ font-weight:700;
467
+ color:#e8c96e;
468
+ text-shadow: 0 0 30px rgba(232,180,80,0.4);
469
+ margin:0 0 0.3rem;
470
+ letter-spacing:0.12em;
471
+ ">β™› ChessSLM</h1>
472
+ <p style="
473
+ font-family:'Crimson Text',serif;
474
+ color:#8a7a5a;
475
+ font-size:1.1em;
476
+ font-style:italic;
477
+ margin:0;
478
+ ">Play against a GPT-2 model trained on 100,000 chess games</p>
479
+ </div>
480
+ """)
481
+
482
+ # ── Main layout ───────────────────────────────────────────────────────────
483
  with gr.Row():
484
 
485
+ # Left: board
486
+ with gr.Column(scale=3):
487
+ board_display = gr.HTML(
488
+ value=render_board_html(chess.Board()),
489
+ label="Board"
490
+ )
491
+ status_display = gr.HTML(
492
+ value="<div id='status-bar'>Choose your colour and press New Game</div>"
493
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
 
495
+ # Right: controls + history
496
+ with gr.Column(scale=2):
497
+
498
+ gr.HTML("<h3 style='font-family:Cinzel,serif;color:#c8a96e;font-size:1em;letter-spacing:0.1em;margin:0 0 0.5rem;'>NEW GAME</h3>")
499
+
500
+ color_choice = gr.Radio(
501
+ choices=["⬜ White (move first)", "⬛ Black (move second)"],
502
+ value="⬜ White (move first)",
503
+ label="Play as",
504
+ )
505
+
506
+ new_game_btn = gr.Button("β™Ÿ New Game", variant="primary", size="lg")
507
+
508
+ gr.HTML("<div style='height:1px;background:#3d2b0e;margin:1rem 0;'></div>")
509
+
510
+ gr.HTML("<h3 style='font-family:Cinzel,serif;color:#c8a96e;font-size:1em;letter-spacing:0.1em;margin:0 0 0.5rem;'>YOUR MOVE</h3>")
511
 
512
+ move_dropdown = gr.Dropdown(
513
+ choices=[],
514
+ value=None,
515
+ label="Select move (SAN notation)",
516
+ interactive=False,
517
+ )
518
 
519
+ move_btn = gr.Button("β–Ά Make Move", variant="secondary")
 
520
 
521
+ gr.HTML("<div style='height:1px;background:#3d2b0e;margin:1rem 0;'></div>")
 
522
 
523
+ gr.HTML("<h3 style='font-family:Cinzel,serif;color:#c8a96e;font-size:1em;letter-spacing:0.1em;margin:0 0 0.5rem;'>MOVE LOG</h3>")
524
+
525
+ log_display = gr.HTML(
526
+ value="<div id='move-log'><em style='color:#555'>Start a new game to begin.</em></div>",
527
+ )
528
+
529
+ gr.HTML("<div style='height:1px;background:#3d2b0e;margin:1rem 0;'></div>")
530
+
531
+ gr.HTML("<h3 style='font-family:Cinzel,serif;color:#c8a96e;font-size:1em;letter-spacing:0.1em;margin:0 0 0.5rem;'>GAME HISTORY</h3>")
532
+
533
+ history_display = gr.HTML(
534
+ value="<div id='history-panel'><em style='color:#555'>No moves yet.</em></div>",
535
+ )
536
+
537
+ # ── Footer ────────────────────────────────────────────────────────────────
538
+ gr.HTML("""
539
+ <div style="
540
+ text-align:center; margin-top:2rem; padding-top:1rem;
541
+ border-top:1px solid #2a1e0a;
542
+ font-family:'Crimson Text',serif; font-size:0.85em; color:#5a4a30;
543
+ ">
544
+ Model: <a href="https://huggingface.co/FlameF0X/ChessSLM" target="_blank"
545
+ style="color:#8a6a30; text-decoration:none;">FlameF0X/ChessSLM</a>
546
+ &nbsp;Β·&nbsp; GPT-2 pre-trained on 100K PGN games
547
+ &nbsp;Β·&nbsp; Move selection uses top-k sampling (temp=0.3)
548
+ </div>
549
+ """)
550
+
551
+ # ── Wiring ────────────────────────────────────────────────────────────────
552
+ new_game_btn.click(
553
+ fn=new_game,
554
+ inputs=[color_choice],
555
+ outputs=[board_display, move_dropdown, status_display, history_display, log_display, state],
556
+ )
557
+
558
+ move_btn.click(
559
+ fn=make_player_move,
560
+ inputs=[move_dropdown, state],
561
+ outputs=[board_display, move_dropdown, status_display, history_display, log_display, state],
562
+ )
563
+
564
+ move_dropdown.select(
565
+ fn=make_player_move,
566
+ inputs=[move_dropdown, state],
567
+ outputs=[board_display, move_dropdown, status_display, history_display, log_display, state],
568
+ )
569
 
570
 
571
  if __name__ == "__main__":