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
// SPDX-License-Identifier: AGPL-3.0-or-later
17
// SPDX-FileCopyrightText: 2025 David Campbell <david@hnefatafl.org>
18

            
19
use std::{cmp::Ordering, collections::HashMap, fmt};
20

            
21
use crate::{
22
    board::BoardSize,
23
    game::Game,
24
    game_tree::Node,
25
    play::{Plae, Vertex},
26
    role::Role,
27
};
28

            
29
#[derive(Clone, Copy, Debug, Default)]
30
pub enum Heat {
31
    Ranked(u8),
32
    Score(f64),
33
    #[default]
34
    UnRanked,
35
}
36

            
37
// It would be Color but iced is only in the examples. This is the alpha value.
38
#[allow(clippy::cast_possible_truncation)]
39
impl From<Heat> for f32 {
40
    fn from(cell: Heat) -> Self {
41
        match cell {
42
            Heat::Score(score) => score as f32,
43
            Heat::UnRanked => 0.25,
44
            Heat::Ranked(rank) => match rank {
45
                0 => 1.0,
46
                1 => 0.5,
47
                2 => 0.25,
48
                3 => 0.125,
49
                4 => 0.0625,
50
                _ => 0.0,
51
            },
52
        }
53
    }
54
}
55

            
56
impl Ord for Heat {
57
    fn cmp(&self, other: &Self) -> Ordering {
58
        match self {
59
            Self::Ranked(rank) => match other {
60
                Self::Ranked(rank_other) => rank.cmp(rank_other),
61
                Self::Score(_) | Self::UnRanked => Ordering::Greater,
62
            },
63
            Self::Score(score) => match other {
64
                Self::Ranked(_) => Ordering::Less,
65
                Self::Score(score_other) => score.total_cmp(score_other),
66
                Self::UnRanked => Ordering::Greater,
67
            },
68
            Self::UnRanked => match other {
69
                Self::Ranked(_) | Self::Score(_) => Ordering::Less,
70
                Self::UnRanked => Ordering::Equal,
71
            },
72
        }
73
    }
74
}
75

            
76
impl PartialOrd for Heat {
77
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
78
        Some(self.cmp(other))
79
    }
80
}
81

            
82
impl PartialEq for Heat {
83
    fn eq(&self, other: &Self) -> bool {
84
        match self {
85
            Self::Ranked(rank) => match other {
86
                Self::Ranked(rank_other) => rank == rank_other,
87
                Self::Score(_) | Self::UnRanked => false,
88
            },
89
            Self::Score(score) => match other {
90
                Self::Ranked(_) | Self::UnRanked => false,
91
                Self::Score(score_other) => score == score_other,
92
            },
93
            Self::UnRanked => match other {
94
                Self::Ranked(_) | Self::Score(_) => false,
95
                Self::UnRanked => true,
96
            },
97
        }
98
    }
99
}
100

            
101
impl Eq for Heat {}
102

            
103
#[derive(Clone, Debug, Default)]
104
pub struct HeatMap {
105
    pub board_size: BoardSize,
106
    pub spaces: HashMap<(Role, Vertex), Vec<Heat>>,
107
}
108

            
109
impl HeatMap {
110
    #[allow(clippy::expect_used)]
111
    #[allow(clippy::type_complexity)]
112
    #[allow(clippy::missing_panics_doc)]
113
    #[must_use]
114
    pub fn draw(&self, role: Role) -> (Vec<Heat>, HashMap<(Role, Vertex), Vec<Heat>>) {
115
        let board_size: usize = self.board_size.into();
116

            
117
        let mut spaces_from = if self.board_size == BoardSize::_11 {
118
            vec![Heat::default(); 11 * 11]
119
        } else {
120
            vec![Heat::default(); 13 * 13]
121
        };
122

            
123
        if role == Role::Roleless {
124
            return (spaces_from, HashMap::new());
125
        }
126

            
127
        let mut froms = Vec::new();
128
        for key in self.spaces.keys() {
129
            let min_max = match role {
130
                Role::Attacker => *self.spaces[key]
131
                    .iter()
132
                    .filter(|heat| **heat != Heat::UnRanked)
133
                    .max_by(|a, b| Heat::cmp(a, b))
134
                    .expect("there is at least one value"),
135
                Role::Defender => *self.spaces[key]
136
                    .iter()
137
                    .filter(|heat| **heat != Heat::UnRanked)
138
                    .min_by(|a, b| Heat::cmp(a, b))
139
                    .expect("there is at least one value"),
140
                Role::Roleless => unreachable!(),
141
            };
142

            
143
            froms.push((key, min_max));
144
        }
145

            
146
        froms.sort_by(|a, b| Heat::cmp(&a.1, &b.1));
147
        if Role::Attacker == role {
148
            froms.reverse();
149
        }
150

            
151
        let mut froms_hash_map = HashMap::new();
152
        for ((play, _), rank) in froms.iter_mut().zip(0u8..) {
153
            froms_hash_map.insert(*play, rank);
154
        }
155

            
156
        for y in 0..board_size {
157
            for x in 0..board_size {
158
                if let Some(i) = froms_hash_map.get(&(
159
                    role,
160
                    Vertex {
161
                        x,
162
                        y,
163
                        size: self.board_size,
164
                    },
165
                )) {
166
                    spaces_from[y * board_size + x] = Heat::Ranked(*i);
167
                } else {
168
                    spaces_from[y * board_size + x] = Heat::UnRanked;
169
                }
170
            }
171
        }
172

            
173
        let mut spaces_to = self.spaces.clone();
174
        for ((role, _vertex), board) in &mut spaces_to {
175
            let mut played_on = Vec::new();
176

            
177
            for y in 0..board_size {
178
                for x in 0..board_size {
179
                    let heat = board[y * board_size + x];
180
                    if let Heat::Score(score) = heat {
181
                        let vertex = Vertex {
182
                            size: self.board_size,
183
                            x,
184
                            y,
185
                        };
186
                        played_on.push((vertex, role, score));
187
                    }
188
                }
189
            }
190

            
191
            played_on.sort_by(|a, b| f64::total_cmp(&a.2, &b.2));
192

            
193
            if *role == Role::Attacker {
194
                played_on.reverse();
195
            }
196

            
197
            let mut rank = 0;
198
            for (vertex, _, _) in played_on {
199
                let heat = &mut board[vertex.y * board_size + vertex.x];
200
                if let Heat::Score(_) = heat {
201
                    *heat = Heat::Ranked(rank);
202
                    rank += 1;
203
                }
204
            }
205
        }
206

            
207
        (spaces_from, spaces_to)
208
    }
209

            
210
    #[must_use]
211
363
    pub fn new(board_size: BoardSize) -> Self {
212
363
        Self {
213
363
            board_size,
214
363
            spaces: HashMap::new(),
215
363
        }
216
363
    }
217
}
218

            
219
impl From<(&Game, &Plae)> for HeatMap {
220
    fn from(game_plae: (&Game, &Plae)) -> Self {
221
        let (game, plae) = game_plae;
222
        let board_size = game.board.size();
223
        let mut heat_map = HeatMap::new(board_size);
224

            
225
        if let Plae::Play(play) = plae {
226
            let size: usize = board_size.into();
227
            let board_index: usize = (&play.to).into();
228
            let mut spaces = vec![Heat::default(); size * size];
229

            
230
            spaces[board_index] = Heat::Ranked(0);
231
            heat_map.spaces.insert((play.role, play.from), spaces);
232
        }
233

            
234
        heat_map
235
    }
236
}
237

            
238
impl From<&Vec<&Node>> for HeatMap {
239
    #[allow(clippy::expect_used)]
240
    #[allow(clippy::float_cmp)]
241
    fn from(nodes: &Vec<&Node>) -> Self {
242
        let mut heat_map = if let Some(node) = nodes.first() {
243
            HeatMap::new(node.board_size)
244
        } else {
245
            HeatMap::default()
246
        };
247

            
248
        for node in nodes {
249
            if let Some(play) = &node.play {
250
                match play {
251
                    Plae::AttackerResigns | Plae::DefenderResigns => {}
252
                    Plae::Play(play) => {
253
                        let board_index: usize = (&play.to).into();
254

            
255
                        heat_map
256
                            .spaces
257
                            .entry((play.role, play.from))
258
                            .and_modify(|board| {
259
                                let score = board
260
                                    .get_mut(board_index)
261
                                    .expect("The board should contain this space.");
262

            
263
                                debug_assert_eq!(*score, Heat::UnRanked);
264
                                *score = Heat::Score(node.score);
265
                            })
266
                            .or_insert({
267
                                let size: usize = play.from.size.into();
268
                                let mut board = vec![Heat::default(); size * size];
269

            
270
                                let score = board
271
                                    .get_mut(board_index)
272
                                    .expect("The board should contain this space.");
273

            
274
                                *score = Heat::Score(node.score);
275

            
276
                                board
277
                            });
278
                    }
279
                }
280
            }
281
        }
282

            
283
        heat_map
284
    }
285
}
286

            
287
impl fmt::Display for HeatMap {
288
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289
        let board_size = if self.board_size == BoardSize::_11 {
290
            11
291
        } else {
292
            13
293
        };
294

            
295
        for ((role, vertex), board) in &self.spaces {
296
            writeln!(f, "vertex: {vertex}, role: {role}")?;
297

            
298
            match self.board_size {
299
                BoardSize::_11 => writeln!(
300
                    f,
301
                    "   A       B       C       D       E       F       G       H       I       J       K"
302
                )?,
303
                BoardSize::_13 => writeln!(
304
                    f,
305
                    "   A       B       C       D       E       F       G       H       I       J       K       L       M"
306
                )?,
307
            }
308

            
309
            for y in 0..board_size {
310
                match self.board_size {
311
                    BoardSize::_11 => write!(f, "{:2} ", 11 - y)?,
312
                    BoardSize::_13 => write!(f, "{:2} ", 13 - y)?,
313
                }
314

            
315
                for x in 0..board_size {
316
                    let score = board[y * board_size + x];
317
                    if let Heat::Score(score) = score {
318
                        write!(f, "{score:+.4} ")?;
319
                    } else {
320
                        write!(f, "------- ")?;
321
                    }
322
                }
323
                writeln!(f)?;
324
            }
325
            writeln!(f)?;
326
        }
327

            
328
        Ok(())
329
    }
330
}