Skip to main content

chess/formats/chessbase/game/
parse.rs

1use core::num::NonZeroU32;
2
3use winnow::binary::bits::{self, Bits as BitInput};
4
5use crate::{
6    Player, Side,
7    board::{Piece, Role},
8    formats::{ByteInput as Input, prelude::*},
9    position::SideTable,
10    square::{File, Rank, Square},
11};
12
13use super::{Castles, Game, Header, Info, Mode, Move, PawnMove, Pieces, Position, Token};
14
15pub fn header(input: &mut Input<'_>) -> ModalResult<Header> {
16    let first_game = be_u16.parse_next(input)?;
17    let len = be_u32.parse_next(input)?;
18    // Accumulated change in game-record byte lengths.
19    let accum = be_u32.parse_next(input)?;
20    let zero1 = be_u32.parse_next(input)?;
21    let len2 = be_u32.parse_next(input)?;
22    let zero2 = be_u32.parse_next(input)?;
23    let accum2 = be_u32.parse_next(input)?;
24
25    debug_assert_eq!(len, len2);
26    debug_assert_eq!(accum, accum2);
27    debug_assert_eq!(zero1, 0);
28    debug_assert_eq!(zero2, 0);
29
30    Ok(Header { first_game, len })
31}
32
33pub fn game_with_info(input: &mut Input<'_>) -> ModalResult<(Info, Game)> {
34    let info = info.parse_next(input)?;
35    let game = take(info.len).and_then(game(info)).parse_next(input)?;
36    Ok((info, game))
37}
38
39pub fn game(info: Info) -> impl FnMut(&mut Input<'_>) -> ModalResult<Game> {
40    let mode = info.mode;
41    move |input: &mut Input<'_>| {
42        if mode.is_undecodable() || mode.has_special_encoding() {
43            return Err(backtrack());
44        }
45
46        let position = if mode.has_setup() {
47            let position = position.parse_next(input)?;
48            if mode.is_freestyle() {
49                let _: (u32, u32) = (be_u32, be_u32).parse_next(input)?;
50            }
51            position
52        } else {
53            Position::standard()
54        };
55        let tokens = tokens.parse_next(input)?;
56        Ok(Game { position, tokens })
57    }
58}
59
60pub fn info(input: &mut Input<'_>) -> ModalResult<Info> {
61    let mode = mode.parse_next(input)?;
62    let len = be_u24.verify(|len| *len >= 4).parse_next(input)? as usize - 4;
63    Ok(Info { len, mode })
64}
65
66pub fn mode(input: &mut Input<'_>) -> ModalResult<Mode> {
67    u8.map(Mode).parse_next(input)
68}
69
70pub fn position(input: &mut Input<'_>) -> ModalResult<Position> {
71    bits::bits::<_, _, ContextMode, _, _>(position_bits).parse_next(input)
72}
73
74fn position_bits(input: &mut BitInput<Input<'_>>) -> ModalResult<Position> {
75    let _: u16 = bits::take(11usize).parse_next(input)?;
76    let turn = turn.parse_next(input)?;
77    let en_passant = en_passant.parse_next(input)?;
78    let _: u8 = bits::take(4usize).parse_next(input)?;
79    let castles = castles.parse_next(input)?;
80    let round = round.parse_next(input)?;
81    let (pieces, count) = pieces.parse_next(input)?;
82    let padding = 128usize.checked_sub(4 * count).ok_or_else(backtrack)?;
83    let _: u128 = bits::take(padding).parse_next(input)?;
84
85    Ok(Position { pieces, turn, castles, en_passant, round })
86}
87
88fn turn(input: &mut BitInput<Input<'_>>) -> ModalResult<Player> {
89    bits::bool.map(|black| if black { Player::Black } else { Player::White }).parse_next(input)
90}
91
92fn en_passant(input: &mut BitInput<Input<'_>>) -> ModalResult<Option<File>> {
93    bits::take(4usize)
94        .verify_map(|file: u8| match file {
95            0 => Some(None),
96            1..=8 => Some(File::from_index(file - 1)),
97            _ => None,
98        })
99        .parse_next(input)
100}
101
102fn castles(input: &mut BitInput<Input<'_>>) -> ModalResult<Castles> {
103    use Player::*;
104    use Side::*;
105
106    let mut castles = Castles::empty();
107    for player in [Black, White] {
108        for side in [King, Queen] {
109            castles[player][side] = bits::bool.parse_next(input)?;
110        }
111    }
112    Ok(castles)
113}
114
115fn round(input: &mut BitInput<Input<'_>>) -> ModalResult<NonZeroU32> {
116    bits::take(8usize)
117        // normalize round 0 to round 1
118        .map(|round: u8| NonZeroU32::new(u32::from(round).max(1)).unwrap())
119        .parse_next(input)
120}
121
122fn pieces(input: &mut BitInput<Input<'_>>) -> ModalResult<(Pieces, usize)> {
123    let mut pieces = Pieces::empty();
124    let mut count = 0;
125    for index in 0..64 {
126        let Some(piece) = piece.parse_next(input)? else { continue };
127        let identity = pieces[piece.player][piece.role]
128            .iter_mut()
129            .find(|square| square.is_none())
130            .ok_or_else(backtrack)?;
131        *identity = Some(square(index));
132        count += 1;
133    }
134    Ok((pieces, count))
135}
136
137fn piece(input: &mut BitInput<Input<'_>>) -> ModalResult<Option<Piece>> {
138    if !bits::bool.parse_next(input)? {
139        return Ok(None);
140    }
141    bits::take(4usize).verify_map(encoded_piece).map(Some).parse_next(input)
142}
143
144pub fn tokens(input: &mut Input<'_>) -> ModalResult<Vec<Token>> {
145    let mut unscrambler = Unscrambler::default();
146    repeat(0.., token(&mut unscrambler)).parse_next(input)
147}
148
149impl Position {
150    fn standard() -> Self {
151        use Player::*;
152
153        Self {
154            pieces: Pieces::standard(),
155            turn: White,
156            castles: Castles::filled(SideTable::filled(true)),
157            en_passant: None,
158            round: NonZeroU32::MIN,
159        }
160    }
161}
162
163fn encoded_piece(piece: u8) -> Option<Piece> {
164    use Player::*;
165
166    Some(match piece {
167        1 => White.king(),
168        2 => White.queen(),
169        3 => White.knight(),
170        4 => White.bishop(),
171        5 => White.rook(),
172        6 => White.pawn(),
173        9 => Black.king(),
174        10 => Black.queen(),
175        11 => Black.knight(),
176        12 => Black.bishop(),
177        13 => Black.rook(),
178        14 => Black.pawn(),
179        _ => return None,
180    })
181}
182
183#[derive(Clone, Copy, Default)]
184pub struct Unscrambler {
185    offset: u8,
186}
187
188impl Unscrambler {
189    pub fn byte(self, input: &mut Input<'_>) -> ModalResult<u8> {
190        u8.map(|byte| {
191            let index = byte.wrapping_sub(self.offset) as usize;
192            PERMUTE[index]
193        })
194        .parse_next(input)
195    }
196
197    pub fn bump(&mut self) {
198        self.offset = self.offset.wrapping_add(1);
199    }
200}
201
202fn token<'i, 'u>(
203    unscrambler: &'u mut Unscrambler,
204) -> impl FnMut(&mut Input<'i>) -> ModalResult<Token> + 'u {
205    move |input| {
206        let token = scrambled_token(*unscrambler).parse_next(input)?;
207        if matches!(token, Token::Move(_)) {
208            unscrambler.bump();
209        }
210        Ok(token)
211    }
212}
213
214fn scrambled_token<'a>(
215    unscrambler: Unscrambler,
216) -> impl FnMut(&mut Input<'a>) -> ModalResult<Token> {
217    use Move::{Castle, King, Null};
218
219    move |input: &mut Input<'_>| {
220        Ok(Token::Move(match unscrambler.byte(input)? {
221            // Null
222            0x00 => Null,
223
224            // King
225            0x01 => King(0, 1),
226            0x02 => King(1, 1),
227            0x03 => King(1, 0),
228            0x04 => King(1, -1),
229            0x05 => King(0, -1),
230            0x06 => King(-1, -1),
231            0x07 => King(-1, 0),
232            0x08 => King(-1, 1),
233            0x09 => Castle(2),
234            0x0a => Castle(-2),
235
236            // Queen
237            queen @ 0x0b..=0x26 => Move::queen(0, queen - 0x0b),
238            queen @ 0x8f..=0xaa => Move::queen(1, queen - 0x8f),
239            queen @ 0xab..=0xc6 => Move::queen(2, queen - 0xab),
240
241            // Rook
242            rook @ 0x27..=0x34 => Move::rook(0, rook - 0x27),
243            rook @ 0x35..=0x42 => Move::rook(1, rook - 0x35),
244            rook @ 0xc7..=0xd4 => Move::rook(2, rook - 0xc7),
245
246            // Bishop
247            bishop @ 0x43..=0x50 => Move::bishop(0, bishop - 0x43),
248            bishop @ 0x51..=0x5e => Move::bishop(1, bishop - 0x51),
249            bishop @ 0xd5..=0xe2 => Move::bishop(2, bishop - 0xd5),
250
251            // Knight
252            knight @ 0x5f..=0x66 => Move::knight(0, knight - 0x5f),
253            knight @ 0x67..=0x6e => Move::knight(1, knight - 0x67),
254            knight @ 0xe3..=0xea => Move::knight(2, knight - 0xe3),
255
256            // Pawn
257            pawn @ 0x6f..=0x8e => Move::pawn(pawn - 0x6f),
258
259            // Promote
260            0xeb => {
261                let hi = u16::from(unscrambler.byte(input)?);
262                let lo = u16::from(unscrambler.byte(input)?);
263                Move::promote((hi << 8) | lo)
264            }
265
266            // Skip
267            0xec..=0xfd => return Ok(Token::Skip),
268
269            // Push
270            0xfe => return Ok(Token::Push),
271
272            // Pop
273            0xff => return Ok(Token::Pop),
274        }))
275    }
276}
277
278fn square(index: u8) -> Square {
279    debug_assert!(index < 64);
280    Square::new(File::panicky_from_index(index >> 3), Rank::panicky_from_index(index & 7))
281}
282
283impl Move {
284    fn bishop(i: u8, bishop: u8) -> Self {
285        use Move::Bishop;
286
287        match bishop {
288            d @ 0x0..=0x6 => Bishop(i, d + 1, d + 1), // (1, 1) ... (7, 7)
289            e @ 0x7..=0xd => Bishop(i, e - 6, 14 - e), // (1, 7) ... (7, 1)
290            _ => unreachable!(),
291        }
292    }
293
294    fn knight(i: u8, knight: u8) -> Self {
295        use Move::Knight;
296
297        match knight {
298            0 => Knight(i, 2, 1),
299            1 => Knight(i, 1, 2),
300            2 => Knight(i, -1, 2),
301            3 => Knight(i, -2, 1),
302            4 => Knight(i, -2, -1),
303            5 => Knight(i, -1, -2),
304            6 => Knight(i, 1, -2),
305            7 => Knight(i, 2, -1),
306            _ => unreachable!(),
307        }
308    }
309
310    fn pawn(pawn: u8) -> Self {
311        use Move::Pawn;
312        use PawnMove::*;
313
314        let file = File::panicky_from_index(pawn / 4);
315        match pawn % 4 {
316            0 => Pawn(file, One),
317            1 => Pawn(file, Two),
318            2 => Pawn(file, Right),
319            3 => Pawn(file, Left),
320            _ => unreachable!(),
321        }
322    }
323
324    fn promote(word: u16) -> Self {
325        use Role::*;
326
327        let from = word & 63;
328        let to = (word >> 6) & 63;
329        let role = match (word >> 12) & 3 {
330            0 => Queen,
331            1 => Rook,
332            2 => Bishop,
333            3 => Knight,
334            _ => unreachable!(),
335        };
336        Self::FromTo(square(from as u8), square(to as u8), role)
337    }
338
339    fn queen(i: u8, queen: u8) -> Self {
340        use Move::Queen;
341
342        match queen {
343            y @ 0x00..=0x06 => Queen(i, 0, y + 1), // (0, 1) ... (0, 7)
344            x @ 0x07..=0x0d => Queen(i, x - 6, 0), // (1, 0) ... (7, 0)
345            d @ 0x0e..=0x14 => Queen(i, d - 0x0d, d - 0x0d), // (1, 1) ... (7, 7)
346            e @ 0x15..=0x1b => Queen(i, e - 0x14, 0x1c - e), // (1, 7) ... (7, 1)
347            _ => unreachable!(),
348        }
349    }
350
351    fn rook(i: u8, rook: u8) -> Self {
352        use Move::Rook;
353
354        match rook {
355            x @ 0x0..=0x6 => Rook(i, 0, x + 1), // (0, 1) .. (0, 7)
356            y @ 0x7..=0xd => Rook(i, y - 6, 0), // (1, 0) .. (7, 0)
357            _ => unreachable!(),
358        }
359    }
360}
361
362pub const PERMUTE: [u8; 256] = [
363    0xa2, 0x95, 0x43, 0xf5, 0xc1, 0x3d, 0x4a, 0x6c, //   0 -   7
364    0x53, 0x83, 0xcc, 0x7c, 0xff, 0xae, 0x68, 0xad, //   8 -  15
365    0xd1, 0x92, 0x8b, 0x8d, 0x35, 0x81, 0x5e, 0x74, //  16 -  23
366    0x26, 0x8e, 0xab, 0xca, 0xfd, 0x9a, 0xf3, 0xa0, //  24 -  31
367    0xa5, 0x15, 0xfc, 0xb1, 0x1e, 0xed, 0x30, 0xea, //  32 -  39
368    0x22, 0xeb, 0xa7, 0xcd, 0x4e, 0x6f, 0x2e, 0x24, //  40 -  47
369    0x32, 0x94, 0x41, 0x8c, 0x6e, 0x58, 0x82, 0x50, //  48 -  55
370    0xbb, 0x02, 0x8a, 0xd8, 0xfa, 0x60, 0xde, 0x52, //  56 -  63
371    0xba, 0x46, 0xac, 0x29, 0x9d, 0xd7, 0xdf, 0x08, //  64 -  71
372    0x21, 0x01, 0x66, 0xa3, 0xf1, 0x19, 0x27, 0xb5, //  72 -  79
373    0x91, 0xd5, 0x42, 0x0e, 0xb4, 0x4c, 0xd9, 0x18, //  80 -  87
374    0x5f, 0xbc, 0x25, 0xa6, 0x96, 0x04, 0x56, 0x6a, //  88 -  95
375    0xaa, 0x33, 0x1c, 0x2b, 0x73, 0xf0, 0xdd, 0xa4, //  96 - 103
376    0x37, 0xd3, 0xc5, 0x10, 0xbf, 0x5a, 0x23, 0x34, // 104 - 111
377    0x75, 0x5b, 0xb8, 0x55, 0xd2, 0x6b, 0x09, 0x3a, // 112 - 119
378    0x57, 0x12, 0xb3, 0x77, 0x48, 0x85, 0x9b, 0x0f, // 120 - 127
379    0x9e, 0xc7, 0xc8, 0xa1, 0x7f, 0x7a, 0xc0, 0xbd, // 128 - 135
380    0x31, 0x6d, 0xf6, 0x3e, 0xc3, 0x11, 0x71, 0xce, // 136 - 143
381    0x7d, 0xda, 0xa8, 0x54, 0x90, 0x97, 0x1f, 0x44, // 144 - 151
382    0x40, 0x16, 0xc9, 0xe3, 0x2c, 0xcb, 0x84, 0xec, // 152 - 159
383    0x9f, 0x3f, 0x5c, 0xe6, 0x76, 0x0b, 0x3c, 0x20, // 160 - 167
384    0xb7, 0x36, 0x00, 0xdc, 0xe7, 0xf9, 0x4f, 0xf7, // 168 - 175
385    0xaf, 0x06, 0x07, 0xe0, 0x1a, 0x0a, 0xa9, 0x4b, // 176 - 183
386    0x0c, 0xd6, 0x63, 0x87, 0x89, 0x1d, 0x13, 0x1b, // 184 - 191
387    0xe4, 0x70, 0x05, 0x47, 0x67, 0x7b, 0x2f, 0xee, // 192 - 199
388    0xe2, 0xe8, 0x98, 0x0d, 0xef, 0xcf, 0xc4, 0xf4, // 200 - 207
389    0xfb, 0xb0, 0x17, 0x99, 0x64, 0xf2, 0xd4, 0x2a, // 208 - 215
390    0x03, 0x4d, 0x78, 0xc6, 0xfe, 0x65, 0x86, 0x88, // 216 - 223
391    0x79, 0x45, 0x3b, 0xe5, 0x49, 0x8f, 0x2d, 0xb9, // 224 - 231
392    0xbe, 0x62, 0x93, 0x14, 0xe9, 0xd0, 0x38, 0x9c, // 232 - 239
393    0xb2, 0xc2, 0x59, 0x5d, 0xb6, 0x72, 0x51, 0xf8, // 240 - 247
394    0x28, 0x7e, 0x61, 0x39, 0xe1, 0xdb, 0x69, 0x80, // 248 - 255
395];
396
397#[cfg(test)]
398mod tests {
399    use std::collections::BTreeMap;
400    use std::io::Cursor;
401
402    use hex_literal::hex;
403    use winnow::Parser as _;
404
405    use crate::formats::{
406        Fen,
407        chessbase::{archive, index},
408        pgn, twic,
409    };
410
411    use super::*;
412
413    fn unpack(input: &[u8]) -> BTreeMap<String, Vec<u8>> {
414        let mut input = input;
415        let header = archive::read_header(&mut input).expect("can parse CBV header");
416        header
417            .members
418            .iter()
419            .map(|member| {
420                let mut data = Vec::new();
421                archive::unpack_to(member, &mut input, &mut data).expect("can unpack CBV member");
422                assert_eq!(data.len(), member.len);
423                (member.name.clone(), data)
424            })
425            .collect()
426    }
427
428    fn encode(tokens: &[u8]) -> Vec<u8> {
429        let mut count = 0_u8;
430        tokens
431            .iter()
432            .map(|token| {
433                let index = PERMUTE
434                    .iter()
435                    .position(|candidate| candidate == token)
436                    .expect("canonical token is represented") as u8;
437                let encoded = index.wrapping_add(count);
438                if *token <= 0xeb {
439                    count = count.wrapping_add(1);
440                }
441                encoded
442            })
443            .collect()
444    }
445
446    fn mainline(game: &crate::Game) -> Vec<crate::Move> {
447        let mut plays = Vec::new();
448        let mut position = game.start();
449        while let Ok(play) = position.main() {
450            plays.push(play.play());
451            position = play.position();
452        }
453        plays
454    }
455
456    #[test]
457    #[ignore = "exhaustive comparison against the external TWIC fixture"]
458    fn twic_games() {
459        let index = twic::data(1616, "cbh");
460        let index = index::index.parse(index.as_slice()).expect("can parse CBH file");
461
462        let input = twic::data(1616, "cbg");
463        let header = header.parse_next(&mut input.as_slice()).expect("can parse CBG header");
464        assert_eq!(input.len(), header.len as usize);
465        let pgns = pgn::stream::pgns(Cursor::new(twic::data(1616, "pgn")));
466
467        let mut count = 0;
468        for (i, (indexed, pgn)) in index.games.iter().zip(pgns).enumerate() {
469            let mut input = &input[indexed.offsets.data..];
470            let encoded = game_with_info.parse_next(&mut input).unwrap().1;
471            let actual = crate::Game::from_chessbase(encoded)
472                .unwrap_or_else(|error| panic!("CBG game {i}: {error}"));
473            let expected = crate::Game::from_pgn(
474                pgn.unwrap_or_else(|error| panic!("PGN game {i}: {error}"))
475                    .unwrap_or_else(|error| panic!("PGN game {i}: {error}")),
476            )
477            .unwrap_or_else(|error| panic!("PGN game {i}: {error}"));
478
479            assert_eq!(mainline(&actual), mainline(&expected), "game {i}");
480            count += 1;
481        }
482        assert_eq!(count, index.games.len());
483    }
484
485    #[test]
486    fn twic_game() {
487        const INPUT: [u8; 109] = hex!(
488            "0000006d0b08dc870210c19a0a110081
489             82882659a72344d4124253447cf81804
490             98bb07014e172835883cab0772e32431
491             097526317d4384464d3e0a1490fc4c78
492             fa61f88cb815f05b46c97dc02979f1e2
493             462aafd3c9de2f9d06aa9914c7ffa818
494             54e1b5db120fc36c1c9b9f2674"
495        );
496
497        let (_, encoded) = game_with_info.parse(INPUT.as_slice()).expect("can parse CBG game");
498        let actual = crate::Game::from_chessbase(encoded).expect("can convert CBG game");
499        let actual = mainline(&actual);
500        let expected = crate::Game::from_pgn(
501            pgn::pgn.parse("1. d4 Nf6 2. c4 e6 *").expect("valid expected PGN"),
502        )
503        .expect("valid expected game");
504
505        assert_eq!(actual.len(), 104);
506        assert!(actual.starts_with(&mainline(&expected)));
507    }
508
509    #[test]
510    fn twic_freestyle_position() {
511        const INPUT: [u8; 116] = hex!(
512            "4a00007401000f019d83db9d83dbad83
513             dd8d83d9a583dc9583daad83dda583dc
514             181f30103717004bb9345889fd31341a
515             aca950064f5218214585e02302590678
516             e2f98f214964f66fbafa7cbc500affe0
517             82feace6bb8c620a20d1db06775b6b9c
518             8b44fd09fb752a58dd162a16e007df08
519             2197d8a6"
520        );
521
522        let (info, encoded) =
523            game_with_info.parse(INPUT.as_slice()).expect("can parse Freestyle CBG game");
524        let position = crate::Position::try_from(encoded.position)
525            .expect("can convert Freestyle CBG starting position");
526        let expected: Fen = "nnrkbqrb/pppppppp/8/8/8/8/PPPPPPPP/NNRKBQRB w GCgc - 0 1"
527            .parse()
528            .expect("valid expected FEN");
529
530        assert!(info.mode.has_setup());
531        assert!(info.mode.is_freestyle());
532        assert_eq!(position, expected.0);
533    }
534
535    #[test]
536    fn variations() {
537        let bytes = encode(&[0xfe, 0x80, 0x80, 0xff, 0x7c, 0x7c, 0xff]);
538        let len = u32::try_from(bytes.len() + 4).unwrap();
539        let mut input = vec![0, (len >> 16) as u8, (len >> 8) as u8, len as u8];
540        input.extend(bytes);
541        let encoded = game_with_info.parse(input.as_slice()).unwrap().1;
542        let actual = crate::Game::from_chessbase(encoded).unwrap();
543        let expected =
544            crate::Game::from_pgn(pgn::pgn.parse("1. e4 (1. d4 d5) 1... e5 *").unwrap()).unwrap();
545
546        assert_eq!(pgn::Pgn::from(actual).movetext(), pgn::Pgn::from(expected).movetext(),);
547    }
548
549    #[test]
550    #[ignore = "exhaustive Freestyle scan of the external TWIC fixture"]
551    fn twic_freestyle() {
552        let files = unpack(&twic::data(1632, "cbv"));
553        let mut input = files["twic1632.cbg"].as_slice();
554        header.parse_next(&mut input).expect("can parse CBG header");
555        let pgns = pgn::stream::pgns(Cursor::new(twic::data(1632, "pgn")));
556
557        let mut games = 0;
558        let mut custom = 0;
559        let mut freestyle = 0;
560        // let mut lookup_tables = PlayerTable::filled([None; 256]);
561        for (i, pgn) in pgns.enumerate() {
562            let (info, encoded) = game_with_info.parse_next(&mut input).unwrap_or_else(|error| {
563                panic!("can parse CBG game {games} with {} bytes remaining: {error}", input.len())
564            });
565            let expected = crate::Game::from_pgn(
566                pgn.unwrap_or_else(|error| panic!("PGN game {i}: {error}"))
567                    .unwrap_or_else(|error| panic!("PGN game {i}: {error}")),
568            )
569            .unwrap_or_else(|error| panic!("PGN game {i}: {error}"));
570            if info.mode.has_setup() {
571                let position = crate::Position::try_from(encoded.position.clone())
572                    .unwrap_or_else(|error| panic!("CBG starting position of game {i}: {error}"));
573                assert_eq!(position, expected.start().position(), "starting position of game {i}");
574                // if i == 0 {
575                //     score_row_seeds(encoded, &lookup, &expected);
576                //     let lookup_conflicts =
577                //         learn_lookup(&mut lookup_tables, encoded, lookup, &expected, i);
578                //     eprintln!("side-specific lookup conflicts: {lookup_conflicts}");
579                // }
580            }
581            games += 1;
582            custom += usize::from(info.mode.has_setup());
583            freestyle += usize::from(info.mode.is_freestyle());
584        }
585
586        assert!(input.is_empty());
587        assert_eq!(games, 5_350);
588        assert_eq!(custom, 61);
589        assert_eq!(freestyle, 61);
590    }
591
592    /*
593    Freestyle move-decoding experiments, disabled after the initial investigation.
594
595    The 36-byte setup decoder reconstructs all 61 Freestyle starting positions in
596    TWIC 1632, including the final big-endian Scharnagl ID. The ordinary CBG
597    `raw - ply` permutation nevertheless fails from the first move of every game.
598
599    For the first game (Scharnagl 75), g2-g4 transforms to 0xd6. Under the normal
600    table this is in the third-bishop row, while its within-row offset would fit a
601    pawn double move if the row beginning at 0xd5 had been reassigned to that pawn.
602    This motivated testing a setup-dependent permutation of the normal move rows.
603
604    A fixed row assignment was not observed: inferred bases changed 48 times for
605    repeated piece identities. Separate lookup tables for White and Black still
606    produced eight conflicting mappings. Adding or subtracting the Scharnagl ID
607    inside `PERMUTE` also produced 48 row changes; adding it outside only shifts
608    every inferred base and cannot improve consistency. Exhaustively trying all
609    256 additive seeds inside `PERMUTE` found a best score of 45 changes (seed 60),
610    which is not evidence of a simple offset.
611
612    The remaining plausible hypothesis is a stateful move mapping derived from the
613    current position or piece ordering, rather than a one-time permutation derived
614    solely from the initial setup.
615
616    fn score_row_seeds(encoded: Game<'_>, lookup: &Lookup, expected: &crate::Game) {
617        let plays = mainline(expected);
618        let score = |seed| {
619            let mut lookup = lookup.clone();
620            let mut bases = BTreeMap::new();
621            let mut changes = 0;
622
623            for (ply, (raw, play)) in encoded.tokens.iter().zip(&plays).enumerate() {
624                let player = lookup.position.turn();
625                let number = lookup.pieces[player][play.role]
626                    .iter()
627                    .position(|square| *square == Some(play.from))
628                    .expect("moving piece has an identity") as u8;
629                let canonical = canonical(&lookup, *play).expect("move has a compact encoding");
630                let normal_base = canonical_base(play.role, number).expect("piece has a row");
631                let transformed =
632                    PERMUTE[raw.wrapping_sub(ply as u8).wrapping_add(seed) as usize];
633                let base = transformed.wrapping_sub(canonical - normal_base);
634                changes += usize::from(
635                    bases
636                        .insert((player, play.role, number), base)
637                        .is_some_and(|previous| previous != base),
638                );
639                lookup.apply(*play);
640            }
641
642            changes
643        };
644
645        let scharnagl = u16::from_be_bytes([encoded.start.unwrap()[34], encoded.start.unwrap()[35]]);
646        let id = scharnagl as u8;
647        let mut scores: Vec<_> = (u8::MIN..=u8::MAX).map(|seed| (score(seed), seed)).collect();
648        scores.sort_unstable();
649        eprintln!(
650            "row seed scores: zero={}, +id={}, -id={}, best={:?}",
651            score(0),
652            score(id),
653            score(id.wrapping_neg()),
654            &scores[..8],
655        );
656    }
657
658    fn learn_lookup(
659        tables: &mut PlayerTable<[Option<u8>; 256]>,
660        encoded: Game<'_>,
661        mut lookup: Lookup,
662        expected: &crate::Game,
663        game: usize,
664    ) -> usize {
665        let plays = mainline(expected);
666        assert_eq!(encoded.tokens.len(), plays.len() + 1, "encoded length of game {game}");
667        let mut conflicts = 0;
668        let mut bases = BTreeMap::new();
669
670        for (ply, (raw, play)) in encoded.tokens.iter().zip(plays).enumerate() {
671            let player = lookup.position.turn();
672            let number = lookup.pieces[player][play.role]
673                .iter()
674                .position(|square| *square == Some(play.from))
675                .expect("moving piece has an identity") as u8;
676            let canonical = canonical(&lookup, play)
677                .unwrap_or_else(|| panic!("non-compact move in game {game}, ply {ply}: {play:?}"));
678            let encoded = raw.wrapping_sub(ply as u8);
679            let transformed = PERMUTE[encoded as usize];
680            if let Some(base) = canonical_base(play.role, number) {
681                let base = transformed.wrapping_sub(canonical - base);
682                if let Some(previous) = bases.insert((player, play.role, number), base)
683                    && previous != base
684                {
685                    eprintln!(
686                        "base changed at ply {ply}: {player} {:?} {number}, {previous:02x} -> {base:02x}",
687                        play.role,
688                    );
689                }
690            }
691            conflicts += usize::from(!learn(&mut tables[player], encoded, canonical));
692            lookup.apply(play);
693        }
694        conflicts
695    }
696
697    fn canonical_base(role: Role, number: u8) -> Option<u8> {
698        match role {
699            Role::King => Some(0x01),
700            Role::Queen => [0x0b, 0x8f, 0xab].get(number as usize).copied(),
701            Role::Rook => [0x27, 0x35, 0xc7].get(number as usize).copied(),
702            Role::Bishop => [0x43, 0x51, 0xd5].get(number as usize).copied(),
703            Role::Knight => [0x5f, 0x67, 0xe3].get(number as usize).copied(),
704            Role::Pawn => Some(0x6f + 4 * number),
705        }
706    }
707
708    fn learn(table: &mut [Option<u8>; 256], encoded: u8, canonical: u8) -> bool {
709        if let Some(previous) = table[encoded as usize] {
710            previous == canonical
711        } else {
712            table[encoded as usize] = Some(canonical);
713            true
714        }
715    }
716
717    fn canonical(lookup: &Lookup, play: crate::Move) -> Option<u8> {
718        if let Some(file) = play.kind.castle_rook_file() {
719            return Some(if Side::of_rook(play.from, file) == Side::King { 0x09 } else { 0x0a });
720        }
721
722        let player = lookup.position.turn();
723        let number = lookup.pieces[player][play.role]
724            .iter()
725            .position(|square| *square == Some(play.from))? as u8;
726        let file = play.to.file() as i8 - play.from.file() as i8;
727        let rank = play.to.rank() as i8 - play.from.rank() as i8;
728
729        match play.role {
730            Role::King => Some(match (file, rank) {
731                (0, 1) => 0x01,
732                (1, 1) => 0x02,
733                (1, 0) => 0x03,
734                (1, -1) => 0x04,
735                (0, -1) => 0x05,
736                (-1, -1) => 0x06,
737                (-1, 0) => 0x07,
738                (-1, 1) => 0x08,
739                _ => return None,
740            }),
741            Role::Queen => {
742                let base = [0x0b, 0x8f, 0xab].get(number as usize)?;
743                Some(base + queen_offset(file, rank)?)
744            }
745            Role::Rook => {
746                let base = [0x27, 0x35, 0xc7].get(number as usize)?;
747                Some(base + rook_offset(file, rank)?)
748            }
749            Role::Bishop => {
750                let base = [0x43, 0x51, 0xd5].get(number as usize)?;
751                Some(base + bishop_offset(file, rank)?)
752            }
753            Role::Knight => {
754                let base = [0x5f, 0x67, 0xe3].get(number as usize)?;
755                let offset = match (file, rank) {
756                    (2, 1) => 0,
757                    (1, 2) => 1,
758                    (-1, 2) => 2,
759                    (-2, 1) => 3,
760                    (-2, -1) => 4,
761                    (-1, -2) => 5,
762                    (1, -2) => 6,
763                    (2, -1) => 7,
764                    _ => return None,
765                };
766                Some(base + offset)
767            }
768            Role::Pawn if play.kind.is_promote() => None,
769            Role::Pawn => {
770                let forward = if player.is_white() { 1 } else { -1 };
771                let movement = match (file, rank) {
772                    (0, rank) if rank == forward => 0,
773                    (0, rank) if rank == 2 * forward => 1,
774                    (file, rank) if file == forward && rank == forward => 2,
775                    (file, rank) if file == -forward && rank == forward => 3,
776                    _ => return None,
777                };
778                Some(0x6f + 4 * number + movement)
779            }
780        }
781    }
782
783    fn queen_offset(file: i8, rank: i8) -> Option<u8> {
784        match (file.rem_euclid(8) as u8, rank.rem_euclid(8) as u8) {
785            (0, rank @ 1..=7) => Some(rank - 1),
786            (file @ 1..=7, 0) => Some(6 + file),
787            (file @ 1..=7, rank) if file == rank => Some(13 + file),
788            (file @ 1..=7, rank) if file + rank == 8 => Some(20 + file),
789            _ => None,
790        }
791    }
792
793    fn rook_offset(file: i8, rank: i8) -> Option<u8> {
794        match (file.rem_euclid(8) as u8, rank.rem_euclid(8) as u8) {
795            (0, rank @ 1..=7) => Some(rank - 1),
796            (file @ 1..=7, 0) => Some(6 + file),
797            _ => None,
798        }
799    }
800
801    fn bishop_offset(file: i8, rank: i8) -> Option<u8> {
802        match (file.rem_euclid(8) as u8, rank.rem_euclid(8) as u8) {
803            (file @ 1..=7, rank) if file == rank => Some(file - 1),
804            (file @ 1..=7, rank) if file + rank == 8 => Some(6 + file),
805            _ => None,
806        }
807    }
808    */
809}