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
// Don't open the terminal on Windows.
17
#![cfg_attr(all(windows, not(feature = "console")), windows_subsystem = "windows")]
18
#![deny(clippy::unwrap_used)]
19

            
20
mod archived_game_handle;
21
mod command_line;
22
mod dimensions;
23
mod enums;
24
mod new_game_settings;
25
mod user;
26

            
27
use std::{
28
    collections::{HashMap, HashSet, VecDeque},
29
    fmt::{self, Write as _},
30
    fs::{self, File},
31
    io::{BufRead, BufReader, Cursor, ErrorKind, Read, Write},
32
    net::{Shutdown, TcpStream, ToSocketAddrs},
33
    process::exit,
34
    str::{FromStr, SplitAsciiWhitespace},
35
    sync::mpsc,
36
    thread::{self, sleep},
37
    time::Duration,
38
};
39

            
40
use ::serde::{Deserialize, Serialize};
41
use chrono::{Local, Utc};
42
use clap::{CommandFactory, Parser};
43
use futures::{SinkExt, executor};
44
use hnefatafl_copenhagen::{
45
    COPYRIGHT, Id, SERVER_PORT, VERSION_ID,
46
    board::{Board, BoardSize},
47
    characters::Characters,
48
    draw::Draw,
49
    email::Email,
50
    game::{Game, LegalMoves, TimeUnix},
51
    glicko::{CONFIDENCE_INTERVAL_95, Rating},
52
    heat_map::{Heat, HeatMap},
53
    locale::Locale,
54
    play::{BOARD_LETTERS, Plae, Plays, Vertex},
55
    rating::Rated,
56
    role::Role,
57
    server_game::{ArchivedGame, ServerGameLight, ServerGamesLight},
58
    space::Space,
59
    status::Status,
60
    time::{TimeEnum, TimeSettings},
61
    tournament::{self, Tournament},
62
    tree::Tree,
63
    utils::{self, choose_ai, create_data_folder, data_file},
64
};
65
#[cfg(target_os = "linux")]
66
use iced::window::settings::PlatformSpecific;
67
use iced::{
68
    Color, Element, Event, Font, Pixels, Subscription, Task,
69
    alignment::{Horizontal, Vertical},
70
    color, event,
71
    futures::Stream,
72
    keyboard::{self, Key, key::Named},
73
    stream,
74
    theme::Palette,
75
    widget::{
76
        self, Column, Container, Row, Scrollable, button, checkbox, column, container,
77
        operation::{focus_next, focus_previous},
78
        pick_list, radio, row, scrollable, text, text_editor,
79
        text_input::Value,
80
        tooltip,
81
    },
82
    window::{self, icon},
83
};
84
use image::ImageFormat;
85
use log::{debug, error, info, trace};
86
use rust_i18n::t;
87
use smol_str::ToSmolStr;
88
use socket2::{Domain, SockAddr, Socket, TcpKeepalive, Type};
89

            
90
use crate::{
91
    archived_game_handle::ArchivedGameHandle,
92
    command_line::Args,
93
    dimensions::Dimensions,
94
    enums::{Coordinates, JoinGame, LoggedIn, Message, Move, Screen, Size, SortBy, State, Theme},
95
    new_game_settings::NewGameSettings,
96
    user::User,
97
};
98

            
99
/// The Muted qualitative color scheme of [Tol]. A color scheme for the
100
/// color blind.
101
///
102
/// [Tol]: https://sronpersonalpages.nl/~pault/#sec:qualitative
103
pub const TOL: Palette = Palette {
104
    background: color!(0xDD, 0xDD, 0xDD), // PALE_GREY
105
    text: color!(0x00, 0x00, 0x00),       // BLACK
106
    primary: color!(0x88, 0xCC, 0xEE),    // CYAN
107
    success: color!(0x11, 0x77, 0x33),    // GREEN
108
    warning: color!(0xDD, 0xCC, 0x77),    // SAND
109
    danger: color!(0xCC, 0x66, 0x77),     // ROSE
110
};
111

            
112
const ALPHABET: [char; 26] = [
113
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
114
    't', 'u', 'v', 'w', 'x', 'y', 'z',
115
];
116

            
117
#[cfg(all(target_os = "linux", not(feature = "icon_2")))]
118
const APPLICATION_ID: &str = "hnefatafl-client";
119

            
120
#[cfg(all(target_os = "linux", feature = "icon_2"))]
121
const APPLICATION_ID: &str = "org.hnefatafl.hnefatafl_client";
122

            
123
const BOARD_LETTERS_LOWERCASE: [char; 13] = [
124
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
125
];
126

            
127
const HELMET: &[u8] = include_bytes!("helmet.png");
128
const ARCHIVED_GAMES_FILE: &str = "archived-games.postcard";
129
const USER_CONFIG_FILE: &str = "user.ron";
130

            
131
const PADDING: u16 = 10;
132
const PADDING_SMALL: u16 = 2;
133
const PADDING_MEDIUM: u16 = 4;
134
const SPACING: Pixels = Pixels(10.0);
135
const SPACING_B: Pixels = Pixels(20.0);
136

            
137
const SOUND_CAPTURE: &[u8] = include_bytes!("sound/capture.ogg");
138
const SOUND_GAME_OVER: &[u8] = include_bytes!("sound/game_over.ogg");
139
const SOUND_MOVE: &[u8] = include_bytes!("sound/move.ogg");
140

            
141
const TROPHY_SIZE: u32 = 32;
142

            
143
rust_i18n::i18n!();
144

            
145
fn i18n_buttons() -> HashMap<String, String> {
146
    let mut strings = HashMap::new();
147

            
148
    strings.insert("Login".to_string(), t!("Login").to_string());
149
    strings.insert(
150
        "Create Account".to_string(),
151
        t!("Create Account").to_string(),
152
    );
153
    strings.insert(
154
        "Reset Password".to_string(),
155
        t!("Reset Password").to_string(),
156
    );
157
    strings.insert("Leave".to_string(), t!("Leave").to_string());
158
    strings.insert("Quit".to_string(), t!("Quit").to_string());
159
    strings.insert("Dark".to_string(), t!("Dark").to_string());
160
    strings.insert("Light".to_string(), t!("Light").to_string());
161
    strings.insert("Create Game".to_string(), t!("Create Game").to_string());
162
    strings.insert("Users".to_string(), t!("Users").to_string());
163
    strings.insert(
164
        "Account Settings".to_string(),
165
        t!("Account Settings").to_string(),
166
    );
167
    strings.insert("Rules".to_string(), t!("Rules").to_string());
168
    strings.insert("Reset Email".to_string(), t!("Reset Email").to_string());
169
    strings.insert(
170
        "Change Password".to_string(),
171
        t!("Change Password").to_string(),
172
    );
173
    strings.insert(
174
        "Delete Account".to_string(),
175
        t!("Delete Account").to_string(),
176
    );
177
    strings.insert(
178
        "REALLY DELETE ACCOUNT".to_string(),
179
        t!("REALLY DELETE ACCOUNT").to_string(),
180
    );
181
    strings.insert("New Game".to_string(), t!("New Game").to_string());
182
    strings.insert("Accept".to_string(), t!("Accept").to_string());
183
    strings.insert("Decline".to_string(), t!("Decline").to_string());
184
    strings.insert("Watch".to_string(), t!("Watch").to_string());
185
    strings.insert("Join".to_string(), t!("Join").to_string());
186
    strings.insert("Resume".to_string(), t!("Resume").to_string());
187
    strings.insert("Resign".to_string(), t!("Resign").to_string());
188
    strings.insert("Request Draw".to_string(), t!("Request Draw").to_string());
189
    strings.insert("Accept Draw".to_string(), t!("Accept Draw").to_string());
190
    strings.insert("Review Game".to_string(), t!("Review Game").to_string());
191
    strings.insert(
192
        "Get Archived Games".to_string(),
193
        t!("Get Archived Games").to_string(),
194
    );
195
    strings.insert("Heat Map".to_string(), t!("Heat Map").to_string());
196
    strings.insert("Join Discord".to_string(), t!("Join Discord").to_string());
197
    strings.insert("Cancel".to_string(), t!("Cancel").to_string());
198
    strings.insert("Tournament".to_string(), t!("Tournament").to_string());
199
    strings.insert(
200
        "Join Tournament".to_string(),
201
        t!("Join Tournament").to_string(),
202
    );
203
    strings.insert(
204
        "Leave Tournament".to_string(),
205
        t!("Leave Tournament").to_string(),
206
    );
207

            
208
    strings
209
}
210

            
211
fn init_client() -> Client {
212
    let user_config_file_postcard = data_file(ARCHIVED_GAMES_FILE);
213
    let user_config_file_ron = data_file(USER_CONFIG_FILE);
214
    let mut error = Vec::new();
215

            
216
    let mut client: Client = match &fs::read_to_string(&user_config_file_ron) {
217
        Ok(string) => match ron::from_str(string) {
218
            Ok(client) => client,
219
            Err(err) => {
220
                error.push(format!(
221
                    "Error parsing the ron file {}: {err}",
222
                    user_config_file_ron.display()
223
                ));
224
                Client::default()
225
            }
226
        },
227
        Err(err) => {
228
            if err.kind() == ErrorKind::NotFound {
229
                error.push(format!(
230
                    "Unable to find User Configuration file: {}",
231
                    user_config_file_ron.display()
232
                ));
233
                Client::default()
234
            } else {
235
                error.push(format!(
236
                    "Error opening the file {}: {err}",
237
                    user_config_file_ron.display()
238
                ));
239
                Client::default()
240
            }
241
        }
242
    };
243

            
244
    rust_i18n::set_locale(&client.locale_selected.txt());
245
    client.strings = i18n_buttons();
246
    client.text_input.clone_from(&client.username);
247

            
248
    let archived_games: Vec<ArchivedGame> = match &fs::read(&user_config_file_postcard) {
249
        Ok(bytes) => match postcard::from_bytes(bytes) {
250
            Ok(client) => client,
251
            Err(err) => {
252
                error.push(format!(
253
                    "Error parsing the postcard file {}: {err}",
254
                    user_config_file_postcard.display()
255
                ));
256
                Vec::new()
257
            }
258
        },
259
        Err(err) => {
260
            if err.kind() == ErrorKind::NotFound {
261
                error.push(format!(
262
                    "{}: {}",
263
                    t!("Unable to find Archived Games file"),
264
                    user_config_file_postcard.display()
265
                ));
266
                Vec::new()
267
            } else {
268
                error.push(format!(
269
                    "{} {}: {err}",
270
                    t!("Error opening the file"),
271
                    user_config_file_postcard.display()
272
                ));
273
                Vec::new()
274
            }
275
        }
276
    };
277

            
278
    client.archived_games = archived_games;
279
    client.error_persistent = error;
280

            
281
    let args = Args::parse();
282
    if args.ascii {
283
        client.chars.ascii();
284
    }
285

            
286
    let mut letters = HashMap::new();
287
    for ch in BOARD_LETTERS_LOWERCASE {
288
        letters.insert(ch, false);
289
    }
290

            
291
    client
292
}
293

            
294
fn main() -> anyhow::Result<()> {
295
    let args = Args::parse();
296
    utils::init_logger("hnefatafl_client", args.debug, false);
297

            
298
    if args.man {
299
        let mut buffer: Vec<u8> = Vec::default();
300
        let cmd = Args::command().name("hnefatafl-client").long_version(None);
301
        let man = clap_mangen::Man::new(cmd).date("2025-06-23");
302

            
303
        man.render(&mut buffer)?;
304
        write!(buffer, "{COPYRIGHT}")?;
305

            
306
        std::fs::write("hnefatafl-client.1", buffer)?;
307
        return Ok(());
308
    }
309

            
310
    create_data_folder()?;
311

            
312
    let mut application = iced::application(init_client, Client::update, Client::view)
313
        .title("Hnefatafl Copenhagen")
314
        .subscription(Client::subscriptions)
315
        .window(window::Settings {
316
            #[cfg(target_os = "linux")]
317
            platform_specific: PlatformSpecific {
318
                application_id: APPLICATION_ID.to_string(),
319
                ..PlatformSpecific::default()
320
            },
321
            icon: Some(icon::from_file_data(HELMET, Some(ImageFormat::Png))?),
322
            ..window::Settings::default()
323
        })
324
        .theme(Client::theme);
325

            
326
    // For screenshots.
327
    if args.tiny_window {
328
        application = application.window_size(iced::Size {
329
            width: 868.0,
330
            height: 541.0,
331
        });
332
    }
333

            
334
    if args.social_preview {
335
        application = application.window_size(iced::Size {
336
            width: 1148.0,
337
            height: 481.0,
338
        });
339
    }
340

            
341
    application.run()?;
342
    Ok(())
343
}
344

            
345
fn estimate_score() -> impl Stream<Item = Message> {
346
    let args = Args::parse();
347

            
348
    stream::channel(
349
        100,
350
        move |mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
351
            let (tx, rx) = mpsc::channel();
352

            
353
            if let Err(error) = sender.send(Message::EstimateScoreConnected(tx)).await {
354
                error!("failed to send channel: {error}");
355
                exit(1);
356
            }
357

            
358
            thread::spawn(move || {
359
                let mut ai = match choose_ai(&args.ai, args.seconds, args.depth, true) {
360
                    Ok(ai) => ai,
361
                    Err(error) => {
362
                        error!("{error}");
363
                        exit(1);
364
                    }
365
                };
366

            
367
                for tree in &rx {
368
                    let mut game = Game::from(&tree);
369
                    let generate_move = ai.generate_move(&mut game).expect("the game is ongoing");
370

            
371
                    if let Err(error) = executor::block_on(
372
                        sender.send(Message::EstimateScoreDisplay((tree.here(), generate_move))),
373
                    ) {
374
                        error!("failed to send channel: {error}");
375
                        exit(1);
376
                    }
377
                }
378
            });
379
        },
380
    )
381
}
382

            
383
fn handle_error<T, E: fmt::Display>(result: Result<T, E>) -> T {
384
    match result {
385
        Ok(value) => value,
386
        Err(error) => {
387
            error!("{error}");
388
            exit(1)
389
        }
390
    }
391
}
392

            
393
fn open_url(url: &str) {
394
    if let Err(error) = webbrowser::open(url) {
395
        error!("{error}");
396
    }
397
}
398

            
399
#[allow(clippy::too_many_lines)]
400
fn pass_messages() -> impl Stream<Item = Message> {
401
    stream::channel(
402
        100,
403
        move |mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
404
            let mut args = Args::parse();
405
            args.host.push_str(SERVER_PORT);
406
            let address_string = args.host;
407

            
408
            thread::spawn(move || {
409
                'start_over: loop {
410
                    let (tx, rx) = mpsc::channel();
411

            
412
                    if let Err(error) =
413
                        executor::block_on(sender.send(Message::StreamConnected(tx.clone())))
414
                    {
415
                        error!("failed to send channel: {error}");
416
                        exit(1);
417
                    }
418

            
419
                    loop {
420
                        let message = match rx.recv() {
421
                            Ok(message) => message,
422
                            Err(error) => {
423
                                error!("rx: {error}");
424

            
425
                                if let Err(error) = executor::block_on(sender.send(Message::Exit)) {
426
                                    error!("{error}");
427
                                }
428

            
429
                                return;
430
                            }
431
                        };
432
                        let message_trim = message.trim();
433

            
434
                        if message_trim == "tcp_connect" {
435
                            break;
436
                        }
437
                    }
438

            
439
                    let mut is_ipv6 = false;
440
                    let mut socket_address = None;
441
                    let socket_addresses =
442
                        address_string.to_socket_addrs().unwrap_or_else(|error| {
443
                            panic!("The socket address resolves to no IPs: {error})")
444
                        });
445

            
446
                    for address in socket_addresses.clone() {
447
                        if address.is_ipv6() {
448
                            socket_address = Some(address);
449
                            is_ipv6 = true;
450
                            break;
451
                        }
452
                    }
453

            
454
                    if !is_ipv6 {
455
                        for address in socket_addresses {
456
                            if address.is_ipv4() {
457
                                socket_address = Some(address);
458
                                break;
459
                            }
460
                        }
461
                    }
462

            
463
                    let socket_address = socket_address.unwrap_or_else(|| {
464
                        panic!("There is no IPv4 address for the host: {address_string}")
465
                    });
466

            
467
                    let address: SockAddr = socket_address.into();
468
                    let keepalive = TcpKeepalive::new()
469
                        .with_time(Duration::from_secs(30))
470
                        .with_interval(Duration::from_secs(30))
471
                        .with_retries(3);
472

            
473
                    let domain_type = if is_ipv6 { Domain::IPV6 } else { Domain::IPV4 };
474
                    let socket = handle_error(Socket::new(domain_type, Type::STREAM, None));
475
                    handle_error(socket.set_tcp_keepalive(&keepalive));
476

            
477
                    if let Err(error) = socket.connect(&address) {
478
                        error!("socket.connect {address_string}: {error}");
479
                        handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
480
                        handle_error(executor::block_on(sender.send(Message::ServerShutdown)));
481

            
482
                        continue 'start_over;
483
                    }
484

            
485
                    info!("connected to {socket_address} ...");
486

            
487
                    let mut tcp_stream: TcpStream = socket.into();
488
                    let mut reader = BufReader::new(handle_error(tcp_stream.try_clone()));
489

            
490
                    let mut sender_clone = sender.clone();
491
                    thread::spawn(move || {
492
                        for message in rx {
493
                            let message_trim = message.trim();
494

            
495
                            if message_trim == "ping" {
496
                                trace!("<- {message_trim}");
497
                            } else {
498
                                debug!("<- {message_trim}");
499
                            }
500

            
501
                            if message_trim == "quit" {
502
                                if cfg!(not(target_os = "redox")) {
503
                                    tcp_stream
504
                                        .shutdown(Shutdown::Both)
505
                                        .expect("shutdown call failed");
506
                                }
507

            
508
                                return;
509
                            }
510

            
511
                            handle_error(tcp_stream.write_all(message.as_bytes()));
512
                        }
513

            
514
                        for _ in 0..2 {
515
                            if let Err(error) =
516
                                executor::block_on(sender_clone.send(Message::LeaveSoft))
517
                            {
518
                                error!("{error}");
519
                            }
520
                        }
521

            
522
                        if let Err(error) =
523
                            executor::block_on(sender_clone.send(Message::ServerShutdown))
524
                        {
525
                            error!("{error}");
526
                        }
527
                    });
528

            
529
                    let mut buffer = String::new();
530
                    handle_error(executor::block_on(
531
                        sender.send(Message::ConnectedTo(address_string.clone())),
532
                    ));
533

            
534
                    if cfg!(target_os = "redox") {
535
                        sleep(Duration::from_secs(1));
536
                    }
537

            
538
                    loop {
539
                        let bytes = handle_error(reader.read_line(&mut buffer));
540
                        if bytes > 0 {
541
                            let buffer_trim = buffer.trim();
542
                            let buffer_trim_vec: Vec<_> =
543
                                buffer_trim.split_ascii_whitespace().collect();
544

            
545
                            if buffer_trim_vec[1] == "display_users"
546
                                || buffer_trim_vec[1] == "display_games"
547
                                || buffer_trim_vec[1] == "ping"
548
                            {
549
                                trace!("-> {buffer_trim}");
550
                            } else {
551
                                debug!("-> {buffer_trim}");
552
                            }
553

            
554
                            handle_error(executor::block_on(
555
                                sender.send(Message::TextReceived(buffer.clone())),
556
                            ));
557

            
558
                            if buffer_trim_vec[1] == "archived_games" {
559
                                let length = handle_error(buffer_trim_vec[2].parse());
560
                                let mut buf = vec![0; length];
561
                                handle_error(reader.read_exact(&mut buf));
562
                                let archived_games: Vec<ArchivedGame> =
563
                                    handle_error(postcard::from_bytes(&buf));
564

            
565
                                handle_error(executor::block_on(
566
                                    sender.send(Message::ArchivedGames(archived_games)),
567
                                ));
568
                            }
569

            
570
                            buffer.clear();
571
                        } else {
572
                            info!("the TCP stream has closed");
573
                            continue 'start_over;
574
                        }
575
                    }
576
                }
577
            });
578
        },
579
    )
580
}
581

            
582
fn text_collect(text: SplitAsciiWhitespace<'_>) -> String {
583
    let text: Vec<&str> = text.collect();
584
    text.join(" ")
585
}
586

            
587
#[allow(clippy::struct_excessive_bools)]
588
#[derive(Debug, Default, Deserialize, Serialize)]
589
struct Client {
590
    #[serde(skip)]
591
    admin: bool,
592
    #[serde(skip)]
593
    attacker: String,
594
    #[serde(default)]
595
    archived_games: Vec<ArchivedGame>,
596
    #[serde(skip)]
597
    archived_games_filtered: Option<Vec<ArchivedGame>>,
598
    #[serde(skip)]
599
    archived_game_selected: Option<ArchivedGame>,
600
    #[serde(skip)]
601
    archived_game_handle: Option<ArchivedGameHandle>,
602
    #[serde(default)]
603
    coordinates: Coordinates,
604
    #[serde(skip)]
605
    defender: String,
606
    #[serde(skip)]
607
    delete_account: bool,
608
    #[serde(skip)]
609
    estimate_score: bool,
610
    #[serde(skip)]
611
    estimate_score_tx: Option<mpsc::Sender<Tree>>,
612
    #[serde(skip)]
613
    captures: HashSet<Vertex>,
614
    #[serde(skip)]
615
    counter: u64,
616
    #[serde(skip)]
617
    chars: Characters,
618
    #[serde(skip)]
619
    challenger: bool,
620
    #[serde(skip)]
621
    connected_tcp: bool,
622
    #[serde(skip)]
623
    connected_to: String,
624
    #[serde(skip)]
625
    content: text_editor::Content,
626
    #[serde(skip)]
627
    email: Option<Email>,
628
    #[serde(skip)]
629
    emails_bcc: Vec<String>,
630
    #[serde(skip)]
631
    error: Option<String>,
632
    #[serde(skip)]
633
    error_email: Option<String>,
634
    #[serde(skip)]
635
    error_persistent: Vec<String>,
636
    #[serde(skip)]
637
    game: Option<Game>,
638
    #[serde(skip)]
639
    game_id: Id,
640
    #[serde(skip)]
641
    games_light: ServerGamesLight,
642
    #[serde(skip)]
643
    game_settings: NewGameSettings,
644
    #[serde(skip)]
645
    heat_map: Option<HeatMap>,
646
    #[serde(skip)]
647
    heat_map_display: bool,
648
    #[serde(default)]
649
    locale_selected: Locale,
650
    #[serde(skip)]
651
    message: String,
652
    #[serde(default)]
653
    my_games_only: bool,
654
    #[serde(skip)]
655
    my_turn: bool,
656
    #[serde(skip)]
657
    now: i64,
658
    #[serde(skip)]
659
    now_diff: i64,
660
    #[serde(default)]
661
    password: String,
662
    #[serde(skip)]
663
    password_ends_with_whitespace: bool,
664
    #[serde(default)]
665
    password_save: bool,
666
    #[serde(default)]
667
    password_show: bool,
668
    #[serde(skip)]
669
    play_from: Option<Vertex>,
670
    #[serde(skip)]
671
    play_from_previous: Option<Vertex>,
672
    #[serde(skip)]
673
    play_to_previous: Option<Vertex>,
674
    #[serde(skip)]
675
    press_letters: HashSet<char>,
676
    #[serde(skip)]
677
    press_numbers: [bool; 13],
678
    #[serde(skip)]
679
    request_draw: bool,
680
    #[serde(skip)]
681
    screen: Screen,
682
    #[serde(skip)]
683
    screen_size: Size,
684
    #[serde(default)]
685
    sound_muted: bool,
686
    #[serde(skip)]
687
    spectators: Vec<String>,
688
    #[serde(skip)]
689
    status: Status,
690
    #[serde(skip)]
691
    texts: VecDeque<String>,
692
    #[serde(skip)]
693
    texts_game: VecDeque<String>,
694
    #[serde(skip)]
695
    text_input: String,
696
    #[serde(default)]
697
    theme: Theme,
698
    #[serde(skip)]
699
    time_attacker: TimeSettings,
700
    #[serde(skip)]
701
    time_defender: TimeSettings,
702
    #[serde(skip)]
703
    tournament: Option<Tournament>,
704
    #[serde(skip)]
705
    tx: Option<mpsc::Sender<String>>,
706
    #[serde(default)]
707
    username: String,
708
    #[serde(skip)]
709
    users: HashMap<String, User>,
710
    #[serde(skip)]
711
    users_sort_by: SortBy,
712
    #[serde(skip)]
713
    strings: HashMap<String, String>,
714
}
715

            
716
impl<'a> Client {
717
    fn add_infinity(&self, column: Column<'a, Message>) -> Column<'a, Message> {
718
        let infinity = radio(
719
            format!("{} (9)", TimeEnum::Infinity),
720
            TimeEnum::Infinity,
721
            self.game_settings.time,
722
            Message::Time,
723
        );
724

            
725
        let row = row![infinity].padding(PADDING).spacing(SPACING);
726
        column.push(row)
727
    }
728

            
729
    fn archived_game_reset(&mut self) {
730
        self.archived_game_handle = None;
731
        self.archived_game_selected = None;
732
    }
733

            
734
    #[must_use]
735
    fn board(&self) -> Row<'_, Message> {
736
        let (board, heat_map) = self.board_and_heatmap();
737
        let board_size = board.size();
738
        let board_size_usize: usize = board_size.into();
739
        let d = Dimensions::new(board_size, &self.screen_size);
740
        let letters: Vec<_> = BOARD_LETTERS[..board_size_usize].chars().collect();
741
        let mut game_display = Row::new().spacing(2);
742
        let possible_moves = self.possible_moves();
743

            
744
        let coordinates: bool = self.coordinates.into();
745
        if coordinates {
746
            game_display =
747
                game_display.push(self.numbers(d.letter_size, d.spacing, board_size_usize));
748
        }
749

            
750
        for (x, letter) in letters.iter().enumerate() {
751
            let mut column = Column::new().spacing(2).align_x(Horizontal::Center);
752

            
753
            if coordinates {
754
                column = self.letter(*letter, column, d.letter_size);
755
            }
756

            
757
            for y in 0..board_size_usize {
758
                let vertex = Vertex {
759
                    size: board.size(),
760
                    x,
761
                    y,
762
                };
763

            
764
                let mut txt = match board.get(&vertex) {
765
                    Space::Attacker => text(&self.chars.attacker),
766
                    Space::Defender => text(&self.chars.defender),
767
                    Space::Empty => {
768
                        if let Some(arrow) = self.draw_arrow(y, x) {
769
                            text(arrow)
770
                        } else if self.captures.contains(&vertex) {
771
                            text(&self.chars.captured)
772
                        } else if vertex.on_restricted_square() {
773
                            text(&self.chars.restricted_square)
774
                        } else {
775
                            text(" ")
776
                        }
777
                    }
778
                    Space::King => text(&self.chars.king),
779
                };
780

            
781
                if let Some((heat_map_from, heat_map_to)) = &heat_map
782
                    && possible_moves.is_some()
783
                {
784
                    if let Some(vertex_from) = self.play_from.as_ref() {
785
                        let space = board.get(vertex_from);
786
                        let turn = Role::from(space);
787
                        if let Some(heat_map_to) = heat_map_to.get(&(turn, *vertex_from)) {
788
                            let heat = heat_map_to[y * board_size_usize + x];
789

            
790
                            if heat == Heat::UnRanked {
791
                                txt = txt.color(Color::from_rgba(0.0, 0.0, 0.0, heat.into()));
792
                            } else {
793
                                let txt_char = match space {
794
                                    Space::Attacker => &self.chars.attacker,
795
                                    Space::Defender => &self.chars.defender,
796
                                    Space::Empty => "",
797
                                    Space::King => &self.chars.king,
798
                                };
799

            
800
                                txt = text(txt_char).color(Color::from_rgba(
801
                                    0.0,
802
                                    0.0,
803
                                    0.0,
804
                                    heat.into(),
805
                                ));
806
                            }
807
                        }
808
                    } else {
809
                        let heat = heat_map_from[y * board_size_usize + x];
810
                        txt = txt.color(Color::from_rgba(0.0, 0.0, 0.0, heat.into()));
811
                    }
812
                }
813

            
814
                txt = txt.font(Font::MONOSPACE).center().size(d.piece_size);
815
                let mut button = button(txt)
816
                    .width(d.board_dimension)
817
                    .height(d.board_dimension);
818

            
819
                match self.board_move(&vertex, possible_moves.as_ref()) {
820
                    Move::From => button = button.on_press(Message::PlayMoveFrom(vertex)),
821
                    Move::To => button = button.on_press(Message::PlayMoveTo(vertex)),
822
                    Move::Revert => button = button.on_press(Message::PlayMoveRevert),
823
                    Move::None => {}
824
                }
825

            
826
                column = column.push(button);
827
            }
828

            
829
            if coordinates {
830
                column = self.letter(*letter, column, d.letter_size);
831
            }
832

            
833
            game_display = game_display.push(column);
834
        }
835

            
836
        if coordinates {
837
            game_display =
838
                game_display.push(self.numbers(d.letter_size, d.spacing, board_size_usize));
839
        }
840

            
841
        game_display
842
    }
843

            
844
    fn board_move(&self, vertex: &Vertex, possible_moves: Option<&LegalMoves>) -> Move {
845
        if let Some(legal_moves) = possible_moves {
846
            if let Some(vertex_from) = self.play_from.as_ref() {
847
                if let Some(vertexes) = legal_moves.moves.get(vertex_from) {
848
                    if vertex == vertex_from {
849
                        Move::Revert
850
                    } else if vertexes.contains(vertex) {
851
                        Move::To
852
                    } else {
853
                        Move::None
854
                    }
855
                } else {
856
                    Move::None
857
                }
858
            } else if legal_moves.moves.contains_key(vertex) {
859
                Move::From
860
            } else {
861
                Move::None
862
            }
863
        } else {
864
            Move::None
865
        }
866
    }
867

            
868
    #[allow(clippy::type_complexity)]
869
    fn board_and_heatmap(
870
        &self,
871
    ) -> (
872
        Board,
873
        Option<(Vec<Heat>, HashMap<(Role, Vertex), Vec<Heat>>)>,
874
    ) {
875
        if let Some(game_handle) = &self.archived_game_handle {
876
            let node = game_handle.boards.here();
877

            
878
            if self.heat_map_display
879
                && let Some(heat_map) = &self.heat_map
880
            {
881
                (node.board.clone(), Some(heat_map.draw(node.turn)))
882
            } else {
883
                (node.board.clone(), None)
884
            }
885
        } else {
886
            let game = self.game.as_ref().expect("we should be in a game");
887

            
888
            (game.board.clone(), None)
889
        }
890
    }
891

            
892
    fn game_state(&self, game_id: u128) -> State {
893
        if let Some(game) = self.games_light.0.get(&game_id) {
894
            if game.challenge_accepted {
895
                return State::Spectator;
896
            }
897

            
898
            if game.attacker.is_none() || game.defender.is_none() {
899
                if let Some(attacker) = &game.attacker
900
                    && &self.username == attacker
901
                {
902
                    return State::CreatorOnly;
903
                }
904

            
905
                if let Some(defender) = &game.defender
906
                    && &self.username == defender
907
                {
908
                    return State::CreatorOnly;
909
                }
910
            }
911

            
912
            if let (Some(attacker), Some(defender)) = (&game.attacker, &game.defender)
913
                && (&self.username == attacker || &self.username == defender)
914
            {
915
                if let Some(challenger) = &game.challenger.0 {
916
                    if &self.username != challenger {
917
                        return State::Creator;
918
                    }
919
                } else {
920
                    return State::Challenger;
921
                }
922
            }
923
        }
924

            
925
        State::Spectator
926
    }
927

            
928
    fn clear_letters_except(&mut self, letter: char) {
929
        for l in BOARD_LETTERS_LOWERCASE {
930
            if l != letter {
931
                self.press_letters.remove(&l);
932
            }
933
        }
934
    }
935

            
936
    fn clear_numbers_except(&mut self, number: usize) {
937
        let (board, _) = self.board_and_heatmap();
938
        let board_size = board.size().into();
939

            
940
        for i in 0..board_size {
941
            let i = board_size - i;
942
            if i != number {
943
                self.press_numbers[i - 1] = false;
944
            }
945
        }
946
    }
947

            
948
    fn create_account(&mut self) {
949
        if !self.connected_tcp {
950
            self.send("tcp_connect\n");
951
            self.connected_tcp = true;
952
        }
953

            
954
        if self.screen == Screen::Login {
955
            if !self.text_input.trim().is_empty() {
956
                let username = self.text_input.clone();
957
                self.send(&format!(
958
                    "{VERSION_ID} create_account {username} {}\n",
959
                    self.password,
960
                ));
961
                self.username = username;
962
            }
963
            self.text_input.clear();
964
            self.archived_game_reset();
965
            handle_error(self.save_client_ron());
966
        }
967
    }
968

            
969
    fn delete_account(&mut self) {
970
        if self.delete_account {
971
            self.send("delete_account\n");
972
            self.screen = Screen::Login;
973
        } else {
974
            self.delete_account = true;
975
        }
976
    }
977

            
978
    fn display_tournament(&self) -> Column<'_, Message> {
979
        let Some(tournament) = &self.tournament else {
980
            return Column::new();
981
        };
982

            
983
        let tournament_string = "Tournament";
984
        let row_1 = text(tournament_string);
985
        let row_2 = text("-".repeat(tournament_string.len())).font(Font::MONOSPACE);
986
        let column_1 = column![row_1, row_2];
987

            
988
        let players_len = tournament.players.len();
989

            
990
        if players_len == 0 {
991
            return Column::new();
992
        }
993

            
994
        let Some(tree) = &tournament.tree else {
995
            return Column::new();
996
        };
997

            
998
        let mut column_2 = Column::new().spacing(SPACING);
999

            
        for (i, round) in tree.rounds.iter().enumerate() {
            let title = format!("{} {}", t!("Round"), i + 1);
            let title_len = title.len();
            let column_title = column![
                text(title),
                text("-".repeat(title_len)).font(Font::MONOSPACE)
            ];
            let mut add_column = column![column_title];
            let round_length = round.len();
            for (i, status) in round.iter().enumerate() {
                let brace = if round_length == 1 {
                    ""
                } else if i % 2 == 0 {
                    "┐"
                } else {
                    "┘"
                };
                let mut row = match &status {
                    tournament::Status::Lost(player) => {
                        let name = player.to_string();
                        let len = 22 - name.len();
                        row![
                            text(name).font(Font::MONOSPACE),
                            text("─".repeat(len)).style(text::danger),
                            text(brace).style(text::danger),
                        ]
                    }
                    tournament::Status::None | tournament::Status::Waiting => {
                        row![text("─".repeat(22)), text(brace)]
                    }
                    tournament::Status::Playing(player) => {
                        let name = player.to_string();
                        let len = 22 - name.len();
                        row![
                            text(name).font(Font::MONOSPACE),
                            text("─".repeat(len)),
                            text(brace),
                        ]
                    }
                    tournament::Status::Ready(player) => {
                        let name = player.to_string();
                        let len = 22 - name.len();
                        row![
                            text(name).font(Font::MONOSPACE),
                            text("─".repeat(len)).style(text::warning),
                            text(brace).style(text::warning),
                        ]
                    }
                    tournament::Status::Won(player) => {
                        let name = player.to_string();
                        let len = 22 - name.len();
                        row![
                            text(name).font(Font::MONOSPACE),
                            text("─".repeat(len)).style(text::success),
                            text(brace).style(text::success),
                        ]
                    }
                };
                if round_length == 1 {
                    row = row.push(text("🏆").size(TROPHY_SIZE));
                }
                add_column = add_column.push(row);
            }
            column_2 = column_2.push(add_column);
        }
        column![column_1, column_2]
    }
    fn draw(&mut self) {
        let game = self.game.as_ref().expect("you should have a game by now");
        self.send(&format!("request_draw {} {}\n", self.game_id, game.turn));
    }
    fn draw_arrow(&self, y: usize, x: usize) -> Option<&str> {
        if let (Some(from), Some(to)) = (&self.play_from_previous, &self.play_to_previous) {
            if (y, x) == (from.y, from.x) {
                let x_diff = from.x as i128 - to.x as i128;
                let y_diff = from.y as i128 - to.y as i128;
                let mut arrow = " ";
                if y_diff < 0 {
                    arrow = &self.chars.arrow_down;
                } else if y_diff > 0 {
                    arrow = &self.chars.arrow_up;
                } else if x_diff < 0 {
                    arrow = &self.chars.arrow_right;
                } else if x_diff > 0 {
                    arrow = &self.chars.arrow_left;
                }
                Some(arrow)
            } else {
                None
            }
        } else {
            None
        }
    }
    fn estimate_score(&mut self) {
        if !self.estimate_score {
            info!("start running score estimator...");
            let handle = self
                .archived_game_handle
                .as_ref()
                .expect("we should have a game handle now");
            self.estimate_score = true;
            self.send_estimate_score(handle.boards.clone());
        }
    }
    fn game_new(&mut self) {
        self.game_settings = NewGameSettings::default();
        self.screen = Screen::GameNew;
    }
    fn game_submit(&mut self) {
        let Some(role) = self.game_settings.role_selected else {
            error!("No role selected.");
            unreachable!();
        };
        let Some(time_settings) = self.game_settings.time else {
            error!("No time settings selected.");
            unreachable!();
        };
        self.game_settings.timed = time_settings.into();
        // <- new_game (attacker | defender) (rated | unrated) (TIME_MINUTES | _) (ADD_SECONDS_AFTER_EACH_MOVE | _) board_size
        // -> game id rated attacker defender un-timed _ _ board_size challenger challenge_accepted spectators
        self.send(&format!(
            "new_game {role} {} {:?} {}\n",
            self.game_settings.rated, self.game_settings.timed, self.game_settings.board_size
        ));
        self.screen = Screen::Games;
    }
    fn press_letter(&mut self, letter: char) {
        self.clear_letters_except(letter);
        self.press_letters.insert(letter);
    }
    fn press_letter_and_number(&mut self) {
        let mut number = None;
        let mut letter = None;
        for i in 0..13 {
            if self.press_numbers[i] {
                number = Some(i);
                break;
            }
        }
        for (i, l) in BOARD_LETTERS_LOWERCASE.iter().enumerate() {
            if self.press_letters.contains(l) {
                letter = Some(i);
                break;
            }
        }
        let size = if let Some(game) = self.game.as_ref() {
            Some(game.board.size())
        } else {
            self.archived_game_handle
                .as_ref()
                .map(|game| game.game.board_size)
        };
        if let (Some(size), Some(number), Some(letter)) = (size, number, letter) {
            let board_usize: usize = size.into();
            let vertex = Vertex {
                size,
                x: letter,
                y: board_usize - number - 1,
            };
            let possible_moves = self.possible_moves();
            match self.board_move(&vertex, possible_moves.as_ref()) {
                Move::From => self.play_from = Some(vertex),
                Move::To => self.play_to(vertex),
                Move::Revert => self.play_from = None,
                Move::None => {}
            }
            for i in 0..13 {
                self.press_numbers[i] = false;
            }
            self.press_letters.clear();
        }
    }
    fn join_game_press(&mut self, i: usize, shift: bool) {
        let mut server_games: Vec<&ServerGameLight> = self.games_light.0.values().collect();
        server_games.sort_by(|a, b| b.id.cmp(&a.id));
        if let Some(game) = server_games.get(i) {
            match self.join_game(game) {
                JoinGame::Cancel => self.send(&format!("decline_game {} switch\n", game.id)),
                JoinGame::Join => self.join(game.id),
                JoinGame::None => match self.game_state(game.id) {
                    State::Challenger | State::Spectator => {}
                    State::Creator => {
                        if shift {
                            self.send(&format!("decline_game {}\n", game.id));
                        } else {
                            self.send(&format!("join_game {}\n", game.id));
                        }
                    }
                    State::CreatorOnly => self.send(&format!("leave_game {}\n", game.id)),
                },
                JoinGame::Resume => self.resume(game.id),
                JoinGame::Watch => self.watch(game.id),
            }
        }
    }
    fn join_game(&self, game: &ServerGameLight) -> JoinGame {
        if game.challenge_accepted {
            if Some(&self.username) == game.attacker.as_ref()
                || Some(&self.username) == game.defender.as_ref()
            {
                JoinGame::Resume
            } else {
                JoinGame::Watch
            }
        } else if game.attacker.is_some()
            && game.defender.is_some()
            && Some(&self.username) == game.challenger.0.as_ref()
        {
            JoinGame::Cancel
        } else if (game.attacker.is_none() || game.defender.is_none())
            && !(Some(&self.username) == game.attacker.as_ref()
                || Some(&self.username) == game.defender.as_ref())
        {
            JoinGame::Join
        } else {
            JoinGame::None
        }
    }
    fn join(&mut self, id: u128) {
        self.game_id = id;
        self.send(&format!("join_game_pending {id}\n"));
        let game = self.games_light.0.get(&id).expect("the game must exist");
        self.game_settings.role_selected = if game.attacker.is_some() {
            Some(Role::Defender)
        } else {
            Some(Role::Attacker)
        };
    }
    fn resume(&mut self, id: u128) {
        self.game_id = id;
        self.send(&format!("resume_game {id}\n"));
    }
    fn watch(&mut self, id: u128) {
        self.game_id = id;
        self.send(&format!("watch_game {id}\n"));
    }
    fn login(&mut self) {
        if !self.connected_tcp {
            self.send("tcp_connect\n");
            self.connected_tcp = true;
        }
        if self.text_input.trim().is_empty() {
            let username = format!("user-{:x}", rand::random::<u16>());
            self.send(&format!(
                "{VERSION_ID} create_account {username} {}\n",
                self.password
            ));
            self.username = username;
        } else {
            let username = self.text_input.clone();
            self.send(&format!(
                "{VERSION_ID} login {username} {}\n",
                self.password
            ));
            self.username = username;
        }
        self.texts.clear();
        self.text_input.clear();
        self.archived_game_reset();
        handle_error(self.save_client_ron());
    }
    fn play_to(&mut self, to: Vertex) {
        let from = self
            .play_from
            .expect("you have to have a from to get to to");
        let mut turn = Role::Roleless;
        if let Some(game) = &self.game {
            turn = game.turn;
        }
        self.handle_play(None, &from.to_string(), &to.to_string());
        if self.archived_game_handle.is_none() {
            self.send(&format!(
                "game {} play {} {from} {to}\n",
                self.game_id, turn
            ));
            let game = self.game.as_ref().expect("you should have a game by now");
            if game.status == Status::Ongoing {
                match game.turn {
                    Role::Attacker => {
                        if let TimeSettings::Timed(time) = &mut self.time_defender {
                            time.milliseconds_left += time.add_seconds * 1_000;
                        }
                    }
                    Role::Roleless => {}
                    Role::Defender => {
                        if let TimeSettings::Timed(time) = &mut self.time_attacker {
                            time.milliseconds_left += time.add_seconds * 1_000;
                        }
                    }
                }
            }
            self.my_turn = false;
        }
        self.play_from_previous = self.play_from;
        self.play_to_previous = Some(to);
        self.play_from = None;
    }
    fn possible_moves(&self) -> Option<LegalMoves> {
        let mut possible_moves = None;
        if self.my_turn {
            if let Some(game) = self.game.as_ref() {
                possible_moves = Some(game.all_legal_moves());
            }
        } else if let Some(handle) = &self.archived_game_handle {
            let game = Game::from(&handle.boards);
            possible_moves = Some(game.all_legal_moves());
        }
        possible_moves
    }
    fn change_theme(&mut self, theme: Theme) {
        self.theme = theme;
        handle_error(self.save_client_ron());
    }
    fn coordinates(&mut self) {
        self.coordinates = !self.coordinates;
        handle_error(self.save_client_ron());
    }
    // Fixme: get the real status when exploring the game tree.
    #[allow(clippy::too_many_lines)]
    fn display_game(&self) -> Element<'_, Message> {
        let mut attacker_rating = String::new();
        let mut defender_rating = String::new();
        let (game_id, attacker, attacker_time, defender, defender_time, board, play, status, texts) =
            if let Some(game_handle) = &self.archived_game_handle {
                attacker_rating = game_handle.game.attacker_rating.to_string_rounded();
                defender_rating = game_handle.game.defender_rating.to_string_rounded();
                let status = if game_handle.play == game_handle.game.plays.len() - 1 {
                    &game_handle.game.status
                } else {
                    &Status::Ongoing
                };
                (
                    &game_handle.game.id,
                    &game_handle.game.attacker,
                    game_handle
                        .game
                        .plays
                        .time_left(Role::Attacker, game_handle.play),
                    &game_handle.game.defender,
                    game_handle
                        .game
                        .plays
                        .time_left(Role::Defender, game_handle.play),
                    &game_handle.boards.here().board,
                    game_handle.play,
                    status,
                    &game_handle.game.texts,
                )
            } else {
                for user in self.users.values() {
                    if self.attacker == user.name {
                        attacker_rating = user.rating.to_string_rounded();
                    }
                    if self.defender == user.name {
                        defender_rating = user.rating.to_string_rounded();
                    }
                }
                let game = self.game.as_ref().expect("we should be in a game");
                (
                    &self.game_id,
                    &self.attacker,
                    self.time_attacker.time_left(),
                    &self.defender,
                    self.time_defender.time_left(),
                    &game.board,
                    game.previous_boards.0.len() - 1,
                    &self.status,
                    &self.texts_game,
                )
            };
        for user in self.users.values() {
            if self.attacker == user.name {
                attacker_rating = user.rating.to_string_rounded();
            }
            if self.defender == user.name {
                defender_rating = user.rating.to_string_rounded();
            }
        }
        let captured = board.captured();
        let attacker = container(
            column![
                row![
                    text(attacker),
                    text(attacker_rating).center(),
                    text(captured.defender(&self.chars).clone()).font(Font::MONOSPACE),
                ]
                .spacing(SPACING),
                row![
                    text(attacker_time).size(35).center(),
                    text(&self.chars.dagger).size(35).center(),
                ]
                .spacing(SPACING),
            ]
            .spacing(SPACING),
        )
        .padding(PADDING)
        .style(container::bordered_box);
        let defender = container(
            column![
                row![
                    text(defender),
                    text(defender_rating).center(),
                    text(captured.attacker(&self.chars).clone()).font(Font::MONOSPACE),
                ]
                .spacing(SPACING),
                row![
                    text(defender_time).size(35).center(),
                    text(&self.chars.shield).size(35.0).center(),
                ]
                .spacing(SPACING),
            ]
            .spacing(SPACING),
        )
        .padding(PADDING)
        .style(container::bordered_box);
        let mut watching = false;
        let sub_second = self.now_diff % 1_000;
        let seconds = self.now_diff / 1_000;
        let mut user_area = column![text!("#{game_id} {}", &self.username)].spacing(SPACING);
        let is_rated = match self.game_settings.rated {
            Rated::No => t!("no"),
            Rated::Yes => t!("yes"),
        };
        user_area = user_area.push(text!("{}: {play} {}: {is_rated}", t!("move"), t!("rated")));
        match self.screen_size {
            Size::Large | Size::Medium | Size::Small | Size::Tiny => {
                user_area = user_area.push(column![attacker, defender].spacing(SPACING));
            }
            Size::Giant | Size::TinyWide => {
                user_area = user_area.push(row![attacker, defender].spacing(SPACING));
            }
        }
        let mut spectators = Column::new();
        for spectator in &self.spectators {
            if self.username.as_str() == spectator.as_str() {
                watching = true;
            }
            let mut spectator = spectator.clone();
            if let Some(user) = self.users.get(&spectator) {
                let _ok = write!(spectator, " ({})", user.rating.to_string_rounded());
            }
            spectators = spectators.push(text(spectator));
        }
        let resign =
            button(text!("{} (p)", self.strings["Resign"].as_str())).on_press(Message::PlayResign);
        let request_draw = button(text!("{} (q)", self.strings["Request Draw"].as_str()))
            .on_press(Message::PlayDraw);
        if !watching {
            if self.my_turn {
                match self.screen_size {
                    Size::Tiny | Size::Small => {
                        user_area = user_area.push(
                            column![
                                row![resign].spacing(SPACING),
                                row![request_draw].spacing(SPACING),
                            ]
                            .spacing(SPACING),
                        );
                    }
                    Size::TinyWide | Size::Medium | Size::Large | Size::Giant => {
                        user_area = user_area.push(row![resign, request_draw].spacing(SPACING));
                    }
                }
            } else {
                let row = if self.request_draw {
                    column![
                        row![
                            button(text(self.strings["Accept Draw"].as_str()))
                                .on_press(Message::PlayDrawDecision(Draw::Accept)),
                        ]
                        .spacing(SPACING)
                    ]
                } else {
                    Column::new()
                };
                user_area = user_area.push(row.spacing(SPACING));
            }
        }
        let coordinates_muted = row![
            checkbox(self.coordinates.into()).on_toggle(Message::Coordinates),
            text!("{} (n)", t!("Coordinates")),
            checkbox(self.sound_muted).on_toggle(Message::SoundMuted),
            text!("{} (o)", t!("Muted"))
        ]
        .spacing(SPACING);
        user_area = user_area.push(coordinates_muted);
        let leave =
            button(text!("{} (Esc)", self.strings["Leave"].as_str())).on_press(Message::Leave);
        match status {
            Status::AttackerWins => {
                user_area = user_area.push(text(t!("Attacker wins!")));
            }
            Status::Draw => {
                user_area = user_area.push(text(t!("It's a draw.")));
            }
            Status::Ongoing => {}
            Status::DefenderWins => {
                user_area = user_area.push(text(t!("Defender wins!")));
            }
        }
        if let Some(handle) = &self.archived_game_handle {
            let mut heat_map = checkbox(self.heat_map_display).size(32);
            if self.heat_map.is_some() {
                heat_map = heat_map.on_toggle(Message::HeatMap);
            }
            let mut heat_map_button =
                button(text!("{} (p) (q)", self.strings["Heat Map"].as_str()));
            if !self.estimate_score && *status == Status::Ongoing {
                heat_map_button = heat_map_button.on_press(Message::EstimateScore);
            }
            user_area = user_area.push(row![heat_map, heat_map_button].spacing(SPACING));
            let child_number = text(handle.boards.next_child);
            let child_right = button(
                text(&self.chars.double_arrow_right)
                    .center()
                    .font(Font::MONOSPACE),
            )
            .on_press(Message::ReviewGameChildNext);
            user_area = user_area.push(
                row![
                    leave,
                    child_right,
                    container(child_number)
                        .style(container::bordered_box)
                        .padding(PADDING_MEDIUM),
                ]
                .spacing(SPACING),
            );
            let mut left_all = button(text(&self.chars.double_arrow_left_full));
            let mut left = button(text(&self.chars.double_arrow_left));
            if handle.play > 0 {
                left_all = left_all.on_press(Message::ReviewGameBackwardAll);
                left = left.on_press(Message::ReviewGameBackward);
            }
            let mut right = button(text(&self.chars.double_arrow_right));
            let mut right_all = button(text(&self.chars.double_arrow_right_full));
            if handle.boards.has_children() {
                right = right.on_press(Message::ReviewGameForward);
                right_all = right_all.on_press(Message::ReviewGameForwardAll);
            }
            user_area = user_area.push(row![left_all, left, right, right_all].spacing(SPACING));
        } else {
            user_area = user_area.push(leave);
            let spectator = text!(
                "{} ({}) {}: {seconds:01}.{sub_second:03} s",
                &self.chars.people,
                self.spectators.len(),
                t!("lag"),
            );
            if self.spectators.is_empty() {
                user_area = user_area.push(spectator);
            } else {
                user_area = user_area.push(tooltip(
                    spectator,
                    container(spectators)
                        .style(container::bordered_box)
                        .padding(PADDING),
                    tooltip::Position::Bottom,
                ));
            }
        }
        if self.archived_game_handle.is_some() {
            user_area = user_area.push(self.texting(texts, false));
        } else {
            user_area = user_area.push(self.texting(texts, true));
        }
        let user_area = container(user_area)
            .padding(PADDING)
            .style(container::bordered_box);
        let board = container(self.board())
            .style(container::bordered_box)
            .padding(PADDING);
        row![board, user_area].spacing(SPACING).into()
    }
    fn leave(&mut self) {
        match self.screen {
            Screen::AccountSettings
            | Screen::EmailEveryone
            | Screen::GameNew
            | Screen::Users
            | Screen::Tournament => {
                self.screen = Screen::Games;
                self.text_input = String::new();
            }
            Screen::Game => {
                self.screen = Screen::Games;
                self.my_turn = false;
                self.request_draw = false;
                if self.spectators.contains(&self.username) {
                    self.send(&format!("leave_game {}\n", self.game_id));
                }
                self.spectators = Vec::new();
            }
            Screen::Games => {
                self.send("quit\n");
                self.admin = false;
                self.connected_tcp = false;
                self.text_input = self.username.clone();
                self.screen = Screen::Login;
            }
            Screen::GameReview => {
                self.heat_map = None;
                self.heat_map_display = false;
                self.screen = Screen::Login;
            }
            Screen::Login => {}
        }
    }
    fn my_games_only(&mut self) {
        let selected = !self.my_games_only;
        if selected {
            self.archived_games_filtered = Some(
                self.archived_games
                    .iter()
                    .filter(|game| game.attacker == self.username || game.defender == self.username)
                    .cloned()
                    .collect(),
            );
        } else {
            self.archived_games_filtered = None;
        }
        self.my_games_only = selected;
        handle_error(self.save_client_ron());
    }
    #[allow(clippy::too_many_lines)]
    #[allow(clippy::collapsible_match)]
    pub(crate) fn subscriptions(&self) -> Subscription<Message> {
        let subscription_1 = if let Some(game) = &self.game {
            if let TimeUnix::Time(_) = game.time {
                iced::time::every(iced::time::Duration::from_millis(100))
                    .map(|_instant| Message::Tick)
            } else {
                Subscription::none()
            }
        } else {
            Subscription::none()
        };
        let subscription_2 = Subscription::run(pass_messages);
        let subscription_3 = Subscription::run(estimate_score);
        let subscription_4 = event::listen_with(|event, _status, _id| match event {
            Event::Window(iced::window::Event::Resized(size)) => {
                Some(Message::WindowResized((size.width, size.height)))
            }
            Event::Keyboard(event) => match event {
                keyboard::Event::KeyPressed {
                    key: Key::Character(ch),
                    modifiers,
                    ..
                } => {
                    let shift = modifiers.shift();
                    if modifiers.control() || modifiers.command() {
                        if *ch == *Value::new("a").to_smolstr() {
                            Some(Message::PressA(shift))
                        } else if *ch == *Value::new("b").to_smolstr() {
                            Some(Message::PressB(shift))
                        } else if *ch == *Value::new("c").to_smolstr() {
                            Some(Message::PressC(shift))
                        } else if *ch == *Value::new("d").to_smolstr() {
                            Some(Message::PressD(shift))
                        } else if *ch == *Value::new("e").to_smolstr() {
                            Some(Message::PressE(shift))
                        } else if *ch == *Value::new("f").to_smolstr() {
                            Some(Message::PressF(shift))
                        } else if *ch == *Value::new("g").to_smolstr() {
                            Some(Message::PressG(shift))
                        } else if *ch == *Value::new("h").to_smolstr() {
                            Some(Message::PressH(shift))
                        } else if *ch == *Value::new("i").to_smolstr() {
                            Some(Message::PressI(shift))
                        } else if *ch == *Value::new("j").to_smolstr() {
                            Some(Message::PressJ(shift))
                        } else if *ch == *Value::new("k").to_smolstr() {
                            Some(Message::PressK(shift))
                        } else if *ch == *Value::new("l").to_smolstr() {
                            Some(Message::PressL(shift))
                        } else if *ch == *Value::new("m").to_smolstr() {
                            Some(Message::PressM(shift))
                        } else if *ch == *Value::new("n").to_smolstr() {
                            Some(Message::PressN(shift))
                        } else if *ch == *Value::new("o").to_smolstr() {
                            Some(Message::PressO(shift))
                        } else if *ch == *Value::new("p").to_smolstr() {
                            Some(Message::PressP(shift))
                        } else if *ch == *Value::new("q").to_smolstr() {
                            Some(Message::PressQ(shift))
                        } else if *ch == *Value::new("r").to_smolstr() {
                            Some(Message::PressR(shift))
                        } else if *ch == *Value::new("s").to_smolstr() {
                            Some(Message::PressS(shift))
                        } else if *ch == *Value::new("t").to_smolstr() {
                            Some(Message::PressT(shift))
                        } else if *ch == *Value::new("u").to_smolstr() {
                            Some(Message::PressU(shift))
                        } else if *ch == *Value::new("v").to_smolstr() {
                            Some(Message::PressV(shift))
                        } else if *ch == *Value::new("w").to_smolstr() {
                            Some(Message::PressW(shift))
                        } else if *ch == *Value::new("x").to_smolstr() {
                            Some(Message::PressX(shift))
                        } else if *ch == *Value::new("y").to_smolstr() {
                            Some(Message::PressY(shift))
                        } else if *ch == *Value::new("z").to_smolstr() {
                            Some(Message::PressZ(shift))
                        } else if *ch == *Value::new("1").to_smolstr() {
                            Some(Message::Press1)
                        } else if *ch == *Value::new("2").to_smolstr() {
                            Some(Message::Press2)
                        } else if *ch == *Value::new("3").to_smolstr() {
                            Some(Message::Press3)
                        } else if *ch == *Value::new("4").to_smolstr() {
                            Some(Message::Press4)
                        } else if *ch == *Value::new("5").to_smolstr() {
                            Some(Message::Press5)
                        } else if *ch == *Value::new("6").to_smolstr() {
                            Some(Message::Press6)
                        } else if *ch == *Value::new("7").to_smolstr() {
                            Some(Message::Press7)
                        } else if *ch == *Value::new("8").to_smolstr() {
                            Some(Message::Press8)
                        } else if *ch == *Value::new("9").to_smolstr() {
                            Some(Message::Press9)
                        } else if *ch == *Value::new("0").to_smolstr() {
                            Some(Message::Press0)
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                }
                keyboard::Event::KeyPressed {
                    key: Key::Named(named),
                    modifiers,
                    ..
                } => {
                    if named == Named::Enter {
                        Some(Message::PressEnter)
                    } else if modifiers.shift() && named == Named::Tab {
                        Some(Message::FocusPrevious)
                    } else if named == Named::Tab {
                        Some(Message::FocusNext)
                    } else if named == Named::ArrowUp {
                        Some(Message::ReviewGameBackwardAll)
                    } else if named == Named::ArrowLeft {
                        Some(Message::ReviewGameBackward)
                    } else if named == Named::ArrowRight && modifiers.shift() {
                        Some(Message::ReviewGameChildNext)
                    } else if named == Named::ArrowRight {
                        Some(Message::ReviewGameForward)
                    } else if named == Named::ArrowDown {
                        Some(Message::ReviewGameForwardAll)
                    } else if named == Named::Escape {
                        Some(Message::Leave)
                    } else {
                        None
                    }
                }
                _ => None,
            },
            _ => None,
        });
        Subscription::batch(vec![
            subscription_1,
            subscription_2,
            subscription_3,
            subscription_4,
        ])
    }
    fn texting(
        &'a self,
        messages: &'a VecDeque<String>,
        enable_texting: bool,
    ) -> Container<'a, Message> {
        let text_input = if enable_texting {
            iced::widget::text_input(&format!("{}…", t!("message")), &self.text_input)
                .on_input(Message::TextChanged)
                .on_paste(Message::TextChanged)
                .on_submit(Message::TextSend)
        } else {
            iced::widget::text_input(&format!("{}…", t!("message")), "")
        };
        let mut text_box = column![text_input].spacing(SPACING);
        let mut texting = Column::new();
        for message in messages {
            texting = texting.push(text(message));
        }
        text_box = text_box.push(scrollable(texting));
        container(text_box)
            .padding(PADDING)
            .style(container::bordered_box)
    }
    pub fn theme(&self) -> iced::Theme {
        match self.theme {
            Theme::Dark => iced::Theme::SolarizedDark,
            Theme::Light => iced::Theme::SolarizedLight,
            Theme::Tol => iced::Theme::custom("Tol", TOL),
        }
    }
    #[allow(clippy::too_many_lines)]
    pub fn update(&mut self, message: Message) -> Task<Message> {
        self.error = None;
        match message {
            Message::AccountSettings => self.screen = Screen::AccountSettings,
            Message::ArchivedGames(mut archived_games) => {
                archived_games.reverse();
                self.archived_games = archived_games;
                self.archived_games_filtered = None;
                handle_error(self.save_client_postcard());
            }
            Message::ArchivedGamesGet => self.send("archived_games\n"),
            Message::ArchivedGameSelected(game) => self.archived_game_selected = Some(game),
            Message::CancelGame(id) => self.send(&format!("leave_game {id}\n")),
            Message::ChangeTheme(theme) => self.change_theme(theme),
            Message::BoardSizeSelected(size) => self.game_settings.board_size = size,
            Message::ConnectedTo(address) => self.connected_to = address,
            Message::Coordinates(_coordinates) => self.coordinates(),
            Message::DeleteAccount => self.delete_account(),
            Message::EmailEveryone => {
                self.screen = Screen::EmailEveryone;
                self.send("emails_bcc\n");
            }
            Message::EmailReset => self.reset_email(),
            Message::EstimateScore => self.estimate_score(),
            Message::EstimateScoreConnected(tx) => self.estimate_score_tx = Some(tx),
            Message::EstimateScoreDisplay((node, generate_move)) => {
                info!("finish running score estimator...");
                if let Some(handle) = self.archived_game_handle.as_ref()
                    && handle.boards.here() == node
                {
                    info!("{generate_move}");
                    debug!("{}", generate_move.heat_map);
                    self.heat_map = Some(generate_move.heat_map);
                }
                self.estimate_score = false;
            }
            Message::Exit => return iced::exit(),
            Message::FocusNext => return focus_next(),
            Message::FocusPrevious => return focus_previous(),
            Message::GameCancel(id) => self.send(&format!("decline_game {id} switch\n")),
            Message::GameAccept(id) => self.send(&format!("join_game {id}\n")),
            Message::GameDecline(id) => self.send(&format!("decline_game {id}\n")),
            Message::GameJoin(id) => self.join(id),
            Message::GameWatch(id) => self.watch(id),
            Message::HeatMap(_display) => self.heat_map_display = !self.heat_map_display,
            Message::Leave => {
                if self.screen == Screen::Login {
                    return iced::exit();
                }
                self.leave();
            }
            Message::LeaveSoft => self.leave(),
            Message::LocaleSelected(locale) => {
                rust_i18n::set_locale(&locale.txt());
                let string_keys: Vec<_> = self.strings.keys().cloned().collect();
                for string in string_keys {
                    self.strings.insert(string.clone(), t!(string).to_string());
                }
                self.locale_selected = locale;
                handle_error(self.save_client_ron());
            }
            Message::MyGamesOnly(_selected) => {
                self.my_games_only();
            }
            Message::OpenUrl(string) => open_url(&string),
            Message::GameNew => self.game_new(),
            Message::GameResume(id) => self.resume(id),
            Message::GameSubmit => self.game_submit(),
            Message::PasswordChanged(password) => {
                let (password, ends_with_whitespace) = utils::split_whitespace_password(&password);
                self.password_ends_with_whitespace = ends_with_whitespace;
                if password.len() <= 32 {
                    self.password = password;
                }
            }
            Message::PasswordSave(_save) => self.toggle_save_password(),
            Message::PasswordShow(_show) => self.toggle_show_password(),
            Message::PlayDraw => self.draw(),
            Message::PlayDrawDecision(draw) => {
                self.send(&format!("draw {} {draw}\n", self.game_id));
            }
            Message::PlayMoveFrom(vertex) => self.play_from = Some(vertex),
            Message::PlayMoveTo(to) => self.play_to(to),
            Message::PlayMoveRevert => self.play_from = None,
            Message::PlayResign => self.resign(),
            Message::PressEnter => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::Game
                | Screen::Games
                | Screen::GameReview
                | Screen::Tournament
                | Screen::Users => {}
                Screen::GameNew => self.game_submit(),
                Screen::Login => self.login(),
            },
            Message::PressA(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('a');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(0, shift),
                Screen::Login => self.change_theme(Theme::Tol),
            },
            Message::PressB(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('b');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(1, shift),
            },
            Message::PressC(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('c');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(2, shift),
            },
            Message::PressD(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('d');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(3, shift),
            },
            Message::PressE(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('e');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(4, shift),
            },
            Message::PressF(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('f');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(5, shift),
            },
            Message::PressG(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('g');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(6, shift),
            },
            Message::PressH(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('h');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(7, shift),
            },
            Message::PressI(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('i');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(8, shift),
            },
            Message::PressJ(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('j');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(9, shift),
            },
            Message::PressK(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('k');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(10, shift),
            },
            Message::PressL(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('l');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(11, shift),
            },
            Message::PressM(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => {
                    self.press_letter('m');
                    self.press_letter_and_number();
                }
                Screen::Games => self.join_game_press(12, shift),
            },
            Message::PressN(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => self.coordinates(),
                Screen::Games => self.join_game_press(13, shift),
            },
            Message::PressO(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game | Screen::GameReview => self.sound_muted(),
                Screen::Games => self.join_game_press(14, shift),
            },
            Message::PressP(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game => self.resign(),
                Screen::Games => self.join_game_press(1, shift),
                Screen::GameReview => self.estimate_score(),
            },
            Message::PressQ(shift) => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::GameNew
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Game => self.draw(),
                Screen::Games => self.join_game_press(16, shift),
                Screen::GameReview => self.heat_map_display = !self.heat_map_display,
            },
            Message::PressR(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(17, shift),
            },
            Message::PressS(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(18, shift),
            },
            Message::PressT(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(19, shift),
            },
            Message::PressU(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(20, shift),
            },
            Message::PressV(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(21, shift),
            },
            Message::PressW(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(22, shift),
            },
            Message::PressX(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(23, shift),
            },
            Message::PressY(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(24, shift),
            },
            Message::PressZ(shift) => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Game => self.draw(),
                Screen::GameNew
                | Screen::GameReview
                | Screen::Login
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.join_game_press(25, shift),
            },
            Message::Press1 => match self.screen {
                Screen::AccountSettings | Screen::Login => self.reset_email(),
                Screen::EmailEveryone | Screen::Tournament | Screen::Users => {}
                Screen::Games => self.send("archived_games\n"),
                Screen::GameNew => self.game_settings.role_selected = Some(Role::Attacker),
                Screen::Game | Screen::GameReview => {
                    if !(self.press_numbers[0]
                        || self.press_numbers[10]
                        || self.press_numbers[11]
                        || self.press_numbers[12])
                    {
                        self.clear_numbers_except(0);
                        self.press_numbers[0] = true;
                    } else if self.press_numbers[0] {
                        self.clear_numbers_except(10);
                        self.press_numbers[10] = true;
                    } else {
                        self.clear_numbers_except(11);
                        self.press_numbers[10] = false;
                    }
                    self.press_letter_and_number();
                }
            },
            Message::Press2 => match self.screen {
                Screen::AccountSettings => {
                    self.send(&format!("change_password {}\n", self.password));
                }
                Screen::EmailEveryone | Screen::Tournament | Screen::Users => {}
                Screen::Games => self.my_games_only(),
                Screen::GameNew => self.game_settings.role_selected = Some(Role::Defender),
                Screen::Game | Screen::GameReview => {
                    let (board, _) = self.board_and_heatmap();
                    match board.size() {
                        BoardSize::_11 => {
                            self.clear_numbers_except(2);
                            self.press_numbers[1] = !self.press_numbers[1];
                            self.press_letter_and_number();
                        }
                        BoardSize::_13 => {
                            if !self.press_numbers[0]
                                && !self.press_numbers[1]
                                && !self.press_numbers[11]
                            {
                                self.clear_numbers_except(2);
                                self.press_numbers[1] = true;
                            } else if self.press_numbers[1] {
                                self.press_numbers[1] = false;
                            } else if self.press_numbers[11] {
                                self.press_numbers[11] = false;
                            } else {
                                self.press_numbers[0] = false;
                                self.press_numbers[11] = true;
                            }
                        }
                    }
                    self.press_letter_and_number();
                }
                Screen::Login => self.toggle_save_password(),
            },
            Message::Press3 => match self.screen {
                Screen::AccountSettings => self.toggle_show_password(),
                Screen::EmailEveryone | Screen::Tournament | Screen::Users => {}
                Screen::Games => self.game_new(),
                Screen::GameNew => self.game_settings.board_size = BoardSize::_11,
                Screen::Login => self.my_games_only(),
                Screen::Game | Screen::GameReview => {
                    let (board, _) = self.board_and_heatmap();
                    match board.size() {
                        BoardSize::_11 => {
                            self.clear_numbers_except(3);
                            self.press_numbers[2] = !self.press_numbers[2];
                            self.press_letter_and_number();
                        }
                        BoardSize::_13 => {
                            if !self.press_numbers[0]
                                && !self.press_numbers[2]
                                && !self.press_numbers[12]
                            {
                                self.clear_numbers_except(3);
                                self.press_numbers[2] = true;
                            } else if self.press_numbers[2] {
                                self.press_numbers[2] = false;
                            } else if self.press_numbers[12] {
                                self.press_numbers[12] = false;
                            } else {
                                self.press_numbers[0] = false;
                                self.press_numbers[12] = true;
                            }
                        }
                    }
                    self.press_letter_and_number();
                }
            },
            Message::Press4 => match self.screen {
                Screen::AccountSettings => self.delete_account(),
                Screen::EmailEveryone | Screen::Tournament | Screen::Users => {}
                Screen::Games => self.screen = Screen::Users,
                Screen::GameNew => self.game_settings.board_size = BoardSize::_13,
                Screen::Login => self.create_account(),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(4);
                    self.press_numbers[3] = !self.press_numbers[3];
                    self.press_letter_and_number();
                }
            },
            Message::Press5 => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::Tournament
                | Screen::Users => {}
                Screen::Games => self.screen = Screen::AccountSettings,
                Screen::GameNew => self.game_settings.time = Some(TimeEnum::Rapid),
                Screen::Login => self.reset_password(),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(5);
                    self.press_numbers[4] = !self.press_numbers[4];
                    self.press_letter_and_number();
                }
            },
            Message::Press6 => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::Tournament
                | Screen::Users => {}
                Screen::GameNew => self.game_settings.time = Some(TimeEnum::Classical),
                Screen::Games => open_url("https://hnefatafl.org/rules.html"),
                Screen::Login => self.review_game(),
                Screen::Game | Screen::GameReview => {
                    if self.screen == Screen::Game || self.screen == Screen::GameReview {
                        self.clear_numbers_except(6);
                        self.press_numbers[5] = !self.press_numbers[5];
                        self.press_letter_and_number();
                    }
                }
            },
            Message::Press7 => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Tournament => {}
                Screen::Games | Screen::Users => self.users_sort_by = SortBy::Rating,
                Screen::GameNew => self.game_settings.time = Some(TimeEnum::Long),
                Screen::Login => self.change_theme(Theme::Dark),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(7);
                    self.press_numbers[6] = !self.press_numbers[6];
                    self.press_letter_and_number();
                }
            },
            Message::Press8 => match self.screen {
                Screen::AccountSettings | Screen::EmailEveryone | Screen::Tournament => {}
                Screen::Games | Screen::Users => self.users_sort_by = SortBy::Name,
                Screen::GameNew => self.game_settings.time = Some(TimeEnum::VeryLong),
                Screen::Login => self.change_theme(Theme::Light),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(8);
                    self.press_numbers[7] = !self.press_numbers[7];
                    self.press_letter_and_number();
                }
            },
            Message::Press9 => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::Games
                | Screen::Tournament
                | Screen::Users => {}
                Screen::GameNew => self.game_settings.time = Some(TimeEnum::Infinity),
                Screen::Login => open_url("https://discord.gg/h56CAHEBXd"),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(9);
                    self.press_numbers[8] = !self.press_numbers[8];
                    self.press_letter_and_number();
                }
            },
            Message::Press0 => match self.screen {
                Screen::AccountSettings
                | Screen::EmailEveryone
                | Screen::Games
                | Screen::Tournament
                | Screen::Users => {}
                Screen::GameNew => self.game_settings.rated = !self.game_settings.rated,
                Screen::Login => open_url("https://hnefatafl.org"),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(10);
                    self.press_numbers[9] = !self.press_numbers[9];
                    self.press_letter_and_number();
                }
            },
            Message::ServerShutdown => {
                self.error_persistent
                    .push(t!("The server was shut down.").to_string());
            }
            Message::SoundMuted(_muted) => self.sound_muted(),
            Message::StreamConnected(tx) => self.tx = Some(tx),
            Message::TcpDisconnect => self.connected_tcp = false,
            Message::Tournament => self.screen = Screen::Tournament,
            Message::TournamentJoin => self.send("join_tournament\n"),
            Message::TournamentLeave => self.send("leave_tournament\n"),
            Message::TournamentStart => self.send("tournament_start\n"),
            Message::RatedSelected(rated) => self.game_settings.rated = rated.into(),
            Message::ResetPassword => self.reset_password(),
            Message::ReviewGame => self.review_game(),
            Message::ReviewGameBackward => {
                if let Some(handle) = &mut self.archived_game_handle {
                    handle.play = handle.play.saturating_sub(1);
                    handle.boards.backward();
                    self.reset_markers();
                }
            }
            Message::ReviewGameBackwardAll => {
                if let Some(handle) = &mut self.archived_game_handle {
                    handle.play = 0;
                    handle.boards.backward_all();
                    self.reset_markers();
                }
            }
            Message::ReviewGameChildNext => {
                if let Some(handle) = &mut self.archived_game_handle {
                    handle.boards.next_child();
                    self.reset_markers();
                }
            }
            Message::ReviewGameForward => {
                if let Some(handle) = &mut self.archived_game_handle
                    && handle.boards.has_children()
                {
                    handle.play += 1;
                    handle.boards.forward();
                    self.reset_markers();
                }
            }
            Message::ReviewGameForwardAll => {
                if let Some(handle) = &mut self.archived_game_handle {
                    let count = handle.boards.forward_all();
                    handle.play += count;
                    self.reset_markers();
                }
            }
            Message::RoleSelected(role) => self.game_settings.role_selected = Some(role),
            Message::TextChanged(string) => {
                if self.screen == Screen::Login {
                    let string: Vec<_> = string.split_whitespace().collect();
                    if let Some(string) = string.first() {
                        if string.len() <= 16 {
                            self.text_input = string.to_ascii_lowercase();
                            self.username = self.text_input.clone();
                        }
                    } else {
                        self.text_input = String::new();
                    }
                } else {
                    self.text_input = string;
                }
            }
            Message::TextEdit(action) => {
                self.content.perform(action);
            }
            Message::TextReceived(string) => {
                let mut text = string.split_ascii_whitespace();
                match text.next() {
                    Some("=") => {
                        let text_next = text.next();
                        match text_next {
                            Some(
                                "archived_games"
                                | "challenge_requested"
                                | "change_password"
                                | "decline_game"
                                | "email_reset"
                                | "game"
                                | "request_draw",
                            ) => {}
                            Some("admin") => self.admin = true,
                            Some("display_games") => {
                                self.games_light.0.clear();
                                let games: Vec<&str> = text.collect();
                                for chunks in games.chunks_exact(12) {
                                    let game = ServerGameLight::try_from(chunks)
                                        .expect("the value should be a valid ServerGameLight");
                                    self.games_light.0.insert(game.id, game);
                                }
                                if let Some(game) = self.games_light.0.get(&self.game_id) {
                                    self.spectators = game.spectators.keys().cloned().collect();
                                    self.spectators.sort();
                                }
                            }
                            Some("display_users") => {
                                let users: Vec<&str> = text.collect();
                                self.users.clear();
                                for user_wins_losses_rating in users.chunks_exact(6) {
                                    let rating = user_wins_losses_rating[4];
                                    let (mut rating, mut deviation) =
                                        rating.split_once("±").unwrap_or_else(|| {
                                            error!("The ratings has this form: {rating}");
                                            unreachable!();
                                        });
                                    rating = rating.trim();
                                    deviation = deviation.trim();
                                    let (Ok(rating), Ok(deviation)) =
                                        (rating.parse::<f64>(), deviation.parse::<f64>())
                                    else {
                                        error!(
                                            "The ratings has this form: ({rating}, {deviation})"
                                        );
                                        unreachable!();
                                    };
                                    let logged_in = if "logged_in" == user_wins_losses_rating[5] {
                                        LoggedIn::Yes
                                    } else {
                                        LoggedIn::No
                                    };
                                    self.users.insert(
                                        user_wins_losses_rating[0].to_string(),
                                        User {
                                            name: user_wins_losses_rating[0].to_string(),
                                            wins: user_wins_losses_rating[1].to_string(),
                                            losses: user_wins_losses_rating[2].to_string(),
                                            draws: user_wins_losses_rating[3].to_string(),
                                            rating: Rating {
                                                rating,
                                                rd: deviation / CONFIDENCE_INTERVAL_95,
                                            },
                                            logged_in,
                                        },
                                    );
                                }
                            }
                            Some("draw") => {
                                self.request_draw = false;
                                if let Some("accept") = text.next() {
                                    self.my_turn = false;
                                    self.status = Status::Draw;
                                    if let Some(game) = &mut self.game {
                                        game.turn = Role::Roleless;
                                    }
                                }
                            }
                            Some("email") => {
                                if let (Some(address), Some(verified)) = (text.next(), text.next())
                                {
                                    self.email = Some(Email {
                                        username: String::new(),
                                        address: address.to_string(),
                                        code: None,
                                        verified: handle_error(verified.parse()),
                                    });
                                }
                            }
                            Some("emails_bcc") => {
                                self.emails_bcc = text.map(ToString::to_string).collect();
                            }
                            Some("email_code") => {
                                if let Some(email) = &mut self.email {
                                    email.verified = true;
                                }
                                self.error_email = None;
                            }
                            Some("game_over") => {
                                self.my_turn = false;
                                if let Some(game) = &mut self.game {
                                    game.turn = Role::Roleless;
                                }
                                text.next();
                                match text.next() {
                                    Some("attacker_wins") => self.status = Status::AttackerWins,
                                    Some("defender_wins") => self.status = Status::DefenderWins,
                                    _ => error!("(1) unexpected text: {}", string.trim()),
                                }
                                if !self.sound_muted {
                                    thread::spawn(move || {
                                        let mut stream =
                                            rodio::OutputStreamBuilder::open_default_stream()?;
                                        let cursor = Cursor::new(SOUND_GAME_OVER);
                                        let sound = rodio::play(stream.mixer(), cursor)?;
                                        sound.set_volume(1.0);
                                        sound.sleep_until_end();
                                        stream.log_on_drop(false);
                                        Ok::<(), anyhow::Error>(())
                                    });
                                }
                            }
                            // = join_game david abby rated fischer 900_000 10
                            Some("join_game" | "resume_game" | "watch_game") => {
                                self.screen = Screen::Game;
                                self.status = Status::Ongoing;
                                self.captures = HashSet::new();
                                self.play_from = None;
                                self.play_from_previous = None;
                                self.play_to_previous = None;
                                self.texts_game = VecDeque::new();
                                self.archived_game_handle = None;
                                let attacker =
                                    text.next().expect("the attacker should be supplied");
                                let defender =
                                    text.next().expect("the defender should be supplied");
                                self.attacker = attacker.to_string();
                                self.defender = defender.to_string();
                                let rated = text
                                    .next()
                                    .expect("there should be rated or unrated supplied");
                                let rated = Rated::from_str(rated).expect("rated should be valid");
                                self.game_settings.rated = rated;
                                let timed = text
                                    .next()
                                    .expect("there should be a time setting supplied");
                                let minutes =
                                    text.next().expect("there should be a minutes supplied");
                                let add_seconds =
                                    text.next().expect("there should be a add_seconds supplied");
                                let timed = TimeSettings::try_from(vec![
                                    "time_settings",
                                    timed,
                                    minutes,
                                    add_seconds,
                                ])
                                .expect("there should be a valid time settings");
                                let board_size =
                                    text.next().expect("there should be a valid board size");
                                let board_size = BoardSize::from_str(board_size)
                                    .expect("there should be a valid board size");
                                let board = Board::new(board_size);
                                let mut game = Game {
                                    attacker_time: timed.clone(),
                                    defender_time: timed.clone(),
                                    plays: Plays::new(&timed),
                                    board,
                                    ..Game::default()
                                };
                                self.time_attacker = timed.clone();
                                self.time_defender = timed;
                                if let Some(game_serialized) = text.next() {
                                    let game_deserialized = ron::from_str(game_serialized)
                                        .expect("we should be able to deserialize the game");
                                    game = game_deserialized;
                                    self.time_attacker = game.attacker_time.clone();
                                    self.time_defender = game.defender_time.clone();
                                    match game.turn {
                                        Role::Attacker => {
                                            if let (
                                                TimeSettings::Timed(time),
                                                TimeUnix::Time(time_ago),
                                            ) = (&mut self.time_attacker, &game.time)
                                            {
                                                let now = Local::now().to_utc().timestamp_millis();
                                                time.milliseconds_left -= now - time_ago;
                                                if time.milliseconds_left < 0 {
                                                    time.milliseconds_left = 0;
                                                }
                                            }
                                        }
                                        Role::Roleless => {}
                                        Role::Defender => {
                                            if let (
                                                TimeSettings::Timed(time),
                                                TimeUnix::Time(time_ago),
                                            ) = (&mut self.time_defender, &game.time)
                                            {
                                                let now = Local::now().to_utc().timestamp_millis();
                                                time.milliseconds_left -= now - time_ago;
                                                if time.milliseconds_left < 0 {
                                                    time.milliseconds_left = 0;
                                                }
                                            }
                                        }
                                    }
                                }
                                let texts: Vec<&str> = text.collect();
                                let texts = texts.join(" ");
                                if !texts.is_empty() {
                                    let texts = ron::from_str(&texts)
                                        .expect("we should be able to deserialize the text");
                                    self.texts_game = texts;
                                }
                                if (self.username == attacker && game.turn == Role::Attacker)
                                    || (self.username == defender && game.turn == Role::Defender)
                                {
                                    self.my_turn = true;
                                }
                                self.game = Some(game);
                            }
                            Some("join_game_pending") => {
                                let id = text.next().expect("there should be an id supplied");
                                let id = id.parse().expect("id should be a valid u128");
                                self.game_id = id;
                                self.challenger = true;
                            }
                            Some("leave_game") => self.game_id = 0,
                            Some("login") => self.screen = Screen::Games,
                            Some("message") => {
                                let message =
                                    text.collect::<Vec<&str>>().join(" ").replace("\\n", "\n");
                                self.message = message;
                            }
                            Some("new_game") => {
                                // = new_game game 15 none david rated fischer 900_000 10
                                if Some("game") == text.next() {
                                    let game_id = text.next().expect("the game id should be next");
                                    let game_id =
                                        game_id.parse().expect("the game_id should be a usize");
                                    self.game_id = game_id;
                                    self.challenger = false;
                                }
                            }
                            Some("ping") => {
                                let after = Utc::now().timestamp_millis();
                                self.now_diff = after - self.now;
                            }
                            Some("text") => self.texts.push_front(text_collect(text)),
                            Some("text_game") => self.texts_game.push_front(text_collect(text)),
                            Some("tournament_status") => {
                                if let Some(tournament) = text.next() {
                                    let tournament: Option<Tournament> = ron::from_str(tournament)
                                        .expect("This is a valid tournament.");
                                    self.tournament = tournament;
                                }
                            }
                            _ => error!("(2) unexpected text: {}", string.trim()),
                        }
                    }
                    Some("?") => {
                        let text_next = text.next();
                        match text_next {
                            Some("create_account") => {
                                self.error = Some(t!("Account already exists.").to_string());
                            }
                            // Fixme: translate.
                            Some("email") => {
                                let text: Vec<_> = text.collect();
                                let text = text.join(" ");
                                self.error_email = Some(text);
                            }
                            Some("email_code") => {
                                self.error_email = Some(t!("invalid email code").to_string());
                            }
                            Some("login") => {
                                let text_next = text.next();
                                match text_next {
                                    Some("multiple_possible_errors") => {
                                        self.error = Some(t!(
                                            "Login password is wrong (try lowercase), account doesn't exist, or you're already logged in."
                                        ).to_string());
                                    }
                                    Some("reset_password") => {
                                        self.error = Some(t!(
                                            "Sent a password reset email if a verified email exists for this account and the last password reset happened more than a day ago.",
                                        ).to_string());
                                    }
                                    Some("wrong_version") => {
                                        self.error = Some(t!(
                                            "Wrong version, update your hnefatafl-copenhagen package.",
                                        ).to_string());
                                    }
                                    _ => {
                                        let text: Vec<_> = text.collect();
                                        let text = text.join(" ");
                                        self.error = Some(text);
                                    }
                                }
                            }
                            _ => error!("(3) unexpected text: {}", string.trim()),
                        }
                    }
                    Some("game") => {
                        // Plays the move then sends the result back.
                        let id = text.next().expect("there should be a game id");
                        let id = id
                            .parse::<Id>()
                            .expect("the game_id should be a valid usize");
                        self.game_id = id;
                        // game 0 generate_move attacker
                        let text_word = text.next();
                        if text_word == Some("generate_move") {
                            self.request_draw = false;
                            self.my_turn = true;
                        // game 0 play attacker a3 a4
                        } else if text_word == Some("play") {
                            let role = text.next().expect("this should be a role string");
                            let role = Role::from_str(role).expect("this should be a role");
                            let from = text.next().expect("this should be from");
                            if from == "resigns" {
                                return Task::none();
                            }
                            let to = text.next().expect("this should be to");
                            if let (Ok(from), Ok(to)) =
                                (Vertex::from_str(from), Vertex::from_str(to))
                            {
                                self.play_from_previous = Some(from);
                                self.play_to_previous = Some(to);
                            }
                            self.handle_play(Some(&role.to_string()), from, to);
                            let game = self.game.as_ref().expect("you should have a game by now");
                            if game.status == Status::Ongoing {
                                match game.turn {
                                    Role::Attacker => {
                                        if let TimeSettings::Timed(time) = &mut self.time_defender {
                                            time.milliseconds_left += time.add_seconds * 1_000;
                                        }
                                    }
                                    Role::Roleless => {}
                                    Role::Defender => {
                                        if let TimeSettings::Timed(time) = &mut self.time_attacker {
                                            time.milliseconds_left += time.add_seconds * 1_000;
                                        }
                                    }
                                }
                            }
                        }
                    }
                    Some("request_draw") => {
                        let id = text.next().expect("there should be a game id");
                        let id = id
                            .parse::<Id>()
                            .expect("the game_id should be a valid usize");
                        if id == self.game_id {
                            self.request_draw = true;
                        }
                    }
                    _ => error!("(4) unexpected text: {}", string.trim()),
                }
            }
            Message::TextSend => {
                match self.screen {
                    Screen::AccountSettings => {
                        self.send(&format!("change_password {}\n", self.password));
                    }
                    Screen::EmailEveryone => {
                        // subject == self.text_input
                        let email = self.content.text().replace('\n', "\\n");
                        self.send(&format!("email_everyone {} {email}\n", self.text_input));
                    }
                    Screen::Game => {
                        if !self.text_input.trim().is_empty() {
                            self.text_input.push('\n');
                            self.send(&format!("text_game {} {}", self.game_id, self.text_input));
                        }
                    }
                    Screen::Games => {
                        if !self.text_input.trim().is_empty() {
                            self.text_input.push('\n');
                            self.send(&format!("text {}", self.text_input));
                        }
                    }
                    Screen::Tournament => {
                        if !self.text_input.trim().is_empty() {
                            self.text_input.push('\n');
                            self.send(&format!("tournament_date {}", self.text_input));
                        }
                    }
                    Screen::GameNew | Screen::GameReview | Screen::Login | Screen::Users => {}
                }
                self.text_input.clear();
            }
            Message::TextSendEmail => {
                self.error_email = None;
                self.send(&format!("email {}\n", self.text_input));
                self.text_input.clear();
            }
            Message::TextSendEmailCode => {
                self.error_email = None;
                self.send(&format!("email_code {}\n", self.text_input));
            }
            Message::TextSendCreateAccount => self.create_account(),
            Message::TextSendLogin => self.login(),
            Message::Tick => {
                self.counter = self.counter.wrapping_add(1);
                if self.counter.is_multiple_of(25) {
                    self.now = Utc::now().timestamp_millis();
                    self.send("ping\n");
                }
                if let Some(game) = &mut self.game {
                    match game.turn {
                        Role::Attacker => {
                            if let TimeSettings::Timed(time) = &mut self.time_attacker {
                                time.milliseconds_left -= 100;
                                if time.milliseconds_left < 0 {
                                    time.milliseconds_left = 0;
                                }
                            }
                        }
                        Role::Roleless => {}
                        Role::Defender => {
                            if let TimeSettings::Timed(time) = &mut self.time_defender {
                                time.milliseconds_left -= 100;
                                if time.milliseconds_left < 0 {
                                    time.milliseconds_left = 0;
                                }
                            }
                        }
                    }
                }
            }
            Message::Time(time) => self.game_settings.time = Some(time),
            Message::TournamentDelete => self.send("tournament_delete\n"),
            Message::TournamentTreeDelete => self.send("tournament_tree_delete\n"),
            Message::Users => self.screen = Screen::Users,
            Message::UsersSortedBy(sort_by) => self.users_sort_by = sort_by,
            Message::WindowResized((width, height)) => {
                if width >= 1_500.0 && height >= 1_000.0 {
                    self.screen_size = Size::Giant;
                } else if width >= 1_300.0 && height >= 1_000.0 {
                    self.screen_size = Size::Large;
                } else if width >= 1_200.0 && height >= 850.0 {
                    self.screen_size = Size::Medium;
                } else if width >= 1_000.0 && height >= 750.0 {
                    self.screen_size = Size::Small;
                } else if width >= 1_100.0 {
                    self.screen_size = Size::TinyWide;
                } else {
                    self.screen_size = Size::Tiny;
                }
            }
        }
        Task::none()
    }
    #[must_use]
    fn users_sorted(&self) -> Vec<User> {
        let mut users: Vec<_> = self.users.values().cloned().collect();
        match self.users_sort_by {
            SortBy::Name => {
                users.sort_by(|a, b| {
                    b.rating
                        .rating
                        .partial_cmp(&a.rating.rating)
                        .expect("The number should be comparable.")
                });
                users.sort_by(|a, b| a.name.cmp(&b.name));
            }
            SortBy::Rating => {
                users.sort_by(|a, b| a.name.cmp(&b.name));
                users.sort_by(|a, b| {
                    b.rating
                        .rating
                        .partial_cmp(&a.rating.rating)
                        .expect("The number should be comparable.")
                });
            }
        }
        users
    }
    #[allow(clippy::too_many_lines)]
    #[must_use]
    fn games(&self) -> Scrollable<'_, Message> {
        let mut game_ids = Column::new().spacing(SPACING_B);
        let mut attackers = Column::new().spacing(SPACING_B);
        let mut defenders = Column::new().spacing(SPACING_B);
        let mut ratings = Column::new().spacing(SPACING_B);
        let mut timings = Column::new().spacing(SPACING_B);
        let mut sizes = Column::new().spacing(SPACING_B);
        let mut buttons = Column::new().spacing(SPACING);
        let mut server_games: Vec<&ServerGameLight> = self.games_light.0.values().collect();
        server_games.sort_by(|a, b| b.id.cmp(&a.id));
        for (i, game) in server_games.iter().enumerate() {
            if self.my_games_only {
                let mut includes_username = false;
                if let Some(attacker) = &game.attacker
                    && attacker == &self.username
                {
                    includes_username = true;
                }
                if let Some(defender) = &game.defender
                    && defender == &self.username
                {
                    includes_username = true;
                }
                if !includes_username {
                    continue;
                }
            }
            let id = game.id;
            game_ids = game_ids.push(text(id));
            attackers = if let Some(attacker) = &game.attacker {
                attackers.push(text(attacker))
            } else {
                attackers.push(text(t!("none")))
            };
            defenders = if let Some(defender) = &game.defender {
                defenders.push(text(defender))
            } else {
                defenders.push(text(t!("none")))
            };
            let rating: bool = game.rated.into();
            let rating = if rating { t!("yes") } else { t!("no") };
            ratings = ratings.push(text(rating));
            timings = timings.push(text(game.timed.to_string()));
            sizes = sizes.push(text(game.board_size.to_string()));
            let mut buttons_row = Row::new().spacing(SPACING);
            let i = if let Some(i) = ALPHABET.get(i) {
                format!(" ({i})")
            } else {
                String::new()
            };
            match self.join_game(game) {
                JoinGame::Cancel => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Cancel"].as_str()))
                            .on_press(Message::GameCancel(id)),
                    );
                }
                JoinGame::Join => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Join"].as_str()))
                            .on_press(Message::GameJoin(id)),
                    );
                }
                JoinGame::None => {}
                JoinGame::Resume => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Resume"].as_str()))
                            .on_press(Message::GameResume(id)),
                    );
                }
                JoinGame::Watch => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Watch"].as_str()))
                            .on_press(Message::GameWatch(id)),
                    );
                }
            }
            match self.game_state(id) {
                State::Challenger | State::Spectator => {}
                State::Creator => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Accept"].as_str()))
                            .on_press(Message::GameAccept(id)),
                    );
                    buttons_row = buttons_row.push(
                        button(text!(
                            "{}{}",
                            self.strings["Decline"].as_str(),
                            i.to_ascii_uppercase()
                        ))
                        .on_press(Message::GameDecline(id)),
                    );
                }
                State::CreatorOnly => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Cancel"].as_str()))
                            .on_press(Message::CancelGame(id)),
                    );
                }
            }
            buttons = buttons.push(buttons_row);
        }
        let game_id = t!("ID");
        let game_ids = column![
            text(game_id.to_string()),
            text("-".repeat(game_id.chars().count())).font(Font::MONOSPACE),
            game_ids
        ]
        .padding(PADDING);
        let attacker = t!("attacker");
        let attackers = column![
            text(attacker.to_string()),
            text("-".repeat(attacker.chars().count())).font(Font::MONOSPACE),
            attackers
        ]
        .padding(PADDING);
        let defender = t!("defender");
        let defenders = column![
            text(defender.to_string()),
            text("-".repeat(defender.chars().count())).font(Font::MONOSPACE),
            defenders
        ]
        .padding(PADDING);
        let rated = t!("rated");
        let ratings = column![
            text(rated.to_string()),
            text("-".repeat(rated.chars().count())).font(Font::MONOSPACE),
            ratings
        ]
        .padding(PADDING);
        let timed = t!("timed");
        let timings = column![
            text(timed.to_string()),
            text("-".repeat(timed.chars().count())).font(Font::MONOSPACE),
            timings
        ]
        .padding(PADDING);
        let size = t!("size");
        let sizes = column![
            text(size.to_string()),
            text("-".repeat(size.chars().count())).font(Font::MONOSPACE),
            sizes
        ]
        .padding(PADDING);
        let buttons = column![text(""), text(""), buttons].padding(PADDING);
        scrollable(row![
            game_ids, attackers, defenders, ratings, timings, sizes, buttons
        ])
    }
    fn handle_play(&mut self, role: Option<&str>, from: &str, to: &str) {
        self.captures = HashSet::new();
        let mut game_handle = None;
        if let Some(handle) = &mut self.archived_game_handle {
            game_handle = Some(Game::from(&handle.boards));
        }
        let game = if let Some(game) = &mut game_handle {
            game
        } else {
            self.game.as_mut().expect("you should have a game by now")
        };
        let role = if let Some(role) = role {
            Role::from_str(role).expect("there should be a valid role")
        } else {
            game.turn
        };
        let play = Plae::try_from(vec!["play", &role.to_string(), from, to])
            .expect("This is a valid plae.");
        let captures = game.play(&play).expect("This should be a legal play.");
        for vertex in captures.0 {
            self.captures.insert(vertex);
        }
        if let Some(handle) = &mut self.archived_game_handle {
            handle.boards.insert(&game.board);
            handle.play += 1;
        }
        if self.sound_muted {
            return;
        }
        let capture = !self.captures.is_empty();
        thread::spawn(move || {
            let mut stream = rodio::OutputStreamBuilder::open_default_stream()?;
            let cursor = if capture {
                Cursor::new(SOUND_CAPTURE)
            } else {
                Cursor::new(SOUND_MOVE)
            };
            let sound = rodio::play(stream.mixer(), cursor)?;
            sound.set_volume(1.0);
            sound.sleep_until_end();
            stream.log_on_drop(false);
            Ok::<(), anyhow::Error>(())
        });
    }
    fn resign(&mut self) {
        let game = self.game.as_ref().expect("you should have a game by now");
        self.send(&format!(
            "game {} play {} resigns _\n",
            self.game_id, game.turn
        ));
    }
    fn sound_muted(&mut self) {
        self.sound_muted = !self.sound_muted;
        handle_error(self.save_client_ron());
    }
    #[must_use]
    fn users(&self, logged_in: &LoggedIn) -> Scrollable<'_, Message> {
        let mut ratings = Column::new();
        let mut usernames = Column::new();
        let mut wins = Column::new();
        let mut losses = Column::new();
        let mut draws = Column::new();
        let mut win_percents = Column::new();
        for user in self.users_sorted() {
            if *logged_in == user.logged_in || *logged_in == LoggedIn::None {
                let wins_number = f64::from_str(&user.wins).expect("This is a f64.");
                let mut win_percentage = wins_number
                    / (wins_number + f64::from_str(&user.losses).expect("This is a f64."));
                win_percentage *= 100.0;
                win_percentage = win_percentage.round_ties_even();
                ratings = ratings.push(text(user.rating.to_string_rounded()));
                usernames = usernames.push(text(user.name));
                wins = wins.push(text(user.wins));
                losses = losses.push(text(user.losses));
                draws = draws.push(text(user.draws));
                win_percents = win_percents.push(text!("{}", win_percentage));
            }
        }
        let rating = t!("rating");
        let mut button_1 = button(text("(7)").size(10)).padding(PADDING_SMALL);
        if self.users_sort_by != SortBy::Rating {
            button_1 = button_1.on_press(Message::UsersSortedBy(SortBy::Rating));
        }
        let ratings = column![
            row![text(rating.to_string()), button_1,].spacing(SPACING),
            text("-".repeat(rating.chars().count())).font(Font::MONOSPACE),
            ratings
        ]
        .padding(PADDING);
        let username = t!("username");
        let mut button_2 = button(text("(8)").size(10)).padding(PADDING_SMALL);
        if self.users_sort_by != SortBy::Name {
            button_2 = button_2.on_press(Message::UsersSortedBy(SortBy::Name));
        }
        let usernames = column![
            row![text(username.to_string()), button_2,].spacing(SPACING),
            text("-".repeat(username.chars().count())).font(Font::MONOSPACE),
            usernames
        ]
        .padding(PADDING);
        let win = t!("wins");
        let wins = column![
            text(win.to_string()),
            text("-".repeat(win.chars().count())).font(Font::MONOSPACE),
            wins
        ]
        .padding(PADDING);
        let loss = t!("losses");
        let losses = column![
            text(loss.to_string()),
            text("-".repeat(loss.chars().count())).font(Font::MONOSPACE),
            losses
        ]
        .padding(PADDING);
        let draw = t!("draws");
        let draws = column![
            text(draw.to_string()),
            text("-".repeat(draw.chars().count())).font(Font::MONOSPACE),
            draws
        ]
        .padding(PADDING);
        let win_percent = format!("{} %", t!("wins"));
        let hyphens_count = win_percent.chars().count();
        let win_percents = column![
            text(win_percent),
            text("-".repeat(hyphens_count)).font(Font::MONOSPACE),
            win_percents
        ]
        .padding(PADDING);
        scrollable(row![ratings, usernames, wins, losses, draws, win_percents]).spacing(SPACING)
    }
    #[must_use]
    fn user_area(&self, in_game: bool) -> Container<'_, Message> {
        let texts = if in_game {
            &self.texts_game
        } else {
            &self.texts
        };
        let games = self.games();
        let texting = self.texting(texts, true).padding(PADDING);
        let users = self.users(&LoggedIn::Yes);
        let user_area = scrollable(column![games, users, texting]);
        container(user_area)
            .padding(PADDING)
            .style(container::bordered_box)
    }
    fn reset_password(&mut self) {
        if !self.connected_tcp {
            self.send("tcp_connect\n");
            self.connected_tcp = true;
        }
        if self.screen == Screen::Login {
            self.send(&format!(
                "{VERSION_ID} reset_password {}\n",
                self.text_input
            ));
        }
    }
    #[must_use]
    #[allow(clippy::too_many_lines)]
    pub fn view(&self) -> Element<'_, Message> {
        match self.screen {
            Screen::AccountSettings => {
                let mut rating = String::new();
                let mut wins = String::new();
                let mut draws = String::new();
                let mut losses = String::new();
                for user in self.users.values() {
                    if self.username == user.name {
                        rating = user.rating.to_string_rounded();
                        wins.clone_from(&user.wins);
                        losses.clone_from(&user.losses);
                        draws.clone_from(&user.draws);
                    }
                }
                let mut columns = column![
                    text!(
                        "{} {} {} TCP",
                        t!("connected to"),
                        &self.connected_to,
                        t!("via")
                    ),
                    text!("{}: {}", t!("username"), &self.username),
                    text!("{}: {rating}", t!("rating")),
                    text!("{}: {wins}", t!("wins")),
                    text!("{}: {losses}", t!("losses")),
                    text!("{}: {draws}", t!("draws")),
                ]
                .padding(PADDING)
                .spacing(SPACING);
                if let Some(email) = &self.email {
                    let mut row = Row::new();
                    if email.verified {
                        row = row.push(text!(
                            "{}: [{}] {} ",
                            t!("email address"),
                            t!("verified"),
                            email.address,
                        ));
                        columns = columns.push(row);
                    } else {
                        row = row.push(text!(
                            "{}: [{}] {} ",
                            t!("email address"),
                            t!("unverified"),
                            email.address,
                        ));
                        columns = columns.push(row);
                        let mut row = Row::new();
                        row = row.push(text!("{}: ", t!("email code")));
                        row = row.push(
                            widget::text_input("", &self.text_input)
                                .on_input(Message::TextChanged)
                                .on_paste(Message::TextChanged)
                                .on_submit(Message::TextSendEmailCode),
                        );
                        columns = columns.push(row);
                    }
                } else {
                    let mut row = Row::new();
                    row = row.push(text!("{}: ", t!("email address")));
                    row = row.push(
                        widget::text_input("", &self.text_input)
                            .on_input(Message::TextChanged)
                            .on_paste(Message::TextChanged)
                            .on_submit(Message::TextSendEmail),
                    );
                    columns = columns.push(row);
                    columns = columns.push(row![text!("{}: ", t!("email code"))]);
                }
                columns = columns.push(row![
                    button(text!("{} (1)", self.strings["Reset Email"].as_str()))
                        .on_press(Message::EmailReset)
                ]);
                if let Some(error) = &self.error_email {
                    columns = columns.push(row![text!("error: {error}").style(text::danger)]);
                }
                let mut change_password_button =
                    button(text!("{} (2)", self.strings["Change Password"].as_str()));
                if !self.password_ends_with_whitespace {
                    change_password_button = change_password_button.on_press(Message::TextSend);
                }
                columns = columns.push(
                    row![
                        change_password_button,
                        widget::text_input("", &self.password)
                            .secure(!self.password_show)
                            .on_input(Message::PasswordChanged)
                            .on_paste(Message::PasswordChanged),
                    ]
                    .spacing(SPACING),
                );
                columns = columns.push(
                    row![
                        checkbox(self.password_show).on_toggle(Message::PasswordShow),
                        text!("{} (3)", t!("show password")),
                    ]
                    .spacing(SPACING),
                );
                if self.delete_account {
                    columns = columns.push(
                        button(text!(
                            "{} (4)",
                            self.strings["REALLY DELETE ACCOUNT"].as_str()
                        ))
                        .on_press(Message::DeleteAccount),
                    );
                } else {
                    columns = columns.push(
                        button(text!("{} (4)", self.strings["Delete Account"].as_str()))
                            .on_press(Message::DeleteAccount),
                    );
                }
                columns = columns.push(
                    button(text!("{} (Esc)", self.strings["Leave"].as_str()))
                        .on_press(Message::Leave),
                );
                columns.into()
            }
            Screen::EmailEveryone => {
                let subject = row![
                    text("Subject: "),
                    widget::text_input("", &self.text_input)
                        .on_input(Message::TextChanged)
                        .on_paste(Message::TextChanged)
                        .on_submit(Message::TextSend),
                ];
                let editor = text_editor(&self.content)
                    .placeholder("Dear User, …")
                    .on_action(Message::TextEdit);
                let send_emails = button("Send Emails").on_press(Message::TextSend);
                let leave = button(text!("{} (Esc)", self.strings["Leave"].as_str()))
                    .on_press(Message::Leave);
                let mut column = column![
                    subject,
                    text("From: Hnefatafl Org <no-reply@hnefatafl.org>"),
                    text("Content-Type: text/plain; charset=utf-8"),
                    text("Content-Transfer-Encoding: 7bit"),
                    text!("Date: {}", Utc::now().to_rfc2822()),
                    text("Body:"),
                    editor,
                    send_emails,
                    leave,
                    text("Bcc:")
                ]
                .spacing(SPACING)
                .padding(PADDING);
                for email in &self.emails_bcc {
                    column = column.push(text(email));
                }
                scrollable(column).spacing(SPACING).into()
            }
            Screen::Game | Screen::GameReview => self.display_game(),
            Screen::GameNew => {
                let attacker = radio(
                    format!("{} (1)", t!("attacker")),
                    Role::Attacker,
                    self.game_settings.role_selected,
                    Message::RoleSelected,
                );
                let defender = radio(
                    format!("{} (2)", t!("defender")),
                    Role::Defender,
                    self.game_settings.role_selected,
                    Message::RoleSelected,
                );
                let rated = row![
                    text!("{} (0):", t!("rated")),
                    checkbox(self.game_settings.rated.into()).on_toggle(Message::RatedSelected)
                ]
                .padding(PADDING)
                .spacing(SPACING);
                let mut new_game = button(text!("{} (Enter)", self.strings["New Game"].as_str()));
                if self.game_settings.role_selected.is_some() && self.game_settings.time.is_some() {
                    new_game = new_game.on_press(Message::GameSubmit);
                }
                let leave = button(text!("{} (Esc)", self.strings["Leave"].as_str()))
                    .on_press(Message::Leave);
                let size_11x11 = radio(
                    "11x11 (3)",
                    BoardSize::_11,
                    Some(self.game_settings.board_size),
                    Message::BoardSizeSelected,
                );
                let size_13x13 = radio(
                    "13x13 (4)",
                    BoardSize::_13,
                    Some(self.game_settings.board_size),
                    Message::BoardSizeSelected,
                );
                let row_1 = row![text!("{}:", t!("role")), attacker, defender]
                    .padding(PADDING)
                    .spacing(SPACING);
                let row_2 = row![text!("{}:", t!("board size")), size_11x11, size_13x13]
                    .padding(PADDING)
                    .spacing(SPACING);
                let rapid = radio(
                    format!("{} (5)", TimeEnum::Rapid),
                    TimeEnum::Rapid,
                    self.game_settings.time,
                    Message::Time,
                );
                let classical = radio(
                    format!("{} (6)", TimeEnum::Classical),
                    TimeEnum::Classical,
                    self.game_settings.time,
                    Message::Time,
                );
                let long = radio(
                    format!("{} (7)", TimeEnum::Long),
                    TimeEnum::Long,
                    self.game_settings.time,
                    Message::Time,
                );
                let very_long = radio(
                    format!("{} (8)", TimeEnum::VeryLong),
                    TimeEnum::VeryLong,
                    self.game_settings.time,
                    Message::Time,
                );
                let row_3 = row![text!("{}:", t!("time"))]
                    .padding(PADDING)
                    .spacing(SPACING);
                let row_4 = row![rapid, classical].padding(PADDING).spacing(SPACING);
                let row_5 = row![long, very_long].padding(PADDING).spacing(SPACING);
                let mut column = column![rated, row_1, row_2, row_3, row_4, row_5];
                column = self.add_infinity(column);
                let row_6 = row![new_game, leave].padding(PADDING).spacing(SPACING);
                column.push(row_6).into()
            }
            Screen::Games => {
                let mut column = Column::new().padding(PADDING).spacing(SPACING);
                if !self.message.is_empty() {
                    column = column.push(text(self.message.clone()));
                }
                if self.admin {
                    column = column.push(button("Email Everyone").on_press(Message::EmailEveryone));
                }
                let username =
                    row![text!("{}: {}", t!("username"), &self.username)].spacing(SPACING);
                let username = container(username)
                    .padding(PADDING / 2)
                    .style(container::bordered_box);
                let tournament = button(text!("{} (0)", self.strings["Tournament"].as_str()))
                    .on_press(Message::Tournament);
                let my_games_text = text!("{} (2)", t!("My Games Only")).center();
                let my_games = checkbox(self.my_games_only)
                    .on_toggle(Message::MyGamesOnly)
                    .size(32);
                let get_archived_games =
                    button(text!("{} (1)", self.strings["Get Archived Games"].as_str()))
                        .on_press(Message::ArchivedGamesGet);
                let username = row![
                    username,
                    tournament,
                    get_archived_games,
                    my_games,
                    my_games_text
                ]
                .spacing(SPACING);
                let create_game = button(text!("{} (3)", self.strings["Create Game"].as_str()))
                    .on_press(Message::GameNew);
                let users = button(text!("{} (4)", self.strings["Users"].as_str()))
                    .on_press(Message::Users);
                let account_setting =
                    button(text!("{} (5)", self.strings["Account Settings"].as_str()))
                        .on_press(Message::AccountSettings);
                let website = button(text!("{} (6)", self.strings["Rules"].as_str())).on_press(
                    Message::OpenUrl("https://hnefatafl.org/rules.html".to_string()),
                );
                let quit = button(text!("{} (Esc)", self.strings["Leave"].as_str()))
                    .on_press(Message::Leave);
                let top = row![create_game, users, account_setting, website, quit].spacing(SPACING);
                let user_area = self.user_area(false);
                column = column.push(username);
                column = column.push(top);
                column = column.push(user_area);
                column.into()
            }
            Screen::Login => {
                let username = row![
                    text!("{}:", t!("username")).size(20),
                    widget::text_input("", &self.text_input)
                        .on_input(Message::TextChanged)
                        .on_paste(Message::TextChanged),
                ]
                .spacing(SPACING);
                let username = container(username)
                    .padding(PADDING)
                    .style(container::bordered_box);
                let password = row![
                    text!("{}:", t!("password")).size(20),
                    widget::text_input("", &self.password)
                        .secure(!self.password_show)
                        .on_input(Message::PasswordChanged)
                        .on_paste(Message::PasswordChanged),
                ]
                .spacing(SPACING);
                let password = container(password)
                    .padding(PADDING)
                    .style(container::bordered_box);
                let show_password_text = text!("{} (1)", t!("show password"));
                let show_password = checkbox(self.password_show).on_toggle(Message::PasswordShow);
                let save_password_text = text!("{} (2)", t!("save password"));
                let save_password = checkbox(self.password_save).on_toggle(Message::PasswordSave);
                let mut login = button(text!("{} (Enter)", self.strings["Login"].as_str()));
                if !self.password_ends_with_whitespace {
                    login = login.on_press(Message::TextSendLogin);
                }
                let mut create_account =
                    button(text!("{} (4)", self.strings["Create Account"].as_str()));
                if !self.text_input.is_empty() && !self.password_ends_with_whitespace {
                    create_account = create_account.on_press(Message::TextSendCreateAccount);
                }
                let mut reset_password =
                    button(text!("{} (5)", self.strings["Reset Password"].as_str()));
                if !self.text_input.is_empty() {
                    reset_password = reset_password.on_press(Message::ResetPassword);
                }
                let mut error = text("");
                if let Some(error_) = &self.error {
                    error = text(error_).style(text::danger);
                }
                let mut error_persistent = Column::new();
                for error in &self.error_persistent {
                    error_persistent = error_persistent.push(text(error).style(text::danger));
                }
                let mut review_game = button(text!("{} (6)", self.strings["Review Game"].as_str()));
                if self.archived_game_selected.is_some() {
                    review_game = review_game.on_press(Message::ReviewGame);
                }
                let archived_games = if let Some(archived_games) = &self.archived_games_filtered {
                    archived_games.clone()
                } else {
                    self.archived_games.clone()
                };
                let my_games_text = text!("{} (3)", t!("My Games Only"));
                let my_games = checkbox(self.my_games_only).on_toggle(Message::MyGamesOnly);
                let buttons_1 =
                    row![login, create_account, reset_password, review_game,].spacing(SPACING);
                let review_game_pick = pick_list(
                    archived_games,
                    self.archived_game_selected.clone(),
                    Message::ArchivedGameSelected,
                )
                .placeholder(t!("Archived Games"));
                let review_game_pick = row![review_game_pick].spacing(SPACING);
                let locale = [
                    Locale::English,
                    Locale::Chinese,
                    Locale::Spanish,
                    Locale::Arabic,
                    Locale::Indonesian,
                    Locale::PortugueseBr,
                    Locale::PortuguesePt,
                    Locale::French,
                    Locale::Japanese,
                    Locale::Russian,
                    Locale::German,
                    Locale::Icelandic,
                    Locale::IcelandicRunic,
                    Locale::Swedish,
                    Locale::Korean,
                ];
                let locale = row![
                    text!("{}: ", t!("locale")).size(20),
                    pick_list(locale, Some(self.locale_selected), Message::LocaleSelected),
                ];
                let mut buttons_2 = if self.theme == Theme::Light {
                    row![
                        button(text!("{} (7)", self.strings["Dark"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Dark)),
                        button(text!("{} (8)", self.strings["Light"].as_str())),
                        button(text("Tol (a)")).on_press(Message::ChangeTheme(Theme::Tol)),
                    ]
                    .spacing(SPACING)
                } else if self.theme == Theme::Dark {
                    row![
                        button(text!("{} (7)", self.strings["Dark"].as_str())),
                        button(text!("{} (8)", self.strings["Light"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Light)),
                        button(text("Tol (a)")).on_press(Message::ChangeTheme(Theme::Tol)),
                    ]
                    .spacing(SPACING)
                } else {
                    row![
                        button(text!("{} (7)", self.strings["Dark"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Dark)),
                        button(text!("{} (8)", self.strings["Light"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Light)),
                        button(text("Tol (a)")),
                    ]
                    .spacing(SPACING)
                };
                let discord = button(text!("{} (9)", self.strings["Join Discord"].as_str()))
                    .on_press(Message::OpenUrl(
                        "https://discord.gg/h56CAHEBXd".to_string(),
                    ));
                let website = button("https://hnefatafl.org (0)")
                    .on_press(Message::OpenUrl("https://hnefatafl.org".to_string()));
                let quit = button(text!("{} (Esc)", self.strings["Quit"].as_str()))
                    .on_press(Message::Leave);
                buttons_2 = buttons_2.push(discord);
                buttons_2 = buttons_2.push(website);
                buttons_2 = buttons_2.push(quit);
                let help_text = container(text!(
                    "Tab: {}, Shift + Tab: {}",
                    self.chars.arrow_right,
                    self.chars.arrow_left
                ))
                .padding(PADDING)
                .style(container::bordered_box);
                let help_text_2 = text(t!(
                    "You must hold down the control (Ctrl) or command (⌘) key when pressing a lettered or numbered hotkey."
                ));
                let help_text_3 = text(t!(
                    "You can play on the board by pressing control (Ctrl) or command (⌘) and a letter then a number or vice versa."
                ));
                let help_text_4 = text(t!("You have a week to move, then you lose the game."));
                column![
                    username,
                    password,
                    row![
                        show_password,
                        show_password_text,
                        save_password,
                        save_password_text,
                        my_games,
                        my_games_text,
                    ]
                    .spacing(SPACING),
                    buttons_1,
                    review_game_pick,
                    locale,
                    buttons_2,
                    help_text,
                    help_text_2,
                    help_text_3,
                    help_text_4,
                    error,
                    error_persistent
                ]
                .padding(PADDING)
                .spacing(SPACING)
                .into()
            }
            Screen::Tournament => {
                let mut column = Column::new().padding(PADDING).spacing(SPACING);
                if self.admin {
                    let input = iced::widget::text_input("????-??-??", &self.text_input)
                        .on_input(Message::TextChanged)
                        .on_paste(Message::TextChanged)
                        .on_submit(Message::TextSend);
                    let mut delete_button = button("Delete Tournament");
                    if self.tournament.is_some() {
                        delete_button = delete_button.on_press(Message::TournamentDelete);
                    }
                    let row = row![input, delete_button].spacing(SPACING);
                    column = column.push(row);
                }
                let Some(tournament) = &self.tournament else {
                    column = column.push(text(t!("There is no tournament.")));
                    column = column.push(
                        button(text!("{} (Esc)", self.strings["Leave"].as_str()))
                            .on_press(Message::Leave),
                    );
                    return column.into();
                };
                let mut date = Row::new().spacing(SPACING);
                let start_date = t!("Tournament Start Date");
                date = date.push(text!("{start_date}: {}", tournament.date.to_rfc2822()));
                let mut button_1 =
                    button(text!("{} (1)", self.strings["Join Tournament"].as_str()));
                let mut button_2 =
                    button(text!("{} (2)", self.strings["Leave Tournament"].as_str()));
                if tournament.players.contains(&self.username) {
                    button_2 = button_2.on_press(Message::TournamentLeave);
                } else {
                    button_1 = button_1.on_press(Message::TournamentJoin);
                }
                let buttons = row![
                    button_1,
                    button_2,
                    button(text!("{} (Esc)", self.strings["Leave"].as_str()))
                        .on_press(Message::Leave),
                ]
                .spacing(SPACING);
                let title = t!("Players");
                let dashes = text("-".repeat(title.len())).font(Font::MONOSPACE);
                let title = text(title);
                let title = column![title, dashes];
                let mut players = Column::new();
                let mut player_names: Vec<_> = tournament.players.iter().collect();
                player_names.sort();
                for player in &player_names {
                    players = players.push(text(*player));
                }
                column = column.push(date);
                column = column.push(buttons);
                column = column.push(title);
                column = column.push(players);
                if self.admin {
                    let mut delete_button = button("Delete Tournament Tree");
                    if let Some(tournament) = &self.tournament
                        && tournament.tree.is_some()
                    {
                        delete_button = delete_button.on_press(Message::TournamentTreeDelete);
                    }
                    let mut start_tournament = button("Start Tournament");
                    if self.tournament.is_none() {
                        start_tournament = start_tournament.on_press(Message::TournamentStart);
                    }
                    column = column.push(row![start_tournament, delete_button].spacing(SPACING));
                }
                column = column.push(self.display_tournament());
                scrollable(column).spacing(SPACING).into()
            }
            Screen::Users => row![
                self.users(&LoggedIn::None),
                button(text!("{} (Esc)", self.strings["Leave"].as_str())).on_press(Message::Leave)
            ]
            .padding(PADDING)
            .spacing(SPACING)
            .into(),
        }
    }
    fn reset_email(&mut self) {
        self.email = None;
        self.send("email_reset\n");
    }
    fn reset_markers(&mut self) {
        self.captures = HashSet::new();
        self.play_from = None;
        self.play_from_previous = None;
        self.play_to_previous = None;
    }
    fn review_game(&mut self) {
        if let Some(archived_game) = &self.archived_game_selected {
            self.archived_game_handle = Some(ArchivedGameHandle::new(archived_game));
            self.screen = Screen::GameReview;
            self.captures = HashSet::new();
            self.reset_markers();
        }
    }
    fn save_client_postcard(&self) -> anyhow::Result<()> {
        let postcard_bytes = postcard::to_allocvec(&self.archived_games)?;
        if !postcard_bytes.is_empty() {
            let mut file = File::create(data_file(ARCHIVED_GAMES_FILE))?;
            file.write_all(&postcard_bytes)?;
        }
        Ok(())
    }
    fn save_client_ron(&self) -> anyhow::Result<()> {
        let password = if self.password_save {
            self.password.clone()
        } else {
            String::new()
        };
        let client = Client {
            archived_games: Vec::new(),
            coordinates: self.coordinates,
            locale_selected: self.locale_selected,
            my_games_only: self.my_games_only,
            password,
            password_save: self.password_save,
            password_show: self.password_show,
            sound_muted: self.sound_muted,
            theme: self.theme,
            username: self.username.clone(),
            ..Client::default()
        };
        let ron_string = ron::ser::to_string_pretty(&client, ron::ser::PrettyConfig::new())?;
        if !ron_string.is_empty() {
            let mut file = File::create(data_file(USER_CONFIG_FILE))?;
            file.write_all(ron_string.as_bytes())?;
        }
        Ok(())
    }
    fn send(&mut self, string: &str) {
        if let Err(error) = self
            .tx
            .as_mut()
            .unwrap_or_else(|| {
                error!("Error sending {string:?}: you should have a tx available by now");
                unreachable!();
            })
            .send(string.to_string())
        {
            error!("{error}: {string}");
            exit(1);
        }
    }
    fn send_estimate_score(&mut self, tree: Tree) {
        handle_error(
            self.estimate_score_tx
                .as_mut()
                .unwrap_or_else(|| {
                    error!("Error sending {tree:?}: you should have a tx available by now");
                    unreachable!();
                })
                .send(tree),
        );
    }
    fn toggle_save_password(&mut self) {
        self.password_save = !self.password_save;
        handle_error(self.save_client_ron());
    }
    fn toggle_show_password(&mut self) {
        self.password_show = !self.password_show;
        handle_error(self.save_client_ron());
    }
    fn letter(
        &self,
        letter: char,
        column: Column<'a, Message>,
        letter_size: u32,
    ) -> Column<'a, Message> {
        let mut text = text(letter).size(letter_size);
        if self.press_letters.contains(&letter.to_ascii_lowercase()) {
            text = text.style(text::success);
        }
        column.push(text)
    }
    fn numbers(&self, letter_size: u32, spacing: u32, board_size: usize) -> Column<'a, Message> {
        let mut column = column![text(" ").size(letter_size)].spacing(spacing);
        for i in 0..board_size {
            let i = board_size - i;
            let mut text = text!("{i:2}").size(letter_size).align_y(Vertical::Center);
            if self.press_numbers[i - 1] {
                text = text.style(text::success);
            }
            column = column.push(text);
        }
        column
    }
}