1use crate::formats::{Bits, ByteInput as Input, prelude::*};
11
12use super::optional;
13
14pub 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#[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, pub white: usize, pub black: usize, pub tournament: usize, pub source: usize, pub annotation: Option<usize>, 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum Volume {
80 A,
81 B,
82 C,
83 D,
84 E,
85}
86
87#[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 Twic,
97 Unknown([u8; 6]),
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum Outcome {
102 BlackWins,
104 Draw,
106 WhiteWins,
108 WhiteBye,
110 BothBye,
112 BlackBye,
114 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 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
181impl 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
193pub 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
214impl 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 take(2u8).parse_next(input)?;
270
271 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 let sub = bits.unsigned(7).ok_or_else(backtrack)?;
287 if sub >= 100 {
288 return Err(backtrack());
289 }
290 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 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 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 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}