head_game.py
python · 114 lines · Exact bytes
Version & provenance · cm7-cm8-comparison-v1
Recorded SHA-256 b27393361bf38dc422ba2c9bf7c38c6920358319bfab89a9606bda28e9ae568dRendered SHA-256 b27393361bf38dc422ba2c9bf7c38c6920358319bfab89a9606bda28e9ae568d
Original LittlePly code: Apache-2.0. External Rust helper remains a separate GPL component. License notes. Line links are stable within this snapshot. Frozen research implementation; host paths redacted. Original and rendered digests identify the derivative; not a portable launcher or complete training payload.
1"""Symmetric local model-versus-model play; no production service requests."""
2import json,math,time
3from pathlib import Path
4import chess,chess.pgn
5from audit_helpers import model_metrics,proof_check,MODEL_METRICS
6import harness as h
7from mate_backend import forced_mate
8
9def names(cm8_white):
10 return {chess.WHITE:'CM-8' if cm8_white else 'CM-7',chess.BLACK:'CM-7' if cm8_white else 'CM-8'}
11
12def game(models,cm8_white,out,opening,*,cap=256,walltime=180,helper=forced_mate,guard=None,pace=.25):
13 out.mkdir(parents=True,exist_ok=False)
14 board=chess.Board();record=chess.pgn.Game();node=record;rows=[];players=names(cm8_white)
15 result='*';reason='ply_cap_unfinished';error=None;started=time.monotonic()
16 try:
17 if set(models)!={'CM-7','CM-8'} or any(m.context_length!=122 for m in models.values()):raise ValueError('two state-v2 models required')
18 if cap<len(opening):raise ValueError('opening exceeds cap')
19 for uci in opening:
20 if h.outcome(board):raise ValueError('opening after terminal')
21 move=chess.Move.from_uci(uci)
22 if move not in board.legal_moves:raise ValueError('illegal opening')
23 board.push(move);node=node.add_variation(move);node.comment='Book autoplay; excluded from both model metrics.'
24 while True:
25 end=h.outcome(board)
26 if end:result,reason=end;break
27 if board.ply()>=cap:break
28 time.sleep(pace)
29 if guard:guard()
30 remaining=walltime-(time.monotonic()-started)
31 if remaining<=0:raise TimeoutError('game walltime')
32 before=time.monotonic();player=players[board.turn];model=models[player]
33 search=helper(board,adjudicate=h.outcome,max_nodes=50000,seconds=min(.25,remaining))
34 if search['status']=='proven':
35 move=chess.Move.from_uci(search['move']);metrics={'decision_source':'forced_mate_override','model_query':'not_queried'}
36 else:
37 remaining=walltime-(time.monotonic()-started)
38 if remaining<=0:raise TimeoutError('game walltime after helper')
39 if isinstance(model,h.ModelWorker):model.timeout=min(model.timeout,remaining)
40 move,metrics=model.choose(board)
41 metrics.update(decision_source='model_policy',model_query='queried')
42 if time.monotonic()-started>walltime:raise TimeoutError('game walltime during decision')
43 if move not in board.legal_moves:raise ValueError('illegal player output')
44 row={'ply':board.ply()+1,'uci':move.uci(),'player':player,'color':'white' if board.turn else 'black','seconds':time.monotonic()-before,'mate_search':search,**metrics}
45 validate_move(board,row,player)
46 board.push(move);node=node.add_variation(move)
47 if row['decision_source']=='forced_mate_override':node.comment=f'{player} forced-mate helper override; proof in game.json at ply {board.ply()}; model not queried.'
48 rows.append(row)
49 except Exception as exc:
50 error=repr(exc);reason='timeout_unfinished' if isinstance(exc,TimeoutError) else 'error_unfinished'
51 record.headers.update(Event='CM-7 versus CM-8; frozen paired openings; no Elo claim',White=players[chess.WHITE],Black=players[chess.BLACK],Result=result,Termination=reason,ModelPolicy='both state-v2 BF16 legal joint argmax + adaptive Rust 1/3/5/7')
52 report={'result':result,'termination':reason,'error':error,'plies':board.ply(),'final_fen':board.fen(),'moves':rows,'seconds':time.monotonic()-started,'players':players_to_json(players),'autoplay_plies':len(opening)}
53 (out/'game.pgn').write_text(str(record)+'\n');(out/'game.json').write_text(json.dumps(report,indent=2))
54 return report
55
56def players_to_json(players):return {'white':players[chess.WHITE],'black':players[chess.BLACK]}
57
58def validate_move(board,row,player):
59 if row.get('player')!=player or row.get('color')!=('white' if board.turn else 'black'):raise ValueError('player/color attribution')
60 if not isinstance(row.get('seconds'),(float,int)) or not math.isfinite(row['seconds']) or row['seconds']<0:raise ValueError('latency')
61 search=row.get('mate_search',{})
62 if search.get('max_plies')!=7 or search.get('node_budget')!=50000 or not 0<=search.get('time_budget_seconds',1)<=.25:raise ValueError('helper budget')
63 if search.get('status') not in ('proven','unknown','no_forced_mate') or not math.isfinite(search.get('seconds',float('nan'))) or search['seconds']<0:raise ValueError('helper status/timing')
64 if row.get('decision_source')=='model_policy':
65 if search['status']=='proven':raise ValueError('ignored proven override')
66 model_metrics(row,board)
67 elif row.get('decision_source')=='forced_mate_override':
68 if row.get('model_query')!='not_queried' or MODEL_METRICS.intersection(row):raise ValueError('helper attribution')
69 proof=search.get('proof',{});branches=proof.get('branches',[])
70 if search['status']!='proven' or search.get('move')!=row['uci'] or len(branches)!=1 or branches[0].get('move')!=row['uci']:raise ValueError('proof root mismatch')
71 proof_check(h,board,proof,board.turn)
72 else:raise ValueError('missing decision source')
73
74def audit(folder,report,opening,cm8_white,cap=256):
75 with (folder/'game.pgn').open() as f:
76 parsed=chess.pgn.read_game(f)
77 if chess.pgn.read_game(f) is not None:raise ValueError('extra PGN')
78 if parsed is None or parsed.errors:raise ValueError('PGN parse')
79 moves=list(parsed.mainline_moves());n=opening['autoplay_plies'];players=names(cm8_white)
80 if [m.uci() for m in moves[:n]]!=opening['uci'] or len(moves)!=report['plies'] or len(moves)>cap:raise ValueError('opening/length')
81 if report['players']!=players_to_json(players) or report['autoplay_plies']!=n:raise ValueError('report players/autoplay')
82 if parsed.headers['White']!=players[chess.WHITE] or parsed.headers['Black']!=players[chess.BLACK]:raise ValueError('PGN players')
83 rows=report['moves'];by={r['ply']:r for r in rows}
84 if len(by)!=len(rows) or set(by)!=set(range(n+1,len(moves)+1)):raise ValueError('missing or autoplay metrics')
85 board=chess.Board();stats={p:{'model_queries':0,'raw_greedy_legal':0,'helper_overrides':0} for p in players.values()}
86 for ply,move in enumerate(moves,1):
87 if h.outcome(board) or move not in board.legal_moves:raise ValueError('illegal or post-terminal move')
88 if ply>n:
89 row=by[ply]
90 if row['uci']!=move.uci():raise ValueError('move mismatch')
91 player=players[board.turn];validate_move(board,row,player)
92 if row['model_query']=='queried':stats[player]['model_queries']+=1;stats[player]['raw_greedy_legal']+=row['raw_greedy_legal']
93 else:stats[player]['helper_overrides']+=1
94 board.push(move)
95 end=h.outcome(board)
96 if board.fen()!=report['final_fen'] or parsed.headers['Result']!=report['result'] or parsed.headers['Termination']!=report['termination']:raise ValueError('final mismatch')
97 if report['result']!='*':
98 if end is None or (report['result'],report['termination'])!=end or report['error']:raise ValueError('fabricated scored outcome')
99 elif report['error'] is None:
100 if end or len(moves)!=cap or report['termination']!='ply_cap_unfinished':raise ValueError('false cap unfinished')
101 elif report['termination'] not in ('error_unfinished','timeout_unfinished'):raise ValueError('failure outcome')
102 return parsed,{'result':report['result'],'termination':report['termination'],'error':report['error'],'plies':len(moves),'autoplay_plies':n,'players':report['players'],'statistics':stats,'seconds':report['seconds'],'pgn_and_proofs_verified':True}
103
104def summarize(rows,total=100):
105 def tally(sub):
106 w=d=l=u=0
107 for r in sub:
108 if r['result']=='*':u+=1
109 elif r['result']=='1/2-1/2':d+=1
110 elif r['result']==('1-0' if r['model_color']=='white' else '0-1'):w+=1
111 else:l+=1
112 return {'CM8_wins':w,'draws':d,'CM7_wins':l,'unfinished':u,'CM8_points':w+d/2,'CM7_points':l+d/2,'CM8_score_bounds_including_unfinished':[w+d/2,w+d/2+u]}
113 return {'scheduled_games':total,'audited_games':len(rows),'unattempted_or_unaudited':total-len(rows),'total':tally(rows),'CM8_white':tally([r for r in rows if r['model_color']=='white']),'CM8_black':tally([r for r in rows if r['model_color']=='black']),'interpretation':'direct system comparison on fixed paired openings; no universal Elo estimate'}
114