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
    collections::{HashMap, VecDeque},
21
    fmt::{self, Write},
22
    str::FromStr,
23
    sync::mpsc::Sender,
24
};
25

            
26
use rust_i18n::t;
27
use serde::{Deserialize, Serialize};
28

            
29
use crate::{
30
    Id,
31
    board::{Board, BoardSize},
32
    game::Game,
33
    glicko::Rating,
34
    play::{PlayRecordTimed, Plays},
35
    rating::Rated,
36
    role::Role,
37
    status::Status,
38
    time::{Time, TimeSettings},
39
};
40

            
41
#[derive(Clone, Debug, Deserialize, Serialize)]
42
pub struct ArchivedGame {
43
    pub id: Id,
44
    pub attacker: String,
45
    pub attacker_rating: Rating,
46
    pub defender: String,
47
    pub defender_rating: Rating,
48
    pub rated: Rated,
49
    pub plays: Plays,
50
    pub status: Status,
51
    pub texts: VecDeque<String>,
52
    pub board_size: BoardSize,
53
}
54

            
55
impl ArchivedGame {
56
    #[must_use]
57
    pub fn new(game: ServerGame, attacker_rating: Rating, defender_rating: Rating) -> Self {
58
        Self {
59
            id: game.id,
60
            attacker: game.attacker,
61
            attacker_rating,
62
            defender: game.defender,
63
            defender_rating,
64
            rated: game.rated,
65
            plays: game.game.plays,
66
            status: game.game.status,
67
            texts: game.texts,
68
            board_size: game.game.board.size(),
69
        }
70
    }
71
}
72

            
73
impl fmt::Display for ArchivedGame {
74
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75
        writeln!(
76
            f,
77
            "{}: {}, {}: {} {}, {}: {} {}, {}: {}",
78
            t!("ID"),
79
            self.id,
80
            t!("Attacker"),
81
            self.attacker,
82
            self.attacker_rating.to_string_rounded(),
83
            t!("Defender"),
84
            self.defender,
85
            self.defender_rating.to_string_rounded(),
86
            t!("Size"),
87
            self.board_size,
88
        )
89
    }
90
}
91

            
92
impl PartialEq for ArchivedGame {
93
    fn eq(&self, other: &Self) -> bool {
94
        self.id == other.id
95
    }
96
}
97

            
98
impl Eq for ArchivedGame {}
99

            
100
#[derive(Clone, Debug)]
101
pub struct Messenger(Option<Sender<String>>);
102

            
103
impl Messenger {
104
    #[must_use]
105
    pub fn new(sender: Sender<String>) -> Self {
106
        Self(Some(sender))
107
    }
108

            
109
    pub fn send(&self, string: String) {
110
        if let Some(sender) = &self.0 {
111
            let _ok = sender.send(string);
112
        }
113
    }
114
}
115

            
116
#[derive(Clone, Debug)]
117
pub struct ServerGame {
118
    pub id: Id,
119
    pub attacker: String,
120
    pub attacker_tx: Messenger,
121
    pub defender: String,
122
    pub defender_tx: Messenger,
123
    pub draw_requested: Role,
124
    pub elapsed_time: i64,
125
    pub rated: Rated,
126
    pub game: Game,
127
    pub texts: VecDeque<String>,
128
}
129

            
130
impl From<ServerGameSerialized> for ServerGame {
131
    fn from(game: ServerGameSerialized) -> Self {
132
        Self {
133
            id: game.id,
134
            attacker: game.attacker,
135
            attacker_tx: Messenger(None),
136
            defender: game.defender,
137
            defender_tx: Messenger(None),
138
            draw_requested: Role::Roleless,
139
            elapsed_time: 0,
140
            rated: game.rated,
141
            game: game.game,
142
            texts: game.texts,
143
        }
144
    }
145
}
146

            
147
impl ServerGame {
148
    #[must_use]
149
    pub fn protocol(&self) -> String {
150
        format!(
151
            "game {} {} {} {}",
152
            self.id, self.attacker, self.defender, self.rated
153
        )
154
    }
155

            
156
    #[allow(clippy::missing_panics_doc)]
157
    #[must_use]
158
    pub fn new(
159
        attacker_tx: Option<Sender<String>>,
160
        defender_tx: Option<Sender<String>>,
161
        game: ServerGameLight,
162
    ) -> Self {
163
        let (Some(attacker), Some(defender)) = (game.attacker, game.defender) else {
164
            unreachable!();
165
        };
166

            
167
        let plays = match game.timed {
168
            TimeSettings::Timed(time) => Plays::PlayRecordsTimed(vec![PlayRecordTimed {
169
                play: None,
170
                attacker_time: time.into(),
171
                defender_time: time.into(),
172
            }]),
173
            TimeSettings::UnTimed => Plays::PlayRecords(vec![None]),
174
        };
175

            
176
        let board = Board::new(game.board_size);
177

            
178
        Self {
179
            id: game.id,
180
            attacker,
181
            attacker_tx: Messenger(attacker_tx),
182
            defender,
183
            defender_tx: Messenger(defender_tx),
184
            draw_requested: Role::Roleless,
185
            elapsed_time: 0,
186
            rated: game.rated,
187
            game: Game {
188
                attacker_time: game.timed.clone(),
189
                defender_time: game.timed,
190
                board,
191
                plays,
192
                ..Game::default()
193
            },
194
            texts: VecDeque::new(),
195
        }
196
    }
197
}
198

            
199
impl fmt::Display for ServerGame {
200
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201
        write!(
202
            f,
203
            "{}: {}, {}, {} ",
204
            self.id, self.attacker, self.defender, self.rated
205
        )
206
    }
207
}
208

            
209
#[derive(Clone, Debug, Deserialize, Serialize)]
210
pub struct ServerGameSerialized {
211
    pub id: Id,
212
    pub attacker: String,
213
    pub defender: String,
214
    pub rated: Rated,
215
    pub game: Game,
216
    pub texts: VecDeque<String>,
217
    pub timed: TimeSettings,
218
}
219

            
220
impl From<&ServerGame> for ServerGameSerialized {
221
    fn from(game: &ServerGame) -> Self {
222
        Self {
223
            id: game.id,
224
            attacker: game.attacker.clone(),
225
            defender: game.defender.clone(),
226
            rated: game.rated,
227
            game: game.game.clone(),
228
            texts: game.texts.clone(),
229
            timed: TimeSettings::default(),
230
        }
231
    }
232
}
233

            
234
#[derive(Clone, Debug, Default)]
235
pub struct ServerGames(pub HashMap<Id, ServerGame>);
236

            
237
#[derive(Clone, Default, Eq, PartialEq)]
238
pub struct Challenger(pub Option<String>);
239

            
240
impl fmt::Debug for Challenger {
241
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242
        if let Some(challenger) = &self.0 {
243
            write!(f, "{challenger}")?;
244
        } else {
245
            write!(f, "_")?;
246
        }
247

            
248
        Ok(())
249
    }
250
}
251

            
252
impl fmt::Display for Challenger {
253
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254
        write!(f, "challenger: ")?;
255
        if let Some(challenger) = &self.0 {
256
            write!(f, "{challenger}")?;
257
        } else {
258
            write!(f, "none")?;
259
        }
260

            
261
        Ok(())
262
    }
263
}
264

            
265
#[derive(Clone, Eq, PartialEq)]
266
pub struct ServerGameLight {
267
    pub id: Id,
268
    pub attacker: Option<String>,
269
    pub defender: Option<String>,
270
    pub challenger: Challenger,
271
    pub rated: Rated,
272
    pub timed: TimeSettings,
273
    pub spectators: HashMap<String, usize>,
274
    pub challenge_accepted: bool,
275
    pub game_over: bool,
276
    pub board_size: BoardSize,
277
    pub turn: Role,
278
}
279

            
280
impl ServerGameLight {
281
    #[must_use]
282
    pub fn new(
283
        game_id: Id,
284
        username: String,
285
        rated: Rated,
286
        timed: TimeSettings,
287
        board_size: BoardSize,
288
        role: Role,
289
    ) -> Self {
290
        if role == Role::Attacker {
291
            Self {
292
                id: game_id,
293
                attacker: Some(username),
294
                defender: None,
295
                challenger: Challenger::default(),
296
                rated,
297
                timed,
298
                board_size,
299
                spectators: HashMap::new(),
300
                challenge_accepted: false,
301
                game_over: false,
302
                turn: Role::Roleless,
303
            }
304
        } else {
305
            Self {
306
                id: game_id,
307
                attacker: None,
308
                defender: Some(username),
309
                challenger: Challenger::default(),
310
                rated,
311
                timed,
312
                board_size,
313
                spectators: HashMap::new(),
314
                challenge_accepted: false,
315
                game_over: false,
316
                turn: Role::Roleless,
317
            }
318
        }
319
    }
320

            
321
    #[must_use]
322
    pub fn spectators(&self) -> Vec<usize> {
323
        let mut ids = Vec::new();
324

            
325
        for (name, id) in &self.spectators {
326
            if Some(name) != self.attacker.as_ref() && Some(name) != self.defender.as_ref() {
327
                ids.push(*id);
328
            }
329
        }
330

            
331
        ids
332
    }
333
}
334

            
335
impl From<&ServerGameSerialized> for ServerGameLight {
336
    fn from(game: &ServerGameSerialized) -> Self {
337
        Self {
338
            id: game.id,
339
            attacker: Some(game.attacker.clone()),
340
            defender: Some(game.defender.clone()),
341
            challenger: Challenger::default(),
342
            rated: game.rated,
343
            timed: game.timed.clone(),
344
            board_size: game.game.board.size(),
345
            spectators: HashMap::new(),
346
            challenge_accepted: true,
347
            game_over: false,
348
            turn: Role::Attacker,
349
        }
350
    }
351
}
352

            
353
impl fmt::Debug for ServerGameLight {
354
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355
        let attacker = if let Some(name) = &self.attacker {
356
            name
357
        } else {
358
            "_"
359
        };
360

            
361
        let defender = if let Some(name) = &self.defender {
362
            name
363
        } else {
364
            "_"
365
        };
366

            
367
        let Ok(spectators) = ron::ser::to_string(&self.spectators) else {
368
            unreachable!();
369
        };
370

            
371
        if self.challenge_accepted {
372
            let challenger = match self.turn {
373
                Role::Attacker => Challenger(Some("A".to_string())),
374
                Role::Defender => Challenger(Some("D".to_string())),
375
                Role::Roleless => Challenger(None),
376
            };
377

            
378
            write!(
379
                f,
380
                "game {} {attacker} {defender} {} {:?} {} {:?} {} {spectators}",
381
                self.id,
382
                self.rated,
383
                self.timed,
384
                self.board_size,
385
                challenger,
386
                self.challenge_accepted,
387
            )
388
        } else {
389
            write!(
390
                f,
391
                "game {} {attacker} {defender} {} {:?} {} {:?} {} {spectators}",
392
                self.id,
393
                self.rated,
394
                self.timed,
395
                self.board_size,
396
                self.challenger,
397
                self.challenge_accepted,
398
            )
399
        }
400
    }
401
}
402

            
403
impl fmt::Display for ServerGameLight {
404
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405
        let attacker = t!("attacker");
406
        let defender = t!("defender");
407
        let rated = t!(self.rated.to_string());
408
        let none = t!("none");
409

            
410
        let attacker = if let Some(name) = &self.attacker {
411
            &format!("{attacker}: {name}")
412
        } else {
413
            &format!("{attacker}: {none}")
414
        };
415

            
416
        let defender = if let Some(name) = &self.defender {
417
            &format!("{defender}: {name}")
418
        } else {
419
            &format!("{defender}: {none}")
420
        };
421

            
422
        write!(
423
            f,
424
            "# {}\n{attacker}, {defender}, {rated}\n{}: {}, {}: {}",
425
            self.id,
426
            t!("time"),
427
            self.timed,
428
            t!("board size"),
429
            self.board_size,
430
        )
431
    }
432
}
433

            
434
impl TryFrom<&[&str; 12]> for ServerGameLight {
435
    type Error = anyhow::Error;
436

            
437
    fn try_from(vector: &[&str; 12]) -> anyhow::Result<Self> {
438
        let [
439
            _,
440
            id,
441
            attacker,
442
            defender,
443
            rated,
444
            timed,
445
            minutes,
446
            add_seconds,
447
            board_size,
448
            challenger,
449
            challenge_accepted,
450
            spectators,
451
        ] = *vector;
452

            
453
        let id = id.parse::<Id>()?;
454

            
455
        let attacker = if attacker == "_" {
456
            None
457
        } else {
458
            Some(attacker.to_string())
459
        };
460

            
461
        let defender = if defender == "_" {
462
            None
463
        } else {
464
            Some(defender.to_string())
465
        };
466

            
467
        let timed = match timed {
468
            "fischer" => TimeSettings::Timed(Time {
469
                add_seconds: add_seconds.parse::<i64>()?,
470
                milliseconds_left: minutes.parse::<i64>()?,
471
            }),
472
            _ => TimeSettings::UnTimed,
473
        };
474

            
475
        let board_size = BoardSize::from_str(board_size)?;
476

            
477
        let Ok(challenge_accepted) = <bool as FromStr>::from_str(challenge_accepted) else {
478
            return Err(anyhow::Error::msg("challenge_accepted is not a bool."));
479
        };
480

            
481
        let spectators = ron::from_str(spectators)?;
482

            
483
        let mut game = Self {
484
            id,
485
            attacker,
486
            defender,
487
            challenger: Challenger::default(),
488
            rated: Rated::from_str(rated)?,
489
            timed,
490
            board_size,
491
            spectators,
492
            challenge_accepted,
493
            game_over: false,
494
            turn: Role::Roleless,
495
        };
496

            
497
        if challenger != "_" {
498
            game.challenger.0 = Some(challenger.to_string());
499
        }
500

            
501
        Ok(game)
502
    }
503
}
504

            
505
#[derive(Clone, Default, Eq, PartialEq)]
506
pub struct ServerGamesLight(pub HashMap<Id, ServerGameLight>);
507

            
508
impl fmt::Debug for ServerGamesLight {
509
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510
        for game in self.0.values().filter(|game| !game.game_over) {
511
            write!(f, "{game:?} ")?;
512
        }
513

            
514
        Ok(())
515
    }
516
}
517

            
518
#[derive(Clone, Default, Eq, PartialEq)]
519
pub struct ServerGamesLightVec(pub Vec<ServerGameLight>);
520

            
521
impl ServerGamesLightVec {
522
    #[allow(clippy::missing_errors_doc)]
523
    pub fn display_games(&self, username: Option<&str>) -> anyhow::Result<String> {
524
        let mut output = String::new();
525

            
526
        for game in &self.0 {
527
            if !game.game_over {
528
                write!(output, "{game:?} ")?;
529
            } else if let Some(username) = username
530
                && game.spectators.contains_key(username)
531
            {
532
                write!(output, "{game:?} ")?;
533
            }
534
        }
535

            
536
        Ok(output)
537
    }
538
}
539

            
540
impl fmt::Debug for ServerGamesLightVec {
541
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542
        for game in self.0.iter().filter(|game| !game.game_over) {
543
            write!(f, "{game:?} ")?;
544
        }
545

            
546
        Ok(())
547
    }
548
}