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::{cmp::Ordering, collections::HashMap, fmt};
17

            
18
use crate::{
19
    board::BoardSize,
20
    game::Game,
21
    game_tree::Node,
22
    play::{Plae, Vertex},
23
    role::Role,
24
};
25

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

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

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

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

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

            
98
impl Eq for Heat {}
99

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

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

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

            
120
        if role == Role::Roleless {
121
            return (spaces_from, HashMap::new());
122
        }
123

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

            
140
            froms.push((key, min_max));
141
        }
142

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

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

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

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

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

            
188
            played_on.sort_by(|a, b| f64::total_cmp(&a.2, &b.2));
189

            
190
            if *role == Role::Attacker {
191
                played_on.reverse();
192
            }
193

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

            
204
        (spaces_from, spaces_to)
205
    }
206

            
207
    #[must_use]
208
726
    pub fn new(board_size: BoardSize) -> Self {
209
726
        Self {
210
726
            board_size,
211
726
            spaces: HashMap::new(),
212
726
        }
213
726
    }
214
}
215

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

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

            
227
            spaces[board_index] = Heat::Ranked(0);
228
            heat_map.spaces.insert((play.role, play.from), spaces);
229
        }
230

            
231
        heat_map
232
    }
233
}
234

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

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

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

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

            
267
                                let score = board
268
                                    .get_mut(board_index)
269
                                    .expect("The board should contain this space.");
270

            
271
                                *score = Heat::Score(node.score);
272

            
273
                                board
274
                            });
275
                    }
276
                }
277
            }
278
        }
279

            
280
        heat_map
281
    }
282
}
283

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

            
292
        for ((role, vertex), board) in &self.spaces {
293
            writeln!(f, "vertex: {vertex}, role: {role}")?;
294

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

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

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

            
325
        Ok(())
326
    }
327
}