Skip to main content

chess/formats/chessbase/
index.rs

1//! Chessbase header file format (CBH)
2
3// https://talkchess.com/forum/viewtopic.php?topic_view=threads&p=287896
4//
5// cbh_codec.cpp, doOpen
6//
7// Header: 46 B
8// Records: 46 B
9
10use crate::formats::{Bits, ByteInput as Input, prelude::*};
11
12use super::optional;
13
14/// ChessBase header filename suffix.
15pub const SUFFIX: &str = "cbh";
16
17pub struct Index {
18    pub header: Header,
19    pub games: Vec<Game>,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct Header {
24    pub software: Software,
25    pub len: usize,
26}
27
28impl Header {
29    pub const LEN: usize = 46;
30}
31
32// between 00 and 99
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct Game {
35    pub deleted: bool,
36    pub offsets: Offsets,
37    pub date: Date,
38    pub outcome: Outcome,
39    pub round: Option<u8>,
40    pub subround: Option<u8>,
41    pub white_elo: Option<u16>,
42    pub black_elo: Option<u16>,
43    pub medals: Medals,
44    pub eco: Option<Eco>,
45}
46
47impl Game {
48    pub const LEN: usize = 46;
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub struct Offsets {
53    pub data: usize,       // -> cbg
54    pub white: usize,      // -> cbp
55    pub black: usize,      // -> cbp
56    pub tournament: usize, // -> cbt
57    pub source: usize,     // -> cbs
58
59    pub annotation: Option<usize>, // -> cba
60    pub annotator: Option<usize>,
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct Date {
65    pub year: Option<u16>,
66    pub month: Option<u8>,
67    pub day: Option<u16>,
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
71pub struct Eco {
72    pub volume: Volume,
73    pub number: Number,
74    pub sub: Option<Number>,
75}
76
77// A - E
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum Volume {
80    A,
81    B,
82    C,
83    D,
84    E,
85}
86
87/// Between 00 and 99
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub struct Number(pub u8);
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum Software {
93    NineToEleven,
94    Light,
95    // corresponds to TWIC 1616
96    Twic,
97    Unknown([u8; 6]),
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum Outcome {
102    // 0-1
103    BlackWins,
104    // 1/2-1/2
105    Draw,
106    // 1-0
107    WhiteWins,
108    // -:+
109    WhiteBye,
110    // =:=
111    BothBye,
112    // +:-
113    BlackBye,
114    // 0-0
115    Lost,
116
117    Unknown(u8),
118}
119
120#[repr(u16)]
121#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
122pub enum Medal {
123    BestGame = 1 << 0,
124    DecidedTournament = 1 << 1,
125    ModelGame = 1 << 2,
126    Novelty = 1 << 3,
127    PawnStructure = 1 << 4,
128    Strategy = 1 << 5,
129    Tactics = 1 << 6,
130    WithAttack = 1 << 7,
131    Sacrifice = 1 << 8,
132    Defense = 1 << 9,
133    Material = 1 << 10,
134    PiecePlay = 1 << 11,
135    EndGame = 1 << 12,
136    TacticalBlunder = 1 << 13,
137    StrategicalBlunder = 1 << 14,
138    User = 1 << 15,
139}
140
141#[repr(transparent)]
142#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
143pub struct Medals(pub u16);
144
145impl Medals {
146    #[inline]
147    pub const fn contains(self, medal: Medal) -> bool {
148        self.0 & medal as u16 != 0
149    }
150}
151
152pub fn index(input: &mut Input<'_>) -> ModalResult<Index> {
153    let header = header.parse_next(input)?;
154    let games = repeat(header.len, game).parse_next(input)?;
155    Ok(Index { header, games })
156}
157
158fn software(input: &mut Input<'_>) -> ModalResult<Software> {
159    use Software::*;
160    const CHESSBASE_9_10_11: &[u8] = &[0x00, 0x00, 0x2C, 0x00, 0x2E, 0x01];
161    const CHESSBASE_LIGHT: &[u8] = &[0x00, 0x00, 0x24, 0x00, 0x2E, 0x01];
162    // Content in TWIC 1616
163    const CHESSBASE_X: &[u8] = &[0x00, 0x00, 0x2c, 0x00, 0x2E, 0x05];
164    alt((
165        CHESSBASE_9_10_11.value(NineToEleven),
166        CHESSBASE_LIGHT.value(Light),
167        CHESSBASE_X.value(Twic),
168        take(6u8).try_map(|bytes: &[u8]| <[u8; 6]>::try_from(bytes)).map(Unknown),
169    ))
170    .parse_next(input)
171}
172
173pub fn header(input: &mut Input<'_>) -> ModalResult<Header> {
174    let software = software.parse_next(input)?;
175    let len = be_u32.verify(|len| *len > 0).map(|len| len as usize - 1).parse_next(input)?;
176    take(36u8).parse_next(input)?;
177
178    Ok(Header { software, len })
179}
180
181// type Index = usize;
182
183impl Date {
184    pub fn pgn(self) -> String {
185        let year = if let Some(year) = self.year { year.to_string() } else { "????".to_string() };
186        let month =
187            if let Some(month) = self.month { format!("{month:02}") } else { "??".to_string() };
188        let day = if let Some(day) = self.day { format!("{day:02}") } else { "??".to_string() };
189        format!("{year}.{month}.{day}")
190    }
191}
192
193// fn be_u24(data: &[u8]) -> u32 {
194//     let mut bytes = [0u8; 4];
195//     bytes[1..].copy_from_slice(data);
196//     u32::from_be_bytes(bytes)
197// }
198
199pub fn be_date(input: &mut Input<'_>) -> ModalResult<Date> {
200    be_u24.map(date).parse_next(input)
201}
202
203pub fn le_date(input: &mut Input<'_>) -> ModalResult<Date> {
204    le_u24.map(date).parse_next(input)
205}
206
207fn date(date: u32) -> Date {
208    let year = (date >> 9) & ((1 << 12) - 1);
209    let month = (date >> 5) & 0b1111;
210    let day = date & 0b11111;
211    Date { year: optional(year as u16), month: optional(month as u8), day: optional(day as u16) }
212}
213
214// #[repr(u8)]
215// #[derive(Clone, Copy, Debug)]
216// pub enum Wins {
217//     Black,
218//     White,
219//     Draw,
220// }
221
222// impl Outcome {
223//     pub fn wins(self) -> Wins {
224//         BlackWins, WhiteBye => Black,
225//         WhiteWins, BlackBye => White,
226//         ...
227//     }
228// }
229
230impl From<u8> for Outcome {
231    fn from(outcome: u8) -> Self {
232        use Outcome::*;
233        match outcome {
234            0 => BlackWins,
235            1 => Draw,
236            2 => WhiteWins,
237            4 => WhiteBye,
238            5 => BothBye,
239            6 => BlackBye,
240            7 => Lost,
241            unknown => Unknown(unknown),
242        }
243    }
244}
245
246impl From<Outcome> for u8 {
247    fn from(outcome: Outcome) -> Self {
248        use Outcome::*;
249        match outcome {
250            BlackWins => 0,
251            Draw => 1,
252            WhiteWins => 2,
253            WhiteBye => 4,
254            BothBye => 5,
255            BlackBye => 6,
256            Lost => 7,
257            Unknown(unknown) => unknown,
258        }
259    }
260}
261
262pub fn eco(input: &mut Input<'_>) -> ModalResult<Option<Eco>> {
263    let mut bits = Bits::new(input);
264    let code = bits.unsigned(9).ok_or_else(backtrack)?;
265    if code > 500 {
266        return Err(backtrack());
267    };
268    // Our Bits don't advance the underlying
269    take(2u8).parse_next(input)?;
270
271    // Convert
272    Ok(if code == 0 {
273        None
274    } else {
275        use Volume::*;
276        let volume = match (code - 1) / 100 {
277            0 => A,
278            1 => B,
279            2 => C,
280            3 => D,
281            4 => E,
282            _ => unreachable!(),
283        };
284        let number = Number(((code - 1) % 100) as u8);
285        // At least in TWIC 1616, this doesn't seem to be set
286        let sub = bits.unsigned(7).ok_or_else(backtrack)?;
287        if sub >= 100 {
288            return Err(backtrack());
289        }
290        // assert!(sub == 0);
291        // TODO: subcode in bits.unsigned(7) from 00 to 99
292        Some(Eco { volume, number, sub: Some(Number(sub as u8)) })
293    })
294}
295
296pub fn game(input: &mut Input<'_>) -> ModalResult<Game> {
297    const GAME: u8 = 1 << 0;
298    const GUIDING_TEXT: u8 = 1 << 1;
299    const DELETED: u8 = 1 << 7;
300
301    // Unhandled cases, cbh_codec.cpp Coddec::decodeIndex doesn't seem to handle these either
302    let deleted = u8
303        .verify(|meta| meta & GAME != 0 && meta & GUIDING_TEXT == 0)
304        .map(|meta| meta & DELETED != 0)
305        .parse_next(input)?;
306
307    let data = be_u32.parse_next(input)? as usize;
308    let annotation = be_u32.map(u32_as_usize).map(optional).parse_next(input)?;
309    let white = be_u24.parse_next(input)? as usize;
310    let black = be_u24.parse_next(input)? as usize;
311    let tournament = be_u24.parse_next(input)? as usize;
312    let annotator = be_u24.map(u32_as_usize).map(optional).parse_next(input)?;
313    let source = be_u24.parse_next(input)? as usize;
314    let offsets = Offsets { data, white, black, tournament, source, annotation, annotator };
315    let date = be_date.parse_next(input)?;
316    let outcome = u8.map(Outcome::from).parse_next(input)?;
317    // skip line evaluation
318    take(1u8).parse_next(input)?;
319
320    let round = u8.map(optional).parse_next(input)?;
321    let subround = u8.map(optional).parse_next(input)?;
322    let white_elo = be_u16.map(optional).parse_next(input)?;
323    let black_elo = be_u16.map(optional).parse_next(input)?;
324
325    let eco = eco.parse_next(input)?;
326
327    let medals = be_u16.map(Medals).parse_next(input)?;
328
329    // skip some information
330    // cf. https://talkchess.com/forum/viewtopic.php?topic_view=threads&p=287896
331    // cf. cbh_codec.cpp
332    take(7u8).parse_next(input)?;
333
334    Ok(Game { deleted, offsets, date, outcome, round, subround, white_elo, black_elo, eco, medals })
335}
336
337fn u32_as_usize(x: u32) -> usize {
338    x as usize
339}
340
341#[cfg(test)]
342mod tests {
343    use hex_literal::hex;
344
345    use crate::formats::twic;
346
347    use super::*;
348
349    const HEADER: [u8; Header::LEN] = hex!(
350        "00002c002e050000186c000000000000
351         00000000000000000000000000000000
352         00000000000000000000186c0000"
353    );
354    const FIRST_GAME: [u8; Game::LEN] = hex!(
355        "010000001a0000000000017f0002db00
356         000e0000000000010fd355000003250a
357         240ac7cd80000000000000000034"
358    );
359
360    #[test]
361    fn header() {
362        let mut input = HEADER.as_slice();
363        let header = super::header.parse_next(&mut input).expect("can parse CBH header");
364
365        assert_eq!(header.software, Software::Twic);
366        assert_eq!(header.len, 6251);
367        assert!(input.is_empty());
368    }
369
370    #[test]
371    fn game() {
372        let game = super::game.parse(FIRST_GAME.as_slice()).expect("can parse CBH game");
373
374        assert_eq!(game.offsets.data, 26);
375        assert_eq!(game.date, Date { year: Some(2025), month: Some(10), day: Some(21) });
376        assert_eq!(game.outcome, Outcome::BlackWins);
377        assert_eq!(game.round, Some(3));
378        assert_eq!(game.subround, Some(37));
379        assert_eq!(game.white_elo, Some(2596));
380        assert_eq!(game.black_elo, Some(2759));
381        assert_eq!(
382            game.eco,
383            Some(Eco { volume: Volume::E, number: Number(10), sub: Some(Number(0)) })
384        );
385    }
386
387    #[test]
388    #[ignore = "exhaustive comparison against the external TWIC fixture"]
389    fn twic_index() {
390        let twic = twic::data(1616, "cbh");
391
392        let index = index.parse(twic.as_slice()).expect("can parse CBH index");
393
394        assert_eq!(index.header.software, Software::Twic);
395        assert_eq!(index.games.len(), index.header.len);
396        assert_eq!(index.games.first().map(|game| game.offsets.data), Some(26));
397        assert_eq!(index.games.first().map(|game| game.outcome), Some(Outcome::BlackWins));
398        assert_eq!(index.games.last().map(|game| game.offsets.data), Some(633949));
399    }
400
401    #[test]
402    fn unknown_outcome() {
403        assert_eq!(Outcome::from(3), Outcome::Unknown(3));
404        assert_eq!(Outcome::from(u8::MAX), Outcome::Unknown(u8::MAX));
405        assert_eq!(u8::from(Outcome::Unknown(3)), 3);
406        assert_eq!(u8::from(Outcome::Unknown(u8::MAX)), u8::MAX);
407    }
408}