1
// This file is part of hnefatafl-copenhagen.
2
//
3
// hnefatafl-copenhagen is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU Affero General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// hnefatafl-copenhagen is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU Affero General Public License for more details.
12
//
13
// You should have received a copy of the GNU Affero General Public License
14
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
//
16
// SPDX-License-Identifier: AGPL-3.0-or-later
17
// SPDX-FileCopyrightText: 2025 David Campbell <david@hnefatafl.org>
18

            
19
// Don't open the terminal on Windows.
20
#![cfg_attr(all(windows, not(feature = "console")), windows_subsystem = "windows")]
21
#![deny(clippy::unwrap_used)]
22

            
23
mod archived_game_handle;
24
mod command_line;
25
mod dimensions;
26
mod enums;
27
mod new_game_settings;
28
mod tabs;
29
mod user;
30
mod volume;
31

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

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

            
98
use crate::{
99
    archived_game_handle::ArchivedGameHandle,
100
    command_line::Args,
101
    dimensions::Dimensions,
102
    enums::{Coordinates, JoinGame, Message, Move, Screen, Size, SortBy, State, Theme},
103
    new_game_settings::NewGameSettings,
104
    tabs::TabId,
105
    user::User,
106
    volume::{MAX_VOLUME, Volume},
107
};
108

            
109
/// The Muted qualitative color scheme of [Tol]. A color scheme for the
110
/// color blind.
111
///
112
/// [Tol]: https://sronpersonalpages.nl/~pault/#sec:qualitative
113
pub const TOL: Palette = Palette {
114
    background: color!(0xDD, 0xDD, 0xDD), // PALE_GREY
115
    text: color!(0x00, 0x00, 0x00),       // BLACK
116
    primary: color!(0x88, 0xCC, 0xEE),    // CYAN
117
    success: color!(0x11, 0x77, 0x33),    // GREEN
118
    warning: color!(0xDD, 0xCC, 0x77),    // SAND
119
    danger: color!(0xCC, 0x66, 0x77),     // ROSE
120
};
121

            
122
const ALPHABET: [char; 26] = [
123
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
124
    't', 'u', 'v', 'w', 'x', 'y', 'z',
125
];
126

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

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

            
133
const BOARD_LETTERS_LOWERCASE: [char; 13] = [
134
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
135
];
136

            
137
const ARCHIVED_GAMES_FILE: &str = "archived-games.postcard";
138
const USER_CONFIG_FILE: &str = "user.ron";
139

            
140
const MAX_RATING: f64 = 100_000.0;
141
const PADDING: u16 = 8;
142
const PADDING_SMALL: u16 = 2;
143
const PADDING_MEDIUM: u16 = 4;
144
const SPACING: Pixels = Pixels(8.0);
145
const SPACING_B: Pixels = Pixels(20.0);
146

            
147
const HELMET: &[u8] = include_bytes!("assets/helmet.png");
148
const SOUND_CAPTURE: &[u8] = include_bytes!("assets/capture.ogg");
149
const SOUND_GAME_OVER: &[u8] = include_bytes!("assets/game_over.ogg");
150
const SOUND_MOVE: &[u8] = include_bytes!("assets/move.ogg");
151

            
152
/// In milliseconds.
153
const TICK: i64 = 100;
154
const TICK_U: u64 = 100;
155

            
156
rust_i18n::i18n!();
157

            
158
#[allow(clippy::too_many_lines)]
159
fn init_client() -> Client {
160
    let archived_games_file = data_file(ARCHIVED_GAMES_FILE);
161
    let user_file = data_file(USER_CONFIG_FILE);
162
    let mut error = Vec::new();
163

            
164
    let mut client: Client = match &fs::read_to_string(&user_file) {
165
        Ok(string) => match ron::from_str(string) {
166
            Ok(client) => client,
167
            Err(err) => {
168
                error.push(format!(
169
                    "Error parsing the ron file {}: {err}",
170
                    user_file.display()
171
                ));
172
                Client::default()
173
            }
174
        },
175
        Err(err) => {
176
            if err.kind() == ErrorKind::NotFound {
177
                error.push(format!(
178
                    "Unable to find User Configuration file: {}",
179
                    user_file.display()
180
                ));
181
                Client::default()
182
            } else {
183
                error.push(format!(
184
                    "Error opening the file {}: {err}",
185
                    user_file.display()
186
                ));
187
                Client::default()
188
            }
189
        }
190
    };
191

            
192
    client.tournament_date = Date::today();
193

            
194
    if let Some(locale) = &client.locale_selected {
195
        rust_i18n::set_locale(&locale.txt());
196
    } else {
197
        let mut locale_1 = None;
198

            
199
        if get_locales().count() > 0 {
200
            for locale_2 in get_locales() {
201
                if let Ok(locale) = locale_2.as_str().try_into() {
202
                    locale_1 = Some(locale);
203
                    break;
204
                }
205
            }
206
        }
207

            
208
        if locale_1.is_none() {
209
            let locale_2 = get_locale().unwrap_or_else(|| String::from("en-US"));
210

            
211
            if let Ok(locale) = locale_2.as_str().try_into() {
212
                locale_1 = Some(locale);
213
            } else {
214
                locale_1 = Some(Locale::English);
215
            }
216
        }
217

            
218
        if let Some(locale) = locale_1 {
219
            rust_i18n::set_locale(&locale.txt());
220
            client.locale_selected = Some(locale);
221
            client
222
                .save_client_ron()
223
                .expect("saving user file should work");
224
        }
225
    }
226

            
227
    client.text_input.clone_from(&client.username);
228

            
229
    let archived_games: Vec<ArchivedGame> = match &fs::read(&archived_games_file) {
230
        Ok(bytes) => match postcard::from_bytes(bytes) {
231
            Ok(client) => client,
232
            Err(err) => {
233
                error.push(format!(
234
                    "Error parsing the postcard file {}: {err}",
235
                    archived_games_file.display()
236
                ));
237
                Vec::new()
238
            }
239
        },
240
        Err(err) => {
241
            if err.kind() == ErrorKind::NotFound {
242
                error.push(format!(
243
                    "{}: {}",
244
                    t!("Unable to find Archived Games file"),
245
                    archived_games_file.display()
246
                ));
247
                Vec::new()
248
            } else {
249
                error.push(format!(
250
                    "{} {}: {err}",
251
                    t!("Error opening the file"),
252
                    archived_games_file.display()
253
                ));
254
                Vec::new()
255
            }
256
        }
257
    };
258

            
259
    client.archived_games = archived_games;
260
    client.error_persistent = error;
261

            
262
    let args = Args::parse();
263
    if args.ascii {
264
        client.chars.ascii();
265
    }
266

            
267
    let mut letters = HashMap::new();
268
    for ch in BOARD_LETTERS_LOWERCASE {
269
        letters.insert(ch, false);
270
    }
271

            
272
    if client.rating_maximum == 0.0 {
273
        client.rating_max();
274
    }
275
    if client.rating_minimum == 0.0 {
276
        client.rating_min();
277
    }
278
    client.games_filtered();
279

            
280
    client
281
}
282

            
283
fn main() -> anyhow::Result<()> {
284
    let args = Args::parse();
285
    utils::init_logger("hnefatafl_client", args.debug, false);
286

            
287
    if args.man {
288
        let mut buffer: Vec<u8> = Vec::default();
289
        let cmd = Args::command().name("hnefatafl-client").long_version(None);
290
        let man = clap_mangen::Man::new(cmd).date("2025-06-23");
291

            
292
        man.render(&mut buffer)?;
293
        write!(buffer, "{COPYRIGHT}")?;
294

            
295
        std::fs::write("hnefatafl-client.1", buffer)?;
296
        return Ok(());
297
    }
298

            
299
    create_data_folder()?;
300

            
301
    let mut application = iced::application(init_client, Client::update, Client::view)
302
        .title("Hnefatafl Copenhagen")
303
        .subscription(Client::subscriptions)
304
        .window(window::Settings {
305
            #[cfg(target_os = "linux")]
306
            platform_specific: PlatformSpecific {
307
                application_id: APPLICATION_ID.to_string(),
308
                ..PlatformSpecific::default()
309
            },
310
            icon: Some(icon::from_file_data(HELMET, Some(ImageFormat::Png))?),
311
            ..window::Settings::default()
312
        })
313
        .font(ICED_AW_FONT_BYTES)
314
        .theme(Client::theme);
315

            
316
    // For screenshots.
317
    if args.tiny_window {
318
        application = application.window_size(iced::Size {
319
            width: 868.0,
320
            height: 541.0,
321
        });
322
    }
323

            
324
    if args.social_preview {
325
        application = application.window_size(iced::Size {
326
            width: 1148.0,
327
            height: 481.0,
328
        });
329
    }
330

            
331
    application.run()?;
332
    Ok(())
333
}
334

            
335
fn estimate_score() -> impl Stream<Item = Message> {
336
    let args = Args::parse();
337

            
338
    stream::channel(
339
        100,
340
        move |mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
341
            let (tx, rx) = mpsc::channel();
342

            
343
            if let Err(error) = sender.send(Message::EstimateScoreConnected(tx)).await {
344
                error!("failed to send channel: {error}");
345
                exit(1);
346
            }
347

            
348
            thread::spawn(move || {
349
                let mut ai = match choose_ai(&args.ai, args.seconds, args.depth, true) {
350
                    Ok(ai) => ai,
351
                    Err(error) => {
352
                        error!("{error}");
353
                        exit(1);
354
                    }
355
                };
356

            
357
                for tree in &rx {
358
                    let mut game = Game::from(&tree);
359
                    let generate_move = ai.generate_move(&mut game).expect("the game is ongoing");
360

            
361
                    if let Err(error) = executor::block_on(
362
                        sender.send(Message::EstimateScoreDisplay((tree.here(), generate_move))),
363
                    ) {
364
                        error!("failed to send channel: {error}");
365
                        exit(1);
366
                    }
367
                }
368
            });
369
        },
370
    )
371
}
372

            
373
fn handle_error<T, E: fmt::Display>(result: Result<T, E>) -> T {
374
    match result {
375
        Ok(value) => value,
376
        Err(error) => {
377
            error!("{error}");
378
            exit(1)
379
        }
380
    }
381
}
382

            
383
fn open_url(url: &str) {
384
    if let Err(error) = webbrowser::open(url) {
385
        error!("{error}");
386
    }
387
}
388

            
389
#[allow(clippy::too_many_lines)]
390
fn pass_messages() -> impl Stream<Item = Message> {
391
    stream::channel(
392
        100,
393
        move |mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
394
            let mut args = Args::parse();
395
            args.host.push_str(SERVER_PORT);
396
            let address_string = args.host;
397

            
398
            thread::spawn(move || {
399
                'start_over: loop {
400
                    let (tx, rx) = mpsc::channel();
401

            
402
                    if let Err(error) =
403
                        executor::block_on(sender.send(Message::StreamConnected(tx.clone())))
404
                    {
405
                        error!("failed to send channel: {error}");
406
                        exit(1);
407
                    }
408

            
409
                    loop {
410
                        let message = match rx.recv() {
411
                            Ok(message) => message,
412
                            Err(error) => {
413
                                error!("rx: {error}");
414

            
415
                                if let Err(error) = executor::block_on(sender.send(Message::Exit)) {
416
                                    error!("{error}");
417
                                }
418

            
419
                                return;
420
                            }
421
                        };
422
                        let message_trim = message.trim();
423

            
424
                        if message_trim == "tcp_connect" {
425
                            break;
426
                        }
427
                    }
428

            
429
                    let mut is_ipv6 = false;
430
                    let mut socket_address = None;
431
                    let socket_addresses = match address_string.to_socket_addrs() {
432
                        Ok(socket_addresses) => socket_addresses,
433
                        Err(error) => {
434
                            error!("The socket address resolves to no IPs: {error})");
435

            
436
                            handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
437
                            handle_error(executor::block_on(
438
                                sender.send(Message::TcpConnectFailed),
439
                            ));
440

            
441
                            continue 'start_over;
442
                        }
443
                    };
444

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

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

            
462
                    let Some(socket_address) = socket_address else {
463
                        error!("There is no IPv4 address for the host: {address_string}");
464

            
465
                        handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
466
                        handle_error(executor::block_on(sender.send(Message::TcpConnectFailed)));
467

            
468
                        continue 'start_over;
469
                    };
470

            
471
                    let address: SockAddr = socket_address.into();
472
                    let keep_alive = tcp_keep_alive();
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(&keep_alive));
476

            
477
                    if let Err(error) = socket.connect(&address) {
478
                        if error.kind() == ErrorKind::NetworkUnreachable {
479
                            handle_error(executor::block_on(
480
                                sender.send(Message::TcpConnectFailed),
481
                            ));
482
                        } else {
483
                            handle_error(executor::block_on(sender.send(Message::ServerShutdown)));
484
                        }
485

            
486
                        error!("socket.connect {address_string}: {error}");
487
                        handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
488

            
489
                        continue 'start_over;
490
                    }
491

            
492
                    info!("connected to {socket_address} ...");
493

            
494
                    let mut tcp_stream: TcpStream = socket.into();
495
                    let mut reader = BufReader::new(handle_error(tcp_stream.try_clone()));
496

            
497
                    let mut sender_clone = sender.clone();
498
                    thread::spawn(move || {
499
                        for message in rx {
500
                            let message_trim = message.trim();
501

            
502
                            if message_trim == "ping" {
503
                                trace!("<- {message_trim}");
504
                            } else {
505
                                debug!("<- {message_trim}");
506
                            }
507

            
508
                            if message_trim == "quit" {
509
                                if cfg!(not(target_os = "redox")) {
510
                                    tcp_stream
511
                                        .shutdown(Shutdown::Both)
512
                                        .expect("shutdown call failed");
513
                                }
514

            
515
                                return;
516
                            }
517

            
518
                            handle_error(tcp_stream.write_all(message.as_bytes()));
519
                        }
520

            
521
                        for _ in 0..2 {
522
                            if let Err(error) =
523
                                executor::block_on(sender_clone.send(Message::LeaveSoft))
524
                            {
525
                                error!("{error}");
526
                            }
527
                        }
528

            
529
                        if let Err(error) =
530
                            executor::block_on(sender_clone.send(Message::ServerShutdown))
531
                        {
532
                            error!("{error}");
533
                        }
534
                    });
535

            
536
                    let mut buffer = String::new();
537
                    handle_error(executor::block_on(
538
                        sender.send(Message::ConnectedTo(address_string.clone())),
539
                    ));
540

            
541
                    if cfg!(target_os = "redox") {
542
                        sleep(Duration::from_secs(1));
543
                    }
544

            
545
                    loop {
546
                        let bytes = handle_error(reader.read_line(&mut buffer));
547
                        if bytes > 0 {
548
                            let buffer_trim = buffer.trim();
549
                            let buffer_trim_vec: Vec<_> =
550
                                buffer_trim.split_ascii_whitespace().collect();
551

            
552
                            if buffer_trim_vec[1] == "display_users"
553
                                || buffer_trim_vec[1] == "display_games"
554
                                || buffer_trim_vec[1] == "ping"
555
                            {
556
                                trace!("-> {buffer_trim}");
557
                            } else {
558
                                debug!("-> {buffer_trim}");
559
                            }
560

            
561
                            handle_error(executor::block_on(
562
                                sender.send(Message::TextReceived(buffer.clone())),
563
                            ));
564

            
565
                            if buffer_trim_vec[1] == "archived_games" {
566
                                let length = handle_error(buffer_trim_vec[2].parse());
567
                                let mut buf = vec![0; length];
568
                                handle_error(reader.read_exact(&mut buf));
569
                                let archived_games: Vec<ArchivedGame> =
570
                                    handle_error(postcard::from_bytes(&buf));
571

            
572
                                handle_error(executor::block_on(
573
                                    sender.send(Message::ArchivedGames(archived_games)),
574
                                ));
575
                            }
576

            
577
                            buffer.clear();
578
                        } else {
579
                            info!("the TCP stream has closed");
580
                            continue 'start_over;
581
                        }
582
                    }
583
                }
584
            });
585
        },
586
    )
587
}
588

            
589
fn text_collect(text: SplitAsciiWhitespace<'_>) -> String {
590
    let text: Vec<&str> = text.collect();
591
    text.join(" ")
592
}
593

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

            
739
impl<'a> Client {
740
    fn account_settings_view(&self) -> Column<'_, Message> {
741
        let mut columns = column![
742
            text!(
743
                "{} {} {} TCP",
744
                t!("connected to"),
745
                &self.connected_to,
746
                t!("via")
747
            ),
748
            text!("{}: {}", t!("username"), &self.username),
749
        ]
750
        .padding(PADDING)
751
        .spacing(SPACING);
752

            
753
        if let Some(email) = &self.email {
754
            let mut row = Row::new();
755
            if email.verified {
756
                row = row.push(text!(
757
                    "{} [{}]: {} ",
758
                    t!("email address"),
759
                    t!("verified"),
760
                    email.address,
761
                ));
762
                columns = columns.push(row);
763
            } else {
764
                row = row.push(text!(
765
                    "{} [{}]: {} ",
766
                    t!("email address"),
767
                    t!("unverified"),
768
                    email.address,
769
                ));
770
                columns = columns.push(row);
771

            
772
                let mut row = Row::new();
773
                row = row.push(text!("{}: ", t!("email code")));
774
                row = row.push(
775
                    widget::text_input("", &self.email_input)
776
                        .on_input(Message::EmailChanged)
777
                        .on_paste(Message::EmailChanged)
778
                        .on_submit(Message::TextSendEmailCode),
779
                );
780
                columns = columns.push(row);
781
            }
782
        } else {
783
            let mut row = Row::new();
784
            row = row.push(text!("{}: ", t!("email address")));
785
            row = row.push(
786
                widget::text_input("", &self.email_input)
787
                    .on_input(Message::EmailChanged)
788
                    .on_paste(Message::EmailChanged)
789
                    .on_submit(Message::TextSendEmail),
790
            );
791

            
792
            columns = columns.push(row);
793
            columns = columns.push(row![text!("{}: ", t!("email code"))]);
794
        }
795

            
796
        columns = columns.push(row![
797
            button(text!("{} (6)", t!("Reset Email"))).on_press(Message::EmailReset)
798
        ]);
799

            
800
        if let Some(error) = &self.error_email {
801
            columns = columns.push(row![text!("error: {error}").style(text::danger)]);
802
        }
803

            
804
        let mut change_password_button = button(text!("{} (7)", t!("Change Password")));
805

            
806
        if !self.password_ends_with_whitespace {
807
            change_password_button = change_password_button.on_press(Message::TextSend);
808
        }
809

            
810
        columns = columns.push(
811
            row![
812
                change_password_button,
813
                widget::text_input("", &self.password)
814
                    .secure(!self.password_show)
815
                    .on_input(Message::PasswordChanged)
816
                    .on_paste(Message::PasswordChanged),
817
            ]
818
            .spacing(SPACING),
819
        );
820

            
821
        columns = columns.push(
822
            row![
823
                checkbox(self.password_show).on_toggle(Message::PasswordShow),
824
                text!("{} (8)", t!("show password")),
825
            ]
826
            .spacing(SPACING),
827
        );
828

            
829
        if self.delete_account {
830
            columns = columns.push(
831
                button(text!("{} (9)", t!("REALLY DELETE ACCOUNT")))
832
                    .on_press(Message::DeleteAccount),
833
            );
834
        } else {
835
            columns = columns.push(
836
                button(text!("{} (9)", t!("Delete Account"))).on_press(Message::DeleteAccount),
837
            );
838
        }
839

            
840
        columns = columns.push(button(text!("{} (Esc)", t!("Quit"))).on_press(Message::Leave));
841

            
842
        columns
843
    }
844

            
845
    fn archived_game_reset(&mut self) {
846
        self.archived_game_handle = None;
847
        self.archived_game_selected = None;
848
    }
849

            
850
    #[must_use]
851
    fn board(&self) -> Row<'_, Message> {
852
        let (board, heat_map) = self.board_and_heatmap();
853
        let board_size = board.size();
854
        let board_size_usize: usize = board_size.into();
855
        let d = Dimensions::new(board_size, &self.screen_size);
856
        let letters: Vec<_> = BOARD_LETTERS[..board_size_usize].chars().collect();
857
        let mut game_display = Row::new().spacing(2);
858
        let possible_moves = self.possible_moves();
859

            
860
        let coordinates: bool = self.coordinates.into();
861
        if coordinates {
862
            game_display =
863
                game_display.push(self.numbers(d.letter_size, d.spacing, board_size_usize));
864
        }
865

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

            
869
            if coordinates {
870
                column = self.letter(*letter, column, d.letter_size);
871
            }
872

            
873
            for y in 0..board_size_usize {
874
                let vertex = Vertex {
875
                    size: board.size(),
876
                    x,
877
                    y,
878
                };
879

            
880
                let mut txt = match board.get(&vertex) {
881
                    Space::Attacker => text(&self.chars.attacker),
882
                    Space::Defender => text(&self.chars.defender),
883
                    Space::Empty => {
884
                        if let Some(arrow) = self.draw_arrow(y, x) {
885
                            text(arrow)
886
                        } else if self.captures.contains(&vertex) {
887
                            text(&self.chars.captured)
888
                        } else if vertex.on_restricted_square() {
889
                            text(&self.chars.restricted_square)
890
                        } else {
891
                            text(" ")
892
                        }
893
                    }
894
                    Space::King => text(&self.chars.king),
895
                };
896

            
897
                if let Some((heat_map_from, heat_map_to)) = &heat_map
898
                    && possible_moves.is_some()
899
                {
900
                    if let Some(vertex_from) = self.play_from.as_ref() {
901
                        let space = board.get(vertex_from);
902
                        let turn = Role::from(space);
903
                        if let Some(heat_map_to) = heat_map_to.get(&(turn, *vertex_from)) {
904
                            let heat = heat_map_to[y * board_size_usize + x];
905

            
906
                            if heat == Heat::UnRanked {
907
                                txt = txt.color(Color::from_rgba(0.0, 0.0, 0.0, heat.into()));
908
                            } else {
909
                                let txt_char = match space {
910
                                    Space::Attacker => &self.chars.attacker,
911
                                    Space::Defender => &self.chars.defender,
912
                                    Space::Empty => "",
913
                                    Space::King => &self.chars.king,
914
                                };
915

            
916
                                txt = text(txt_char).color(Color::from_rgba(
917
                                    0.0,
918
                                    0.0,
919
                                    0.0,
920
                                    heat.into(),
921
                                ));
922
                            }
923
                        }
924
                    } else {
925
                        let heat = heat_map_from[y * board_size_usize + x];
926
                        txt = txt.color(Color::from_rgba(0.0, 0.0, 0.0, heat.into()));
927
                    }
928
                }
929

            
930
                txt = txt.font(Font::MONOSPACE).center().size(d.piece_size);
931
                let mut button = button(txt)
932
                    .width(d.board_dimension)
933
                    .height(d.board_dimension);
934

            
935
                match self.board_move(&vertex, possible_moves.as_ref()) {
936
                    Move::From => button = button.on_press(Message::PlayMoveFrom(vertex)),
937
                    Move::To => button = button.on_press(Message::PlayMoveTo(vertex)),
938
                    Move::Revert => button = button.on_press(Message::PlayMoveRevert),
939
                    Move::None => {}
940
                }
941

            
942
                column = column.push(button);
943
            }
944

            
945
            if coordinates {
946
                column = self.letter(*letter, column, d.letter_size);
947
            }
948

            
949
            game_display = game_display.push(column);
950
        }
951

            
952
        if coordinates {
953
            game_display =
954
                game_display.push(self.numbers(d.letter_size, d.spacing, board_size_usize));
955
        }
956

            
957
        game_display
958
    }
959

            
960
    fn board_move(&self, vertex: &Vertex, possible_moves: Option<&LegalMoves>) -> Move {
961
        if let Some(legal_moves) = possible_moves {
962
            if let Some(vertex_from) = self.play_from.as_ref() {
963
                if let Some(vertexes) = legal_moves.moves.get(vertex_from) {
964
                    if vertex == vertex_from {
965
                        Move::Revert
966
                    } else if vertexes.contains(vertex) {
967
                        Move::To
968
                    } else {
969
                        Move::None
970
                    }
971
                } else {
972
                    Move::None
973
                }
974
            } else if legal_moves.moves.contains_key(vertex) {
975
                Move::From
976
            } else {
977
                Move::None
978
            }
979
        } else {
980
            Move::None
981
        }
982
    }
983

            
984
    #[allow(clippy::type_complexity)]
985
    fn board_and_heatmap(
986
        &self,
987
    ) -> (
988
        Board,
989
        Option<(Vec<Heat>, HashMap<(Role, Vertex), Vec<Heat>>)>,
990
    ) {
991
        if let Some(game_handle) = &self.archived_game_handle {
992
            let node = game_handle.boards.here();
993

            
994
            if self.heat_map_display
995
                && let Some(heat_map) = &self.heat_map
996
            {
997
                (node.board.clone(), Some(heat_map.draw(node.turn)))
998
            } else {
999
                (node.board.clone(), None)
            }
        } else {
            let game = self.game.as_ref().expect("we should be in a game");
            (game.board.clone(), None)
        }
    }
    fn game_state(&self, game_id: u128) -> State {
        if let Some(game) = self.games_light.0.get(&game_id) {
            if game.challenge_accepted {
                return State::Spectator;
            }
            if game.attacker.is_none() || game.defender.is_none() {
                if let Some(attacker) = &game.attacker
                    && &self.username == attacker
                {
                    return State::CreatorOnly;
                }
                if let Some(defender) = &game.defender
                    && &self.username == defender
                {
                    return State::CreatorOnly;
                }
            }
            if let (Some(attacker), Some(defender)) = (&game.attacker, &game.defender)
                && (&self.username == attacker || &self.username == defender)
            {
                if let Some(challenger) = &game.challenger.0 {
                    if &self.username != challenger {
                        return State::Creator;
                    }
                } else {
                    return State::Challenger;
                }
            }
        }
        State::Spectator
    }
    fn clear_letters_except(&mut self, letter: char) {
        for l in BOARD_LETTERS_LOWERCASE {
            if l != letter {
                self.press_letters.remove(&l);
            }
        }
    }
    fn clear_numbers_except(&mut self, number: usize) {
        let (board, _) = self.board_and_heatmap();
        let board_size = board.size().into();
        for i in 0..board_size {
            let i = board_size - i;
            if i != number {
                self.press_numbers[i - 1] = false;
            }
        }
    }
    fn create_account(&mut self) {
        if !self.connected_tcp {
            self.send("tcp_connect\n");
            self.connected_tcp = true;
        }
        if self.screen == Screen::Login {
            if !self.text_input.trim().is_empty() {
                let username = self.text_input.clone();
                self.send(&format!(
                    "{VERSION_ID} create_account {username} {}\n",
                    self.password,
                ));
                self.username = username;
            }
            self.text_input.clear();
            self.archived_game_reset();
            handle_error(self.save_client_ron());
        }
    }
    fn delete_account(&mut self) {
        if self.delete_account {
            self.send("delete_account\n");
            self.leave();
            self.delete_account = false;
        } else {
            self.delete_account = true;
        }
    }
    #[allow(clippy::too_many_lines)]
    fn display_tournament(&self) -> Column<'_, Message> {
        let Some(tournament) = &self.tournament else {
            return Column::new();
        };
        let tournament_string = t!("Tournament");
        let row_1 = text(tournament_string.to_string());
        let row_2 = text("-".repeat(tournament_string.len())).font(Font::MONOSPACE);
        let column_1 = column![row_1, row_2];
        let players_len = tournament.players.len();
        if players_len == 0 {
            return Column::new();
        }
        let mut column_rounds = Column::new();
        if let Some(round) = &tournament.groups {
            let mut column_round = Column::new();
            for (i, group) in round.iter().enumerate() {
                let round_title = if tournament.tournament_games.is_empty() && i + 1 == round.len()
                {
                    let winner = t!("Winner");
                    let row_1 = text(winner.to_string());
                    let row_2 = text!("{}", "-".repeat(winner.len())).font(Font::MONOSPACE);
                    column![row_1, row_2]
                } else {
                    let round = t!("Round");
                    let row_1 = text!("{} {}", round.to_string(), i + 1);
                    let row_2 = text!(
                        "{}-{}",
                        "-".repeat(round.len()),
                        "-".repeat((i + 1).to_string().len())
                    )
                    .font(Font::MONOSPACE);
                    column![row_1, row_2]
                };
                column_round = column_round.push(round_title);
                let mut column_groups = Column::new();
                for (j, players) in group.iter().enumerate() {
                    if let Ok(players) = players.lock() {
                        if players.records.iter().last().is_none() {
                            continue;
                        }
                        let mut games_count = 0;
                        let mut column_group = Column::new();
                        if j > 0 {
                            column_group = column_group.push(text("---").font(Font::MONOSPACE));
                        }
                        let mut column_group_vec = Vec::new();
                        for (player, record) in &players.records {
                            games_count += record.games_count();
                            if tournament.tournament_games.is_empty() && i + 1 == round.len() {
                                column_group_vec.push(
                                    text!("{:16} {:10}", player, record.rating.to_string_rounded())
                                        .font(Font::MONOSPACE),
                                );
                            } else {
                                column_group_vec.push(
                                    text!(
                                        "{:16} {:10} {}: {}, {}: {}, {}: {}",
                                        player,
                                        record.rating.to_string_rounded(),
                                        t!("wins"),
                                        record.wins,
                                        t!("losses"),
                                        record.losses,
                                        t!("draws"),
                                        record.draws,
                                    )
                                    .font(Font::MONOSPACE),
                                );
                            }
                        }
                        // If round finished:
                        if games_count / 2 == players.total_games {
                            column_group_vec.clear();
                            let mut records: Vec<_> = players
                                .records
                                .iter()
                                .map(|(name, record)| (name, record.clone()))
                                .collect();
                            records.sort_by(|(_, record_1), (_, record_2)| {
                                record_2.draws.cmp(&record_1.draws)
                            });
                            records.sort_by(|(_, record_1), (_, record_2)| {
                                record_2.wins.cmp(&record_1.wins)
                            });
                            if let Some((_, record_1)) = records.first() {
                                column_group_vec.clear();
                                for (name_2, record_2) in &records {
                                    if record_1.wins == record_2.wins
                                        && record_1.losses == record_2.losses
                                        && record_1.draws == record_2.draws
                                    {
                                        column_group_vec.push(
                                            text!(
                                                "{:16} {:10} {}: {}, {}: {}, {}: {}",
                                                name_2,
                                                record_2.rating.to_string_rounded(),
                                                t!("wins"),
                                                record_2.wins,
                                                t!("losses"),
                                                record_2.losses,
                                                t!("draws"),
                                                record_2.draws,
                                            )
                                            .font(Font::MONOSPACE)
                                            .style(text::success),
                                        );
                                    } else {
                                        column_group_vec.push(
                                            text!(
                                                "{:16} {:10} {}: {}, {}: {}, {}: {}",
                                                name_2,
                                                record_2.rating.to_string_rounded(),
                                                t!("wins"),
                                                record_2.wins,
                                                t!("losses"),
                                                record_2.losses,
                                                t!("draws"),
                                                record_2.draws,
                                            )
                                            .font(Font::MONOSPACE)
                                            .style(text::danger),
                                        );
                                    }
                                }
                            }
                        }
                        for player in column_group_vec {
                            column_group = column_group.push(player);
                        }
                        column_groups = column_groups.push(column_group);
                    }
                }
                column_round = column_round.push(column_groups);
            }
            column_rounds = column_rounds.push(column_round.spacing(SPACING));
        }
        column![column_1, column_rounds.spacing(SPACING)].spacing(SPACING)
    }
    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_view(&self) -> Column<'_, Message> {
        let attacker = radio(
            format!("{} (7)", t!("attacker")),
            Role::Attacker,
            self.game_settings.role_selected,
            Message::RoleSelected,
        );
        let defender = radio(
            format!("{} (8)", t!("defender")),
            Role::Defender,
            self.game_settings.role_selected,
            Message::RoleSelected,
        );
        let rated = LabeledFrame::new(
            text(t!("rated")),
            row![
                text!("(6)"),
                checkbox(self.game_settings.rated.into()).on_toggle(Message::RatedSelected)
            ]
            .padding(PADDING)
            .spacing(SPACING),
        );
        let mut new_game = button(text!("{} (Enter)", t!("New Game")));
        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)", t!("Quit"))).on_press(Message::Leave);
        let size_11x11 = radio(
            "11x11 (9)",
            BoardSize::_11,
            Some(self.game_settings.board_size),
            Message::BoardSizeSelected,
        );
        let size_13x13 = radio(
            "13x13 (0)",
            BoardSize::_13,
            Some(self.game_settings.board_size),
            Message::BoardSizeSelected,
        );
        let row_role = LabeledFrame::new(
            text(t!("role")),
            row![attacker, defender].padding(PADDING).spacing(SPACING),
        );
        let row_board_size = LabeledFrame::new(
            text(t!("board size")),
            row![size_11x11, size_13x13]
                .padding(PADDING)
                .spacing(SPACING),
        );
        let rapid = radio(
            format!("{} (a)", TimeEnum::Rapid),
            TimeEnum::Rapid,
            self.game_settings.time,
            Message::Time,
        );
        let classical = radio(
            format!("{} (b)", TimeEnum::Classical),
            TimeEnum::Classical,
            self.game_settings.time,
            Message::Time,
        );
        let long = radio(
            format!("{} (c)", TimeEnum::Long),
            TimeEnum::Long,
            self.game_settings.time,
            Message::Time,
        );
        let very_long = radio(
            format!("{} (d)", TimeEnum::VeryLong),
            TimeEnum::VeryLong,
            self.game_settings.time,
            Message::Time,
        );
        let infinity = radio(
            format!("{} (e)", TimeEnum::Infinity),
            TimeEnum::Infinity,
            self.game_settings.time,
            Message::Time,
        );
        let row_1 = row![rapid, classical].spacing(SPACING);
        let row_2 = row![long, very_long].spacing(SPACING);
        let row_3 = row![infinity].spacing(SPACING);
        let row_time = LabeledFrame::new(
            text(t!("time")),
            column![row_1, row_2, row_3].padding(PADDING).spacing(3),
        );
        let leave = row![new_game, leave].padding(PADDING).spacing(SPACING);
        column![rated, row_role, row_board_size, row_time, leave]
    }
    fn games_filtered(&mut self) {
        let filtered_games = self.archived_games.iter().filter(|game| {
            let max_rating = f64::max(game.attacker_rating.rating, game.defender_rating.rating);
            let min_rating = f64::min(game.attacker_rating.rating, game.defender_rating.rating);
            min_rating >= self.rating_minimum && max_rating <= self.rating_maximum
        });
        if self.my_games_only {
            self.archived_games_filtered = Some(
                filtered_games
                    .filter(|game| game.attacker == self.username || game.defender == self.username)
                    .cloned()
                    .collect(),
            );
        } else {
            self.archived_games_filtered = Some(filtered_games.cloned().collect());
        }
    }
    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) {
        if let Some(game) = self.games_light_vec.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 if game.challenge_accepted {
                JoinGame::Watch
            } else {
                JoinGame::None
            }
        } 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_string,
            attacker_time,
            defender_string,
            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 {
            if let Some(user) = self.users.get(&self.attacker) {
                attacker_rating = user.rating.to_string_rounded();
            }
            if let Some(user) = self.users.get(&self.defender) {
                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,
            )
        };
        if let Some(user) = self.users.get(&self.attacker) {
            attacker_rating = user.rating.to_string_rounded();
        }
        if let Some(user) = self.users.get(&self.defender) {
            defender_rating = user.rating.to_string_rounded();
        }
        let captured = board.captured();
        let mut row_1 = row![
            text(attacker_string),
            text(attacker_rating).center(),
            text(captured.defender(&self.chars).clone()).font(Font::MONOSPACE),
        ];
        if !self.spectators.contains(attacker_string) {
            row_1 = row_1.push(text(&self.chars.warning).style(text::danger));
        }
        let attacker = container(
            column![
                row_1.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 mut row_2 = row![
            text(defender_string),
            text(defender_rating).center(),
            text(captured.attacker(&self.chars).clone()).font(Font::MONOSPACE),
        ];
        if !self.spectators.contains(defender_string) {
            row_2 = row_2.push(text(&self.chars.warning).style(text::danger));
        }
        let defender = container(
            column![
                row_2.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));
            }
        }
        if self.username.as_str() != attacker_string && self.username.as_str() != defender_string {
            watching = true;
        }
        let mut spectators = Column::new();
        let mut players_length = 0;
        for spectator in &self.spectators {
            if spectator == attacker_string || spectator == defender_string {
                players_length += 1;
                continue;
            }
            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)", t!("Resign"))).on_press(Message::PlayResign);
        let request_draw = button(text!("{} (q)", t!("Request Draw"))).on_press(Message::PlayDraw);
        if !watching {
            if self.my_turn {
                user_area = user_area.push(row![resign, request_draw].spacing(SPACING));
            } else {
                let row = if self.request_draw {
                    column![
                        row![
                            button(text!("{} (r)", t!("Accept Draw")))
                                .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 volume = row![
            text!("{} (- +)", t!("Volume")),
            slider(0..=MAX_VOLUME, self.volume.0, Message::VolumeChanged),
        ]
        .spacing(SPACING);
        user_area = user_area.push(volume);
        let leave = button(text!("{} (Esc)", t!("Leave"))).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)", t!("Heat Map")));
            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() - players_length,
                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::EmailEveryone => {
                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.admin_tournament = false;
                self.connected_tcp = false;
                self.text_input = self.username.clone();
                self.screen = Screen::Login;
                self.email = None;
            }
            Screen::GameReview => {
                self.heat_map = None;
                self.heat_map_display = false;
                self.screen = Screen::Login;
            }
            Screen::Login => {}
        }
    }
    fn my_games_only(&mut self) {
        self.my_games_only = !self.my_games_only;
        self.games_filtered();
        handle_error(self.save_client_ron());
    }
    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(TICK_U))
                    .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(keyboard::Event::KeyPressed { key, modifiers, .. }) => {
                let shift = modifiers.shift();
                match key {
                    Key::Character(ch) if modifiers.control() || modifiers.command() => match ch {
                        ch if *ch == *"a".to_smolstr() => Some(Message::PressA(shift)),
                        ch if *ch == *"b".to_smolstr() => Some(Message::PressB(shift)),
                        // Fixme: ctrl + shift + "c" is copy.
                        ch if *ch == *"c".to_smolstr() && !shift => Some(Message::PressC(shift)),
                        ch if *ch == *"d".to_smolstr() => Some(Message::PressD(shift)),
                        ch if *ch == *"e".to_smolstr() => Some(Message::PressE(shift)),
                        ch if *ch == *"f".to_smolstr() => Some(Message::PressF(shift)),
                        ch if *ch == *"g".to_smolstr() => Some(Message::PressG(shift)),
                        ch if *ch == *"h".to_smolstr() => Some(Message::PressH(shift)),
                        ch if *ch == *"i".to_smolstr() => Some(Message::PressI(shift)),
                        ch if *ch == *"j".to_smolstr() => Some(Message::PressJ(shift)),
                        ch if *ch == *"k".to_smolstr() => Some(Message::PressK(shift)),
                        ch if *ch == *"l".to_smolstr() => Some(Message::PressL(shift)),
                        ch if *ch == *"m".to_smolstr() => Some(Message::PressM(shift)),
                        ch if *ch == *"n".to_smolstr() => Some(Message::PressN(shift)),
                        ch if *ch == *"o".to_smolstr() => Some(Message::PressO(shift)),
                        ch if *ch == *"p".to_smolstr() => Some(Message::PressP(shift)),
                        ch if *ch == *"q".to_smolstr() => Some(Message::PressQ(shift)),
                        ch if *ch == *"r".to_smolstr() => Some(Message::PressR(shift)),
                        ch if *ch == *"s".to_smolstr() => Some(Message::PressS(shift)),
                        ch if *ch == *"t".to_smolstr() => Some(Message::PressT(shift)),
                        ch if *ch == *"u".to_smolstr() => Some(Message::PressU(shift)),
                        // Fixme: ctrl + shift + "v" is paste.
                        ch if *ch == *"v".to_smolstr() && !shift => Some(Message::PressV(shift)),
                        ch if *ch == *"w".to_smolstr() => Some(Message::PressW(shift)),
                        ch if *ch == *"x".to_smolstr() => Some(Message::PressX(shift)),
                        ch if *ch == *"y".to_smolstr() => Some(Message::PressY(shift)),
                        ch if *ch == *"z".to_smolstr() => Some(Message::PressZ(shift)),
                        ch if *ch == *"1".to_smolstr() => Some(Message::Press1),
                        ch if *ch == *"2".to_smolstr() => Some(Message::Press2),
                        ch if *ch == *"3".to_smolstr() => Some(Message::Press3),
                        ch if *ch == *"4".to_smolstr() => Some(Message::Press4),
                        ch if *ch == *"5".to_smolstr() => Some(Message::Press5),
                        ch if *ch == *"6".to_smolstr() => Some(Message::Press6),
                        ch if *ch == *"7".to_smolstr() => Some(Message::Press7),
                        ch if *ch == *"8".to_smolstr() => Some(Message::Press8),
                        ch if *ch == *"9".to_smolstr() => Some(Message::Press9),
                        ch if *ch == *"0".to_smolstr() => Some(Message::Press0),
                        ch if *ch == *"-".to_smolstr() => Some(Message::PressMinus),
                        ch if (*ch == *"=".to_smolstr() && shift) || *ch == *"+".to_smolstr() => {
                            Some(Message::PressPlus)
                        }
                        _ => None,
                    },
                    Key::Named(name) => match name {
                        Named::Enter => Some(Message::PressEnter),
                        Named::Tab if shift => Some(Message::FocusPrevious),
                        Named::Tab => Some(Message::FocusNext),
                        Named::ArrowUp => Some(Message::ReviewGameBackwardAll),
                        Named::ArrowLeft => Some(Message::ReviewGameBackward),
                        Named::ArrowRight if shift => Some(Message::ReviewGameChildNext),
                        Named::ArrowRight => Some(Message::ReviewGameForward),
                        Named::ArrowDown => Some(Message::ReviewGameForwardAll),
                        Named::Escape => Some(Message::Leave),
                        _ => 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::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::DateCancel => self.tournament_date_show_picker = false,
            Message::DateChoose => self.tournament_date_show_picker = true,
            Message::DateSubmit(date) => {
                self.send(&format!("tournament_date {date}T00:00:00Z\n"));
                self.tournament_date = date;
                self.tournament_date_show_picker = false;
            }
            Message::Coordinates(_coordinates) => self.coordinates(),
            Message::DeleteAccount => self.delete_account(),
            Message::EmailChanged(email) => self.email_input = email,
            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.game_id = 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());
                self.locale_selected = Some(locale);
                handle_error(self.save_client_ron());
            }
            Message::MyGamesOnly(_selected) => {
                self.my_games_only();
            }
            Message::OpenUrl(string) => open_url(&string),
            Message::GameResume(id) => self.resume(id),
            Message::GameSubmit => {
                self.game_submit();
                self.active_tab = TabId::Games;
            }
            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::Games if self.active_tab == TabId::GameNew => self.game_submit(),
                Screen::Login => self.login(),
                Screen::EmailEveryone | Screen::Game | Screen::Games | Screen::GameReview => {}
            },
            Message::PressA(shift) => match self.screen {
                Screen::Game | Screen::GameReview => {
                    self.press_letter('a');
                    self.press_letter_and_number();
                }
                Screen::Games if self.active_tab == TabId::Games => self.join_game_press(0, shift),
                Screen::Games if self.active_tab == TabId::GameNew => {
                    self.game_settings.time = Some(TimeEnum::Rapid);
                }
                Screen::Login => self.review_game(),
                Screen::EmailEveryone | Screen::Games => {}
            },
            Message::PressB(shift) => match self.screen {
                Screen::Game | Screen::GameReview => {
                    self.press_letter('b');
                    self.press_letter_and_number();
                }
                Screen::Games if self.active_tab == TabId::Games => self.join_game_press(1, shift),
                Screen::Games if self.active_tab == TabId::GameNew => {
                    self.game_settings.time = Some(TimeEnum::Classical);
                }
                Screen::EmailEveryone | Screen::Games => {}
                Screen::Login => {
                    self.rating_min();
                    self.games_filtered();
                    handle_error(self.save_client_ron());
                }
            },
            Message::PressC(shift) => match self.screen {
                Screen::Game | Screen::GameReview => {
                    self.press_letter('c');
                    self.press_letter_and_number();
                }
                Screen::Games if self.active_tab == TabId::Games => self.join_game_press(2, shift),
                Screen::Games if self.active_tab == TabId::GameNew => {
                    self.game_settings.time = Some(TimeEnum::Long);
                }
                Screen::EmailEveryone | Screen::Games => {}
                Screen::Login => {
                    self.rating_max();
                    self.games_filtered();
                    handle_error(self.save_client_ron());
                }
            },
            Message::PressD(shift) => match self.screen {
                Screen::Game | Screen::GameReview => {
                    self.press_letter('d');
                    self.press_letter_and_number();
                }
                Screen::Games if self.active_tab == TabId::Games => self.join_game_press(3, shift),
                Screen::Games if self.active_tab == TabId::GameNew => {
                    self.game_settings.time = Some(TimeEnum::VeryLong);
                }
                Screen::EmailEveryone | Screen::Games | Screen::Login => {}
            },
            Message::PressE(shift) => match self.screen {
                Screen::Game | Screen::GameReview => {
                    self.press_letter('e');
                    self.press_letter_and_number();
                }
                Screen::Games if self.active_tab == TabId::Games => self.join_game_press(4, shift),
                Screen::Games if self.active_tab == TabId::GameNew => {
                    self.game_settings.time = Some(TimeEnum::Infinity);
                }
                Screen::EmailEveryone | Screen::Games | Screen::Login => {}
            },
            Message::PressF(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::Login => {}
                Screen::Game | Screen::GameReview => self.coordinates(),
                Screen::Games => self.join_game_press(13, shift),
            },
            Message::PressO(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Login => {}
                Screen::Game | Screen::GameReview => self.sound_muted(),
                Screen::Games => self.join_game_press(14, shift),
            },
            Message::PressP(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Login => {}
                Screen::Game => self.resign(),
                Screen::Games => self.join_game_press(1, shift),
                Screen::GameReview => self.estimate_score(),
            },
            Message::PressQ(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Login => {}
                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::EmailEveryone | Screen::GameReview | Screen::Login => {}
                Screen::Game => {
                    if self.request_draw {
                        self.send(&format!("draw {} {}\n", self.game_id, Draw::Accept));
                    }
                }
                Screen::Games => self.join_game_press(17, shift),
            },
            Message::PressS(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(18, shift),
            },
            Message::PressT(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(19, shift),
            },
            Message::PressU(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(20, shift),
            },
            Message::PressV(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(21, shift),
            },
            Message::PressW(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(22, shift),
            },
            Message::PressX(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(23, shift),
            },
            Message::PressY(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(24, shift),
            },
            Message::PressZ(shift) => match self.screen {
                Screen::EmailEveryone | Screen::Game | Screen::GameReview | Screen::Login => {}
                Screen::Games => self.join_game_press(25, shift),
            },
            Message::Press1 => match self.screen {
                Screen::Login => self.toggle_show_password(),
                Screen::EmailEveryone => {}
                Screen::Games => self.active_tab = TabId::Games,
                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::EmailEveryone => {}
                Screen::Games => self.active_tab = TabId::GameNew,
                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::EmailEveryone => {}
                Screen::Games => self.active_tab = TabId::Tournament,
                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::EmailEveryone => {}
                Screen::Games => self.active_tab = TabId::AccountSettings,
                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::EmailEveryone => {}
                Screen::Games => self.active_tab = TabId::Users,
                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::EmailEveryone => {}
                Screen::Games => match self.active_tab {
                    TabId::AccountSettings => self.reset_email(),
                    TabId::GameNew => self.game_settings.rated = !self.game_settings.rated,
                    TabId::Games => self.send("archived_games\n"),
                    TabId::Tournament => open_url("https://hnefatafl.org/tournaments.html"),
                    TabId::Users => {}
                },
                Screen::Login => self.change_theme(Theme::Dark),
                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::EmailEveryone => {}
                Screen::Games => match self.active_tab {
                    TabId::AccountSettings => {
                        self.send(&format!("change_password {}\n", self.password));
                    }
                    TabId::GameNew => self.game_settings.role_selected = Some(Role::Attacker),
                    TabId::Games | TabId::Users => {}
                    TabId::Tournament => self.send("join_tournament\n"),
                },
                Screen::Login => self.change_theme(Theme::Light),
                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::EmailEveryone => {}
                Screen::Games => match self.active_tab {
                    TabId::AccountSettings => self.toggle_show_password(),
                    TabId::GameNew => self.game_settings.role_selected = Some(Role::Defender),
                    TabId::Games => self.my_games_only(),
                    TabId::Tournament => self.send("leave_tournament\n"),
                    TabId::Users => {}
                },
                Screen::Login => self.change_theme(Theme::Tol),
                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::EmailEveryone => {}
                Screen::Games => match self.active_tab {
                    TabId::AccountSettings => self.delete_account(),
                    TabId::GameNew => self.game_settings.board_size = BoardSize::_11,
                    TabId::Games | TabId::Users => self.users_sort_by = SortBy::Rating,
                    TabId::Tournament => {}
                },
                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::EmailEveryone => {}
                Screen::Games => match self.active_tab {
                    TabId::AccountSettings | TabId::Tournament => {}
                    TabId::GameNew => self.game_settings.board_size = BoardSize::_13,
                    TabId::Games | TabId::Users => self.users_sort_by = SortBy::Name,
                },
                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::PressMinus => match self.screen {
                Screen::EmailEveryone | Screen::Games | Screen::Login => {}
                Screen::Game | Screen::GameReview => {
                    self.volume.0 = self.volume.0.saturating_sub(1);
                }
            },
            Message::PressPlus => match self.screen {
                Screen::EmailEveryone | Screen::Games | Screen::Login => {}
                Screen::Game | Screen::GameReview => {
                    self.volume.0 = self.volume.0.saturating_add(1);
                }
            },
            Message::RatingMaximum => {
                self.rating_max();
                self.games_filtered();
                handle_error(self.save_client_ron());
            }
            Message::RatingMaximumChanged(rating) => {
                self.rating_maximum = rating;
                self.games_filtered();
                handle_error(self.save_client_ron());
            }
            Message::RatingMinimum => {
                self.rating_min();
                self.games_filtered();
                handle_error(self.save_client_ron());
            }
            Message::RatingMinimumChanged(rating) => {
                self.rating_minimum = rating;
                self.games_filtered();
                handle_error(self.save_client_ron());
            }
            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::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::TcpConnectFailed => {
                self.error_persistent
                    .push(t!("The TCP connection failed.").to_string());
            }
            Message::TabSelected(tab) => self.active_tab = tab,
            Message::TcpDisconnect => self.connected_tcp = false,
            Message::TournamentJoin => self.send("join_tournament\n"),
            Message::TournamentLeave => self.send("leave_tournament\n"),
            Message::TournamentStart => self.send("tournament_start\n"),
            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("admin_tournament") => self.admin_tournament = true,
                            Some("display_games") => {
                                self.games_light.0.clear();
                                self.games_light_vec.clear();
                                let games: Vec<&str> = text.collect();
                                let (chunks, []) = games.as_chunks::<12>() else {
                                    panic!("chunks is an exact multiple of 12");
                                };
                                for chunks in chunks {
                                    let game = ServerGameLight::try_from(chunks)
                                        .expect("the value should be a valid ServerGameLight");
                                    self.games_light.0.insert(game.id, game.clone());
                                    self.games_light_vec.push(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") => {
                                self.users.clear();
                                let users: Vec<&str> = text.collect();
                                let (chunks, []) = users.as_chunks::<6>() else {
                                    panic!("chunks is an exact multiple of 6");
                                };
                                for user in chunks {
                                    let user: User = user.into();
                                    self.users.insert(user.name.clone(), user);
                                }
                            }
                            Some("display_users_admin") => {
                                let accounts: Accounts = ron::from_str(
                                    text.next().expect("there should be a nex value"),
                                )
                                .expect("we should be able to deserialize accounts");
                                self.accounts = accounts;
                            }
                            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;
                                    }
                                    if !self.sound_muted {
                                        let volume = self.volume.volume();
                                        thread::spawn(move || {
                                            let mut stream =
                                                rodio::DeviceSinkBuilder::open_default_sink()?;
                                            let cursor = Cursor::new(SOUND_GAME_OVER);
                                            let sound = rodio::play(stream.mixer(), cursor)?;
                                            sound.set_volume(volume);
                                            sound.sleep_until_end();
                                            stream.log_on_drop(false);
                                            Ok::<(), anyhow::Error>(())
                                        });
                                    }
                                }
                            }
                            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 {
                                    let volume = self.volume.volume();
                                    thread::spawn(move || {
                                        let mut stream =
                                            rodio::DeviceSinkBuilder::open_default_sink()?;
                                        let cursor = Cursor::new(SOUND_GAME_OVER);
                                        let sound = rodio::play(stream.mixer(), cursor)?;
                                        sound.set_volume(volume);
                                        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 = Timestamp::now().as_millisecond();
                                                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 = Timestamp::now().as_millisecond();
                                                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("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 = Timestamp::now().as_millisecond();
                                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_0") => {
                                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");
                        if id != self.game_id {
                            return Task::none();
                        }
                        // 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::EmailEveryone => {
                        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 => match self.active_tab {
                        TabId::Games if !self.text_input.trim().is_empty() => {
                            self.text_input.push('\n');
                            self.send(&format!("text {}", self.text_input));
                        }
                        TabId::AccountSettings => {
                            self.send(&format!("change_password {}\n", self.password));
                        }
                        _ => {}
                    },
                    Screen::GameReview | Screen::Login => {}
                }
                self.text_input.clear();
            }
            Message::TextSendEmail => {
                self.error_email = None;
                self.send(&format!("email {}\n", self.email_input));
                self.email_input.clear();
            }
            Message::TextSendEmailCode => {
                self.error_email = None;
                self.send(&format!("email_code {}\n", self.email_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 = Timestamp::now().as_millisecond();
                    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 -= TICK;
                                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 -= TICK;
                                if time.milliseconds_left < 0 {
                                    time.milliseconds_left = 0;
                                }
                            }
                        }
                    }
                }
            }
            Message::Time(time) => self.game_settings.time = Some(time),
            Message::Tournaments => open_url("https://hnefatafl.org/tournaments.html"),
            Message::TournamentDelete => self.send("tournament_delete\n"),
            Message::TournamentTreeDelete => self.send("tournament_groups_delete\n"),
            Message::UsersSortedBy(sort_by) => self.users_sort_by = sort_by,
            Message::VolumeChanged(volume) => self.volume.0 = volume,
            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 accounts_sorted(&self) -> Vec<(String, Account)> {
        let mut accounts: Vec<_> = self.accounts.clone().0.into_iter().collect();
        match self.users_sort_by {
            SortBy::Name => {
                accounts.sort_by(|(_, a_account), (_, b_account)| {
                    b_account
                        .rating
                        .rating
                        .partial_cmp(&a_account.rating.rating)
                        .expect("The number should be comparable.")
                });
                accounts.sort_by(|(a_name, _), (b_name, _)| a_name.cmp(b_name));
            }
            SortBy::Rating => {
                accounts.sort_by(|(a_name, _), (b_name, _)| a_name.cmp(b_name));
                accounts.sort_by(|(_, a_account), (_, b_account)| {
                    b_account
                        .rating
                        .rating
                        .partial_cmp(&a_account.rating.rating)
                        .expect("The number should be comparable.")
                });
            }
        }
        accounts
    }
    #[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);
        for (i, game) in self.games_light_vec.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_str) = &game.attacker {
                let mut attacker = if self.admin {
                    if let Some(account) = self.accounts.0.get(attacker_str) {
                        text!("{attacker_str} ({})", account.rating.to_string_rounded())
                    } else {
                        text(attacker_str)
                    }
                } else {
                    if let Some(user) = self.users.get(attacker_str) {
                        text!("{attacker_str} ({})", user.rating.to_string_rounded())
                    } else {
                        text(attacker_str)
                    }
                };
                if game.challenge_accepted
                    && let Challenger(Some(name)) = &game.challenger
                    && name == "A"
                {
                    attacker = attacker.style(text::success);
                }
                attackers.push(attacker)
            } else {
                attackers.push(text(t!("none")))
            };
            defenders = if let Some(defender_str) = &game.defender {
                let mut defender = if self.admin {
                    if let Some(account) = self.accounts.0.get(defender_str) {
                        text!("{defender_str} ({})", account.rating.to_string_rounded())
                    } else {
                        text(defender_str)
                    }
                } else {
                    if let Some(user) = self.users.get(defender_str) {
                        text!("{defender_str} ({})", user.rating.to_string_rounded())
                    } else {
                        text(defender_str)
                    }
                };
                if game.challenge_accepted
                    && let Challenger(Some(name)) = &game.challenger
                    && name == "D"
                {
                    defender = defender.style(text::success);
                }
                defenders.push(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}", t!("Cancel"))).on_press(Message::GameCancel(id)),
                    );
                }
                JoinGame::Join => {
                    buttons_row = buttons_row
                        .push(button(text!("{}{i}", t!("Join"))).on_press(Message::GameJoin(id)));
                }
                JoinGame::None => {}
                JoinGame::Resume => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", t!("Resume"))).on_press(Message::GameResume(id)),
                    );
                }
                JoinGame::Watch => {
                    buttons_row = buttons_row
                        .push(button(text!("{}{i}", t!("Watch"))).on_press(Message::GameWatch(id)));
                }
            }
            match self.game_state(id) {
                State::Challenger | State::Spectator => {}
                State::Creator => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", t!("Accept"))).on_press(Message::GameAccept(id)),
                    );
                    buttons_row = buttons_row.push(
                        button(text!("{}{}", t!("Decline"), i.to_ascii_uppercase()))
                            .on_press(Message::GameDecline(id)),
                    );
                }
                State::CreatorOnly => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", t!("Cancel"))).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!("time");
        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 games_view(&self) -> Column<'_, Message> {
        let username = row![text!("{}: {}", t!("username"), &self.username)].spacing(SPACING);
        let username = container(username)
            .padding(PADDING / 2)
            .style(container::bordered_box);
        let my_games_text = text!("{} (8)", t!("My Games Only")).center();
        let my_games = checkbox(self.my_games_only).on_toggle(Message::MyGamesOnly);
        let get_archived_games =
            button(text!("{} (6)", t!("Get Archived Games"))).on_press(Message::ArchivedGamesGet);
        let quit = button(text!("{} (Esc)", t!("Quit"))).on_press(Message::Leave);
        let mut middle = row![get_archived_games, quit].spacing(SPACING);
        if self.admin {
            middle = middle.push(button("Email Everyone").on_press(Message::EmailEveryone));
        }
        let username = row![username, my_games, my_games_text].spacing(SPACING);
        let user_area = self.user_area(false);
        column![middle, username, user_area]
            .spacing(SPACING)
            .padding(PADDING)
    }
    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();
        let volume = self.volume.volume();
        thread::spawn(move || {
            let mut stream = rodio::DeviceSinkBuilder::open_default_sink()?;
            let cursor = if capture {
                Cursor::new(SOUND_CAPTURE)
            } else {
                Cursor::new(SOUND_MOVE)
            };
            let sound = rodio::play(stream.mixer(), cursor)?;
            sound.set_volume(volume);
            sound.sleep_until_end();
            stream.log_on_drop(false);
            Ok::<(), anyhow::Error>(())
        });
    }
    fn rating_max(&mut self) {
        let mut max_rating = 0.0;
        for game in &self.archived_games {
            let new_max_rating = f64::max(game.attacker_rating.rating, game.defender_rating.rating);
            if new_max_rating > max_rating {
                max_rating = new_max_rating;
            }
        }
        if max_rating == 0.0 {
            self.rating_maximum = MAX_RATING;
        } else {
            self.rating_maximum = max_rating.round_ties_even() + 1.0;
        }
    }
    fn rating_min(&mut self) {
        let mut min_rating = MAX_RATING;
        for game in &self.archived_games {
            let new_min_rating = f64::min(game.attacker_rating.rating, game.defender_rating.rating);
            if new_min_rating < min_rating {
                min_rating = new_min_rating;
            }
        }
        if min_rating == MAX_RATING {
            self.rating_minimum = 0.0;
        } else {
            self.rating_minimum = min_rating.round_ties_even() - 1.0;
        }
    }
    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());
    }
    #[allow(
        clippy::cast_precision_loss,
        clippy::similar_names,
        clippy::too_many_lines
    )]
    #[must_use]
    fn users(&self, show_logged_out_users: bool) -> Scrollable<'_, Message> {
        if self.admin {
            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();
            let mut emails = Column::new();
            let mut emails_sent = Column::new();
            let mut send_emails = Column::new();
            let mut creation_dates = Column::new();
            let mut last_logged_in = Column::new();
            for (name, account) in self.accounts_sorted() {
                if show_logged_out_users || account.logged_in.is_some() {
                    let wins_number = account.wins as f64;
                    let mut win_percentage = wins_number / (wins_number + account.losses as f64);
                    win_percentage *= 100.0;
                    win_percentage = win_percentage.round_ties_even();
                    ratings = ratings.push(text(account.rating.to_string_rounded()));
                    usernames = usernames.push(text(name));
                    wins = wins.push(text(account.wins));
                    losses = losses.push(text(account.losses));
                    draws = draws.push(text(account.draws));
                    win_percents = win_percents.push(text!("{}", win_percentage));
                    emails = if let Some(email) = &account.email {
                        emails.push(text(email.address.clone()))
                    } else {
                        emails.push(text(""))
                    };
                    if let Ok(timestamp) = Timestamp::from_second(account.email_sent) {
                        emails_sent =
                            emails_sent.push(text(timestamp.strftime("%Y-%m-%d").to_string()));
                    } else {
                        emails_sent = emails_sent.push(text(""));
                    }
                    send_emails = send_emails.push(text(account.send_emails));
                    let date = account.creation_date.0.strftime("%Y-%m-%d").to_string();
                    creation_dates = creation_dates.push(text(date));
                    let date = account
                        .last_logged_in
                        .0
                        .strftime("%Y-%m-%d %H:%M:%S %z")
                        .to_string();
                    last_logged_in = if account.logged_in.is_some() {
                        last_logged_in.push(text(date).style(text::success))
                    } else {
                        last_logged_in.push(text(date))
                    };
                }
            }
            let rating = t!("rating");
            let mut button_1 = button(text("(9)").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("(0)").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);
            let email = "email".to_string();
            let hyphens_count = email.chars().count();
            let emails = column![
                text(email),
                text("-".repeat(hyphens_count)).font(Font::MONOSPACE),
                emails
            ]
            .padding(PADDING);
            let email_sent = "email sent".to_string();
            let hyphens_count = email_sent.chars().count();
            let emails_sent = column![
                text(email_sent),
                text("-".repeat(hyphens_count)).font(Font::MONOSPACE),
                emails_sent
            ]
            .padding(PADDING);
            let send_email = "send emails".to_string();
            let hyphens_count = send_email.chars().count();
            let send_emails = column![
                text(send_email),
                text("-".repeat(hyphens_count)).font(Font::MONOSPACE),
                send_emails
            ]
            .padding(PADDING);
            let creation_date = "creation".to_string();
            let hyphens_count = creation_date.chars().count();
            let creation_dates = column![
                text(creation_date),
                text("-".repeat(hyphens_count)).font(Font::MONOSPACE),
                creation_dates
            ]
            .padding(PADDING);
            let last_logged_in_ = "logged in".to_string();
            let hyphens_count = last_logged_in_.chars().count();
            let last_logged_in = column![
                text(last_logged_in_),
                text("-".repeat(hyphens_count)).font(Font::MONOSPACE),
                last_logged_in
            ]
            .padding(PADDING);
            let mut rows = row![ratings, usernames, wins, losses, draws, win_percents,];
            if show_logged_out_users {
                rows = rows.push(emails);
                rows = rows.push(emails_sent);
                rows = rows.push(send_emails);
                rows = rows.push(creation_dates);
                rows = rows.push(last_logged_in);
            }
            scrollable(rows).spacing(SPACING)
        } else {
            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 show_logged_out_users || user.logged_in {
                    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 = if user.logged_in && show_logged_out_users {
                        usernames.push(text(user.name).style(text::success))
                    } else {
                        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("(9)").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("(0)").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(false);
        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::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)", t!("Quit"))).on_press(Message::Leave);
                let mut column = column![
                    subject,
                    text("From: Hnefatafl Org <noreply@hnefatafl.org>"),
                    text("Content-Type: text/plain; charset=utf-8"),
                    text("Content-Transfer-Encoding: 7bit"),
                    text!("Date: {}", Timestamp::now().strftime("%F %T UTC")),
                    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::Games => Tabs::new(Message::TabSelected)
                .push(
                    TabId::Games,
                    iced_aw::TabLabel::Text(format!("{} (1)", t!("Games"))),
                    self.games_view(),
                )
                .push(
                    TabId::GameNew,
                    iced_aw::TabLabel::Text(format!("{} (2)", t!("Create Game"))),
                    self.game_new_view(),
                )
                .push(
                    TabId::Tournament,
                    iced_aw::TabLabel::Text(format!("{} (3)", t!("Tournament"))),
                    self.tournament_view(),
                )
                .push(
                    TabId::AccountSettings,
                    iced_aw::TabLabel::Text(format!("{} (4)", t!("Account Settings"))),
                    self.account_settings_view(),
                )
                .push(
                    TabId::Users,
                    iced_aw::TabLabel::Text(format!("{} (5)", t!("Users"))),
                    column![
                        button(text!("{} (Esc)", t!("Quit"))).on_press(Message::Leave),
                        self.users(true)
                    ]
                    .padding(PADDING)
                    .spacing(SPACING),
                )
                .height(Length::Shrink)
                .width(Length::Fill)
                .set_active_tab(&self.active_tab)
                .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)", t!("Login")));
                if !self.password_ends_with_whitespace {
                    login = login.on_press(Message::TextSendLogin);
                }
                let mut create_account = button(text!("{} (4)", t!("Create Account")));
                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)", t!("Reset Password")));
                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 minimum_rating = number_input(
                    &self.rating_minimum,
                    0.0..=self.rating_maximum,
                    Message::RatingMinimumChanged,
                );
                let maximum_rating = number_input(
                    &self.rating_maximum,
                    self.rating_minimum..=MAX_RATING,
                    Message::RatingMaximumChanged,
                );
                let mut review_game = button(text!("{} (a)", t!("Review Game")));
                if self.archived_game_selected.is_some() {
                    review_game = review_game.on_press(Message::ReviewGame);
                }
                let review_game = row![
                    review_game,
                    button(text!("{} (b)", t!("Minimum Rating"))).on_press(Message::RatingMinimum),
                    minimum_rating,
                    button(text!("{} (c)", t!("Maximum Rating"))).on_press(Message::RatingMaximum),
                    maximum_rating
                ]
                .spacing(SPACING);
                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 quit = button(text!("{} (Esc)", t!("Quit"))).on_press(Message::Leave);
                let buttons_1 = row![login, create_account, reset_password, quit].spacing(SPACING);
                let review_game_pick = pick_list(
                    archived_games,
                    self.archived_game_selected.clone(),
                    Message::ArchivedGameSelected,
                )
                .placeholder(t!("Archived Games"));
                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, self.locale_selected, Message::LocaleSelected),
                ];
                let theme = if self.theme == Theme::Light {
                    row![
                        button(text!("{} (6)", t!("Dark")))
                            .on_press(Message::ChangeTheme(Theme::Dark)),
                        button(text!("{} (7)", t!("Light"))),
                        button(text("Tol (8)")).on_press(Message::ChangeTheme(Theme::Tol)),
                    ]
                    .spacing(SPACING)
                } else if self.theme == Theme::Dark {
                    row![
                        button(text!("{} (6)", t!("Dark"))),
                        button(text!("{} (7)", t!("Light")))
                            .on_press(Message::ChangeTheme(Theme::Light)),
                        button(text("Tol (8)")).on_press(Message::ChangeTheme(Theme::Tol)),
                    ]
                    .spacing(SPACING)
                } else {
                    row![
                        button(text!("{} (6)", t!("Dark")))
                            .on_press(Message::ChangeTheme(Theme::Dark)),
                        button(text!("{} (7)", t!("Light")))
                            .on_press(Message::ChangeTheme(Theme::Light)),
                        button(text("Tol (8)")),
                    ]
                    .spacing(SPACING)
                };
                let theme = LabeledFrame::new(text(t!("Theme")), theme);
                let discord = button(text!("Discord (9)")).on_press(Message::OpenUrl(
                    "https://discord.gg/h56CAHEBXd".to_string(),
                ));
                let website = button(text!("{} (0)", t!("Rules"))).on_press(Message::OpenUrl(
                    "https://hnefatafl.org/rules.html".to_string(),
                ));
                let websites = row![discord, website].spacing(SPACING);
                let websites = LabeledFrame::new(text(t!("Websites")), websites);
                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 at maximum 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,
                    row![theme, websites].spacing(SPACING),
                    locale,
                    review_game,
                    review_game_pick,
                    help_text,
                    help_text_2,
                    help_text_3,
                    help_text_4,
                    error,
                    error_persistent
                ]
                .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,
            rating_maximum: self.rating_maximum,
            rating_minimum: self.rating_minimum,
            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 tournament_view(&self) -> Scrollable<'_, Message> {
        let mut column = Column::new().padding(PADDING).spacing(SPACING);
        if self.admin_tournament {
            let date_button = Button::new(text("Tournament Date")).on_press(Message::DateChoose);
            let date_picker = date_picker(
                self.tournament_date_show_picker,
                self.tournament_date,
                date_button,
                Message::DateCancel,
                Message::DateSubmit,
            );
            let mut delete_button = button("Delete Tournament");
            if self.tournament.is_some() {
                delete_button = delete_button.on_press(Message::TournamentDelete);
            }
            let row = row![date_picker, 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)", t!("Quit"))).on_press(Message::Leave));
            return scrollable(column).spacing(SPACING);
        };
        let mut date = Row::new().spacing(SPACING);
        let start_date = t!("Tournament Start Date");
        date = date.push(text!(
            "{start_date}: {}",
            tournament.date.strftime("%F %T UTC")
        ));
        let button_0 =
            button(text!("{} (6)", t!("Tournaments Described"))).on_press(Message::Tournaments);
        let mut button_1 = button(text!("{} (7)", t!("Join Tournament")));
        let mut button_2 = button(text!("{} (8)", t!("Leave Tournament")));
        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_0,
            button_1,
            button_2,
            button(text!("{} (Esc)", t!("Quit"))).on_press(Message::Leave),
        ]
        .spacing(SPACING);
        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(LabeledFrame::new(text(t!("Players")), players));
        if self.admin_tournament {
            let mut delete_button = button("Delete Tournament Tree");
            if let Some(tournament) = &self.tournament
                && tournament.groups.is_some()
            {
                delete_button = delete_button.on_press(Message::TournamentTreeDelete);
            }
            let mut start_tournament = button("Start Tournament");
            if let Some(tournament) = &self.tournament {
                if tournament.groups.is_none() {
                    start_tournament = start_tournament.on_press(Message::TournamentStart);
                }
            } else {
                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)
    }
    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
    }
}