1
// This file is part of hnefatafl-copenhagen.
2
//
3
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU Affero General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// hnefatafl-copenhagen is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU Affero General Public License for more details.
12
//
13
// You should have received a copy of the GNU Affero General Public License
14
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15

            
16
use std::{fmt, sync::mpsc::channel, time::Duration};
17

            
18
use chrono::Utc;
19
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
20
use rustc_hash::FxHashMap;
21

            
22
use crate::{
23
    board::InvalidMove,
24
    game::{EscapeVec, Game},
25
    game_tree::{Node, Tree},
26
    heat_map::HeatMap,
27
    play::Plae,
28
    role::Role,
29
    status::Status,
30
};
31

            
32
pub trait AI: Send {
33
    /// # Errors
34
    ///
35
    /// When the game is already over.
36
    fn generate_move(&mut self, game: &mut Game) -> anyhow::Result<GenerateMove>;
37
    #[allow(clippy::missing_errors_doc)]
38
    fn play(&mut self, game: &mut Game, play: &Plae) -> anyhow::Result<()> {
39
        game.play(play)?;
40
        Ok(())
41
    }
42
}
43

            
44
#[derive(Clone, Debug)]
45
pub struct GenerateMove {
46
    pub play: Plae,
47
    pub score: f64,
48
    pub delay_milliseconds: i64,
49
    pub loops: u64,
50
    pub heat_map: HeatMap,
51
    pub escape_vec: Option<EscapeVec>,
52
}
53

            
54
impl fmt::Display for GenerateMove {
55
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56
        writeln!(
57
            f,
58
            "{}, score: {}, delay milliseconds: {}, loops: {}",
59
            self.play, self.score, self.delay_milliseconds, self.loops
60
        )?;
61

            
62
        if let Some(escape_vec) = &self.escape_vec {
63
            write!(f, "escape_vec:\n\n{escape_vec}")?;
64
        }
65

            
66
        Ok(())
67
    }
68
}
69

            
70
#[derive(Clone, Debug, Default)]
71
pub struct AiBanal;
72

            
73
impl AI for AiBanal {
74
732
    fn generate_move(&mut self, game: &mut Game) -> anyhow::Result<GenerateMove> {
75
732
        if game.status != Status::Ongoing {
76
6
            return Err(InvalidMove::GameOver.into());
77
726
        }
78

            
79
726
        let play = game.all_legal_plays()[0].clone();
80
726
        game.play(&play)?;
81

            
82
726
        Ok(GenerateMove {
83
726
            play,
84
726
            score: 0.0,
85
726
            delay_milliseconds: 0,
86
726
            loops: 0,
87
726
            heat_map: HeatMap::new(game.board.size()),
88
726
            escape_vec: None,
89
726
        })
90
732
    }
91
}
92

            
93
pub struct AiBasic {
94
    depth: u8,
95
    sequential: bool,
96
}
97

            
98
impl AiBasic {
99
    #[must_use]
100
    pub fn new(depth: u8, sequential: bool) -> Self {
101
        Self { depth, sequential }
102
    }
103
}
104

            
105
impl AI for AiBasic {
106
    fn generate_move(&mut self, game: &mut Game) -> anyhow::Result<GenerateMove> {
107
        let t0 = Utc::now().timestamp_millis();
108

            
109
        if game.status != Status::Ongoing {
110
            return Err(InvalidMove::GameOver.into());
111
        }
112

            
113
        if let Some(play) = game.obvious_play() {
114
            println!("1 turn: {} play: {play}", game.turn);
115

            
116
            game.play(&play)?;
117
            let score = match game.turn {
118
                Role::Attacker => f64::INFINITY,
119
                Role::Defender => -f64::INFINITY,
120
                Role::Roleless => unreachable!(),
121
            };
122

            
123
            let heat_map = HeatMap::from((&*game, &play));
124
            let t1 = Utc::now().timestamp_millis();
125
            let delay_milliseconds = t1 - t0;
126

            
127
            return Ok(GenerateMove {
128
                play,
129
                score,
130
                delay_milliseconds,
131
                loops: 0,
132
                heat_map,
133
                escape_vec: None,
134
            });
135
        }
136

            
137
        let (play, score, escape_vec) = if self.sequential {
138
            game.alpha_beta(
139
                self.depth as usize,
140
                self.depth,
141
                None,
142
                -f64::INFINITY,
143
                f64::INFINITY,
144
            )
145
        } else {
146
            game.alpha_beta_parallel(
147
                self.depth as usize,
148
                self.depth,
149
                None,
150
                -f64::INFINITY,
151
                f64::INFINITY,
152
            )
153
        };
154

            
155
        let play = match play {
156
            Some(play) => play,
157
            None => match &game.turn {
158
                Role::Attacker => Plae::AttackerResigns,
159
                Role::Defender => Plae::DefenderResigns,
160
                Role::Roleless => unreachable!(),
161
            },
162
        };
163

            
164
        println!("2 turn: {} play: {play}", game.turn);
165
        game.play(&play)?;
166

            
167
        let heat_map = HeatMap::from((&*game, &play));
168

            
169
        let t1 = Utc::now().timestamp_millis();
170
        let delay_milliseconds = t1 - t0;
171

            
172
        Ok(GenerateMove {
173
            play,
174
            score,
175
            delay_milliseconds,
176
            loops: 0,
177
            heat_map,
178
            escape_vec,
179
        })
180
    }
181
}
182

            
183
#[derive(Clone, Debug)]
184
pub struct AiMonteCarlo {
185
    duration: Duration,
186
    depth: u8,
187
}
188

            
189
impl Default for AiMonteCarlo {
190
    fn default() -> Self {
191
        Self {
192
            duration: Duration::from_secs(1),
193
            depth: 80,
194
        }
195
    }
196
}
197

            
198
impl AI for AiMonteCarlo {
199
    fn generate_move(&mut self, game: &mut Game) -> anyhow::Result<GenerateMove> {
200
        if game.status != Status::Ongoing {
201
            return Err(InvalidMove::GameOver.into());
202
        }
203

            
204
        let t0 = Utc::now().timestamp_millis();
205
        let mut trees = AiMonteCarlo::make_trees(game)?;
206
        let (tx, rx) = channel();
207

            
208
        trees.par_iter_mut().try_for_each_with(tx, |tx, tree| {
209
            let nodes = tree.monte_carlo_tree_search(self.duration, self.depth);
210
            tx.send(nodes)
211
        })?;
212

            
213
        let mut loops_total = 0;
214
        let mut nodes_master = FxHashMap::default();
215

            
216
        while let Ok((loops, nodes)) = rx.recv() {
217
            loops_total += loops;
218
            for mut node in nodes {
219
                if let Some(Plae::Play(play)) = node.clone().play {
220
                    nodes_master
221
                        .entry(play)
222
                        .and_modify(|node_master: &mut Node| {
223
                            if node_master.count == 0.0 {
224
                                node_master.count = 1.0;
225
                                node_master.score = node.score;
226
                            } else {
227
                                node_master.count += 1.0;
228
                                node_master.score += node.score;
229
                            }
230
                        })
231
                        .or_insert({
232
                            node.count = 1.0;
233
                            node
234
                        });
235
                }
236
            }
237
        }
238

            
239
        for node in nodes_master.values_mut() {
240
            node.score /= node.count;
241
            node.count = 1.0;
242
        }
243

            
244
        let mut nodes: Vec<_> = nodes_master.values().collect();
245
        nodes.sort_by(|a, b| a.score.total_cmp(&b.score));
246

            
247
        let turn = game.turn;
248
        let message = anyhow::Error::msg("The nodes are empty.");
249
        let node = match turn {
250
            Role::Attacker => nodes.last().ok_or(message)?,
251
            Role::Defender => nodes.first().ok_or(message)?,
252
            Role::Roleless => unreachable!(),
253
        };
254

            
255
        let play = node
256
            .play
257
            .as_ref()
258
            .ok_or(anyhow::Error::msg("A move has not been played yet."))?;
259

            
260
        game.play(play)?;
261

            
262
        let here_tree = Tree::from(game.clone());
263
        for tree in &mut trees {
264
            *tree = here_tree.clone();
265
        }
266

            
267
        let t1 = Utc::now().timestamp_millis();
268
        let delay_milliseconds = t1 - t0;
269
        let heat_map = HeatMap::from(&nodes);
270

            
271
        Ok(GenerateMove {
272
            play: play.clone(),
273
            score: node.score,
274
            delay_milliseconds,
275
            loops: loops_total,
276
            heat_map,
277
            escape_vec: None,
278
        })
279
    }
280
}
281

            
282
impl AiMonteCarlo {
283
    fn make_trees(game: &Game) -> anyhow::Result<Vec<Tree>> {
284
        let count = std::thread::available_parallelism()?.get();
285
        let mut trees = Vec::with_capacity(count);
286

            
287
        for _ in 0..count {
288
            trees.push(Tree::new(game.clone()));
289
        }
290

            
291
        Ok(trees)
292
    }
293

            
294
    #[must_use]
295
    pub fn new(duration: Duration, depth: u8) -> Self {
296
        Self { duration, depth }
297
    }
298
}