Skip to main content

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