LittlePlyResearch notebook

lib.rs

rust · 140 lines · Exact bytes

Line 1 ↗
Version & provenance · rust-codec-345ea0dc14f1

Recorded SHA-256 345ea0dc14f1251b861fecb5b3f7be8eb76dcf7f3e3bd842a62d206fa2a6fd18Rendered SHA-256 345ea0dc14f1251b861fecb5b3f7be8eb76dcf7f3e3bd842a62d206fa2a6fd18

Apache-2.0 original code; cozy-chess dependencies MIT License notes. Line links are stable within this snapshot. Exact Rust files match the build receipt and Elite codec identity. src/ paths are flattened only for source browsing; downloadable ZIP preserves original tree. README is a new public build guide.

1//! Experimental move codec. PGN lexical/header policy remains Python-owned.
2//! No changed token IDs, no engine evaluation, no inference probability masking.
3use cozy_chess::{Board, Move, Piece, Color};
4use cozy_chess::util::{parse_san_move,parse_uci_move,display_uci_move,display_san_move};
5
6pub const BOS:u8=69;
7pub const EOS:u8=70;
8pub struct Encoded {pub tokens:Vec<u8>,pub uci:Vec<String>,pub fen:String}
9
10fn normalized_fen(board:&Board,halfmove:u32,fullmove:u32)->String {
11    let raw=board.to_string();
12    let mut parts:Vec<String>=raw.split_whitespace().map(str::to_string).collect();
13    if board.en_passant().is_some() {
14        let mut legal_ep=false;
15        board.generate_moves(|set| {
16            for mv in set {if board.piece_on(mv.from)==Some(Piece::Pawn)
17                && mv.from.file()!=mv.to.file() && board.piece_on(mv.to).is_none(){legal_ep=true;}}
18            false
19        });
20        if !legal_ep {parts[3]="-".into();}
21    }
22    parts[4]=halfmove.to_string();parts[5]=fullmove.to_string();parts.join(" ")
23}
24fn initial(fen:Option<&str>)->Result<(Board,u32,u32),String> {
25    if let Some(f)=fen {
26        let p:Vec<_>=f.split_whitespace().collect();
27        if p.len()!=6{return Err("invalid_fen".into())}
28        if p[2].chars().any(|c|!"KQkq-".contains(c)){return Err("unsupported_castling_variant".into())}
29        let half=p[4].parse::<u32>().map_err(|_|"invalid_halfmove")?;
30        let full=p[5].parse::<u32>().map_err(|_|"invalid_fullmove")?;
31        if full==0{return Err("invalid_fullmove".into())}
32        let neutral=format!("{} {} {} {} 0 1",p[0],p[1],p[2],p[3]);
33        let board=Board::from_fen(&neutral,false).map_err(|_|"invalid_fen")?;
34        Ok((board,half,full))
35    } else {Ok((Board::default(),0,1))}
36}
37fn apply(board:&mut Board,mv:Move,half:&mut u32,full:&mut u32)->Result<(String,[u8;3]),String> {
38    if !board.is_legal(mv){return Err("illegal_move".into())}
39    let uci=display_uci_move(board,mv).to_string();let bytes=uci.as_bytes();
40    let from=(bytes[0]-b'a')+8*(bytes[1]-b'1');let to=(bytes[2]-b'a')+8*(bytes[3]-b'1');
41    let promo=match mv.promotion {None=>64,Some(Piece::Knight)=>65,Some(Piece::Bishop)=>66,
42        Some(Piece::Rook)=>67,Some(Piece::Queen)=>68,_=>return Err("invalid_promotion".into())};
43    let pawn=board.piece_on(mv.from)==Some(Piece::Pawn);
44    let capture=board.color_on(mv.to)==Some(!board.side_to_move());
45    *half=if pawn||capture {0}else{half.checked_add(1).ok_or("clock_overflow")?};
46    if board.side_to_move()==Color::Black {*full=full.checked_add(1).ok_or("clock_overflow")?;}
47    board.play(mv);Ok((uci,[from,to,promo]))
48}
49pub fn encode_san(moves:&str,fen:Option<&str>)->Result<Encoded,String> {
50    let (mut board,mut half,mut full)=initial(fen)?;
51    let mut tokens=vec![BOS];let mut uci=Vec::new();
52    for san in moves.split_whitespace() {
53        let normalized=san.trim_end_matches(|c|c=='!'||c=='?').replace('0',"O");
54        let mv=parse_san_move(&board,&normalized).map_err(|_|format!("illegal_san:{san}"))?;
55        let canonical=display_san_move(&board,mv).to_string();
56        if normalized.trim_end_matches(|c|c=='+'||c=='#')!=canonical.trim_end_matches(|c|c=='+'||c=='#')
57            || ((normalized.ends_with('+')||normalized.ends_with('#')) && normalized.chars().last()!=canonical.chars().last()) {
58            return Err("noncanonical_or_false_check_san".into())
59        }
60        let (text,triple)=apply(&mut board,mv,&mut half,&mut full)?;
61        uci.push(text);tokens.extend_from_slice(&triple);
62    }
63    tokens.push(EOS);Ok(Encoded{tokens,uci,fen:normalized_fen(&board,half,full)})
64}
65pub fn decode_tokens(tokens:&[u8],fen:Option<&str>)->Result<Encoded,String> {
66    if tokens.first()!=Some(&BOS)||tokens.last()!=Some(&EOS)||tokens.len()<2||(tokens.len()-2)%3!=0 {
67        return Err("invalid_boundary_or_phase".into())
68    }
69    let (mut board,mut half,mut full)=initial(fen)?;let mut uci=Vec::new();
70    for triple in tokens[1..tokens.len()-1].chunks_exact(3) {
71        let (from,to,promo)=(triple[0],triple[1],triple[2]);
72        if from>=64||to>=64||!(64..=68).contains(&promo){return Err("invalid_token_or_phase".into())}
73        let mut text=format!("{}{}{}{}",(b'a'+from%8)as char,(b'1'+from/8)as char,(b'a'+to%8)as char,(b'1'+to/8)as char);
74        if promo!=64{text.push([b'n',b'b',b'r',b'q'][(promo-65)as usize]as char);}
75        let mv=parse_uci_move(&board,&text).map_err(|_|"invalid_uci")?;
76        let (canonical,triple2)=apply(&mut board,mv,&mut half,&mut full)?;
77        if triple!=triple2{return Err("noncanonical_move".into())}
78        uci.push(canonical);
79    }
80    Ok(Encoded{tokens:tokens.to_vec(),uci,fen:normalized_fen(&board,half,full)})
81}
82
83/// Strict movetext interpreter after the unchanged Python lexical/header gate.
84/// Every variation is validated; only mainline moves enter the model sequence.
85pub fn encode_movetext(text:&str)->Result<(Encoded,String),String> {
86    // Persistent undo chain: moves/branches retain only O(moves+depth) states.
87    // A saved branch copies exactly one undo entry, matching Board.copy(stack=1).
88    // Use an index arena rather than recursive Rc nodes so dropping a very long
89    // legal history cannot overflow the native call stack.
90    struct Node {board:Board,half:u32,full:u32,previous:Option<usize>}
91    let mut nodes=vec![Node{board:Board::default(),half:0,full:1,previous:None}];
92    let mut current=0usize;let mut variations:Vec<usize>=Vec::new();
93    let mut tokens=vec![BOS];let mut uci=Vec::new();let mut ending=None;
94    for atom in text.split_whitespace() {
95        if ending.is_some(){return Err("trailing_movetext".into())}
96        if atom=="@invalid_san_or_null" {return Err("invalid_san_or_null".into())}
97        if atom=="(" {
98            let previous=nodes[current].previous.ok_or("invalid_variation")?;
99            let undo=Node{board:nodes[previous].board.clone(),half:nodes[previous].half,full:nodes[previous].full,previous:None};
100            let saved=Node{board:nodes[current].board.clone(),half:nodes[current].half,full:nodes[current].full,previous:Some(nodes.len())};
101            nodes.push(undo);nodes.push(saved);variations.push(nodes.len()-1);current=previous;continue;
102        }
103        if atom==")" {current=variations.pop().ok_or("unbalanced_variation")?;continue;}
104        if ["1-0","0-1","1/2-1/2","*"].contains(&atom) {
105            if !variations.is_empty(){return Err("result_inside_variation".into())}
106            ending=Some(atom.to_string());continue;
107        }
108        if (atom.starts_with('$')&&atom.len()>1&&atom[1..].bytes().all(|c|c.is_ascii_digit()))
109            || ["!","?","!!","??","!?","?!"].contains(&atom) {continue;}
110        let state=&nodes[current];let (board,half,full)=(&state.board,&state.half,&state.full);
111        if atom.ends_with('.') {
112            let digits=atom.trim_end_matches('.');
113            if !digits.is_empty()&&digits.bytes().all(|c|c.is_ascii_digit()) {
114                if digits.len()>10||digits.parse::<u32>().ok()!=Some(*full)
115                    || (atom.ends_with("...")!=(board.side_to_move()==Color::Black)) {return Err("incorrect_move_number".into())}
116                continue;
117            }
118        }
119        let normalized=atom.trim_end_matches(|c|c=='!'||c=='?').replace('0',"O");
120        let mv=parse_san_move(board,&normalized).map_err(|_|"illegal_or_ambiguous_move")?;
121        let canonical=display_san_move(board,mv).to_string();
122        if normalized.trim_end_matches(|c|c=='+'||c=='#')!=canonical.trim_end_matches(|c|c=='+'||c=='#')
123            || ((normalized.ends_with('+')||normalized.ends_with('#'))&&normalized.chars().last()!=canonical.chars().last()) {
124            return Err("noncanonical_or_false_check_san".into())
125        }
126        let (mut next,mut h,mut f)=(board.clone(),*half,*full);
127        let (word,triple)=apply(&mut next,mv,&mut h,&mut f)?;
128        if variations.is_empty(){uci.push(word);tokens.extend_from_slice(&triple);}
129        nodes.push(Node{board:next,half:h,full:f,previous:Some(current)});current=nodes.len()-1;
130    }
131    if !variations.is_empty()||ending.is_none(){return Err("incomplete_game".into())}
132    tokens.push(EOS);let state=&nodes[current];let (board,half,full)=(&state.board,&state.half,&state.full);
133    let encoded=Encoded{tokens,uci,fen:normalized_fen(board,*half,*full)};
134    // Independent token-path replay is mandatory, including final board equality.
135    let decoded=decode_tokens(&encoded.tokens,None)?;
136    if encoded.uci!=decoded.uci||encoded.fen!=decoded.fen{return Err("round_trip_mismatch".into())}
137    Ok((encoded,ending.unwrap()))
138}
139
140