FlameF0X commited on
Commit
9633f26
Β·
verified Β·
1 Parent(s): 6dd0494

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -373
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- ChessSLM β€” Play, Watch AI vs AI, or Battle Custom Models
3
  HF Space | Gradio 4
4
  """
5
 
@@ -14,15 +14,15 @@ from transformers import (
14
  # CONFIG
15
  # ══════════════════════════════════════════════════════════════════════════════
16
  MODEL_ID = "FlameF0X/ChessSLM"
17
- BOARD_SZ = 400
18
 
19
  # ══════════════════════════════════════════════════════════════════════════════
20
- # MODEL LOADING (lazy β€” happens 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 = {}
26
 
27
  def _ensure_main_model():
28
  global _tok, _mdl
@@ -35,16 +35,24 @@ def _ensure_main_model():
35
  _mdl.config.use_cache = True
36
  print(f"βœ“ {MODEL_ID} ready on {device}")
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  # ══════════════════════════════════════════════════════════════════════════════
39
  # CHESS HELPERS
40
  # ══════════════════════════════════════════════════════════════════════════════
41
 
42
- def board_from_state(s: dict) -> chess.Board:
43
- b = chess.Board()
44
- for uci in s.get("move_stack", []):
45
- b.push(chess.Move.from_uci(uci))
46
- return b
47
-
48
  def board_to_prompt(board: chess.Board) -> str:
49
  game = chess.pgn.Game()
50
  node = game
@@ -73,7 +81,7 @@ def parse_model_move(text: str, board: chess.Board) -> Optional[chess.Move]:
73
 
74
  @torch.no_grad()
75
  def ai_move(board: chess.Board, mdl=None, tok=None):
76
- """Returns (move, was_legal)."""
77
  if mdl is None:
78
  _ensure_main_model()
79
  mdl, tok = _mdl, _tok
@@ -88,119 +96,66 @@ def ai_move(board: chess.Board, mdl=None, tok=None):
88
  if mv: return mv, True
89
  return random.choice(list(board.legal_moves)), False
90
 
91
- def load_custom(model_id: str):
92
- if model_id in _extra: return _extra[model_id]
93
- tok = AutoTokenizer.from_pretrained(model_id)
94
- if tok.pad_token is None: tok.pad_token = tok.eos_token
95
- mdl = AutoModelForCausalLM.from_pretrained(
96
- model_id,
97
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
98
- ).to(device).eval()
99
- mdl.config.use_cache = True
100
- _extra[model_id] = (mdl, tok)
101
- return mdl, tok
102
-
103
- def legal_moves_san(board: chess.Board) -> list[str]:
104
- return sorted(board.san(m) for m in board.legal_moves)
105
-
106
- def game_over_status(board: chess.Board, player_color: Optional[str],
107
- label_a="ChessSLM", label_b="Opponent") -> tuple[str, bool]:
108
  if board.is_checkmate():
109
- winner_white = board.turn == chess.BLACK
110
- winner_name = label_a if winner_white else label_b
111
- if player_color:
112
- if winner_white == (player_color == "white"):
113
- return "β™Ÿ Checkmate β€” You win! πŸŽ‰", True
114
- return f"β™Ÿ Checkmate β€” {label_a} wins!", True
115
- return f"β™Ÿ Checkmate β€” {winner_name} wins!", True
116
  if board.is_stalemate(): return "Β½ Stalemate β€” Draw", True
117
  if board.is_insufficient_material(): return "Β½ Draw (material)", True
118
  if board.is_seventyfive_moves(): return "Β½ Draw (75 moves)", True
119
  if board.is_fivefold_repetition(): return "Β½ Draw (repetition)", True
120
- if board.is_check(): return "⚠ Check!", False
121
- if player_color is None:
122
- who = label_a if board.turn == chess.WHITE else label_b
123
- return f"{who} to move…", False
124
- yours = (board.turn == chess.WHITE) == (player_color == "white")
125
- return ("Your turn", f"{label_a} thinking…")[not yours], False
126
 
127
  # ══════════════════════════════════════════════════════════════════════════════
128
- # HALLUCINATION STATS
129
  # ══════════════════════════════════════════════════════════════════════════════
130
 
131
- def halluc_html(legal: int, halluc: int, label="ChessSLM") -> str:
132
- """Render a K/D-style hallucination ratio card."""
 
 
 
 
 
133
  total = legal + halluc
134
  ratio = f"{legal/halluc:.2f}" if halluc > 0 else "∞"
135
  pct = (halluc / total * 100) if total > 0 else 0
136
- # colour the ratio: green if >3, amber if 1-3, red if <1
137
- if halluc == 0: col = "#50e090"
138
- elif legal / max(halluc,1) >= 3: col = "#50e090"
139
- elif legal / max(halluc,1) >= 1: col = "#e0b840"
140
- else: col = "#e05050"
141
-
142
- bar_w = max(2, int(pct)) # fill = halluc share
143
  return f"""
144
- <div style="
145
- background:#0d0a05;border:1px solid #2e1f08;border-radius:8px;
146
- padding:.7rem 1rem;font-family:'Cinzel',serif;
147
- ">
148
- <div style="font-size:.7em;letter-spacing:.08em;color:#6a5a38;margin-bottom:.4rem;">
149
- HALLUCINATION RATIO Β· {label}
150
- </div>
151
- <div style="display:flex;align-items:baseline;gap:.6rem;margin-bottom:.5rem;">
152
- <span style="font-size:1.9em;font-weight:700;color:{col};line-height:1">{ratio}</span>
153
- <span style="font-size:.8em;color:#7a6a48;">legal / hallucinated</span>
154
- </div>
155
- <div style="display:flex;gap:1.2rem;font-size:.82em;margin-bottom:.55rem;">
156
- <span>βœ“ <b style="color:#e8d5a3">{legal}</b> <span style="color:#555">legal</span></span>
157
- <span>βœ— <b style="color:#e8d5a3">{halluc}</b> <span style="color:#555">hallucinated</span></span>
158
- <span style="color:#555">{pct:.1f}% halluc rate</span>
159
  </div>
160
  <div style="background:#1a1408;border-radius:3px;height:5px;overflow:hidden;">
161
  <div style="height:100%;width:{bar_w}%;background:{col};border-radius:3px;
162
- transition:width .4s ease;"></div>
163
  </div>
164
  </div>"""
165
 
166
- def halluc_html_battle(
167
- legal_a: int, halluc_a: int, label_a: str,
168
  legal_b: int, halluc_b: int, label_b: str,
169
  ) -> str:
170
- """Two-column hallucination card for battle / aivsai modes."""
171
- def side(legal, halluc, label):
172
- total = legal + halluc
173
- ratio = f"{legal/halluc:.2f}" if halluc > 0 else "∞"
174
- pct = (halluc / total * 100) if total > 0 else 0
175
- if halluc == 0: col = "#50e090"
176
- elif legal / max(halluc,1) >= 3: col = "#50e090"
177
- elif legal / max(halluc,1) >= 1: col = "#e0b840"
178
- else: col = "#e05050"
179
- bar_w = max(2, int(pct)) if total > 0 else 0
180
- return f"""
181
- <div style="flex:1;min-width:0">
182
- <div style="font-size:.68em;color:#6a5a38;letter-spacing:.06em;
183
- margin-bottom:.25rem;white-space:nowrap;overflow:hidden;
184
- text-overflow:ellipsis">{label}</div>
185
- <div style="font-size:1.6em;font-weight:700;color:{col};line-height:1.1">{ratio}</div>
186
- <div style="font-size:.76em;color:#7a6a48;margin:.15rem 0 .4rem">
187
- {legal}βœ“ Β· {halluc}βœ— Β· {pct:.0f}%
188
- </div>
189
- <div style="background:#1a1408;border-radius:3px;height:4px;">
190
- <div style="height:100%;width:{bar_w}%;background:{col};border-radius:3px;
191
- transition:width .4s ease;"></div>
192
- </div>
193
- </div>"""
194
  return f"""
195
  <div style="background:#0d0a05;border:1px solid #2e1f08;border-radius:8px;
196
- padding:.7rem 1rem;font-family:'Cinzel',serif;">
197
- <div style="font-size:.7em;letter-spacing:.08em;color:#6a5a38;margin-bottom:.5rem;">
198
- HALLUCINATION RATIO
199
- </div>
200
- <div style="display:flex;gap:1.2rem;">
201
- {side(legal_a, halluc_a, label_a)}
202
- <div style="width:1px;background:#2e1f08;"></div>
203
- {side(legal_b, halluc_b, label_b)}
204
  </div>
205
  </div>"""
206
 
@@ -215,12 +170,11 @@ _SVG_COLORS = {
215
  "square dark lastmove": "#aaa23a",
216
  }
217
 
218
- def render_board(board: chess.Board, last_move=None, flipped=False) -> str:
219
  check_sq = board.king(board.turn) if board.is_check() else None
220
  svg = chess.svg.board(
221
  board, lastmove=last_move, check=check_sq,
222
- flipped=flipped, size=BOARD_SZ,
223
- coordinates=True, colors=_SVG_COLORS,
224
  )
225
  return f"""
226
  <div style="display:flex;justify-content:center;align-items:center;
@@ -249,227 +203,143 @@ def fmt_history(board: chess.Board) -> str:
249
  lines.append(
250
  f"<span style='color:#555;font-size:.78em'>{n}.</span> "
251
  f"<span style='color:#e8d5a3'>{w}</span>")
252
- inner = "<br>".join(lines[-14:])
253
- return f"<div style='font-family:\"Courier New\",mono;line-height:2;font-size:.88em'>{inner}</div>"
 
254
 
255
  def status_html(msg: str) -> str:
256
  return f"<div id='status-bar'>{msg}</div>"
257
 
258
  def log_html(lines: list[str]) -> str:
259
  if not lines: return "<em style='color:#555'>β€”</em>"
260
- items = "".join(f"<div>{l}</div>" for l in lines[-6:])
261
- return f"<div style='font-family:\"Crimson Text\",serif;line-height:1.8;font-size:.95em;color:#c8a96e'>{items}</div>"
262
-
263
- # ══════════════════════════════════════════════════════════════════════════════
264
- # MODE 1 β€” HUMAN vs ChessSLM
265
- # ══════════════════════════════════════════════════════════════════════════════
266
-
267
- def hvai_new_game(color_choice: str):
268
- board = chess.Board()
269
- player_color = "white" if "White" in color_choice else "black"
270
- flipped = (player_color == "black")
271
- last_move = None
272
- log_lines = []
273
- state = {
274
- "mode": "hvai", "move_stack": [], "player_color": player_color,
275
- "flipped": flipped, "last_move_uci": None, "game_over": False,
276
- "legal_moves": 0, "halluc_moves": 0,
277
- }
278
-
279
- if player_color == "black":
280
- mv, legal_flag = ai_move(board)
281
- san = board.san(mv); board.push(mv); last_move = mv
282
- state["move_stack"] = [m.uci() for m in board.move_stack]
283
- state["last_move_uci"] = mv.uci()
284
- if legal_flag: state["legal_moves"] += 1
285
- else: state["halluc_moves"] += 1
286
- flag = "" if legal_flag else " <em>(random)</em>"
287
- log_lines.append(f"β™Ÿ ChessSLM opens: <b>{san}</b>{flag}")
288
-
289
- stxt, over = game_over_status(board, player_color)
290
- state["game_over"] = over
291
- moves = [] if over else legal_moves_san(board)
292
-
293
- return (
294
- render_board(board, last_move, flipped),
295
- gr.Dropdown(choices=moves, value=None, interactive=not over),
296
- status_html(stxt),
297
- fmt_history(board),
298
- log_html(log_lines),
299
- halluc_html(state["legal_moves"], state["halluc_moves"]),
300
- state,
301
- )
302
-
303
- def hvai_make_move(move_san: str, state: dict):
304
- if not state or state.get("game_over") or not move_san:
305
- return (gr.update(),) * 6 + (state,)
306
-
307
- board = board_from_state(state)
308
- player_color = state["player_color"]
309
- flipped = state["flipped"]
310
- log_lines = []
311
-
312
- try: player_move = board.parse_san(move_san)
313
- except: return (gr.update(),) * 6 + (state,)
314
-
315
- san = board.san(player_move)
316
- board.push(player_move)
317
- log_lines.append(f"You played <b>{san}</b>")
318
-
319
- stxt, over = game_over_status(board, player_color)
320
-
321
- if not over:
322
- mv, legal_flag = ai_move(board)
323
- ai_san = board.san(mv); board.push(mv)
324
- if legal_flag: state["legal_moves"] += 1
325
- else: state["halluc_moves"] += 1
326
- flag = "" if legal_flag else " <em style='color:#e05050'>(hallucinated)</em>"
327
- log_lines.append(f"β™Ÿ ChessSLM: <b>{ai_san}</b>{flag}")
328
- stxt, over = game_over_status(board, player_color)
329
-
330
- state["move_stack"] = [m.uci() for m in board.move_stack]
331
- state["last_move_uci"] = board.move_stack[-1].uci() if board.move_stack else None
332
- state["game_over"] = over
333
- last_move = chess.Move.from_uci(state["last_move_uci"]) if state["last_move_uci"] else None
334
- moves = [] if over else legal_moves_san(board)
335
-
336
- return (
337
- render_board(board, last_move, flipped),
338
- gr.Dropdown(choices=moves, value=None, interactive=not over),
339
- status_html(stxt),
340
- fmt_history(board),
341
- log_html(log_lines),
342
- halluc_html(state["legal_moves"], state["halluc_moves"]),
343
- state,
344
- )
345
 
346
  # ══════════════════════════════════════════════════════════════════════════════
347
- # MODE 2 β€” AI vs AI
348
  # ══════════════════════════════════════════════════════════════════════════════
349
 
350
- def aivsai_start(delay_val: float):
351
- board = chess.Board()
 
 
352
  log_lines = []
353
- legal_w, halluc_w = 0, 0
354
- legal_b, halluc_b = 0, 0
355
- state = {"mode": "aivsai", "move_stack": [], "game_over": False}
356
-
357
- yield (
358
- render_board(board),
359
- status_html("AI vs AI β€” starting…"),
360
- fmt_history(board),
361
- log_html([]),
362
- halluc_html_battle(0, 0, "White (ChessSLM)", 0, 0, "Black (ChessSLM)"),
363
- state,
364
- )
365
 
366
  while not board.is_game_over() and len(board.move_stack) < 200:
367
- mv, legal_flag = ai_move(board)
368
  is_white = board.turn == chess.WHITE
369
- san = board.san(mv); board.push(mv)
 
 
370
 
371
  if is_white:
372
- if legal_flag: legal_w += 1
373
- else: halluc_w += 1
374
- side = "White β™Ÿ"
375
  else:
376
- if legal_flag: legal_b += 1
377
- else: halluc_b += 1
378
- side = "Black β™Ÿ"
379
-
380
- flag = "" if legal_flag else " <em style='color:#e05050'>(hallucinated)</em>"
381
- log_lines.append(f"<b>{side}</b>: {san}{flag}")
382
- stxt, over = game_over_status(board, None, "White", "Black")
383
- state = {"mode": "aivsai",
384
- "move_stack": [m.uci() for m in board.move_stack],
385
- "game_over": over}
386
-
387
- yield (
388
- render_board(board, mv),
389
- status_html(stxt),
390
- fmt_history(board),
391
- log_html(log_lines),
392
- halluc_html_battle(legal_w, halluc_w, "White (ChessSLM)",
393
- legal_b, halluc_b, "Black (ChessSLM)"),
394
- state,
395
- )
396
- time.sleep(max(0.1, float(delay_val)))
 
 
397
 
398
  # ══════════════════════════════════════════════════════════════════════════════
399
- # MODE 3 β€” MODEL BATTLE
400
  # ═════════════════════════════════════════════════════════════════��════════════
401
 
402
- def battle_load(model_id_input: str):
403
- mid = model_id_input.strip()
404
  if not mid:
405
- return status_html("⚠ Enter a model ID first"), gr.update()
406
  try:
407
  load_custom(mid)
408
- return status_html(f"βœ“ <b>{mid}</b> loaded β€” press β–Ά Play"), \
409
- gr.update(interactive=True)
410
  except Exception as e:
411
  return status_html(f"βœ— Could not load <em>{mid}</em>: {e}"), gr.update()
412
 
413
- def battle_start(model_id_input: str, delay_val: float):
414
- mid = model_id_input.strip()
415
  if not mid:
416
  yield (gr.update(), status_html("⚠ Load a model first"),
417
- gr.update(), gr.update(), gr.update(), {})
418
  return
419
  try:
420
  mdl2, tok2 = load_custom(mid)
421
  except Exception as e:
422
  yield (gr.update(), status_html(f"βœ— {e}"),
423
- gr.update(), gr.update(), gr.update(), {})
424
  return
425
 
426
  label_w = "ChessSLM (W)"
427
  label_b = f"{mid.split('/')[-1]} (B)"
428
- board = chess.Board()
 
429
  log_lines = []
430
- legal_a, halluc_a = 0, 0
431
- legal_b_cnt, halluc_b_cnt = 0, 0
432
- state = {"mode": "battle", "move_stack": [], "game_over": False}
433
-
434
- yield (
435
- render_board(board),
436
- status_html(f"{label_w} vs {label_b}"),
437
- fmt_history(board),
438
- log_html([]),
439
- halluc_html_battle(0, 0, label_w, 0, 0, label_b),
440
- state,
441
- )
442
 
443
  while not board.is_game_over() and len(board.move_stack) < 200:
444
  if board.turn == chess.WHITE:
445
- mv, legal_flag = ai_move(board)
446
- label = label_w
447
- if legal_flag: legal_a += 1
448
- else: halluc_a += 1
449
  else:
450
- mv, legal_flag = ai_move(board, mdl2, tok2)
451
- label = label_b
452
- if legal_flag: legal_b_cnt += 1
453
- else: halluc_b_cnt += 1
454
-
455
- san = board.san(mv); board.push(mv)
456
- flag = "" if legal_flag else " <em style='color:#e05050'>(hallucinated)</em>"
457
- log_lines.append(f"<b>{label}</b>: {san}{flag}")
458
- stxt, over = game_over_status(board, None, label_w, label_b)
459
- state = {"mode": "battle",
460
- "move_stack": [m.uci() for m in board.move_stack],
461
- "game_over": over}
462
-
463
- yield (
464
- render_board(board, mv),
465
- status_html(stxt),
466
- fmt_history(board),
467
- log_html(log_lines),
468
- halluc_html_battle(legal_a, halluc_a, label_w,
469
- legal_b_cnt, halluc_b_cnt, label_b),
470
- state,
471
- )
472
- time.sleep(max(0.1, float(delay_val)))
 
 
 
473
 
474
  # ══════════════════════════════════════════════════════════════════════════════
475
  # CSS
@@ -479,12 +349,13 @@ CSS = """
479
  @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');
480
 
481
  *, *::before, *::after { box-sizing: border-box; }
 
482
  body, .gradio-container {
483
  background: #0a0805 !important;
484
  color: #e8d5a3 !important;
485
  font-family: 'Crimson Text', Georgia, serif !important;
486
  }
487
- .gradio-container { max-width: 1160px !important; margin: 0 auto !important; }
488
 
489
  #site-header {
490
  text-align: center;
@@ -498,32 +369,32 @@ body, .gradio-container {
498
  font-family: 'Cinzel', serif;
499
  font-size: 1em;
500
  letter-spacing: 0.05em;
501
- padding: 0.55rem 1rem;
502
  border-radius: 5px;
503
  background: #110e07;
504
  border: 1px solid #3d2b0e;
505
  color: #f0c060;
506
- min-height: 2.4rem;
507
  }
508
 
509
  .tabs { border: none !important; }
510
  .tabitem { background: transparent !important; }
511
  .tab-nav button {
512
  font-family: 'Cinzel', serif !important;
513
- font-size: 0.78em !important;
514
  letter-spacing: 0.07em !important;
515
  color: #8a7a5a !important;
516
  border-bottom: 2px solid transparent !important;
517
  background: transparent !important;
518
- padding: 0.5rem 1rem !important;
519
- transition: all 0.2s !important;
520
  }
521
  .tab-nav button.selected {
522
  color: #e8c060 !important;
523
  border-bottom-color: #c49020 !important;
524
  }
525
 
526
- button.primary, .gr-button-primary {
527
  background: linear-gradient(135deg, #7a5a10, #c4922a, #7a5a10) !important;
528
  border: 1px solid #c49020 !important;
529
  color: #fff8e8 !important;
@@ -531,11 +402,11 @@ button.primary, .gr-button-primary {
531
  letter-spacing: 0.07em !important;
532
  font-size: 0.85em !important;
533
  border-radius: 4px !important;
534
- transition: all 0.2s !important;
535
  }
536
  button.primary:hover {
537
- box-shadow: 0 0 18px rgba(196,144,32,.45) !important;
538
- filter: brightness(1.1) !important;
539
  }
540
  button.secondary {
541
  background: #1a140a !important;
@@ -560,19 +431,19 @@ input, textarea, select {
560
  }
561
  label span {
562
  font-family: 'Cinzel', serif !important;
563
- font-size: 0.74em !important;
564
  letter-spacing: 0.07em !important;
565
  text-transform: uppercase !important;
566
  color: #8a6a30 !important;
567
  }
568
 
569
- .log-panel, .hist-panel {
570
  background: #0d0a05;
571
  border: 1px solid #2e1f08;
572
  border-radius: 6px;
573
  padding: 0.8rem 1rem;
574
  min-height: 80px;
575
- max-height: 220px;
576
  overflow-y: auto;
577
  }
578
 
@@ -589,121 +460,97 @@ input[type=range] { accent-color: #c49020 !important; }
589
  # LAYOUT
590
  # ══════════════════════════════════════════════════════════════════════════════
591
 
592
- _BOARD_0 = render_board(chess.Board())
593
- _HALLUC_0 = halluc_html(0, 0)
594
- _STATUS_0 = status_html("Choose a mode and press New Game")
595
 
596
- with gr.Blocks(css=CSS, title="ChessSLM") as demo:
597
-
598
- game_state = gr.State({})
599
 
600
  gr.HTML("""
601
  <div id="site-header">
602
- <h1 style="font-family:'Cinzel',serif;font-size:2.2em;font-weight:700;
603
- color:#e8c060;text-shadow:0 0 28px rgba(232,180,60,.35);
604
- margin:0 0 .3rem;letter-spacing:.14em;">β™› ChessSLM</h1>
605
  <p style="font-family:'Crimson Text',serif;color:#7a6a48;
606
  font-size:1.05em;font-style:italic;margin:0;">
607
- GPT-2 pre-trained on 100,000 chess games Β· three ways to play</p>
608
  </div>""")
609
 
610
  with gr.Row():
611
 
612
- # ── Left: board + status + hallucination card ─────────────────────────
613
- with gr.Column(scale=3, min_width=440):
614
  board_out = gr.HTML(value=_BOARD_0)
615
- status_out = gr.HTML(value=status_html("Choose a mode and press New Game"))
616
  halluc_out = gr.HTML(value=_HALLUC_0)
617
 
618
- # ── Right: tabs + controls ────────────────────────────────────────────
619
  with gr.Column(scale=2, min_width=300):
620
 
621
  with gr.Tabs():
622
 
623
- # Tab 1 β€” Human vs AI ──��──────────────────────────────────────
624
- with gr.Tab("πŸ‘€ You vs AI"):
625
- color_radio = gr.Radio(
626
- choices=["⬜ White (move first)", "⬛ Black (move second)"],
627
- value="⬜ White (move first)", label="Play as",
628
- )
629
- hvai_btn = gr.Button("β™Ÿ New Game", variant="primary")
630
- gr.HTML("<div class='div-line'></div>")
631
- move_drop = gr.Dropdown(
632
- choices=[], value=None, interactive=False,
633
- label="Your move (SAN)",
634
- )
635
- move_btn = gr.Button("β–Ά Make Move", variant="secondary")
636
-
637
- # Tab 2 β€” AI vs AI ────────────────────────────────────────────
638
  with gr.Tab("πŸ€– AI vs AI"):
639
- delay_slider = gr.Slider(
640
- minimum=0.3, maximum=3.0, value=0.8, step=0.1,
641
- label="Delay between moves (s)",
642
- )
 
643
  with gr.Row():
644
- aivsai_btn = gr.Button("β–Ά Play", variant="primary")
645
- aivsai_stop = gr.Button("⏹ Stop", variant="secondary",
646
- elem_classes=["stop"])
647
-
648
- # Tab 3 β€” Model Battle ────────────────────────────────────────
649
- with gr.Tab("βš”οΈ Model Battle"):
650
- custom_id = gr.Textbox(
 
 
 
651
  placeholder="e.g. another-user/chess-gpt",
652
- label="Opponent model (HF repo ID)",
653
  )
654
- load_btn = gr.Button("⬇ Load Model", variant="secondary")
655
  gr.HTML("<div class='div-line'></div>")
656
- delay_battle = gr.Slider(
657
- minimum=0.3, maximum=3.0, value=0.8, step=0.1,
658
- label="Delay between moves (s)",
659
- )
660
  with gr.Row():
661
- battle_btn = gr.Button("β–Ά Play", variant="primary",
662
- interactive=False)
663
- battle_stop = gr.Button("⏹ Stop", variant="secondary",
664
- elem_classes=["stop"])
665
 
666
  gr.HTML("<div class='div-line'></div>")
667
- gr.HTML("<p style='font-family:Cinzel,serif;font-size:.74em;"
668
- "color:#8a6a30;letter-spacing:.07em;margin:0 0 .4rem'>COMMENTARY</p>")
669
  log_out = gr.HTML(
670
- value="<div class='log-panel'><em style='color:#555'>β€”</em></div>")
671
 
672
  gr.HTML("<div class='div-line'></div>")
673
- gr.HTML("<p style='font-family:Cinzel,serif;font-size:.74em;"
674
- "color:#8a6a30;letter-spacing:.07em;margin:0 0 .4rem'>MOVE LIST</p>")
675
  hist_out = gr.HTML(
676
- value="<div class='hist-panel'><em style='color:#555'>No moves yet.</em></div>")
677
 
678
  gr.HTML("""
679
  <div style="text-align:center;margin-top:1.6rem;padding-top:.9rem;
680
  border-top:1px solid #1e1508;font-family:'Crimson Text',serif;
681
  font-size:.82em;color:#4a3a20;">
682
- Model: <a href="https://huggingface.co/FlameF0X/ChessSLM" target="_blank"
683
  style="color:#7a5a28;text-decoration:none;">FlameF0X/ChessSLM</a>
684
- &nbsp;Β·&nbsp; GPT-2 pre-trained on 100K PGN games
685
- &nbsp;Β·&nbsp; Moves via top-k sampling (temp=0.3)
686
  </div>""")
687
 
688
- # ── Shared outputs ────────────────────────────────────────────────────────
689
- _shared = [board_out, status_out, hist_out, log_out, halluc_out, game_state]
690
- _hvai = [board_out, move_drop, status_out, hist_out, log_out, halluc_out, game_state]
691
-
692
- # Mode 1
693
- hvai_btn.click(fn=hvai_new_game, inputs=[color_radio], outputs=_hvai)
694
- move_btn.click(fn=hvai_make_move, inputs=[move_drop, game_state], outputs=_hvai)
695
- move_drop.select(fn=hvai_make_move,inputs=[move_drop, game_state], outputs=_hvai)
696
-
697
- # Mode 2
698
- ev2 = aivsai_btn.click(fn=aivsai_start, inputs=[delay_slider], outputs=_shared)
699
- aivsai_stop.click(fn=None, cancels=[ev2])
700
-
701
- # Mode 3
702
- load_btn.click(fn=battle_load, inputs=[custom_id],
703
- outputs=[status_out, battle_btn])
704
- ev3 = battle_btn.click(fn=battle_start,
705
- inputs=[custom_id, delay_battle], outputs=_shared)
706
- battle_stop.click(fn=None, cancels=[ev3])
707
 
708
 
709
  if __name__ == "__main__":
 
1
  """
2
+ ChessSLM β€” AI vs AI & Model Arena
3
  HF Space | Gradio 4
4
  """
5
 
 
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
 
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
 
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
 
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
 
 
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;
 
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
 
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;
 
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;
 
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;
 
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
 
 
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__":