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::{
20
    fmt,
21
    hash::{Hash, Hasher},
22
    str::FromStr,
23
};
24

            
25
use anyhow::Context;
26
use serde::{Deserialize, Serialize};
27

            
28
use crate::{
29
    board::BoardSize,
30
    role::Role,
31
    time::{TimeLeft, TimeSettings},
32
};
33

            
34
pub const BOARD_LETTERS: &str = "ABCDEFGHIJKLM";
35

            
36
pub const EXIT_SQUARES_11X11: [Vertex; 4] = [
37
    Vertex {
38
        size: BoardSize::_11,
39
        x: 0,
40
        y: 0,
41
    },
42
    Vertex {
43
        size: BoardSize::_11,
44
        x: 10,
45
        y: 0,
46
    },
47
    Vertex {
48
        size: BoardSize::_11,
49
        x: 0,
50
        y: 10,
51
    },
52
    Vertex {
53
        size: BoardSize::_11,
54
        x: 10,
55
        y: 10,
56
    },
57
];
58

            
59
const THRONE_11X11: Vertex = Vertex {
60
    size: BoardSize::_11,
61
    x: 5,
62
    y: 5,
63
};
64

            
65
const RESTRICTED_SQUARES_11X11: [Vertex; 5] = [
66
    Vertex {
67
        size: BoardSize::_11,
68
        x: 0,
69
        y: 0,
70
    },
71
    Vertex {
72
        size: BoardSize::_11,
73
        x: 10,
74
        y: 0,
75
    },
76
    Vertex {
77
        size: BoardSize::_11,
78
        x: 0,
79
        y: 10,
80
    },
81
    Vertex {
82
        size: BoardSize::_11,
83
        x: 10,
84
        y: 10,
85
    },
86
    THRONE_11X11,
87
];
88

            
89
pub const EXIT_SQUARES_13X13: [Vertex; 4] = [
90
    Vertex {
91
        size: BoardSize::_13,
92
        x: 0,
93
        y: 0,
94
    },
95
    Vertex {
96
        size: BoardSize::_13,
97
        x: 12,
98
        y: 0,
99
    },
100
    Vertex {
101
        size: BoardSize::_13,
102
        x: 0,
103
        y: 12,
104
    },
105
    Vertex {
106
        size: BoardSize::_13,
107
        x: 12,
108
        y: 12,
109
    },
110
];
111

            
112
const THRONE_13X13: Vertex = Vertex {
113
    size: BoardSize::_13,
114
    x: 6,
115
    y: 6,
116
};
117

            
118
const RESTRICTED_SQUARES_13X13: [Vertex; 5] = [
119
    Vertex {
120
        size: BoardSize::_13,
121
        x: 0,
122
        y: 0,
123
    },
124
    Vertex {
125
        size: BoardSize::_13,
126
        x: 12,
127
        y: 0,
128
    },
129
    Vertex {
130
        size: BoardSize::_13,
131
        x: 0,
132
        y: 12,
133
    },
134
    Vertex {
135
        size: BoardSize::_13,
136
        x: 12,
137
        y: 12,
138
    },
139
    THRONE_13X13,
140
];
141

            
142
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialOrd, Serialize)]
143
pub struct PlayRecordTimed {
144
    pub play: Option<Plae>,
145
    pub attacker_time: TimeLeft,
146
    pub defender_time: TimeLeft,
147
}
148

            
149
impl Hash for PlayRecordTimed {
150
609058
    fn hash<H: Hasher>(&self, state: &mut H) {
151
609058
        self.play.hash(state);
152
609058
    }
153
}
154

            
155
impl PartialEq for PlayRecordTimed {
156
    fn eq(&self, other: &Self) -> bool {
157
        self.play == other.play
158
    }
159
}
160

            
161
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
162
pub enum Plae {
163
    Play(Play),
164
    AttackerResigns,
165
    DefenderResigns,
166
}
167

            
168
impl fmt::Display for Plae {
169
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170
        match self {
171
            Self::Play(play) => write!(f, "play {} {} {}", play.role, play.from, play.to),
172
            Self::AttackerResigns => write!(f, "play attacker resigns _"),
173
            Self::DefenderResigns => write!(f, "play defender resigns _"),
174
        }
175
    }
176
}
177

            
178
impl Plae {
179
    /// # Errors
180
    ///
181
    /// If you try to convert an illegal character or you don't get vertex-vertex.
182
    pub fn from_str_(play: &str, role: &Role) -> anyhow::Result<Self> {
183
        let Some((from, to)) = play.split_once('-') else {
184
            return Err(anyhow::Error::msg("expected: vertex-vertex"));
185
        };
186

            
187
        Ok(Self::Play(Play {
188
            role: *role,
189
            from: Vertex::from_str(from)?,
190
            to: Vertex::from_str(to)?,
191
        }))
192
    }
193
}
194

            
195
impl TryFrom<Vec<&str>> for Plae {
196
    type Error = anyhow::Error;
197

            
198
219
    fn try_from(args: Vec<&str>) -> Result<Self, Self::Error> {
199
219
        let error_str = "expected: 'play ROLE FROM TO' or 'play ROLE resign'";
200

            
201
219
        if args.len() < 3 {
202
            return Err(anyhow::Error::msg(error_str));
203
219
        }
204

            
205
219
        let role = Role::from_str(args[1])?;
206
219
        if args[2] == "resigns" {
207
            if role == Role::Defender {
208
                return Ok(Self::DefenderResigns);
209
            }
210

            
211
            return Ok(Self::AttackerResigns);
212
219
        }
213

            
214
219
        if args.len() < 4 {
215
            return Err(anyhow::Error::msg(error_str));
216
219
        }
217

            
218
        Ok(Self::Play(Play {
219
219
            role: Role::from_str(args[1])?,
220
219
            from: Vertex::from_str(args[2])?,
221
216
            to: Vertex::from_str(args[3])?,
222
        }))
223
219
    }
224
}
225

            
226
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
227
pub enum Plays {
228
    PlayRecordsTimed(Vec<PlayRecordTimed>),
229
    PlayRecords(Vec<Option<Plae>>),
230
}
231

            
232
impl Plays {
233
    #[must_use]
234
    pub fn is_empty(&self) -> bool {
235
        match self {
236
            Plays::PlayRecordsTimed(plays) => plays.is_empty(),
237
            Plays::PlayRecords(plays) => plays.is_empty(),
238
        }
239
    }
240

            
241
    #[must_use]
242
    pub fn len(&self) -> usize {
243
        match self {
244
            Plays::PlayRecordsTimed(plays) => plays.len(),
245
            Plays::PlayRecords(plays) => plays.len(),
246
        }
247
    }
248

            
249
    #[must_use]
250
24528
    pub fn new(time_settings: &TimeSettings) -> Self {
251
24528
        match time_settings {
252
            TimeSettings::Timed(_) => Plays::PlayRecordsTimed(Vec::new()),
253
24528
            TimeSettings::UnTimed => Plays::PlayRecords(Vec::new()),
254
        }
255
24528
    }
256

            
257
    #[must_use]
258
    pub fn time_left(&self, role: Role, index: usize) -> String {
259
        match self {
260
            Plays::PlayRecordsTimed(plays) => match role {
261
                Role::Attacker => {
262
                    if let Some(play) = plays.get(index) {
263
                        play.attacker_time.to_string()
264
                    } else {
265
                        "-".to_string()
266
                    }
267
                }
268
                Role::Defender => {
269
                    if let Some(play) = plays.get(index) {
270
                        play.defender_time.to_string()
271
                    } else {
272
                        "-".to_string()
273
                    }
274
                }
275
                Role::Roleless => unreachable!(),
276
            },
277
            Plays::PlayRecords(_) => "-".to_string(),
278
        }
279
    }
280
}
281

            
282
impl Default for Plays {
283
24705
    fn default() -> Self {
284
24705
        Plays::PlayRecordsTimed(Vec::new())
285
24705
    }
286
}
287

            
288
impl fmt::Display for Plays {
289
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290
        match self {
291
            Plays::PlayRecordsTimed(plays) => {
292
                for play in plays {
293
                    if let Some(play) = &play.play {
294
                        write!(f, "{play}, ")?;
295
                    }
296
                }
297
            }
298
            Plays::PlayRecords(plays) => {
299
                for play in plays {
300
                    if let Some(play) = &play {
301
                        write!(f, "{play}, ")?;
302
                    }
303
                }
304
            }
305
        }
306

            
307
        Ok(())
308
    }
309
}
310

            
311
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
312
pub struct Play {
313
    pub role: Role,
314
    pub from: Vertex,
315
    pub to: Vertex,
316
}
317

            
318
impl fmt::Display for Play {
319
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320
        write!(f, "{:?} from {} to {}", self.role, self.from, self.to)
321
    }
322
}
323

            
324
#[derive(Debug, Default)]
325
pub struct Captures(pub Vec<Vertex>);
326

            
327
impl fmt::Display for Captures {
328
180
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329
180
        for vertex in &self.0 {
330
108
            write!(f, "{vertex} ")?;
331
        }
332

            
333
180
        Ok(())
334
180
    }
335
}
336

            
337
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
338
pub struct Vertex {
339
    pub size: BoardSize,
340
    pub x: usize,
341
    pub y: usize,
342
}
343

            
344
impl fmt::Display for Vertex {
345
108
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346
108
        let letters = match self.size {
347
99
            BoardSize::_11 => &BOARD_LETTERS.to_lowercase(),
348
99
            BoardSize::_13 => BOARD_LETTERS,
349
        };
350

            
351
108
        let board_size: usize = self.size.into();
352

            
353
108
        write!(
354
108
            f,
355
            "{}{}",
356
108
            letters.chars().collect::<Vec<_>>()[self.x],
357
108
            board_size - self.y
358
        )
359
108
    }
360
}
361

            
362
impl FromStr for Vertex {
363
    type Err = anyhow::Error;
364

            
365
2633085
    fn from_str(vertex: &str) -> anyhow::Result<Self> {
366
2633085
        let mut chars = vertex.chars();
367

            
368
2633085
        if let Some(mut ch) = chars.next() {
369
2633085
            let size = if ch.is_lowercase() { 11 } else { 13 };
370

            
371
2633085
            ch = ch.to_ascii_uppercase();
372
2633085
            let x = BOARD_LETTERS[..size]
373
2633085
                .find(ch)
374
2633085
                .context("play: the first letter is not a legal char")?;
375

            
376
2633082
            let mut y = chars.as_str().parse()?;
377
2633076
            if y > 0 && y <= size {
378
2633070
                y = size - y;
379
                return Ok(Self {
380
2633070
                    size: size.try_into()?,
381
2633070
                    x,
382
2633070
                    y,
383
                });
384
6
            }
385
        }
386

            
387
6
        Err(anyhow::Error::msg("play: invalid coordinate"))
388
2633085
    }
389
}
390

            
391
impl Vertex {
392
    #[must_use]
393
11641196
    pub fn up(&self) -> Option<Vertex> {
394
11641196
        if self.y > 0 {
395
11392662
            Some(Vertex {
396
11392662
                size: self.size,
397
11392662
                x: self.x,
398
11392662
                y: self.y - 1,
399
11392662
            })
400
        } else {
401
248534
            None
402
        }
403
11641196
    }
404

            
405
    #[must_use]
406
11009253
    pub fn left(&self) -> Option<Vertex> {
407
11009253
        if self.x > 0 {
408
10802924
            Some(Vertex {
409
10802924
                size: self.size,
410
10802924
                x: self.x - 1,
411
10802924
                y: self.y,
412
10802924
            })
413
        } else {
414
206329
            None
415
        }
416
11009253
    }
417

            
418
    #[must_use]
419
10870060
    pub fn down(&self) -> Option<Vertex> {
420
10870060
        let board_size: usize = self.size.into();
421

            
422
10870060
        if self.y < board_size - 1 {
423
10564076
            Some(Vertex {
424
10564076
                size: self.size,
425
10564076
                x: self.x,
426
10564076
                y: self.y + 1,
427
10564076
            })
428
        } else {
429
305984
            None
430
        }
431
10870060
    }
432

            
433
    #[inline]
434
    #[must_use]
435
1254127
    pub fn on_exit_square(&self) -> bool {
436
1254127
        match self.size {
437
1254121
            BoardSize::_11 => EXIT_SQUARES_11X11.contains(self),
438
6
            BoardSize::_13 => EXIT_SQUARES_13X13.contains(self),
439
        }
440
1254127
    }
441

            
442
    #[inline]
443
    #[must_use]
444
724434
    pub fn on_throne(&self) -> bool {
445
724434
        match self.size {
446
724434
            BoardSize::_11 => THRONE_11X11 == *self,
447
            BoardSize::_13 => THRONE_13X13 == *self,
448
        }
449
724434
    }
450

            
451
    #[must_use]
452
49900426
    pub fn on_restricted_square(&self) -> bool {
453
49900426
        match &self.size {
454
49900057
            BoardSize::_11 => RESTRICTED_SQUARES_11X11.contains(self),
455
369
            BoardSize::_13 => RESTRICTED_SQUARES_13X13.contains(self),
456
        }
457
49900426
    }
458

            
459
    #[must_use]
460
10684585
    pub fn right(&self) -> Option<Vertex> {
461
10684585
        let board_size: usize = self.size.into();
462

            
463
10684585
        if self.x < board_size - 1 {
464
9490033
            Some(Vertex {
465
9490033
                size: self.size,
466
9490033
                x: self.x + 1,
467
9490033
                y: self.y,
468
9490033
            })
469
        } else {
470
1194552
            None
471
        }
472
10684585
    }
473

            
474
    #[must_use]
475
1215414
    pub fn touches_wall(&self) -> bool {
476
1215414
        let board_size: usize = self.size.into();
477

            
478
1215414
        self.x == 0 || self.x == board_size - 1 || self.y == 0 || self.y == board_size - 1
479
1215414
    }
480
}
481

            
482
impl From<&Vertex> for usize {
483
    #[inline]
484
    fn from(vertex: &Vertex) -> Self {
485
        let board_size: usize = vertex.size.into();
486
        vertex.y * board_size + vertex.x
487
    }
488
}