LittlePlyResearch notebook

root_policy.py

python · 128 lines · Exact bytes

Line 1 ↗
Version & provenance · data-cm7

Recorded SHA-256 4c9daa4621205902a84c382908ae0e2b3eff62b236c33d77a6995ea8c62dc722Rendered SHA-256 4c9daa4621205902a84c382908ae0e2b3eff62b236c33d77a6995ea8c62dc722

Apache-2.0 original project code; dependencies retain their own licenses. License notes. Line links are stable within this snapshot. CM-7 completed implementation source; original and public rendered hashes recorded. Host-specific paths use /work placeholders; inspect reproduction instructions before running. Separate Rust helper retains its GPL license.

1"""Conservative rule-aware Syzygy policy with explicit unresolved intervals.
2
3No unknown-history root is exact-labelled. A conservative DTZ margin with no
4repeated position can certify outcome; borderline cases use interval search.
5A bounded game tree can finish at an irreversible zero-counter state,
6where five-valued WDL is exact under claimable 50-move chess. Cursed/blessed WDL
7map to draw. At a limit, interval bounds keep uncertainty rather than guess.
8"""
9from dataclasses import dataclass
10from inventory_inputs import c
11
12class Unresolved(Exception):pass
13
14@dataclass(frozen=True)
15class Interval:
16    lo:int
17    hi:int
18    reason:str
19    @property
20    def exact(self):return self.lo==self.hi
21
22def wdl_outcome(wdl):
23    if type(wdl) is not int or wdl not in (-2,-1,0,1,2):raise ValueError('invalid WDL')
24    return 1 if wdl==2 else -1 if wdl==-2 else 0
25
26def check_probe(wdl,dtz):
27    wdl_outcome(wdl)
28    if type(dtz) is not int:raise ValueError('invalid DTZ')
29    if (wdl==0 and dtz!=0) or (wdl!=0 and ((dtz>0)-(dtz<0))!=((wdl>0)-(wdl<0))):
30        raise ValueError('WDL/DTZ sign inconsistency')
31    if abs(wdl)==1 and abs(dtz)<=100:raise ValueError('cursed/blessed DTZ range')
32    if abs(wdl)==2 and not 1<=abs(dtz)<=100:raise ValueError('unconditional DTZ range')
33
34def negate(v):return Interval(-v.hi,-v.lo,'negated:'+v.reason)
35
36def validate_history(board,history_known):
37    if not history_known:raise Unresolved('unknown repetition history')
38    if len(board.move_stack)<board.halfmove_clock:raise Unresolved('history shorter than reversible halfmove window')
39    b=board.copy(stack=True);moves=[]
40    for _ in range(board.halfmove_clock):moves.append(b.pop())
41    if b.halfmove_clock!=0:raise Unresolved('history does not reach zero-counter boundary')
42    for move in reversed(moves):
43        if not move or move not in b.legal_moves or b.is_game_over(claim_draw=False):raise ValueError('invalid historical move')
44        b.push(move)
45    if b.fen(en_passant='fen')!=board.fen(en_passant='fen'):raise ValueError('historical clock/state mismatch')
46
47def claims(board):
48    return board.can_claim_fifty_moves() or board.can_claim_threefold_repetition()
49
50def no_repetition_since_zeroing(board):
51    if len(board.move_stack)<board.halfmove_clock:return False
52    b=board.copy(stack=True);keys=set()
53    for n in range(board.halfmove_clock+1):
54        key=c.position_key(b)
55        if key in keys:return False
56        keys.add(key)
57        if n<board.halfmove_clock:b.pop()
58    return True
59
60class Solver:
61    def __init__(self,tb,max_nodes=20000):self.tb=tb;self.max_nodes=max_nodes;self.nodes=0
62    def value(self,board,depth):
63        if self.nodes>=self.max_nodes:return Interval(-1,1,'node_budget')
64        self.nodes+=1
65        if board.is_checkmate():return Interval(-1,-1,'checkmate')
66        if board.is_game_over(claim_draw=False):return Interval(0,0,'automatic_draw')
67        wdl=self.tb.probe_wdl(board);dtz=self.tb.probe_dtz(board);check_probe(wdl,dtz)
68        raw=wdl_outcome(wdl)
69        # Draw-only extra rules cannot turn a zero-counter draw into a loss or win.
70        if raw==0:return Interval(0,0,'tablebase_draw')
71        if board.halfmove_clock==0:return Interval(raw,raw,'zeroing_boundary')
72        claim=claims(board)
73        if raw==-1 and claim:return Interval(0,0,'claim_saves_loss')
74        # Conservative margin (stricter than positive DTZ+clock<=99); no
75        # historical twofold anywhere in the reversible window. See cited
76        # Stockfish Syzygy outcome bounds, not an engine score or mate distance.
77        if 2*abs(dtz)+board.halfmove_clock<100 and no_repetition_since_zeroing(board):
78            return Interval(raw,raw,'conservative_dtz_margin_no_repetition')
79        bounds=Interval(0,1,'limit') if raw==1 else Interval(-1,0,'limit')
80        if depth<=0 or self.nodes>=self.max_nodes:return bounds
81        low=0 if claim else -1;high=0 if claim else -1
82        moves=list(board.legal_moves)
83        # Zeroing/mating checks can certify a branch cheaply; ordering isn't a label.
84        moves.sort(key=lambda m:(not board.is_zeroing(m),m.uci()))
85        for move in moves:
86            board.push(move)
87            v=negate(self.value(board,depth-1))
88            board.pop()
89            low=max(low,v.lo);high=max(high,v.hi)
90            if low==1:return Interval(1,1,'proved_winning_child')
91        low=max(low,bounds.lo);high=min(high,bounds.hi)
92        if low>high:raise ValueError('tablebase/tree contradiction')
93        return Interval(low,high,'tree')
94
95def root_policy(tb,board,*,history_known=False,depth=2,max_nodes=20000):
96    c.check_board(board)
97    validate_history(board,history_known)
98    if board.castling_rights or len(board.piece_map())>5:raise ValueError('unsupported board')
99    if board.is_game_over(claim_draw=False):raise ValueError('terminal root')
100    if type(depth) is not int or depth<0 or type(max_nodes) is not int or max_nodes<1:raise ValueError('invalid bound')
101    original=board.fen(en_passant='fen');stack=len(board.move_stack)
102    solver=Solver(tb,max_nodes);wdl=tb.probe_wdl(board);dtz=tb.probe_dtz(board);check_probe(wdl,dtz)
103    root_bound=solver.value(board,0)
104    entries=[]
105    for move in sorted(board.legal_moves,key=lambda m:m.uci()):
106        child=board.copy(stack=True);zero=child.is_zeroing(move);child.push(move)
107        v=negate(solver.value(child,depth))
108        if v.lo>root_bound.hi:raise ValueError('child contradicts independent root bound')
109        v=Interval(v.lo,min(v.hi,root_bound.hi),v.reason+';root_upper_bound')
110        entries.append(dict(move=move.uci(),lo=v.lo,hi=v.hi,reason=v.reason,zeroing=zero,
111                            child_halfmove_clock=child.halfmove_clock))
112    claim=claims(board)
113    lo=max([x['lo'] for x in entries]+([0] if claim else []));hi=max([x['hi'] for x in entries]+([0] if claim else []))
114    lo=max(lo,root_bound.lo);hi=min(hi,root_bound.hi)
115    if lo>hi:raise ValueError('root contradiction')
116    exact=lo==hi
117    tied=[x['move'] for x in entries if x['lo']==x['hi']==lo] if exact else []
118    unresolved_ties=[x['move'] for x in entries if x['lo']!=x['hi'] and x['hi']>=lo] if exact else [x['move'] for x in entries if x['lo']!=x['hi']]
119    claim_optimal=claim and exact and lo==0
120    ready=exact and not unresolved_ties and bool(tied) and not claim_optimal
121    if board.fen(en_passant='fen')!=original or len(board.move_stack)!=stack:raise AssertionError('mutated caller board')
122    return dict(outcome_interval=[lo,hi],root_outcome_exact=exact,preserving_moves=tied,
123        complete_preserving_move_set=exact and not unresolved_ties,unresolved_ties=unresolved_ties,
124        draw_claim_available=claim,draw_claim_optimal=claim_optimal,move_only_ready=ready,
125        raw_wdl=wdl,raw_dtz=dtz,halfmove_clock=board.halfmove_clock,history_known=True,
126        moves=entries,nodes=solver.nodes,depth=depth,node_budget=max_nodes,
127        method='bounded interval tree; zeroing WDL; conservative DTZ margin with verified repetition window')
128