1
// SPDX-FileCopyrightText: 2025 David Campbell <david@hnefatafl.org>
2
// SPDX-License-Identifier: MIT
3

            
4
#![cfg(test)]
5

            
6
use std::{fmt, io::Cursor, str::FromStr};
7

            
8
use rustc_hash::FxHashSet;
9

            
10
use hnefatafl_copenhagen::{
11
    game::{self, Game},
12
    play::{Plae, Play, Plays, Vertex},
13
    role::Role,
14
    status::Status,
15
    time,
16
};
17

            
18
/// # Errors
19
///
20
/// If the game records are invalid.
21
2
pub fn setup_hnefatafl_rs() -> anyhow::Result<Vec<(usize, GameRecord)>> {
22
2
    let copenhagen_csv = include_str!("copenhagen.csv");
23
2
    game_records_from_path(copenhagen_csv)
24
2
}
25

            
26
/// # Errors
27
///
28
/// If the captures or game status don't match for an engine game and a record
29
/// game.
30
#[allow(clippy::cast_precision_loss, clippy::missing_panics_doc)]
31
2
pub fn hnefatafl_rs(records: &[(usize, GameRecord)]) {
32
2
    let mut already_played = 0;
33
2
    let mut already_over = 0;
34

            
35
2
    records
36
2
        .iter()
37
3504
        .map(|(i, record)| play_game(*i, record))
38
3504
        .for_each(|result| match result {
39
3432
            Ok((i, game)) => {
40
3432
                if game.status != Status::Ongoing {
41
722
                    assert_eq!(game.status, records[i].1.status);
42
2710
                }
43
            }
44
72
            Err(error) => {
45
72
                if &error.to_string() == "play: you already reached that position" {
46
72
                    already_played += 1;
47
72
                } else if &error.to_string() == "play: the game is already over" {
48
                    already_over += 1;
49
                } else {
50
                    panic!("{}", error.to_string());
51
                }
52
            }
53
3504
        });
54

            
55
2
    assert_eq!(already_over, 0);
56
2
    assert_eq!(already_played, 36);
57

            
58
2
    let already_played_error = f64::from(already_played) / records.len() as f64;
59
2
    assert!(already_played_error > 0.020_5 && already_played_error < 0.020_6);
60
2
}
61

            
62
#[inline]
63
3504
fn play_game(i: usize, record: &GameRecord) -> Result<(usize, Game), anyhow::Error> {
64
3504
    let mut game = Game {
65
3504
        plays: Plays::new(&time::TimeSettings::UnTimed),
66
3504
        time: game::TimeUnix::UnTimed,
67
3504
        attacker_time: time::TimeSettings::UnTimed,
68
3504
        defender_time: time::TimeSettings::UnTimed,
69
3504
        ..Game::default()
70
3504
    };
71

            
72
172014
    for (play, captures_1) in record.clone().plays {
73
172014
        let mut captures_2 = FxHashSet::default();
74
172014
        let play = Plae::Play(play);
75
172014
        let captures = game.play(&play)?;
76

            
77
171942
        for vertex in captures.0 {
78
26348
            captures_2.insert(vertex);
79
26348
        }
80

            
81
171942
        if let Some(king) = game.board.find_the_king() {
82
171856
            captures_2.remove(&king);
83
171856
        }
84

            
85
171942
        let captures_2 = Captures(captures_2);
86

            
87
171942
        if !game.board.captured_the_king() {
88
171856
            if let Some(captures_1) = captures_1 {
89
25800
                assert_eq!(captures_1, captures_2);
90
146056
            } else if !captures_2.0.is_empty() {
91
                panic!("The engine reports captures, but the record says there are none.");
92
146056
            }
93
86
        }
94
    }
95

            
96
3432
    Ok((i, game))
97
3504
}
98

            
99
#[derive(Debug, serde::Deserialize)]
100
struct Record {
101
    moves: String,
102
    _attacker_captures: u64,
103
    _defender_captures: u64,
104
    status: String,
105
}
106

            
107
#[derive(Clone, Debug, Eq, PartialEq)]
108
pub struct Captures(pub FxHashSet<Vertex>);
109

            
110
impl fmt::Display for Captures {
111
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112
        for vertex in &self.0 {
113
            write!(f, "{vertex} ")?;
114
        }
115

            
116
        Ok(())
117
    }
118
}
119

            
120
#[derive(Clone, Debug)]
121
pub struct GameRecord {
122
    pub plays: Vec<(Play, Option<Captures>)>,
123
    pub status: Status,
124
}
125

            
126
/// # Errors
127
///
128
/// If the game records are invalid.
129
2
pub fn game_records_from_path(string: &str) -> anyhow::Result<Vec<(usize, GameRecord)>> {
130
2
    let cursor = Cursor::new(string);
131
2
    let mut rdr = csv::ReaderBuilder::new()
132
2
        .has_headers(false)
133
2
        .from_reader(cursor);
134

            
135
2
    let mut game_records = Vec::with_capacity(1_800);
136
3504
    for (i, result) in rdr.deserialize().enumerate() {
137
3504
        let record: Record = result?;
138
3504
        let mut role = Role::Defender;
139
3504
        let mut plays = Vec::new();
140

            
141
175094
        for play in record.moves.split_ascii_whitespace() {
142
175094
            role = role.opposite();
143

            
144
175094
            if play.contains('-') {
145
174548
                let vertexes: Vec<_> = play.split('-').collect();
146
174548
                let vertex_1_captures: Vec<_> = vertexes[1].split('x').collect();
147

            
148
174548
                if let (Ok(from), Ok(to)) = (
149
174548
                    Vertex::from_str(vertexes[0]),
150
174548
                    Vertex::from_str(vertex_1_captures[0]),
151
                ) {
152
174548
                    let play = Play { role, from, to };
153

            
154
174548
                    if vertex_1_captures.get(1).is_some() {
155
26060
                        let mut captures = FxHashSet::default();
156
26996
                        for capture in vertex_1_captures.into_iter().skip(1) {
157
26996
                            let vertex = Vertex::from_str(capture)?;
158
26996
                            if !captures.contains(&vertex) {
159
26516
                                captures.insert(vertex);
160
26516
                            }
161
                        }
162

            
163
26060
                        plays.push((play, Some(Captures(captures))));
164
148488
                    } else {
165
148488
                        plays.push((play, None));
166
148488
                    }
167
                }
168
546
            }
169
        }
170

            
171
3504
        let game_record = GameRecord {
172
3504
            plays,
173
3504
            status: Status::from_str(record.status.as_str())?,
174
        };
175

            
176
3504
        game_records.push((i, game_record));
177
    }
178

            
179
2
    Ok(game_records)
180
2
}
181

            
182
#[test]
183
2
fn hnefatafl_games() -> anyhow::Result<()> {
184
2
    hnefatafl_rs(&setup_hnefatafl_rs()?);
185

            
186
2
    Ok(())
187
2
}