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::{
17
    collections::{HashMap, VecDeque},
18
    fmt,
19
    str::FromStr,
20
    sync::mpsc::Sender,
21
};
22

            
23
use rust_i18n::t;
24
use serde::{Deserialize, Serialize};
25

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

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

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

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

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

            
95
impl Eq for ArchivedGame {}
96

            
97
#[derive(Clone, Debug)]
98
pub struct Messenger(Option<Sender<String>>);
99

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

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

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

            
126
impl From<ServerGameSerialized> for ServerGame {
127
    fn from(game: ServerGameSerialized) -> Self {
128
        Self {
129
            id: game.id,
130
            attacker: game.attacker,
131
            attacker_tx: Messenger(None),
132
            defender: game.defender,
133
            defender_tx: Messenger(None),
134
            elapsed_time: 0,
135
            rated: game.rated,
136
            game: game.game,
137
            texts: game.texts,
138
        }
139
    }
140
}
141

            
142
impl ServerGame {
143
    #[must_use]
144
    pub fn protocol(&self) -> String {
145
        format!(
146
            "game {} {} {} {}",
147
            self.id, self.attacker, self.defender, self.rated
148
        )
149
    }
150

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

            
162
        let plays = match game.timed {
163
            TimeSettings::Timed(time) => Plays::PlayRecordsTimed(vec![PlayRecordTimed {
164
                play: None,
165
                attacker_time: time.into(),
166
                defender_time: time.into(),
167
            }]),
168
            TimeSettings::UnTimed => Plays::PlayRecords(vec![None]),
169
        };
170

            
171
        let board = Board::new(game.board_size);
172

            
173
        Self {
174
            id: game.id,
175
            attacker,
176
            attacker_tx: Messenger(attacker_tx),
177
            defender,
178
            defender_tx: Messenger(defender_tx),
179
            elapsed_time: 0,
180
            rated: game.rated,
181
            game: Game {
182
                attacker_time: game.timed.clone(),
183
                defender_time: game.timed,
184
                board,
185
                plays,
186
                ..Game::default()
187
            },
188
            texts: VecDeque::new(),
189
        }
190
    }
191
}
192

            
193
impl fmt::Display for ServerGame {
194
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195
        write!(
196
            f,
197
            "{}: {}, {}, {} ",
198
            self.id, self.attacker, self.defender, self.rated
199
        )
200
    }
201
}
202

            
203
#[derive(Clone, Debug, Deserialize, Serialize)]
204
pub struct ServerGameSerialized {
205
    pub id: Id,
206
    pub attacker: String,
207
    pub defender: String,
208
    pub rated: Rated,
209
    pub game: Game,
210
    pub texts: VecDeque<String>,
211
    pub timed: TimeSettings,
212
}
213

            
214
impl From<&ServerGame> for ServerGameSerialized {
215
    fn from(game: &ServerGame) -> Self {
216
        Self {
217
            id: game.id,
218
            attacker: game.attacker.clone(),
219
            defender: game.defender.clone(),
220
            rated: game.rated,
221
            game: game.game.clone(),
222
            texts: game.texts.clone(),
223
            timed: TimeSettings::default(),
224
        }
225
    }
226
}
227

            
228
#[derive(Clone, Debug, Default)]
229
pub struct ServerGames(pub HashMap<Id, ServerGame>);
230

            
231
#[derive(Clone, Default, Eq, PartialEq)]
232
pub struct Challenger(pub Option<String>);
233

            
234
impl fmt::Debug for Challenger {
235
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236
        if let Some(challenger) = &self.0 {
237
            write!(f, "{challenger}")?;
238
        } else {
239
            write!(f, "_")?;
240
        }
241

            
242
        Ok(())
243
    }
244
}
245

            
246
impl fmt::Display for Challenger {
247
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248
        write!(f, "challenger: ")?;
249
        if let Some(challenger) = &self.0 {
250
            write!(f, "{challenger}")?;
251
        } else {
252
            write!(f, "none")?;
253
        }
254

            
255
        Ok(())
256
    }
257
}
258

            
259
#[derive(Clone, Eq, PartialEq)]
260
pub struct ServerGameLight {
261
    pub id: Id,
262
    pub attacker: Option<String>,
263
    pub defender: Option<String>,
264
    pub challenger: Challenger,
265
    pub rated: Rated,
266
    pub timed: TimeSettings,
267
    pub attacker_channel: Option<usize>,
268
    pub defender_channel: Option<usize>,
269
    pub spectators: HashMap<String, usize>,
270
    pub challenge_accepted: bool,
271
    pub game_over: bool,
272
    pub board_size: BoardSize,
273
}
274

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

            
320
impl From<&ServerGameSerialized> for ServerGameLight {
321
    fn from(game: &ServerGameSerialized) -> Self {
322
        Self {
323
            id: game.id,
324
            attacker: Some(game.attacker.clone()),
325
            defender: Some(game.defender.clone()),
326
            challenger: Challenger::default(),
327
            rated: game.rated,
328
            timed: game.timed.clone(),
329
            board_size: game.game.board.size(),
330
            attacker_channel: None,
331
            defender_channel: None,
332
            spectators: HashMap::new(),
333
            challenge_accepted: true,
334
            game_over: false,
335
        }
336
    }
337
}
338

            
339
impl fmt::Debug for ServerGameLight {
340
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341
        let attacker = if let Some(name) = &self.attacker {
342
            name
343
        } else {
344
            "_"
345
        };
346

            
347
        let defender = if let Some(name) = &self.defender {
348
            name
349
        } else {
350
            "_"
351
        };
352

            
353
        let Ok(spectators) = ron::ser::to_string(&self.spectators) else {
354
            unreachable!();
355
        };
356

            
357
        write!(
358
            f,
359
            "game {} {attacker} {defender} {} {:?} {} {:?} {} {spectators}",
360
            self.id,
361
            self.rated,
362
            self.timed,
363
            self.board_size,
364
            self.challenger,
365
            self.challenge_accepted,
366
        )
367
    }
368
}
369

            
370
impl fmt::Display for ServerGameLight {
371
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372
        let attacker = t!("attacker");
373
        let defender = t!("defender");
374
        let rated = t!(self.rated.to_string());
375
        let none = t!("none");
376

            
377
        let attacker = if let Some(name) = &self.attacker {
378
            &format!("{attacker}: {name}")
379
        } else {
380
            &format!("{attacker}: {none}")
381
        };
382

            
383
        let defender = if let Some(name) = &self.defender {
384
            &format!("{defender}: {name}")
385
        } else {
386
            &format!("{defender}: {none}")
387
        };
388

            
389
        write!(
390
            f,
391
            "# {}\n{attacker}, {defender}, {rated}\n{}: {}, {}: {}",
392
            self.id,
393
            t!("time"),
394
            self.timed,
395
            t!("board size"),
396
            self.board_size,
397
        )
398
    }
399
}
400

            
401
impl TryFrom<&[&str]> for ServerGameLight {
402
    type Error = anyhow::Error;
403

            
404
    fn try_from(vector: &[&str]) -> anyhow::Result<Self> {
405
        if vector.len() < 12 {
406
            return Err(anyhow::Error::msg("ServerGameLight has too few words."));
407
        }
408

            
409
        let id = vector[1];
410
        let attacker = vector[2];
411
        let defender = vector[3];
412
        let rated = vector[4];
413
        let timed = vector[5];
414
        let minutes = vector[6];
415
        let add_seconds = vector[7];
416
        let board_size = vector[8];
417
        let challenger = vector[9];
418
        let challenge_accepted = vector[10];
419
        let spectators = vector[11];
420

            
421
        let id = id.parse::<Id>()?;
422

            
423
        let attacker = if attacker == "_" {
424
            None
425
        } else {
426
            Some(attacker.to_string())
427
        };
428

            
429
        let defender = if defender == "_" {
430
            None
431
        } else {
432
            Some(defender.to_string())
433
        };
434

            
435
        let timed = match timed {
436
            "fischer" => TimeSettings::Timed(Time {
437
                add_seconds: add_seconds.parse::<i64>()?,
438
                milliseconds_left: minutes.parse::<i64>()?,
439
            }),
440
            // "un-timed"
441
            _ => TimeSettings::UnTimed,
442
        };
443

            
444
        let board_size = BoardSize::from_str(board_size)?;
445

            
446
        let Ok(challenge_accepted) = <bool as FromStr>::from_str(challenge_accepted) else {
447
            return Err(anyhow::Error::msg("challenge_accepted is not a bool."));
448
        };
449

            
450
        let spectators = ron::from_str(spectators)?;
451

            
452
        let mut game = Self {
453
            id,
454
            attacker,
455
            defender,
456
            challenger: Challenger::default(),
457
            rated: Rated::from_str(rated)?,
458
            timed,
459
            board_size,
460
            attacker_channel: None,
461
            defender_channel: None,
462
            spectators,
463
            challenge_accepted,
464
            game_over: false,
465
        };
466

            
467
        if challenger != "_" {
468
            game.challenger.0 = Some(challenger.to_string());
469
        }
470

            
471
        Ok(game)
472
    }
473
}
474

            
475
#[derive(Clone, Default, Eq, PartialEq)]
476
pub struct ServerGamesLight(pub HashMap<Id, ServerGameLight>);
477

            
478
impl fmt::Debug for ServerGamesLight {
479
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480
        for game in self.0.values().filter(|game| !game.game_over) {
481
            write!(f, "{game:?} ")?;
482
        }
483

            
484
        Ok(())
485
    }
486
}