tostido commited on
Commit
2ec90a4
·
1 Parent(s): 959e9e0

Add Gradio Space: HuggingFace-ready chess demo with cascade-lattice

Browse files
Files changed (4) hide show
  1. README.md +60 -0
  2. app.py +938 -0
  3. packages.txt +1 -0
  4. requirements.txt +7 -0
README.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Cascade Lattice Chess
3
+ emoji: ♟️
4
+ colorFrom: cyan
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ tags:
12
+ - chess
13
+ - ai
14
+ - human-in-the-loop
15
+ - cascade-lattice
16
+ - provenance
17
+ - merkle
18
+ - explainable-ai
19
+ ---
20
+
21
+ # ♟️ CASCADE LATTICE CHESS // SUPER SAIYAN MODE
22
+
23
+ **Human-in-the-loop chess with cryptographic provenance via cascade-lattice**
24
+
25
+ ## What is this?
26
+
27
+ This is a chess demo showcasing the [cascade-lattice](https://pypi.org/project/cascade-lattice/) package - a universal AI provenance layer that provides cryptographic receipts for every AI decision.
28
+
29
+ ### Features
30
+
31
+ 🧠 **Neural Chess Engine** - Powered by [Maxlegrec/ChessBot](https://huggingface.co/Maxlegrec/ChessBot), a Transformer-based model trained on 750M positions
32
+
33
+ ⏸️ **HOLD System** - The AI halts before every move, showing you its top candidates with confidence scores
34
+
35
+ 🔮 **Cascade Predictions** - "Minority Report" style forecasting of move consequences before they happen
36
+
37
+ 🔗 **Merkle Chain** - Every decision is cryptographically hashed into a provenance chain
38
+
39
+ 📊 **Real-time Analytics** - Track override rates, combos, and decision patterns
40
+
41
+ ### How to Play
42
+
43
+ 1. **Click** on a candidate move to drill down into its analysis
44
+ 2. **Review** the AI's reasoning, confidence, and cascade predictions
45
+ 3. **Execute** to accept the move, or select a different candidate to override
46
+ 4. **Track** your decisions through the Merkle chain tab
47
+
48
+ ### The CASCADE-LATTICE Package
49
+
50
+ This demo uses cascade-lattice to:
51
+ - Register every game event in a **CausationGraph**
52
+ - Generate **Merkle roots** for cryptographic provenance
53
+ - Track **session statistics** (holds, overrides, combos)
54
+ - Predict **cascade effects** of each move
55
+
56
+ Install it yourself: `pip install cascade-lattice`
57
+
58
+ ---
59
+
60
+ *Built with ❤️ using Gradio, PyTorch, and cascade-lattice*
app.py ADDED
@@ -0,0 +1,938 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CASCADE LATTICE CHESS // SUPER SAIYAN MODE
3
+ Gradio Space Demo - HuggingFace Spaces Compatible
4
+
5
+ Human-in-the-loop chess with cryptographic provenance via cascade-lattice
6
+ """
7
+
8
+ import gradio as gr
9
+ import chess
10
+ import chess.engine
11
+ import os
12
+ import sys
13
+ import random
14
+ import time
15
+ import math
16
+ import hashlib
17
+ import numpy as np
18
+ from PIL import Image, ImageDraw, ImageFont
19
+ from collections import deque
20
+ from dataclasses import dataclass, field
21
+ from typing import List, Dict, Any, Optional, Tuple
22
+ import threading
23
+
24
+ # Conditional imports for HF Spaces
25
+ try:
26
+ import torch
27
+ from transformers import AutoModel
28
+ TORCH_AVAILABLE = True
29
+ except ImportError:
30
+ TORCH_AVAILABLE = False
31
+ print("[WARN] PyTorch not available - using random moves")
32
+
33
+ try:
34
+ import cascade
35
+ CASCADE_AVAILABLE = True
36
+ except ImportError:
37
+ CASCADE_AVAILABLE = False
38
+ print("[WARN] cascade-lattice not available - using mock")
39
+
40
+ # ═══════════════════════════════════════════════════════════════════════════════
41
+ # CONFIGURATION
42
+ # ═══════════════════════════════════════════════════════════════════════════════
43
+
44
+ MODEL_NAME = "Maxlegrec/ChessBot"
45
+ BOARD_SIZE = 480 # Smaller for web
46
+ PANEL_WIDTH = 520
47
+ TOTAL_WIDTH = BOARD_SIZE + PANEL_WIDTH
48
+ TOTAL_HEIGHT = 600
49
+ SQUARE_SIZE = BOARD_SIZE // 8
50
+
51
+ # Stockfish path - check multiple locations
52
+ STOCKFISH_PATHS = [
53
+ "/usr/games/stockfish", # Linux apt install
54
+ "/usr/bin/stockfish",
55
+ "stockfish", # PATH
56
+ os.path.join("stockfish", "stockfish-windows-x86-64-avx2.exe"), # Windows local
57
+ ]
58
+
59
+ def find_stockfish():
60
+ for path in STOCKFISH_PATHS:
61
+ if os.path.exists(path):
62
+ return path
63
+ # Try as command
64
+ try:
65
+ result = os.popen(f"which {path} 2>/dev/null || where {path} 2>nul").read().strip()
66
+ if result:
67
+ return result
68
+ except:
69
+ pass
70
+ return None
71
+
72
+ STOCKFISH_PATH = find_stockfish()
73
+ print(f"[CONFIG] Stockfish: {STOCKFISH_PATH or 'NOT FOUND'}")
74
+
75
+ # Colors
76
+ COLORS = {
77
+ 'bg_dark': (18, 18, 22),
78
+ 'bg_panel': (25, 25, 30),
79
+ 'bg_card': (35, 35, 45),
80
+ 'bg_card_hover': (50, 55, 70),
81
+ 'board_light': (235, 236, 208),
82
+ 'board_dark': (119, 149, 86),
83
+ 'accent_cyan': (0, 200, 255),
84
+ 'accent_green': (0, 255, 150),
85
+ 'accent_gold': (255, 215, 0),
86
+ 'accent_red': (255, 70, 70),
87
+ 'accent_purple': (180, 100, 255),
88
+ 'accent_orange': (255, 150, 50),
89
+ 'text_primary': (255, 255, 255),
90
+ 'text_secondary': (180, 180, 180),
91
+ 'text_muted': (120, 120, 120),
92
+ 'merkle_glow': (100, 255, 218),
93
+ }
94
+
95
+ # Unicode chess pieces
96
+ PIECE_SYMBOLS = {
97
+ 'P': '♙', 'N': '♘', 'B': '♗', 'R': '♖', 'Q': '♕', 'K': '♔',
98
+ 'p': '♟', 'n': '♞', 'b': '♝', 'r': '♜', 'q': '♛', 'k': '♚',
99
+ }
100
+
101
+ # ═══════════════════════════════════════════════════════════════════════════════
102
+ # CASCADE MOCK (for when cascade-lattice isn't available)
103
+ # ═══════════════════════════════════════════════════════════════════════════════
104
+
105
+ class MockHold:
106
+ _instance = None
107
+ def __init__(self):
108
+ self.timeout = 3600
109
+ self.listeners = []
110
+ self.stats = {'total_holds': 0, 'overrides': 0}
111
+
112
+ @classmethod
113
+ def get(cls):
114
+ if cls._instance is None:
115
+ cls._instance = MockHold()
116
+ return cls._instance
117
+
118
+ def register_listener(self, fn):
119
+ self.listeners.append(fn)
120
+
121
+ def resolve(self, action=None):
122
+ pass
123
+
124
+ class MockCausationGraph:
125
+ def __init__(self):
126
+ self.events = []
127
+ self.links = []
128
+ def add_event(self, e): self.events.append(e)
129
+ def add_link(self, l): self.links.append(l)
130
+ def get_stats(self): return {'events': len(self.events), 'links': len(self.links)}
131
+
132
+ class MockMetricsEngine:
133
+ def __init__(self): self.metrics = {}
134
+ def ingest(self, e): pass
135
+ def summary(self): return {}
136
+
137
+ class MockEvent:
138
+ def __init__(self, **kwargs):
139
+ for k, v in kwargs.items():
140
+ setattr(self, k, v)
141
+
142
+ class MockCausationLink:
143
+ def __init__(self, **kwargs):
144
+ for k, v in kwargs.items():
145
+ setattr(self, k, v)
146
+
147
+ # Use real or mock
148
+ if CASCADE_AVAILABLE:
149
+ CascadeHold = cascade.Hold
150
+ CausationGraph = cascade.CausationGraph
151
+ MetricsEngine = cascade.MetricsEngine
152
+ CascadeEvent = cascade.Event
153
+ CausationLink = cascade.CausationLink
154
+ cascade.init()
155
+ else:
156
+ CascadeHold = MockHold
157
+ CausationGraph = MockCausationGraph
158
+ MetricsEngine = MockMetricsEngine
159
+ CascadeEvent = MockEvent
160
+ CausationLink = MockCausationLink
161
+
162
+ # ═══════════════════════════════════════════════════════════════════════════════
163
+ # DATA CLASSES
164
+ # ═══════════════════════════════════════════════════════════════════════════════
165
+
166
+ @dataclass
167
+ class CausalNode:
168
+ event_id: str
169
+ move: str
170
+ player: str
171
+ fen: str
172
+ timestamp: float
173
+ merkle: str
174
+ value: float
175
+ was_override: bool = False
176
+ depth: int = 0
177
+
178
+ @dataclass
179
+ class PredictionRisk:
180
+ move: str
181
+ risk_score: float
182
+ predicted_responses: List[str]
183
+ material_delta: int
184
+ positional_delta: float
185
+ tactical_threats: List[str]
186
+
187
+ @dataclass
188
+ class SessionStats:
189
+ total_holds: int = 0
190
+ human_overrides: int = 0
191
+ ai_accepted: int = 0
192
+ current_combo: int = 0
193
+ max_combo: int = 0
194
+ override_rate: float = 0.0
195
+
196
+ # ═══════════════════════════════════════════════════════════════════════════════
197
+ # GAME STATE CLASS
198
+ # ═══════════════════════════════════════════════════════════════════════════════
199
+
200
+ class ChessGameState:
201
+ """Manages all game state for the Gradio interface"""
202
+
203
+ def __init__(self):
204
+ self.reset_game()
205
+
206
+ # Cascade systems
207
+ self.causation_graph = CausationGraph()
208
+ self.metrics_engine = MetricsEngine()
209
+ self.causal_chain: List[CausalNode] = []
210
+ self.merkle_chain: List[str] = []
211
+ self.last_event_id = None
212
+
213
+ # AI Model
214
+ self.model = None
215
+ self.device = "cpu"
216
+ self._load_model()
217
+
218
+ # Stockfish
219
+ self.engine = None
220
+ self._init_engine()
221
+
222
+ def reset_game(self):
223
+ """Reset to new game"""
224
+ self.board = chess.Board()
225
+ self.move_history = []
226
+ self.session = SessionStats()
227
+ self.candidates = []
228
+ self.predictions = []
229
+ self.selected_idx = 0
230
+ self.hovered_idx = -1
231
+ self.ui_mode = "LIST" # LIST, DRILL
232
+ self.active_tab = "WEALTH"
233
+ self.game_over = False
234
+ self.game_result = ""
235
+ self.hold_active = False
236
+ self.current_merkle = ""
237
+ self.value_history = []
238
+ self.last_observation = {}
239
+
240
+ def _load_model(self):
241
+ """Load the ChessBot model"""
242
+ if not TORCH_AVAILABLE:
243
+ print("[MODEL] PyTorch not available, using random moves")
244
+ return
245
+
246
+ try:
247
+ print(f"[MODEL] Loading {MODEL_NAME}...")
248
+ self.model = AutoModel.from_pretrained(MODEL_NAME, trust_remote_code=True)
249
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
250
+ self.model.to(self.device)
251
+ self.model.eval()
252
+ print(f"[MODEL] Loaded on {self.device}")
253
+ except Exception as e:
254
+ print(f"[MODEL] Failed to load: {e}")
255
+ self.model = None
256
+
257
+ def _init_engine(self):
258
+ """Initialize Stockfish engine"""
259
+ if STOCKFISH_PATH:
260
+ try:
261
+ self.engine = chess.engine.SimpleEngine.popen_uci(STOCKFISH_PATH)
262
+ print("[ENGINE] Stockfish ready")
263
+ except Exception as e:
264
+ print(f"[ENGINE] Failed: {e}")
265
+ self.engine = None
266
+ else:
267
+ print("[ENGINE] Stockfish not found")
268
+
269
+ def _compute_merkle(self, data: Dict) -> str:
270
+ content = str(sorted(data.items())).encode()
271
+ return hashlib.sha256(content).hexdigest()[:16]
272
+
273
+ def register_event(self, event_data: Dict) -> str:
274
+ """Register event in causation graph"""
275
+ event_id = f"evt_{len(self.causal_chain)}_{int(time.time()*1000)}"
276
+
277
+ if CASCADE_AVAILABLE:
278
+ event = CascadeEvent(
279
+ event_id=event_id,
280
+ timestamp=time.time(),
281
+ component=event_data.get('player', 'chess'),
282
+ event_type=event_data.get('type', 'move'),
283
+ data=event_data
284
+ )
285
+ self.causation_graph.add_event(event)
286
+
287
+ if self.last_event_id and self.causal_chain:
288
+ link = CausationLink(
289
+ from_event=self.last_event_id,
290
+ to_event=event_id,
291
+ causation_type="game_sequence",
292
+ strength=1.0,
293
+ explanation=f"Move sequence"
294
+ )
295
+ self.causation_graph.add_link(link)
296
+
297
+ # Create causal node
298
+ node = CausalNode(
299
+ event_id=event_id,
300
+ move=event_data.get('move', ''),
301
+ player=event_data.get('player', ''),
302
+ fen=event_data.get('fen', ''),
303
+ timestamp=time.time(),
304
+ merkle=self._compute_merkle(event_data),
305
+ value=event_data.get('value', 0.0),
306
+ was_override=event_data.get('was_override', False),
307
+ depth=len(self.causal_chain)
308
+ )
309
+
310
+ self.causal_chain.append(node)
311
+ self.merkle_chain.append(node.merkle)
312
+ self.last_event_id = event_id
313
+
314
+ return event_id
315
+
316
+ def get_ai_move(self) -> Tuple[str, List[Dict], float]:
317
+ """Get AI's move recommendation with candidates"""
318
+ fen = self.board.fen()
319
+ legal_moves = [m.uci() for m in self.board.legal_moves]
320
+
321
+ if not legal_moves:
322
+ return None, [], 0.0
323
+
324
+ # Get model prediction
325
+ move_uci = None
326
+ pos_eval = [0.33, 0.34, 0.33] # Default
327
+
328
+ if self.model is not None:
329
+ try:
330
+ with torch.no_grad():
331
+ pos_eval = self.model.get_position_value(fen, device=self.device).tolist()
332
+ move_uci = self.model.get_move_from_fen_no_thinking(fen, T=0.1, device=self.device)
333
+ except Exception as e:
334
+ print(f"[AI] Error: {e}")
335
+
336
+ if move_uci is None or move_uci not in legal_moves:
337
+ move_uci = random.choice(legal_moves)
338
+
339
+ # Build candidates
340
+ candidates = []
341
+ candidates.append({
342
+ "move": move_uci,
343
+ "prob": 0.82,
344
+ "v_head": pos_eval,
345
+ "reason": "Primary engine recommendation"
346
+ })
347
+
348
+ others = [m for m in legal_moves if m != move_uci]
349
+ random.shuffle(others)
350
+ for i, m in enumerate(others[:4]):
351
+ v_noise = [v + random.uniform(-0.08, 0.08) for v in pos_eval]
352
+ candidates.append({
353
+ "move": m,
354
+ "prob": max(0.01, 0.15 - i * 0.03),
355
+ "v_head": v_noise,
356
+ "reason": "Alternative candidate"
357
+ })
358
+
359
+ value = pos_eval[2] - pos_eval[0]
360
+ return move_uci, candidates, value
361
+
362
+ def get_stockfish_move(self) -> str:
363
+ """Get Stockfish's move"""
364
+ if self.engine:
365
+ try:
366
+ result = self.engine.play(self.board, chess.engine.Limit(time=0.5))
367
+ return result.move.uci()
368
+ except:
369
+ pass
370
+ return random.choice([m.uci() for m in self.board.legal_moves])
371
+
372
+ def predict_cascade(self, candidates: List[Dict]) -> List[PredictionRisk]:
373
+ """Generate cascade predictions for candidates"""
374
+ predictions = []
375
+ for c in candidates:
376
+ try:
377
+ move = chess.Move.from_uci(c['move'])
378
+ temp = self.board.copy()
379
+
380
+ # Material before/after
381
+ mat_before = self._count_material(temp)
382
+ temp.push(move)
383
+ mat_after = self._count_material(temp)
384
+
385
+ # Opponent responses
386
+ responses = [m.uci() for m in list(temp.legal_moves)[:3]]
387
+
388
+ # Threats
389
+ threats = []
390
+ if temp.is_check():
391
+ threats.append("CHECK")
392
+
393
+ # Risk score
394
+ risk = (1 - c['prob']) * 0.5 + len(threats) * 0.1
395
+
396
+ predictions.append(PredictionRisk(
397
+ move=c['move'],
398
+ risk_score=min(1.0, risk),
399
+ predicted_responses=responses,
400
+ material_delta=mat_after - mat_before,
401
+ positional_delta=c['v_head'][2] - c['v_head'][0],
402
+ tactical_threats=threats
403
+ ))
404
+ except:
405
+ pass
406
+ return predictions
407
+
408
+ def _count_material(self, board) -> int:
409
+ values = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3,
410
+ chess.ROOK: 5, chess.QUEEN: 9}
411
+ score = 0
412
+ for sq in chess.SQUARES:
413
+ piece = board.piece_at(sq)
414
+ if piece and piece.piece_type in values:
415
+ val = values[piece.piece_type]
416
+ score += val if piece.color == chess.WHITE else -val
417
+ return score
418
+
419
+ def make_move(self, move_uci: str, player: str, was_override: bool = False):
420
+ """Execute a move on the board"""
421
+ try:
422
+ move = chess.Move.from_uci(move_uci)
423
+ if move in self.board.legal_moves:
424
+ self.board.push(move)
425
+
426
+ # Get evaluation
427
+ value = 0.0
428
+ if self.model:
429
+ try:
430
+ with torch.no_grad():
431
+ ev = self.model.get_position_value(self.board.fen(), device=self.device)
432
+ value = float(ev[2] - ev[0])
433
+ self.value_history.append(value)
434
+ except:
435
+ pass
436
+
437
+ self.move_history.append({
438
+ 'move': move_uci,
439
+ 'player': player,
440
+ 'was_override': was_override
441
+ })
442
+
443
+ # Register in causation graph
444
+ self.register_event({
445
+ 'type': 'move',
446
+ 'move': move_uci,
447
+ 'player': player,
448
+ 'fen': self.board.fen(),
449
+ 'value': value,
450
+ 'was_override': was_override
451
+ })
452
+
453
+ # Update session stats
454
+ if was_override:
455
+ self.session.human_overrides += 1
456
+ self.session.current_combo = 0
457
+ else:
458
+ self.session.ai_accepted += 1
459
+ self.session.current_combo += 1
460
+ self.session.max_combo = max(self.session.max_combo, self.session.current_combo)
461
+
462
+ self.session.total_holds += 1
463
+ if self.session.total_holds > 0:
464
+ self.session.override_rate = self.session.human_overrides / self.session.total_holds
465
+
466
+ # Check game over
467
+ if self.board.is_game_over():
468
+ self.game_over = True
469
+ result = self.board.result()
470
+ if result == "1-0":
471
+ self.game_result = "WHITE WINS (ChessBot)"
472
+ elif result == "0-1":
473
+ self.game_result = "BLACK WINS (Stockfish)"
474
+ else:
475
+ self.game_result = f"DRAW ({result})"
476
+
477
+ return True
478
+ except Exception as e:
479
+ print(f"[MOVE] Error: {e}")
480
+ return False
481
+
482
+ # ═══════════════════════════════════════════════════════════════════════════════
483
+ # RENDERING
484
+ # ═══════════════════════════════════════════════════════════════════════════════
485
+
486
+ def render_game(state: ChessGameState) -> Image.Image:
487
+ """Render the complete game view"""
488
+ img = Image.new('RGB', (TOTAL_WIDTH, TOTAL_HEIGHT), COLORS['bg_dark'])
489
+ draw = ImageDraw.Draw(img)
490
+
491
+ # Try to load fonts
492
+ try:
493
+ font = ImageFont.truetype("arial.ttf", 14)
494
+ font_small = ImageFont.truetype("arial.ttf", 11)
495
+ font_large = ImageFont.truetype("seguisym.ttf", 36)
496
+ font_title = ImageFont.truetype("arial.ttf", 18)
497
+ except:
498
+ font = ImageFont.load_default()
499
+ font_small = font
500
+ font_large = font
501
+ font_title = font
502
+
503
+ # Draw board
504
+ _draw_board(img, draw, state, font_large)
505
+
506
+ # Draw panel
507
+ _draw_panel(img, draw, state, font, font_small, font_title)
508
+
509
+ return img
510
+
511
+ def _draw_board(img: Image.Image, draw: ImageDraw.Draw, state: ChessGameState, font_large):
512
+ """Draw the chess board"""
513
+ for r in range(8):
514
+ for c in range(8):
515
+ x = c * SQUARE_SIZE
516
+ y = r * SQUARE_SIZE
517
+
518
+ color = COLORS['board_light'] if (r + c) % 2 == 0 else COLORS['board_dark']
519
+ draw.rectangle([x, y, x + SQUARE_SIZE, y + SQUARE_SIZE], fill=color)
520
+
521
+ # Draw piece
522
+ sq = chess.square(c, 7 - r)
523
+ piece = state.board.piece_at(sq)
524
+ if piece:
525
+ symbol = piece.symbol()
526
+ if symbol in PIECE_SYMBOLS:
527
+ piece_char = PIECE_SYMBOLS[symbol]
528
+ # Center the piece
529
+ try:
530
+ bbox = font_large.getbbox(piece_char)
531
+ tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
532
+ except:
533
+ tw, th = 30, 30
534
+ px = x + (SQUARE_SIZE - tw) // 2
535
+ py = y + (SQUARE_SIZE - th) // 2
536
+
537
+ # Draw with shadow
538
+ p_color = (255, 255, 255) if piece.color == chess.WHITE else (30, 30, 30)
539
+ shadow_color = (0, 0, 0) if piece.color == chess.WHITE else (200, 200, 200)
540
+ draw.text((px + 1, py + 1), piece_char, fill=shadow_color, font=font_large)
541
+ draw.text((px, py), piece_char, fill=p_color, font=font_large)
542
+
543
+ # Draw selected move arrow if in drill mode
544
+ if state.ui_mode == "DRILL" and state.candidates and state.selected_idx < len(state.candidates):
545
+ move_uci = state.candidates[state.selected_idx]['move']
546
+ try:
547
+ move = chess.Move.from_uci(move_uci)
548
+ _draw_arrow(draw, move.from_square, move.to_square, COLORS['accent_gold'], width=6)
549
+ except:
550
+ pass
551
+
552
+ def _draw_arrow(draw: ImageDraw.Draw, from_sq: int, to_sq: int, color: Tuple, width: int = 4):
553
+ """Draw an arrow between squares"""
554
+ fc, fr = chess.square_file(from_sq), 7 - chess.square_rank(from_sq)
555
+ tc, tr = chess.square_file(to_sq), 7 - chess.square_rank(to_sq)
556
+
557
+ x1 = fc * SQUARE_SIZE + SQUARE_SIZE // 2
558
+ y1 = fr * SQUARE_SIZE + SQUARE_SIZE // 2
559
+ x2 = tc * SQUARE_SIZE + SQUARE_SIZE // 2
560
+ y2 = tr * SQUARE_SIZE + SQUARE_SIZE // 2
561
+
562
+ draw.line([(x1, y1), (x2, y2)], fill=color, width=width)
563
+
564
+ # Arrowhead
565
+ angle = math.atan2(y1 - y2, x1 - x2)
566
+ arr_len = 15
567
+ p1 = (x2 + arr_len * math.cos(angle + 0.5), y2 + arr_len * math.sin(angle + 0.5))
568
+ p2 = (x2 + arr_len * math.cos(angle - 0.5), y2 + arr_len * math.sin(angle - 0.5))
569
+ draw.polygon([(x2, y2), p1, p2], fill=color)
570
+
571
+ def _draw_panel(img: Image.Image, draw: ImageDraw.Draw, state: ChessGameState,
572
+ font, font_small, font_title):
573
+ """Draw the side panel"""
574
+ px = BOARD_SIZE + 10
575
+ py = 10
576
+ pw = PANEL_WIDTH - 20
577
+
578
+ # Panel background
579
+ draw.rectangle([BOARD_SIZE, 0, TOTAL_WIDTH, TOTAL_HEIGHT], fill=COLORS['bg_panel'])
580
+
581
+ # Title
582
+ title = "CASCADE LATTICE CHESS"
583
+ draw.text((px, py), title, fill=COLORS['accent_cyan'], font=font_title)
584
+ py += 30
585
+
586
+ # Session stats
587
+ stats_text = f"Holds: {state.session.total_holds} | Combo: {state.session.current_combo} | OR: {state.session.override_rate*100:.0f}%"
588
+ draw.text((px, py), stats_text, fill=COLORS['text_secondary'], font=font_small)
589
+ py += 25
590
+
591
+ # Tab bar
592
+ tabs = ["WEALTH", "CASCADE", "CHAIN"]
593
+ tab_w = pw // len(tabs)
594
+ for i, tab in enumerate(tabs):
595
+ tx = px + i * tab_w
596
+ is_active = state.active_tab == tab
597
+ tab_color = COLORS['accent_cyan'] if is_active else COLORS['bg_card']
598
+ draw.rectangle([tx, py, tx + tab_w - 5, py + 22], fill=tab_color)
599
+ text_color = COLORS['text_primary'] if is_active else COLORS['text_muted']
600
+ draw.text((tx + 10, py + 4), tab, fill=text_color, font=font_small)
601
+ py += 30
602
+
603
+ # Game over banner
604
+ if state.game_over:
605
+ draw.rectangle([px, py, px + pw, py + 40], fill=(100, 30, 30))
606
+ draw.text((px + 10, py + 10), f"GAME OVER: {state.game_result}",
607
+ fill=COLORS['text_primary'], font=font)
608
+ py += 50
609
+
610
+ # Content based on tab
611
+ if state.active_tab == "WEALTH":
612
+ _draw_wealth_tab(draw, state, px, py, pw, font, font_small)
613
+ elif state.active_tab == "CASCADE":
614
+ _draw_cascade_tab(draw, state, px, py, pw, font, font_small)
615
+ elif state.active_tab == "CHAIN":
616
+ _draw_chain_tab(draw, state, px, py, pw, font, font_small)
617
+
618
+ def _draw_wealth_tab(draw, state, x, y, w, font, font_small):
619
+ """Draw the wealth/decision tab"""
620
+
621
+ # Last move info
622
+ if state.last_observation:
623
+ ev = state.last_observation.get('evaluation', [0.33, 0.34, 0.33])
624
+ draw.text((x, y), f"NNUE: W:{ev[2]:.2f} D:{ev[1]:.2f} B:{ev[0]:.2f}",
625
+ fill=COLORS['accent_green'], font=font_small)
626
+ y += 20
627
+
628
+ if state.hold_active:
629
+ # Merkle root
630
+ draw.rectangle([x, y, x + w, y + 30], fill=COLORS['bg_card'])
631
+ draw.text((x + 5, y + 8), f"MERKLE: {state.current_merkle}",
632
+ fill=COLORS['merkle_glow'], font=font_small)
633
+ y += 40
634
+
635
+ if state.ui_mode == "LIST":
636
+ # Candidate list
637
+ draw.text((x, y), "SELECT MOVE TO ANALYZE:", fill=COLORS['accent_cyan'], font=font)
638
+ y += 25
639
+
640
+ for i, c in enumerate(state.candidates[:5]):
641
+ is_hover = i == state.hovered_idx
642
+ row_color = COLORS['bg_card_hover'] if is_hover else COLORS['bg_card']
643
+ draw.rectangle([x, y, x + w, y + 35], fill=row_color)
644
+
645
+ # Move and confidence
646
+ draw.text((x + 5, y + 5), f"#{i+1}", fill=COLORS['text_muted'], font=font_small)
647
+ draw.text((x + 30, y + 5), c['move'], fill=COLORS['text_primary'], font=font)
648
+ draw.text((x + 90, y + 5), f"{c['prob']*100:.1f}%", fill=COLORS['accent_green'], font=font)
649
+
650
+ # Value head
651
+ vh = c.get('v_head', [0.33, 0.34, 0.33])
652
+ draw.text((x + 150, y + 5), f"W:{vh[2]:.2f}", fill=COLORS['text_secondary'], font=font_small)
653
+
654
+ y += 40
655
+
656
+ elif state.ui_mode == "DRILL" and state.candidates:
657
+ c = state.candidates[state.selected_idx]
658
+
659
+ draw.text((x, y), f"ANALYZING: {c['move']}", fill=COLORS['accent_gold'], font=font)
660
+ y += 30
661
+
662
+ # Metrics
663
+ draw.rectangle([x, y, x + w, y + 80], fill=COLORS['bg_card'])
664
+ draw.text((x + 10, y + 10), f"Confidence: {c['prob']*100:.2f}%", fill=COLORS['text_primary'], font=font)
665
+
666
+ vh = c.get('v_head', [0.33, 0.34, 0.33])
667
+ draw.text((x + 10, y + 30), f"White: {vh[2]:.4f}", fill=COLORS['accent_green'], font=font_small)
668
+ draw.text((x + 100, y + 30), f"Draw: {vh[1]:.4f}", fill=COLORS['accent_orange'], font=font_small)
669
+ draw.text((x + 190, y + 30), f"Black: {vh[0]:.4f}", fill=COLORS['accent_red'], font=font_small)
670
+
671
+ draw.text((x + 10, y + 55), c.get('reason', ''), fill=COLORS['text_muted'], font=font_small)
672
+ y += 90
673
+
674
+ # Predictions
675
+ pred = next((p for p in state.predictions if p.move == c['move']), None)
676
+ if pred:
677
+ draw.text((x, y), "CASCADE PREDICTION:", fill=COLORS['accent_purple'], font=font)
678
+ y += 20
679
+ draw.text((x + 10, y), f"Risk: {pred.risk_score:.2f}",
680
+ fill=COLORS['accent_red'] if pred.risk_score > 0.5 else COLORS['accent_green'], font=font_small)
681
+ draw.text((x + 100, y), f"Material: {pred.material_delta:+d}", fill=COLORS['text_secondary'], font=font_small)
682
+ y += 20
683
+ if pred.predicted_responses:
684
+ draw.text((x + 10, y), f"Response: {pred.predicted_responses[0]}",
685
+ fill=COLORS['prediction_blue'] if 'prediction_blue' in COLORS else COLORS['accent_cyan'], font=font_small)
686
+ else:
687
+ draw.text((x, y + 50), "Waiting for AI decision...", fill=COLORS['text_muted'], font=font)
688
+
689
+ def _draw_cascade_tab(draw, state, x, y, w, font, font_small):
690
+ """Draw cascade predictions tab"""
691
+ draw.text((x, y), "CASCADE PREDICTIONS", fill=COLORS['accent_purple'], font=font)
692
+ y += 25
693
+
694
+ if not state.predictions:
695
+ draw.text((x, y), "No predictions available", fill=COLORS['text_muted'], font=font_small)
696
+ return
697
+
698
+ for pred in state.predictions[:5]:
699
+ draw.rectangle([x, y, x + w, y + 50], fill=COLORS['bg_card'])
700
+
701
+ # Risk color
702
+ risk_color = COLORS['accent_green'] if pred.risk_score < 0.3 else \
703
+ COLORS['accent_orange'] if pred.risk_score < 0.6 else COLORS['accent_red']
704
+
705
+ draw.text((x + 5, y + 5), pred.move, fill=COLORS['text_primary'], font=font)
706
+ draw.text((x + 60, y + 5), f"Risk: {pred.risk_score:.2f}", fill=risk_color, font=font_small)
707
+ draw.text((x + 150, y + 5), f"Mat: {pred.material_delta:+d}", fill=COLORS['text_secondary'], font=font_small)
708
+
709
+ if pred.predicted_responses:
710
+ resp_str = " → ".join(pred.predicted_responses[:2])
711
+ draw.text((x + 5, y + 28), f"Chain: {resp_str}", fill=COLORS['accent_cyan'], font=font_small)
712
+
713
+ y += 55
714
+
715
+ def _draw_chain_tab(draw, state, x, y, w, font, font_small):
716
+ """Draw merkle chain tab"""
717
+ draw.text((x, y), "MERKLE PROVENANCE CHAIN", fill=COLORS['merkle_glow'], font=font)
718
+ y += 25
719
+
720
+ if not state.causal_chain:
721
+ draw.text((x, y), "Chain empty...", fill=COLORS['text_muted'], font=font_small)
722
+ return
723
+
724
+ for node in state.causal_chain[-8:]:
725
+ bg_color = COLORS['bg_card_hover'] if node.was_override else COLORS['bg_card']
726
+ draw.rectangle([x, y, x + w, y + 35], fill=bg_color)
727
+
728
+ draw.text((x + 5, y + 5), f"#{node.depth}", fill=COLORS['text_muted'], font=font_small)
729
+ draw.text((x + 35, y + 5), node.move or "START",
730
+ fill=COLORS['accent_gold'] if node.was_override else COLORS['text_primary'], font=font)
731
+ draw.text((x + 90, y + 5), node.merkle[:12], fill=COLORS['merkle_glow'], font=font_small)
732
+ draw.text((x + 200, y + 5), node.player[:10], fill=COLORS['text_secondary'], font=font_small)
733
+
734
+ y += 40
735
+
736
+ # ═══════════════════════════════════════════════════════════════════════════════
737
+ # GRADIO INTERFACE
738
+ # ═══════════════════════════════════════════════════════════════════════════════
739
+
740
+ # Global game state
741
+ game_state = ChessGameState()
742
+
743
+ def process_click(evt: gr.SelectData, current_image):
744
+ """Handle clicks on the game image"""
745
+ global game_state
746
+
747
+ if evt is None:
748
+ return render_game(game_state), get_status_text()
749
+
750
+ x, y = evt.index
751
+
752
+ # Check if click is in panel area (tabs or candidates)
753
+ if x > BOARD_SIZE:
754
+ px = x - BOARD_SIZE - 10
755
+ py = y
756
+
757
+ # Tab clicks (around y=65-87)
758
+ if 65 <= py <= 87:
759
+ tab_w = (PANEL_WIDTH - 20) // 3
760
+ tab_idx = px // tab_w
761
+ tabs = ["WEALTH", "CASCADE", "CHAIN"]
762
+ if 0 <= tab_idx < len(tabs):
763
+ game_state.active_tab = tabs[tab_idx]
764
+
765
+ # Candidate clicks in LIST mode
766
+ elif game_state.hold_active and game_state.ui_mode == "LIST" and py > 130:
767
+ # Each candidate row is ~40px starting around y=130
768
+ candidate_idx = (py - 130) // 40
769
+ if 0 <= candidate_idx < len(game_state.candidates):
770
+ game_state.selected_idx = candidate_idx
771
+ game_state.ui_mode = "DRILL"
772
+
773
+ return render_game(game_state), get_status_text()
774
+
775
+ def execute_move():
776
+ """Execute the currently selected move"""
777
+ global game_state
778
+
779
+ if not game_state.hold_active or game_state.game_over:
780
+ return render_game(game_state), get_status_text()
781
+
782
+ if game_state.candidates and game_state.selected_idx < len(game_state.candidates):
783
+ selected = game_state.candidates[game_state.selected_idx]
784
+ ai_move = game_state.candidates[0]['move']
785
+ was_override = selected['move'] != ai_move
786
+
787
+ # Make white's move
788
+ game_state.make_move(selected['move'], "ChessBot", was_override)
789
+ game_state.hold_active = False
790
+ game_state.ui_mode = "LIST"
791
+
792
+ # Stockfish responds (if game not over)
793
+ if not game_state.game_over and game_state.board.turn == chess.BLACK:
794
+ sf_move = game_state.get_stockfish_move()
795
+ game_state.make_move(sf_move, "Stockfish", False)
796
+
797
+ # Get next AI move if it's white's turn again
798
+ if not game_state.game_over and game_state.board.turn == chess.WHITE:
799
+ trigger_ai_turn()
800
+
801
+ return render_game(game_state), get_status_text()
802
+
803
+ def trigger_ai_turn():
804
+ """Trigger AI to generate move candidates"""
805
+ global game_state
806
+
807
+ if game_state.game_over:
808
+ return
809
+
810
+ ai_move, candidates, value = game_state.get_ai_move()
811
+ if candidates:
812
+ game_state.candidates = candidates
813
+ game_state.predictions = game_state.predict_cascade(candidates)
814
+ game_state.hold_active = True
815
+ game_state.current_merkle = game_state._compute_merkle({
816
+ 'fen': game_state.board.fen(),
817
+ 'candidates': [c['move'] for c in candidates]
818
+ })
819
+ game_state.selected_idx = 0
820
+
821
+ # Store observation
822
+ game_state.last_observation = {
823
+ 'evaluation': candidates[0].get('v_head', [0.33, 0.34, 0.33])
824
+ }
825
+
826
+ def back_to_list():
827
+ """Go back to list mode"""
828
+ global game_state
829
+ game_state.ui_mode = "LIST"
830
+ return render_game(game_state), get_status_text()
831
+
832
+ def new_game():
833
+ """Start a new game"""
834
+ global game_state
835
+ game_state.reset_game()
836
+ game_state.causal_chain = []
837
+ game_state.merkle_chain = []
838
+
839
+ # Register game start
840
+ game_state.register_event({
841
+ 'type': 'game_start',
842
+ 'move': '',
843
+ 'player': 'SYSTEM',
844
+ 'fen': game_state.board.fen(),
845
+ 'value': 0.0
846
+ })
847
+
848
+ # Trigger first AI move
849
+ trigger_ai_turn()
850
+
851
+ return render_game(game_state), get_status_text()
852
+
853
+ def get_status_text():
854
+ """Get current status text"""
855
+ global game_state
856
+
857
+ if game_state.game_over:
858
+ return f"🏁 {game_state.game_result}"
859
+ elif game_state.hold_active:
860
+ mode = "DRILL" if game_state.ui_mode == "DRILL" else "SELECT"
861
+ return f"⏸️ HOLD ACTIVE ({mode}) - {len(game_state.candidates)} candidates | Merkle: {game_state.current_merkle[:12]}..."
862
+ else:
863
+ turn = "White (ChessBot)" if game_state.board.turn == chess.WHITE else "Black (Stockfish)"
864
+ return f"▶️ {turn} to move | Moves: {len(game_state.move_history)}"
865
+
866
+ def get_info_text():
867
+ """Get info panel text"""
868
+ global game_state
869
+
870
+ lines = [
871
+ "═══ CASCADE LATTICE CHESS ═══",
872
+ "",
873
+ f"Chain Length: {len(game_state.causal_chain)}",
874
+ f"Total Holds: {game_state.session.total_holds}",
875
+ f"Override Rate: {game_state.session.override_rate*100:.1f}%",
876
+ f"Max Combo: {game_state.session.max_combo}",
877
+ "",
878
+ "Controls:",
879
+ "• Click candidates to drill down",
880
+ "• Execute to confirm move",
881
+ "• Back to return to list",
882
+ "• New Game to restart",
883
+ ]
884
+
885
+ if game_state.hold_active and game_state.ui_mode == "DRILL" and game_state.candidates:
886
+ c = game_state.candidates[game_state.selected_idx]
887
+ lines.extend([
888
+ "",
889
+ f"═══ SELECTED: {c['move']} ═══",
890
+ f"Confidence: {c['prob']*100:.2f}%",
891
+ f"Reason: {c.get('reason', 'N/A')}",
892
+ ])
893
+
894
+ return "\n".join(lines)
895
+
896
+ # Build Gradio interface
897
+ with gr.Blocks(title="Cascade Lattice Chess", theme=gr.themes.Base(primary_hue="cyan")) as demo:
898
+ gr.Markdown("""
899
+ # 🏁 CASCADE LATTICE CHESS // SUPER SAIYAN MODE
900
+
901
+ **Human-in-the-loop chess with cryptographic provenance via cascade-lattice**
902
+
903
+ Watch the AI think, override its decisions, and track every choice through a Merkle chain!
904
+ """)
905
+
906
+ with gr.Row():
907
+ with gr.Column(scale=3):
908
+ game_image = gr.Image(
909
+ value=render_game(game_state),
910
+ label="Game Board",
911
+ interactive=True,
912
+ height=TOTAL_HEIGHT
913
+ )
914
+ status = gr.Textbox(value=get_status_text(), label="Status", interactive=False)
915
+
916
+ with gr.Column(scale=1):
917
+ info = gr.Textbox(value=get_info_text(), label="Info", lines=20, interactive=False)
918
+
919
+ with gr.Row():
920
+ new_btn = gr.Button("🆕 New Game", variant="secondary")
921
+ back_btn = gr.Button("⬅️ Back", variant="secondary")
922
+
923
+ execute_btn = gr.Button("✅ Execute Move", variant="primary", size="lg")
924
+
925
+ # Event handlers
926
+ game_image.select(process_click, inputs=[game_image], outputs=[game_image, status])
927
+ execute_btn.click(execute_move, outputs=[game_image, status])
928
+ back_btn.click(back_to_list, outputs=[game_image, status])
929
+ new_btn.click(new_game, outputs=[game_image, status])
930
+
931
+ # Auto-refresh info
932
+ game_image.change(lambda: get_info_text(), outputs=[info])
933
+
934
+ # Initialize first game
935
+ trigger_ai_turn()
936
+
937
+ if __name__ == "__main__":
938
+ demo.launch()
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ stockfish
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ chess
3
+ torch
4
+ transformers
5
+ numpy
6
+ Pillow
7
+ cascade-lattice>=0.5.0