1
#![cfg(test)]
2

            
3
use std::{fmt, io::Cursor, str::FromStr};
4

            
5
use rustc_hash::FxHashSet;
6

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

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

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

            
32
5
    records
33
5
        .iter()
34
8760
        .map(|(i, record)| play_game(*i, record))
35
8760
        .for_each(|result| match result {
36
8580
            Ok((i, game)) => {
37
8580
                if game.status != Status::Ongoing {
38
1805
                    assert_eq!(game.status, records[i].1.status);
39
6775
                }
40
            }
41
180
            Err(error) => {
42
180
                if &error.to_string() == "play: you already reached that position" {
43
180
                    already_played += 1;
44
180
                } else if &error.to_string() == "play: the game is already over" {
45
                    already_over += 1;
46
                } else {
47
                    panic!("{}", error.to_string());
48
                }
49
            }
50
8760
        });
51

            
52
5
    assert_eq!(already_over, 0);
53
5
    assert_eq!(already_played, 36);
54

            
55
5
    let already_played_error = f64::from(already_played) / records.len() as f64;
56
5
    assert!(already_played_error > 0.020_5 && already_played_error < 0.020_6);
57
5
}
58

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

            
69
430035
    for (play, captures_1) in record.clone().plays {
70
430035
        let mut captures_2 = FxHashSet::default();
71
430035
        let play = Plae::Play(play);
72
430035
        let captures = game.play(&play)?;
73

            
74
429855
        for vertex in captures.0 {
75
65870
            captures_2.insert(vertex);
76
65870
        }
77

            
78
429855
        if let Some(king) = game.board.find_the_king() {
79
429640
            captures_2.remove(&king);
80
429640
        }
81

            
82
429855
        let captures_2 = Captures(captures_2);
83

            
84
429855
        if !game.board.captured_the_king() {
85
429640
            if let Some(captures_1) = captures_1 {
86
64500
                assert_eq!(captures_1, captures_2);
87
365140
            } else if !captures_2.0.is_empty() {
88
                panic!("The engine reports captures, but the record says there are none.");
89
365140
            }
90
215
        }
91
    }
92

            
93
8580
    Ok((i, game))
94
8760
}
95

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

            
104
#[derive(Clone, Debug, Eq, PartialEq)]
105
pub struct Captures(pub FxHashSet<Vertex>);
106

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

            
113
        Ok(())
114
    }
115
}
116

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

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

            
132
5
    let mut game_records = Vec::with_capacity(1_800);
133
8760
    for (i, result) in rdr.deserialize().enumerate() {
134
8760
        let record: Record = result?;
135
8760
        let mut role = Role::Defender;
136
8760
        let mut plays = Vec::new();
137

            
138
437735
        for play in record.moves.split_ascii_whitespace() {
139
437735
            role = role.opposite();
140

            
141
437735
            if play.contains('-') {
142
436370
                let vertexes: Vec<_> = play.split('-').collect();
143
436370
                let vertex_1_captures: Vec<_> = vertexes[1].split('x').collect();
144

            
145
436370
                if let (Ok(from), Ok(to)) = (
146
436370
                    Vertex::from_str(vertexes[0]),
147
436370
                    Vertex::from_str(vertex_1_captures[0]),
148
                ) {
149
436370
                    let play = Play { role, from, to };
150

            
151
436370
                    if vertex_1_captures.get(1).is_some() {
152
65150
                        let mut captures = FxHashSet::default();
153
67490
                        for capture in vertex_1_captures.into_iter().skip(1) {
154
67490
                            let vertex = Vertex::from_str(capture)?;
155
67490
                            if !captures.contains(&vertex) {
156
66290
                                captures.insert(vertex);
157
66290
                            }
158
                        }
159

            
160
65150
                        plays.push((play, Some(Captures(captures))));
161
371220
                    } else {
162
371220
                        plays.push((play, None));
163
371220
                    }
164
                }
165
1365
            }
166
        }
167

            
168
8760
        let game_record = GameRecord {
169
8760
            plays,
170
8760
            status: Status::from_str(record.status.as_str())?,
171
        };
172

            
173
8760
        game_records.push((i, game_record));
174
    }
175

            
176
5
    Ok(game_records)
177
5
}
178

            
179
#[test]
180
5
fn hnefatafl_games() -> anyhow::Result<()> {
181
5
    hnefatafl_rs(&setup_hnefatafl_rs()?);
182

            
183
5
    Ok(())
184
5
}