LittlePlyResearch notebook

head_game.py

python · 115 lines · Exact bytes

Line 1 ↗
Version & provenance · cm7-cm5-comparison-v1

Recorded SHA-256 b41360fcf9778c6258840fd86bca5ed9f2bdf47c2953a538168b4ae47ac47c26Rendered SHA-256 b41360fcf9778c6258840fd86bca5ed9f2bdf47c2953a538168b4ae47ac47c26

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(cm5_white):
10    return {chess.WHITE:'CM-5' if cm5_white else 'CM-7',chess.BLACK:'CM-7' if cm5_white else 'CM-5'}
11
12def game(models,cm5_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(cm5_white)
15    result='*';reason='ply_cap_unfinished';error=None;started=time.monotonic()
16    try:
17        if set(models)!={'CM-7','CM-5'} or models['CM-7'].context_length!=122 or models['CM-5'].context_length!=1024: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-5; frozen paired openings; no Elo claim',White=players[chess.WHITE],Black=players[chess.BLACK],Result=result,Termination=reason,ModelPolicy='CM-7 state-v2 / CM-5 full-history; both 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        if player=='CM-5' and any(t>=128 for t in row['raw_greedy_tokens']):raise ValueError('legacy vocabulary')
68    elif row.get('decision_source')=='forced_mate_override':
69        if row.get('model_query')!='not_queried' or MODEL_METRICS.intersection(row):raise ValueError('helper attribution')
70        proof=search.get('proof',{});branches=proof.get('branches',[])
71        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')
72        proof_check(h,board,proof,board.turn)
73    else:raise ValueError('missing decision source')
74
75def audit(folder,report,opening,cm5_white,cap=256):
76    with (folder/'game.pgn').open() as f:
77        parsed=chess.pgn.read_game(f)
78        if chess.pgn.read_game(f) is not None:raise ValueError('extra PGN')
79    if parsed is None or parsed.errors:raise ValueError('PGN parse')
80    moves=list(parsed.mainline_moves());n=opening['autoplay_plies'];players=names(cm5_white)
81    if [m.uci() for m in moves[:n]]!=opening['uci'] or len(moves)!=report['plies'] or len(moves)>cap:raise ValueError('opening/length')
82    if report['players']!=players_to_json(players) or report['autoplay_plies']!=n:raise ValueError('report players/autoplay')
83    if parsed.headers['White']!=players[chess.WHITE] or parsed.headers['Black']!=players[chess.BLACK]:raise ValueError('PGN players')
84    rows=report['moves'];by={r['ply']:r for r in rows}
85    if len(by)!=len(rows) or set(by)!=set(range(n+1,len(moves)+1)):raise ValueError('missing or autoplay metrics')
86    board=chess.Board();stats={p:{'model_queries':0,'raw_greedy_legal':0,'helper_overrides':0} for p in players.values()}
87    for ply,move in enumerate(moves,1):
88        if h.outcome(board) or move not in board.legal_moves:raise ValueError('illegal or post-terminal move')
89        if ply>n:
90            row=by[ply]
91            if row['uci']!=move.uci():raise ValueError('move mismatch')
92            player=players[board.turn];validate_move(board,row,player)
93            if row['model_query']=='queried':stats[player]['model_queries']+=1;stats[player]['raw_greedy_legal']+=row['raw_greedy_legal']
94            else:stats[player]['helper_overrides']+=1
95        board.push(move)
96    end=h.outcome(board)
97    if board.fen()!=report['final_fen'] or parsed.headers['Result']!=report['result'] or parsed.headers['Termination']!=report['termination']:raise ValueError('final mismatch')
98    if report['result']!='*':
99        if end is None or (report['result'],report['termination'])!=end or report['error']:raise ValueError('fabricated scored outcome')
100    elif report['error'] is None:
101        if end or len(moves)!=cap or report['termination']!='ply_cap_unfinished':raise ValueError('false cap unfinished')
102    elif report['termination'] not in ('error_unfinished','timeout_unfinished'):raise ValueError('failure outcome')
103    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}
104
105def summarize(rows,total=100):
106    def tally(sub):
107        w=d=l=u=0
108        for r in sub:
109            if r['result']=='*':u+=1
110            elif r['result']=='1/2-1/2':d+=1
111            elif r['result']==('1-0' if r['model_color']=='white' else '0-1'):w+=1
112            else:l+=1
113        return {'CM5_wins':w,'draws':d,'CM7_wins':l,'unfinished':u,'CM5_points':w+d/2,'CM7_points':l+d/2,'CM5_score_bounds_including_unfinished':[w+d/2,w+d/2+u]}
114    return {'scheduled_games':total,'audited_games':len(rows),'unattempted_or_unaudited':total-len(rows),'total':tally(rows),'CM5_white':tally([r for r in rows if r['model_color']=='white']),'CM5_black':tally([r for r in rows if r['model_color']=='black']),'interpretation':'direct system comparison on fixed paired openings; no universal Elo estimate'}
115