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
// Fixme: we reuse the email when you log out ans switch accounts.
24

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

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

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

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

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

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

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

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

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

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

            
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
fn i18n_buttons() -> HashMap<String, String> {
159
    let mut strings = HashMap::new();
160

            
161
    strings.insert("Login".to_string(), t!("Login").to_string());
162
    strings.insert(
163
        "Create Account".to_string(),
164
        t!("Create Account").to_string(),
165
    );
166
    strings.insert(
167
        "Reset Password".to_string(),
168
        t!("Reset Password").to_string(),
169
    );
170
    strings.insert("Leave".to_string(), t!("Leave").to_string());
171
    strings.insert("Quit".to_string(), t!("Quit").to_string());
172
    strings.insert("Dark".to_string(), t!("Dark").to_string());
173
    strings.insert("Light".to_string(), t!("Light").to_string());
174
    strings.insert("Create Game".to_string(), t!("Create Game").to_string());
175
    strings.insert("Users".to_string(), t!("Users").to_string());
176
    strings.insert(
177
        "Account Settings".to_string(),
178
        t!("Account Settings").to_string(),
179
    );
180
    strings.insert("Rules".to_string(), t!("Rules").to_string());
181
    strings.insert("Reset Email".to_string(), t!("Reset Email").to_string());
182
    strings.insert(
183
        "Change Password".to_string(),
184
        t!("Change Password").to_string(),
185
    );
186
    strings.insert(
187
        "Delete Account".to_string(),
188
        t!("Delete Account").to_string(),
189
    );
190
    strings.insert(
191
        "REALLY DELETE ACCOUNT".to_string(),
192
        t!("REALLY DELETE ACCOUNT").to_string(),
193
    );
194
    strings.insert("New Game".to_string(), t!("New Game").to_string());
195
    strings.insert("Accept".to_string(), t!("Accept").to_string());
196
    strings.insert("Decline".to_string(), t!("Decline").to_string());
197
    strings.insert("Watch".to_string(), t!("Watch").to_string());
198
    strings.insert("Join".to_string(), t!("Join").to_string());
199
    strings.insert("Resume".to_string(), t!("Resume").to_string());
200
    strings.insert("Resign".to_string(), t!("Resign").to_string());
201
    strings.insert("Request Draw".to_string(), t!("Request Draw").to_string());
202
    strings.insert("Accept Draw".to_string(), t!("Accept Draw").to_string());
203
    strings.insert("Review Game".to_string(), t!("Review Game").to_string());
204
    strings.insert(
205
        "Get Archived Games".to_string(),
206
        t!("Get Archived Games").to_string(),
207
    );
208
    strings.insert("Heat Map".to_string(), t!("Heat Map").to_string());
209
    strings.insert("Cancel".to_string(), t!("Cancel").to_string());
210
    strings.insert("Tournament".to_string(), t!("Tournament").to_string());
211
    strings.insert(
212
        "Join Tournament".to_string(),
213
        t!("Join Tournament").to_string(),
214
    );
215
    strings.insert(
216
        "Leave Tournament".to_string(),
217
        t!("Leave Tournament").to_string(),
218
    );
219
    strings.insert(
220
        "Tournaments Described".to_string(),
221
        t!("Tournaments Described").to_string(),
222
    );
223

            
224
    strings
225
}
226

            
227
#[allow(clippy::too_many_lines)]
228
fn init_client() -> Client {
229
    let archived_games_file = data_file(ARCHIVED_GAMES_FILE);
230
    let user_file = data_file(USER_CONFIG_FILE);
231
    let mut error = Vec::new();
232

            
233
    let mut client: Client = match &fs::read_to_string(&user_file) {
234
        Ok(string) => match ron::from_str(string) {
235
            Ok(client) => client,
236
            Err(err) => {
237
                error.push(format!(
238
                    "Error parsing the ron file {}: {err}",
239
                    user_file.display()
240
                ));
241
                Client::default()
242
            }
243
        },
244
        Err(err) => {
245
            if err.kind() == ErrorKind::NotFound {
246
                error.push(format!(
247
                    "Unable to find User Configuration file: {}",
248
                    user_file.display()
249
                ));
250
                Client::default()
251
            } else {
252
                error.push(format!(
253
                    "Error opening the file {}: {err}",
254
                    user_file.display()
255
                ));
256
                Client::default()
257
            }
258
        }
259
    };
260

            
261
    client.tournament_date = Date::today();
262

            
263
    if let Some(locale) = &client.locale_selected {
264
        rust_i18n::set_locale(&locale.txt());
265
    } else {
266
        let mut locale_1 = None;
267

            
268
        if get_locales().count() > 0 {
269
            for locale_2 in get_locales() {
270
                if let Ok(locale) = locale_2.as_str().try_into() {
271
                    locale_1 = Some(locale);
272
                    break;
273
                }
274
            }
275
        }
276

            
277
        if locale_1.is_none() {
278
            let locale_2 = get_locale().unwrap_or_else(|| String::from("en-US"));
279

            
280
            if let Ok(locale) = locale_2.as_str().try_into() {
281
                locale_1 = Some(locale);
282
            } else {
283
                locale_1 = Some(Locale::English);
284
            }
285
        }
286

            
287
        if let Some(locale) = locale_1 {
288
            rust_i18n::set_locale(&locale.txt());
289
            client.locale_selected = Some(locale);
290
            client
291
                .save_client_ron()
292
                .expect("saving user file should work");
293
        }
294
    }
295

            
296
    client.strings = i18n_buttons();
297
    client.text_input.clone_from(&client.username);
298

            
299
    let archived_games: Vec<ArchivedGame> = match &fs::read(&archived_games_file) {
300
        Ok(bytes) => match postcard::from_bytes(bytes) {
301
            Ok(client) => client,
302
            Err(err) => {
303
                error.push(format!(
304
                    "Error parsing the postcard file {}: {err}",
305
                    archived_games_file.display()
306
                ));
307
                Vec::new()
308
            }
309
        },
310
        Err(err) => {
311
            if err.kind() == ErrorKind::NotFound {
312
                error.push(format!(
313
                    "{}: {}",
314
                    t!("Unable to find Archived Games file"),
315
                    archived_games_file.display()
316
                ));
317
                Vec::new()
318
            } else {
319
                error.push(format!(
320
                    "{} {}: {err}",
321
                    t!("Error opening the file"),
322
                    archived_games_file.display()
323
                ));
324
                Vec::new()
325
            }
326
        }
327
    };
328

            
329
    client.archived_games = archived_games;
330
    client.error_persistent = error;
331

            
332
    let args = Args::parse();
333
    if args.ascii {
334
        client.chars.ascii();
335
    }
336

            
337
    let mut letters = HashMap::new();
338
    for ch in BOARD_LETTERS_LOWERCASE {
339
        letters.insert(ch, false);
340
    }
341

            
342
    client
343
}
344

            
345
fn main() -> anyhow::Result<()> {
346
    let args = Args::parse();
347
    utils::init_logger("hnefatafl_client", args.debug, false);
348

            
349
    if args.man {
350
        let mut buffer: Vec<u8> = Vec::default();
351
        let cmd = Args::command().name("hnefatafl-client").long_version(None);
352
        let man = clap_mangen::Man::new(cmd).date("2025-06-23");
353

            
354
        man.render(&mut buffer)?;
355
        write!(buffer, "{COPYRIGHT}")?;
356

            
357
        std::fs::write("hnefatafl-client.1", buffer)?;
358
        return Ok(());
359
    }
360

            
361
    create_data_folder()?;
362

            
363
    let mut application = iced::application(init_client, Client::update, Client::view)
364
        .title("Hnefatafl Copenhagen")
365
        .subscription(Client::subscriptions)
366
        .window(window::Settings {
367
            #[cfg(target_os = "linux")]
368
            platform_specific: PlatformSpecific {
369
                application_id: APPLICATION_ID.to_string(),
370
                ..PlatformSpecific::default()
371
            },
372
            icon: Some(icon::from_file_data(HELMET, Some(ImageFormat::Png))?),
373
            ..window::Settings::default()
374
        })
375
        .font(ICED_AW_FONT_BYTES)
376
        .theme(Client::theme);
377

            
378
    // For screenshots.
379
    if args.tiny_window {
380
        application = application.window_size(iced::Size {
381
            width: 868.0,
382
            height: 541.0,
383
        });
384
    }
385

            
386
    if args.social_preview {
387
        application = application.window_size(iced::Size {
388
            width: 1148.0,
389
            height: 481.0,
390
        });
391
    }
392

            
393
    application.run()?;
394
    Ok(())
395
}
396

            
397
fn estimate_score() -> impl Stream<Item = Message> {
398
    let args = Args::parse();
399

            
400
    stream::channel(
401
        100,
402
        move |mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
403
            let (tx, rx) = mpsc::channel();
404

            
405
            if let Err(error) = sender.send(Message::EstimateScoreConnected(tx)).await {
406
                error!("failed to send channel: {error}");
407
                exit(1);
408
            }
409

            
410
            thread::spawn(move || {
411
                let mut ai = match choose_ai(&args.ai, args.seconds, args.depth, true) {
412
                    Ok(ai) => ai,
413
                    Err(error) => {
414
                        error!("{error}");
415
                        exit(1);
416
                    }
417
                };
418

            
419
                for tree in &rx {
420
                    let mut game = Game::from(&tree);
421
                    let generate_move = ai.generate_move(&mut game).expect("the game is ongoing");
422

            
423
                    if let Err(error) = executor::block_on(
424
                        sender.send(Message::EstimateScoreDisplay((tree.here(), generate_move))),
425
                    ) {
426
                        error!("failed to send channel: {error}");
427
                        exit(1);
428
                    }
429
                }
430
            });
431
        },
432
    )
433
}
434

            
435
fn handle_error<T, E: fmt::Display>(result: Result<T, E>) -> T {
436
    match result {
437
        Ok(value) => value,
438
        Err(error) => {
439
            error!("{error}");
440
            exit(1)
441
        }
442
    }
443
}
444

            
445
fn open_url(url: &str) {
446
    if let Err(error) = webbrowser::open(url) {
447
        error!("{error}");
448
    }
449
}
450

            
451
#[allow(clippy::too_many_lines)]
452
fn pass_messages() -> impl Stream<Item = Message> {
453
    stream::channel(
454
        100,
455
        move |mut sender: iced::futures::channel::mpsc::Sender<Message>| async move {
456
            let mut args = Args::parse();
457
            args.host.push_str(SERVER_PORT);
458
            let address_string = args.host;
459

            
460
            thread::spawn(move || {
461
                'start_over: loop {
462
                    let (tx, rx) = mpsc::channel();
463

            
464
                    if let Err(error) =
465
                        executor::block_on(sender.send(Message::StreamConnected(tx.clone())))
466
                    {
467
                        error!("failed to send channel: {error}");
468
                        exit(1);
469
                    }
470

            
471
                    loop {
472
                        let message = match rx.recv() {
473
                            Ok(message) => message,
474
                            Err(error) => {
475
                                error!("rx: {error}");
476

            
477
                                if let Err(error) = executor::block_on(sender.send(Message::Exit)) {
478
                                    error!("{error}");
479
                                }
480

            
481
                                return;
482
                            }
483
                        };
484
                        let message_trim = message.trim();
485

            
486
                        if message_trim == "tcp_connect" {
487
                            break;
488
                        }
489
                    }
490

            
491
                    let mut is_ipv6 = false;
492
                    let mut socket_address = None;
493
                    let socket_addresses = match address_string.to_socket_addrs() {
494
                        Ok(socket_addresses) => socket_addresses,
495
                        Err(error) => {
496
                            error!("The socket address resolves to no IPs: {error})");
497

            
498
                            handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
499
                            handle_error(executor::block_on(
500
                                sender.send(Message::TcpConnectFailed),
501
                            ));
502

            
503
                            continue 'start_over;
504
                        }
505
                    };
506

            
507
                    for address in socket_addresses.clone() {
508
                        if address.is_ipv6() {
509
                            socket_address = Some(address);
510
                            is_ipv6 = true;
511
                            break;
512
                        }
513
                    }
514

            
515
                    if !is_ipv6 {
516
                        for address in socket_addresses {
517
                            if address.is_ipv4() {
518
                                socket_address = Some(address);
519
                                break;
520
                            }
521
                        }
522
                    }
523

            
524
                    let Some(socket_address) = socket_address else {
525
                        error!("There is no IPv4 address for the host: {address_string}");
526

            
527
                        handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
528
                        handle_error(executor::block_on(sender.send(Message::TcpConnectFailed)));
529

            
530
                        continue 'start_over;
531
                    };
532

            
533
                    let address: SockAddr = socket_address.into();
534
                    let keep_alive = tcp_keep_alive();
535
                    let domain_type = if is_ipv6 { Domain::IPV6 } else { Domain::IPV4 };
536
                    let socket = handle_error(Socket::new(domain_type, Type::STREAM, None));
537
                    handle_error(socket.set_tcp_keepalive(&keep_alive));
538

            
539
                    if let Err(error) = socket.connect(&address) {
540
                        if error.kind() == ErrorKind::NetworkUnreachable {
541
                            handle_error(executor::block_on(
542
                                sender.send(Message::TcpConnectFailed),
543
                            ));
544
                        } else {
545
                            handle_error(executor::block_on(sender.send(Message::ServerShutdown)));
546
                        }
547

            
548
                        error!("socket.connect {address_string}: {error}");
549
                        handle_error(executor::block_on(sender.send(Message::TcpDisconnect)));
550

            
551
                        continue 'start_over;
552
                    }
553

            
554
                    info!("connected to {socket_address} ...");
555

            
556
                    let mut tcp_stream: TcpStream = socket.into();
557
                    let mut reader = BufReader::new(handle_error(tcp_stream.try_clone()));
558

            
559
                    let mut sender_clone = sender.clone();
560
                    thread::spawn(move || {
561
                        for message in rx {
562
                            let message_trim = message.trim();
563

            
564
                            if message_trim == "ping" {
565
                                trace!("<- {message_trim}");
566
                            } else {
567
                                debug!("<- {message_trim}");
568
                            }
569

            
570
                            if message_trim == "quit" {
571
                                if cfg!(not(target_os = "redox")) {
572
                                    tcp_stream
573
                                        .shutdown(Shutdown::Both)
574
                                        .expect("shutdown call failed");
575
                                }
576

            
577
                                return;
578
                            }
579

            
580
                            handle_error(tcp_stream.write_all(message.as_bytes()));
581
                        }
582

            
583
                        for _ in 0..2 {
584
                            if let Err(error) =
585
                                executor::block_on(sender_clone.send(Message::LeaveSoft))
586
                            {
587
                                error!("{error}");
588
                            }
589
                        }
590

            
591
                        if let Err(error) =
592
                            executor::block_on(sender_clone.send(Message::ServerShutdown))
593
                        {
594
                            error!("{error}");
595
                        }
596
                    });
597

            
598
                    let mut buffer = String::new();
599
                    handle_error(executor::block_on(
600
                        sender.send(Message::ConnectedTo(address_string.clone())),
601
                    ));
602

            
603
                    if cfg!(target_os = "redox") {
604
                        sleep(Duration::from_secs(1));
605
                    }
606

            
607
                    loop {
608
                        let bytes = handle_error(reader.read_line(&mut buffer));
609
                        if bytes > 0 {
610
                            let buffer_trim = buffer.trim();
611
                            let buffer_trim_vec: Vec<_> =
612
                                buffer_trim.split_ascii_whitespace().collect();
613

            
614
                            if buffer_trim_vec[1] == "display_users"
615
                                || buffer_trim_vec[1] == "display_games"
616
                                || buffer_trim_vec[1] == "ping"
617
                            {
618
                                trace!("-> {buffer_trim}");
619
                            } else {
620
                                debug!("-> {buffer_trim}");
621
                            }
622

            
623
                            handle_error(executor::block_on(
624
                                sender.send(Message::TextReceived(buffer.clone())),
625
                            ));
626

            
627
                            if buffer_trim_vec[1] == "archived_games" {
628
                                let length = handle_error(buffer_trim_vec[2].parse());
629
                                let mut buf = vec![0; length];
630
                                handle_error(reader.read_exact(&mut buf));
631
                                let archived_games: Vec<ArchivedGame> =
632
                                    handle_error(postcard::from_bytes(&buf));
633

            
634
                                handle_error(executor::block_on(
635
                                    sender.send(Message::ArchivedGames(archived_games)),
636
                                ));
637
                            }
638

            
639
                            buffer.clear();
640
                        } else {
641
                            info!("the TCP stream has closed");
642
                            continue 'start_over;
643
                        }
644
                    }
645
                }
646
            });
647
        },
648
    )
649
}
650

            
651
fn text_collect(text: SplitAsciiWhitespace<'_>) -> String {
652
    let text: Vec<&str> = text.collect();
653
    text.join(" ")
654
}
655

            
656
#[allow(clippy::struct_excessive_bools)]
657
#[derive(Debug, Default, Deserialize, Serialize)]
658
struct Client {
659
    #[serde(skip)]
660
    accounts: Accounts,
661
    #[serde(skip)]
662
    active_tab: TabId,
663
    #[serde(skip)]
664
    admin: bool,
665
    #[serde(skip)]
666
    admin_tournament: bool,
667
    #[serde(skip)]
668
    attacker: String,
669
    #[serde(default)]
670
    archived_games: Vec<ArchivedGame>,
671
    #[serde(skip)]
672
    archived_games_filtered: Option<Vec<ArchivedGame>>,
673
    #[serde(skip)]
674
    archived_game_selected: Option<ArchivedGame>,
675
    #[serde(skip)]
676
    archived_game_handle: Option<ArchivedGameHandle>,
677
    #[serde(default)]
678
    coordinates: Coordinates,
679
    #[serde(skip)]
680
    defender: String,
681
    #[serde(skip)]
682
    delete_account: bool,
683
    #[serde(skip)]
684
    estimate_score: bool,
685
    #[serde(skip)]
686
    estimate_score_tx: Option<mpsc::Sender<Tree>>,
687
    #[serde(skip)]
688
    captures: HashSet<Vertex>,
689
    #[serde(skip)]
690
    counter: u64,
691
    #[serde(skip)]
692
    chars: Characters,
693
    #[serde(skip)]
694
    challenger: bool,
695
    #[serde(skip)]
696
    connected_tcp: bool,
697
    #[serde(skip)]
698
    connected_to: String,
699
    #[serde(skip)]
700
    content: text_editor::Content,
701
    #[serde(skip)]
702
    email: Option<Email>,
703
    #[serde(skip)]
704
    email_input: String,
705
    #[serde(skip)]
706
    emails_bcc: Vec<String>,
707
    #[serde(skip)]
708
    error: Option<String>,
709
    #[serde(skip)]
710
    error_email: Option<String>,
711
    #[serde(skip)]
712
    error_persistent: Vec<String>,
713
    #[serde(skip)]
714
    game: Option<Game>,
715
    #[serde(skip)]
716
    game_id: Id,
717
    #[serde(skip)]
718
    games_light: ServerGamesLight,
719
    #[serde(skip)]
720
    games_light_vec: Vec<ServerGameLight>,
721
    #[serde(skip)]
722
    game_settings: NewGameSettings,
723
    #[serde(skip)]
724
    heat_map: Option<HeatMap>,
725
    #[serde(skip)]
726
    heat_map_display: bool,
727
    #[serde(default)]
728
    locale_selected: Option<Locale>,
729
    #[serde(default)]
730
    my_games_only: bool,
731
    #[serde(skip)]
732
    my_turn: bool,
733
    #[serde(skip)]
734
    now: i64,
735
    #[serde(skip)]
736
    now_diff: i64,
737
    #[serde(default)]
738
    password: String,
739
    #[serde(skip)]
740
    password_ends_with_whitespace: bool,
741
    #[serde(default)]
742
    password_save: bool,
743
    #[serde(default)]
744
    password_show: bool,
745
    #[serde(skip)]
746
    play_from: Option<Vertex>,
747
    #[serde(skip)]
748
    play_from_previous: Option<Vertex>,
749
    #[serde(skip)]
750
    play_to_previous: Option<Vertex>,
751
    #[serde(skip)]
752
    press_letters: HashSet<char>,
753
    #[serde(skip)]
754
    press_numbers: [bool; 13],
755
    #[serde(skip)]
756
    request_draw: bool,
757
    #[serde(skip)]
758
    screen: Screen,
759
    #[serde(skip)]
760
    screen_size: Size,
761
    #[serde(default)]
762
    sound_muted: bool,
763
    #[serde(skip)]
764
    spectators: Vec<String>,
765
    #[serde(skip)]
766
    status: Status,
767
    #[serde(skip)]
768
    strings: HashMap<String, String>,
769
    #[serde(skip)]
770
    texts: VecDeque<String>,
771
    #[serde(skip)]
772
    texts_game: VecDeque<String>,
773
    #[serde(skip)]
774
    text_input: String,
775
    #[serde(default)]
776
    theme: Theme,
777
    #[serde(skip)]
778
    time_attacker: TimeSettings,
779
    #[serde(skip)]
780
    time_defender: TimeSettings,
781
    #[serde(skip)]
782
    tournament: Option<Tournament>,
783
    #[serde(skip)]
784
    tournament_date: Date,
785
    #[serde(skip)]
786
    tournament_date_show_picker: bool,
787
    #[serde(skip)]
788
    tx: Option<mpsc::Sender<String>>,
789
    #[serde(default)]
790
    username: String,
791
    #[serde(skip)]
792
    users: HashMap<String, User>,
793
    #[serde(skip)]
794
    users_sort_by: SortBy,
795
    #[serde(default)]
796
    volume: Volume,
797
}
798

            
799
impl<'a> Client {
800
    fn account_settings_view(&self) -> Column<'_, Message> {
801
        let mut columns = column![
802
            text!(
803
                "{} {} {} TCP",
804
                t!("connected to"),
805
                &self.connected_to,
806
                t!("via")
807
            ),
808
            text!("{}: {}", t!("username"), &self.username),
809
        ]
810
        .padding(PADDING)
811
        .spacing(SPACING);
812

            
813
        if let Some(email) = &self.email {
814
            let mut row = Row::new();
815
            if email.verified {
816
                row = row.push(text!(
817
                    "{} [{}]: {} ",
818
                    t!("email address"),
819
                    t!("verified"),
820
                    email.address,
821
                ));
822
                columns = columns.push(row);
823
            } else {
824
                row = row.push(text!(
825
                    "{} [{}]: {} ",
826
                    t!("email address"),
827
                    t!("unverified"),
828
                    email.address,
829
                ));
830
                columns = columns.push(row);
831

            
832
                let mut row = Row::new();
833
                row = row.push(text!("{}: ", t!("email code")));
834
                row = row.push(
835
                    widget::text_input("", &self.email_input)
836
                        .on_input(Message::EmailChanged)
837
                        .on_paste(Message::EmailChanged)
838
                        .on_submit(Message::TextSendEmailCode),
839
                );
840
                columns = columns.push(row);
841
            }
842
        } else {
843
            let mut row = Row::new();
844
            row = row.push(text!("{}: ", t!("email address")));
845
            row = row.push(
846
                widget::text_input("", &self.email_input)
847
                    .on_input(Message::EmailChanged)
848
                    .on_paste(Message::EmailChanged)
849
                    .on_submit(Message::TextSendEmail),
850
            );
851

            
852
            columns = columns.push(row);
853
            columns = columns.push(row![text!("{}: ", t!("email code"))]);
854
        }
855

            
856
        columns = columns.push(row![
857
            button(text!("{} (6)", self.strings["Reset Email"].as_str()))
858
                .on_press(Message::EmailReset)
859
        ]);
860

            
861
        if let Some(error) = &self.error_email {
862
            columns = columns.push(row![text!("error: {error}").style(text::danger)]);
863
        }
864

            
865
        let mut change_password_button =
866
            button(text!("{} (7)", self.strings["Change Password"].as_str()));
867

            
868
        if !self.password_ends_with_whitespace {
869
            change_password_button = change_password_button.on_press(Message::TextSend);
870
        }
871

            
872
        columns = columns.push(
873
            row![
874
                change_password_button,
875
                widget::text_input("", &self.password)
876
                    .secure(!self.password_show)
877
                    .on_input(Message::PasswordChanged)
878
                    .on_paste(Message::PasswordChanged),
879
            ]
880
            .spacing(SPACING),
881
        );
882

            
883
        columns = columns.push(
884
            row![
885
                checkbox(self.password_show).on_toggle(Message::PasswordShow),
886
                text!("{} (8)", t!("show password")),
887
            ]
888
            .spacing(SPACING),
889
        );
890

            
891
        if self.delete_account {
892
            columns = columns.push(
893
                button(text!(
894
                    "{} (9)",
895
                    self.strings["REALLY DELETE ACCOUNT"].as_str()
896
                ))
897
                .on_press(Message::DeleteAccount),
898
            );
899
        } else {
900
            columns = columns.push(
901
                button(text!("{} (9)", self.strings["Delete Account"].as_str()))
902
                    .on_press(Message::DeleteAccount),
903
            );
904
        }
905

            
906
        columns = columns.push(
907
            button(text!("{} (Esc)", self.strings["Quit"].as_str())).on_press(Message::Leave),
908
        );
909

            
910
        columns
911
    }
912

            
913
    fn archived_game_reset(&mut self) {
914
        self.archived_game_handle = None;
915
        self.archived_game_selected = None;
916
    }
917

            
918
    #[must_use]
919
    fn board(&self) -> Row<'_, Message> {
920
        let (board, heat_map) = self.board_and_heatmap();
921
        let board_size = board.size();
922
        let board_size_usize: usize = board_size.into();
923
        let d = Dimensions::new(board_size, &self.screen_size);
924
        let letters: Vec<_> = BOARD_LETTERS[..board_size_usize].chars().collect();
925
        let mut game_display = Row::new().spacing(2);
926
        let possible_moves = self.possible_moves();
927

            
928
        let coordinates: bool = self.coordinates.into();
929
        if coordinates {
930
            game_display =
931
                game_display.push(self.numbers(d.letter_size, d.spacing, board_size_usize));
932
        }
933

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

            
937
            if coordinates {
938
                column = self.letter(*letter, column, d.letter_size);
939
            }
940

            
941
            for y in 0..board_size_usize {
942
                let vertex = Vertex {
943
                    size: board.size(),
944
                    x,
945
                    y,
946
                };
947

            
948
                let mut txt = match board.get(&vertex) {
949
                    Space::Attacker => text(&self.chars.attacker),
950
                    Space::Defender => text(&self.chars.defender),
951
                    Space::Empty => {
952
                        if let Some(arrow) = self.draw_arrow(y, x) {
953
                            text(arrow)
954
                        } else if self.captures.contains(&vertex) {
955
                            text(&self.chars.captured)
956
                        } else if vertex.on_restricted_square() {
957
                            text(&self.chars.restricted_square)
958
                        } else {
959
                            text(" ")
960
                        }
961
                    }
962
                    Space::King => text(&self.chars.king),
963
                };
964

            
965
                if let Some((heat_map_from, heat_map_to)) = &heat_map
966
                    && possible_moves.is_some()
967
                {
968
                    if let Some(vertex_from) = self.play_from.as_ref() {
969
                        let space = board.get(vertex_from);
970
                        let turn = Role::from(space);
971
                        if let Some(heat_map_to) = heat_map_to.get(&(turn, *vertex_from)) {
972
                            let heat = heat_map_to[y * board_size_usize + x];
973

            
974
                            if heat == Heat::UnRanked {
975
                                txt = txt.color(Color::from_rgba(0.0, 0.0, 0.0, heat.into()));
976
                            } else {
977
                                let txt_char = match space {
978
                                    Space::Attacker => &self.chars.attacker,
979
                                    Space::Defender => &self.chars.defender,
980
                                    Space::Empty => "",
981
                                    Space::King => &self.chars.king,
982
                                };
983

            
984
                                txt = text(txt_char).color(Color::from_rgba(
985
                                    0.0,
986
                                    0.0,
987
                                    0.0,
988
                                    heat.into(),
989
                                ));
990
                            }
991
                        }
992
                    } else {
993
                        let heat = heat_map_from[y * board_size_usize + x];
994
                        txt = txt.color(Color::from_rgba(0.0, 0.0, 0.0, heat.into()));
995
                    }
996
                }
997

            
998
                txt = txt.font(Font::MONOSPACE).center().size(d.piece_size);
999
                let mut button = button(txt)
                    .width(d.board_dimension)
                    .height(d.board_dimension);
                match self.board_move(&vertex, possible_moves.as_ref()) {
                    Move::From => button = button.on_press(Message::PlayMoveFrom(vertex)),
                    Move::To => button = button.on_press(Message::PlayMoveTo(vertex)),
                    Move::Revert => button = button.on_press(Message::PlayMoveRevert),
                    Move::None => {}
                }
                column = column.push(button);
            }
            if coordinates {
                column = self.letter(*letter, column, d.letter_size);
            }
            game_display = game_display.push(column);
        }
        if coordinates {
            game_display =
                game_display.push(self.numbers(d.letter_size, d.spacing, board_size_usize));
        }
        game_display
    }
    fn board_move(&self, vertex: &Vertex, possible_moves: Option<&LegalMoves>) -> Move {
        if let Some(legal_moves) = possible_moves {
            if let Some(vertex_from) = self.play_from.as_ref() {
                if let Some(vertexes) = legal_moves.moves.get(vertex_from) {
                    if vertex == vertex_from {
                        Move::Revert
                    } else if vertexes.contains(vertex) {
                        Move::To
                    } else {
                        Move::None
                    }
                } else {
                    Move::None
                }
            } else if legal_moves.moves.contains_key(vertex) {
                Move::From
            } else {
                Move::None
            }
        } else {
            Move::None
        }
    }
    #[allow(clippy::type_complexity)]
    fn board_and_heatmap(
        &self,
    ) -> (
        Board,
        Option<(Vec<Heat>, HashMap<(Role, Vertex), Vec<Heat>>)>,
    ) {
        if let Some(game_handle) = &self.archived_game_handle {
            let node = game_handle.boards.here();
            if self.heat_map_display
                && let Some(heat_map) = &self.heat_map
            {
                (node.board.clone(), Some(heat_map.draw(node.turn)))
            } else {
                (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)", self.strings["New Game"].as_str()));
        if self.game_settings.role_selected.is_some() && self.game_settings.time.is_some() {
            new_game = new_game.on_press(Message::GameSubmit);
        }
        let leave =
            button(text!("{} (Esc)", self.strings["Quit"].as_str())).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 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)", self.strings["Resign"].as_str())).on_press(Message::PlayResign);
        let request_draw = button(text!("{} (q)", self.strings["Request Draw"].as_str()))
            .on_press(Message::PlayDraw);
        if !watching {
            if self.my_turn {
                user_area = user_area.push(row![resign, request_draw].spacing(SPACING));
            } else {
                let row = if self.request_draw {
                    column![
                        row![
                            button(text!("{} (r)", self.strings["Accept Draw"].as_str()))
                                .on_press(Message::PlayDrawDecision(Draw::Accept)),
                        ]
                        .spacing(SPACING)
                    ]
                } else {
                    Column::new()
                };
                user_area = user_area.push(row.spacing(SPACING));
            }
        }
        let coordinates_muted = row![
            checkbox(self.coordinates.into()).on_toggle(Message::Coordinates),
            text!("{} (n)", t!("Coordinates")),
            checkbox(self.sound_muted).on_toggle(Message::SoundMuted),
            text!("{} (o)", t!("Muted")),
        ]
        .spacing(SPACING);
        user_area = user_area.push(coordinates_muted);
        let 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)", self.strings["Leave"].as_str())).on_press(Message::Leave);
        match status {
            Status::AttackerWins => {
                user_area = user_area.push(text(t!("Attacker wins!")));
            }
            Status::Draw => {
                user_area = user_area.push(text(t!("It's a draw.")));
            }
            Status::Ongoing => {}
            Status::DefenderWins => {
                user_area = user_area.push(text(t!("Defender wins!")));
            }
        }
        if let Some(handle) = &self.archived_game_handle {
            let mut heat_map = checkbox(self.heat_map_display).size(32);
            if self.heat_map.is_some() {
                heat_map = heat_map.on_toggle(Message::HeatMap);
            }
            let mut heat_map_button =
                button(text!("{} (p) (q)", self.strings["Heat Map"].as_str()));
            if !self.estimate_score && *status == Status::Ongoing {
                heat_map_button = heat_map_button.on_press(Message::EstimateScore);
            }
            user_area = user_area.push(row![heat_map, heat_map_button].spacing(SPACING));
            let child_number = text(handle.boards.next_child);
            let child_right = button(
                text(&self.chars.double_arrow_right)
                    .center()
                    .font(Font::MONOSPACE),
            )
            .on_press(Message::ReviewGameChildNext);
            user_area = user_area.push(
                row![
                    leave,
                    child_right,
                    container(child_number)
                        .style(container::bordered_box)
                        .padding(PADDING_MEDIUM),
                ]
                .spacing(SPACING),
            );
            let mut left_all = button(text(&self.chars.double_arrow_left_full));
            let mut left = button(text(&self.chars.double_arrow_left));
            if handle.play > 0 {
                left_all = left_all.on_press(Message::ReviewGameBackwardAll);
                left = left.on_press(Message::ReviewGameBackward);
            }
            let mut right = button(text(&self.chars.double_arrow_right));
            let mut right_all = button(text(&self.chars.double_arrow_right_full));
            if handle.boards.has_children() {
                right = right.on_press(Message::ReviewGameForward);
                right_all = right_all.on_press(Message::ReviewGameForwardAll);
            }
            user_area = user_area.push(row![left_all, left, right, right_all].spacing(SPACING));
        } else {
            user_area = user_area.push(leave);
            let spectator = text!(
                "{} ({}) {}: {seconds:01}.{sub_second:03} s",
                &self.chars.people,
                self.spectators.len() - 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) {
        let selected = !self.my_games_only;
        if selected {
            self.archived_games_filtered = Some(
                self.archived_games
                    .iter()
                    .filter(|game| game.attacker == self.username || game.defender == self.username)
                    .cloned()
                    .collect(),
            );
        } else {
            self.archived_games_filtered = None;
        }
        self.my_games_only = selected;
        handle_error(self.save_client_ron());
    }
    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());
                let string_keys: Vec<_> = self.strings.keys().cloned().collect();
                for string in string_keys {
                    self.strings.insert(string.clone(), t!(string).to_string());
                }
                self.locale_selected = 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 => {}
            },
            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 => {}
            },
            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,
                // Fixme: Screen::GameNew => self.game_settings.time = Some(TimeEnum::Rapid),
                Screen::Login => self.reset_password(),
                Screen::Game | Screen::GameReview => {
                    self.clear_numbers_except(5);
                    self.press_numbers[4] = !self.press_numbers[4];
                    self.press_letter_and_number();
                }
            },
            Message::Press6 => match self.screen {
                Screen::EmailEveryone => {}
                // Fixme: Screen::GameNew => self.game_settings.time = Some(TimeEnum::Classical),
                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 {
                    // Fixme!
                    TabId::AccountSettings => {
                        self.send(&format!("change_password {}\n", self.password));
                    }
                    TabId::GameNew => self.game_settings.role_selected = Some(Role::Attacker),
                    TabId::Games => open_url("https://hnefatafl.org/rules.html"),
                    TabId::Tournament => self.send("join_tournament\n"),
                    TabId::Users => {}
                },
                // Fixme: Screen::GameNew => self.game_settings.time = Some(TimeEnum::Long),
                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 => {}
                },
                // Fixme: Screen::GameNew => self.game_settings.time = Some(TimeEnum::VeryLong),
                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 => {}
                // Fixme: Screen::GameNew => self.game_settings.time = Some(TimeEnum::Infinity),
                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::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::RatedSelected(rated) => self.game_settings.rated = rated.into(),
            Message::ResetPassword => self.reset_password(),
            Message::ReviewGame => self.review_game(),
            Message::ReviewGameBackward => {
                if let Some(handle) = &mut self.archived_game_handle {
                    handle.play = handle.play.saturating_sub(1);
                    handle.boards.backward();
                    self.reset_markers();
                }
            }
            Message::ReviewGameBackwardAll => {
                if let Some(handle) = &mut self.archived_game_handle {
                    handle.play = 0;
                    handle.boards.backward_all();
                    self.reset_markers();
                }
            }
            Message::ReviewGameChildNext => {
                if let Some(handle) = &mut self.archived_game_handle {
                    handle.boards.next_child();
                    self.reset_markers();
                }
            }
            Message::ReviewGameForward => {
                if let Some(handle) = &mut self.archived_game_handle
                    && handle.boards.has_children()
                {
                    handle.play += 1;
                    handle.boards.forward();
                    self.reset_markers();
                }
            }
            Message::ReviewGameForwardAll => {
                if let Some(handle) = &mut self.archived_game_handle {
                    let count = handle.boards.forward_all();
                    handle.play += count;
                    self.reset_markers();
                }
            }
            Message::RoleSelected(role) => self.game_settings.role_selected = Some(role),
            Message::TextChanged(string) => {
                if self.screen == Screen::Login {
                    let string: Vec<_> = string.split_whitespace().collect();
                    if let Some(string) = string.first() {
                        if string.len() <= 16 {
                            self.text_input = string.to_ascii_lowercase();
                            self.username = self.text_input.clone();
                        }
                    } else {
                        self.text_input = String::new();
                    }
                } else {
                    self.text_input = string;
                }
            }
            Message::TextEdit(action) => {
                self.content.perform(action);
            }
            Message::TextReceived(string) => {
                let mut text = string.split_ascii_whitespace();
                match text.next() {
                    Some("=") => {
                        let text_next = text.next();
                        match text_next {
                            Some(
                                "archived_games"
                                | "challenge_requested"
                                | "change_password"
                                | "decline_game"
                                | "email_reset"
                                | "game"
                                | "request_draw",
                            ) => {}
                            Some("admin") => self.admin = true,
                            Some("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 {
                    // Fixme: account settings self.send(&format!("change_password {}\n", self.password));
                    Screen::EmailEveryone => {
                        // subject == self.text_input
                        let email = self.content.text().replace('\n', "\\n");
                        self.send(&format!("email_everyone {} {email}\n", self.text_input));
                    }
                    Screen::Game => {
                        if !self.text_input.trim().is_empty() {
                            self.text_input.push('\n');
                            self.send(&format!("text_game {} {}", self.game_id, self.text_input));
                        }
                    }
                    Screen::Games => {
                        if !self.text_input.trim().is_empty() {
                            self.text_input.push('\n');
                            self.send(&format!("text {}", self.text_input));
                        }
                    }
                    Screen::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}", self.strings["Cancel"].as_str()))
                            .on_press(Message::GameCancel(id)),
                    );
                }
                JoinGame::Join => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Join"].as_str()))
                            .on_press(Message::GameJoin(id)),
                    );
                }
                JoinGame::None => {}
                JoinGame::Resume => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Resume"].as_str()))
                            .on_press(Message::GameResume(id)),
                    );
                }
                JoinGame::Watch => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Watch"].as_str()))
                            .on_press(Message::GameWatch(id)),
                    );
                }
            }
            match self.game_state(id) {
                State::Challenger | State::Spectator => {}
                State::Creator => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Accept"].as_str()))
                            .on_press(Message::GameAccept(id)),
                    );
                    buttons_row = buttons_row.push(
                        button(text!(
                            "{}{}",
                            self.strings["Decline"].as_str(),
                            i.to_ascii_uppercase()
                        ))
                        .on_press(Message::GameDecline(id)),
                    );
                }
                State::CreatorOnly => {
                    buttons_row = buttons_row.push(
                        button(text!("{}{i}", self.strings["Cancel"].as_str()))
                            .on_press(Message::CancelGame(id)),
                    );
                }
            }
            buttons = buttons.push(buttons_row);
        }
        let game_id = t!("ID");
        let game_ids = column![
            text(game_id.to_string()),
            text("-".repeat(game_id.chars().count())).font(Font::MONOSPACE),
            game_ids
        ]
        .padding(PADDING);
        let attacker = t!("attacker");
        let attackers = column![
            text(attacker.to_string()),
            text("-".repeat(attacker.chars().count())).font(Font::MONOSPACE),
            attackers
        ]
        .padding(PADDING);
        let defender = t!("defender");
        let defenders = column![
            text(defender.to_string()),
            text("-".repeat(defender.chars().count())).font(Font::MONOSPACE),
            defenders
        ]
        .padding(PADDING);
        let rated = t!("rated");
        let ratings = column![
            text(rated.to_string()),
            text("-".repeat(rated.chars().count())).font(Font::MONOSPACE),
            ratings
        ]
        .padding(PADDING);
        let timed = t!("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)", self.strings["Get Archived Games"].as_str()))
                .on_press(Message::ArchivedGamesGet);
        let website = button(text!("{} (7)", self.strings["Rules"].as_str())).on_press(
            Message::OpenUrl("https://hnefatafl.org/rules.html".to_string()),
        );
        let quit =
            button(text!("{} (Esc)", self.strings["Quit"].as_str())).on_press(Message::Leave);
        let mut middle = row![get_archived_games, website, 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 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)", self.strings["Quit"].as_str()))
                    .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)",
                        self.strings["Create Game"].as_str()
                    )),
                    self.game_new_view(),
                )
                .push(
                    TabId::Tournament,
                    iced_aw::TabLabel::Text(format!("{} (3)", self.strings["Tournament"].as_str())),
                    self.tournament_view(),
                )
                .push(
                    TabId::AccountSettings,
                    iced_aw::TabLabel::Text(format!("{} (4)", self.strings["Account Settings"])),
                    self.account_settings_view(),
                )
                .push(
                    TabId::Users,
                    iced_aw::TabLabel::Text(format!("{} (5)", self.strings["Users"])),
                    column![
                        button(text!("{} (Esc)", self.strings["Quit"].as_str()))
                            .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)", self.strings["Login"].as_str()));
                if !self.password_ends_with_whitespace {
                    login = login.on_press(Message::TextSendLogin);
                }
                let mut create_account =
                    button(text!("{} (4)", self.strings["Create Account"].as_str()));
                if !self.text_input.is_empty() && !self.password_ends_with_whitespace {
                    create_account = create_account.on_press(Message::TextSendCreateAccount);
                }
                let mut reset_password =
                    button(text!("{} (5)", self.strings["Reset Password"].as_str()));
                if !self.text_input.is_empty() {
                    reset_password = reset_password.on_press(Message::ResetPassword);
                }
                let mut error = text("");
                if let Some(error_) = &self.error {
                    error = text(error_).style(text::danger);
                }
                let mut error_persistent = Column::new();
                for error in &self.error_persistent {
                    error_persistent = error_persistent.push(text(error).style(text::danger));
                }
                let mut review_game = button(text!("{} (a)", self.strings["Review Game"].as_str()));
                if self.archived_game_selected.is_some() {
                    review_game = review_game.on_press(Message::ReviewGame);
                }
                let archived_games = if let Some(archived_games) = &self.archived_games_filtered {
                    archived_games.clone()
                } else {
                    self.archived_games.clone()
                };
                let my_games_text = text!("{} (3)", t!("My Games Only"));
                let my_games = checkbox(self.my_games_only).on_toggle(Message::MyGamesOnly);
                let quit = button(text!("{} (Esc)", self.strings["Quit"].as_str()))
                    .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 review_game_pick = row![review_game, review_game_pick].spacing(SPACING);
                let locale = [
                    Locale::English,
                    Locale::Chinese,
                    Locale::Spanish,
                    Locale::Arabic,
                    Locale::Indonesian,
                    Locale::PortugueseBr,
                    Locale::PortuguesePt,
                    Locale::French,
                    Locale::Japanese,
                    Locale::Russian,
                    Locale::German,
                    Locale::Icelandic,
                    Locale::IcelandicRunic,
                    Locale::Swedish,
                    Locale::Korean,
                ];
                let locale = row![
                    text!("{}: ", t!("locale")).size(20),
                    pick_list(locale, self.locale_selected, Message::LocaleSelected),
                ];
                let theme = if self.theme == Theme::Light {
                    row![
                        button(text!("{} (6)", self.strings["Dark"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Dark)),
                        button(text!("{} (7)", self.strings["Light"].as_str())),
                        button(text("Tol (8)")).on_press(Message::ChangeTheme(Theme::Tol)),
                    ]
                    .spacing(SPACING)
                } else if self.theme == Theme::Dark {
                    row![
                        button(text!("{} (6)", self.strings["Dark"].as_str())),
                        button(text!("{} (7)", self.strings["Light"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Light)),
                        button(text("Tol (8)")).on_press(Message::ChangeTheme(Theme::Tol)),
                    ]
                    .spacing(SPACING)
                } else {
                    row![
                        button(text!("{} (6)", self.strings["Dark"].as_str()))
                            .on_press(Message::ChangeTheme(Theme::Dark)),
                        button(text!("{} (7)", self.strings["Light"].as_str()))
                            .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("Hnefatafl Org (0)")
                    .on_press(Message::OpenUrl("https://hnefatafl.org".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 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_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,
            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)", self.strings["Quit"].as_str())).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)",
            self.strings["Tournaments Described"].as_str()
        ))
        .on_press(Message::Tournaments);
        let mut button_1 = button(text!("{} (7)", self.strings["Join Tournament"].as_str()));
        let mut button_2 = button(text!("{} (8)", self.strings["Leave Tournament"].as_str()));
        if tournament.players.contains(&self.username) {
            button_2 = button_2.on_press(Message::TournamentLeave);
        } else {
            button_1 = button_1.on_press(Message::TournamentJoin);
        }
        let buttons = row![
            button_0,
            button_1,
            button_2,
            button(text!("{} (Esc)", self.strings["Quit"].as_str())).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
    }
}