tictactoe_rust/interface/
mod.rs

1use ansi_term::Color::*;
2static TICTACTOE_LOGO: &str = "
3| |_(_) ___| |_ __ _  ___| |_ ___   ___       _ __ _   _ ___| |_ 
4| __| |/ __| __/ _` |/ __| __/ _ \\ / _ \\_____| '__| | | / __| __|
5| |_| | (__| || (_| | (__| || (_) |  __/_____| |  | |_| \\__ \\ |_ 
6 \\__|_|\\___|\\__\\__,_|\\___|\\__\\___/ \\___|     |_|   \\__,_|___/\\__|
7";
8
9static GAME_RULE: &str = "Game rule:
10
11Two player put their piece turn by turn.
12If a player have 3 same pieces on a line 
13horizontally, or vertically, or diagonally,
14that player wins.";
15
16static FRONT_PAGE_AVAILABLE_COMMANDS: [Commands; 9] = [
17    Commands::Help,
18    Commands::Start,
19    Commands::Quit,
20    Commands::Command1,
21    Commands::Command2,
22    Commands::Command3,
23    Commands::Command4,
24    Commands::Command5,
25    Commands::Command6,
26];
27static FRONT_PAGE_HELP_AVAILABLE_COMMANDS: [Commands; 3] =
28    [Commands::Start, Commands::Back, Commands::Quit];
29static CONFG_PLAYER_AVAILABLE_COMMANDS: [Commands; 6] = [
30    Commands::Command1,
31    Commands::Command2,
32    Commands::Command3,
33    Commands::Command4,
34    Commands::Back,
35    Commands::Quit,
36];
37
38static EMPTY_PIECE_TABLE: &'static [(&str, &str, &str)] =
39    &[(&"sp", &" ", &"(space)"), (&"dt", &".", &"(dot)")];
40
41static PIECE_TABLE: &'static [(&str, &str, &str)] = &[
42    (&"bc", &"●", &"(black circle)"),
43    (&"wc", &"○", &"(white circle)"),
44    (&"bs", &"■", &"(black square)"),
45    (&"ws", &"□", &"(white square)"),
46    (&"bk", &"♚", &"(black king)"),
47    (&"wk", &"♔", &"(white king)"),
48    (&"bq", &"♛", &"(black queen)"),
49    (&"wq", &"♕", &"(white queen)"),
50    (&"bp", &"♟", &"(black pawn)"),
51    (&"wp", &"♙", &"(white pawn)"),
52];
53
54use super::board::Board;
55use super::game::Config;
56
57fn print_table(table: &[(&str, &str, &str)], num_of_spaces: usize) {
58    let spaces = " ".repeat(num_of_spaces);
59    for i in table {
60        println!("{}[{}] {} {}", spaces, i.0, i.1, i.2);
61    }
62}
63
64fn choose_string_from_table(
65    table: &'static [(&str, &'static str, &str)],
66    string: &str,
67) -> Option<&'static str> {
68    for i in table {
69        if string == i.0 {
70            return Some(i.1);
71        }
72        continue;
73    }
74    None
75}
76
77fn ask_for_user_name() -> String {
78    use std::io;
79    let mut input = String::new();
80    io::stdin()
81        .read_line(&mut input)
82        .expect("Failed to read line when asking for user command");
83    let input = input.trim();
84    input.to_string()
85}
86
87fn ask_user_move_input() -> Result<(usize, usize), &'static str> {
88    use std::io;
89    println!("Where you want to put your piece: Ex: 0, 2 or (q)uit");
90    let mut input = String::new();
91    io::stdin()
92        .read_line(&mut input)
93        .expect("Could not read line when asking for user input");
94
95    let input = input.trim();
96
97    if input == "q" || input == "quit" {
98        return Err("Quit");
99    }
100
101    let string_pool: Vec<&str> = input.split(|c| c == ' ' || c == ',').collect();
102
103    let nums: Vec<usize> = string_pool
104        .iter()
105        .filter(|num| num.parse::<usize>().is_ok())
106        .map(|num| num.parse::<usize>().unwrap())
107        .collect();
108
109    if nums.len() != 2 {
110        return Err("Could not parse input to numbers");
111    }
112    Ok((nums[0], nums[1]))
113}
114
115fn ask_for_user_string(
116    table: &'static [(&'static str, &str, &str)],
117) -> Result<String, &'static str> {
118    use std::io;
119    let mut input = String::new();
120    io::stdin()
121        .read_line(&mut input)
122        .expect("Failed to read line when asking for user command");
123    let input = input.trim();
124    if input.len() == 2 {
125        let option = choose_string_from_table(table, input);
126        if option.is_none() {
127            Err("String is of incorrect format.")
128        } else {
129            Ok(option.unwrap().to_string())
130        }
131    } else if input.chars().count() == 1 {
132        Ok(input.to_owned())
133    } else {
134        Err("String is of incorrect format.")
135    }
136}
137
138pub fn ask_and_run_command(mut config: &mut Config, mut board: &mut Board) {
139    let mut cache = CommandCache::new();
140    let mut command: Commands = Commands::Quit;
141    loop {
142        cache.print_current_page(config, board);
143
144        match cache.current_page {
145            Page::ConfigEmptyPiece
146            | Page::EnterUserName1
147            | Page::EnterUserName2
148            | Page::EnterUserPiece1
149            | Page::EnterUserPiece2
150            | Page::Playing => {}
151            _ => {
152                command = Commands::keep_asking_for_valid_user_command();
153
154                if command == Commands::Quit {
155                    break;
156                }
157            }
158        }
159
160        println!("{}", Black.paint("=".repeat(51)));
161
162        println!();
163
164        cache
165            .execute_command_and_update_command_cache(command, &mut config, &mut board)
166            .unwrap_or_else(|err| {
167                println!("{}", err);
168            });
169    }
170}
171
172enum Page {
173    FrontPage,
174    FrontPageHelp,
175    Playing,
176    ConfigMode,
177    ConfigPlayer,
178    ConfigFirst,
179    ConfigAiSmartness,
180    ConfigEmptyPiece,
181    EnterUserName1,
182    EnterUserName2,
183    EnterUserPiece1,
184    EnterUserPiece2,
185    Winner,
186}
187
188#[derive(PartialEq, Debug, Copy, Clone)]
189enum Commands {
190    Help,
191    Start,
192    Back,
193    Quit,
194    Command1,
195    Command2,
196    Command3,
197    Command4,
198    Command5,
199    Command6,
200    Restart,
201    FrontPage,
202}
203
204impl Commands {
205    fn ask_for_user_command() -> Option<Commands> {
206        use std::io;
207        let mut input = String::new();
208        io::stdin()
209            .read_line(&mut input)
210            .expect("Failed to read line when asking for user command");
211        let flag = input == "\n";
212        let input = input.trim();
213        if input == "h" || input == "help" {
214            Some(Commands::Help)
215        } else if input == "s" || input == "start" {
216            Some(Commands::Start)
217        } else if input == "q" || input == "quit" {
218            Some(Commands::Quit)
219        } else if input == "r" || input == "restart" {
220            Some(Commands::Restart)
221        } else if input == "f" || input == "frontpage" {
222            Some(Commands::FrontPage)
223        } else if input == "b" || input == "back" || flag {
224            Some(Commands::Back)
225        } else if input == "1" {
226            Some(Commands::Command1)
227        } else if input == "2" {
228            Some(Commands::Command2)
229        } else if input == "3" {
230            Some(Commands::Command3)
231        } else if input == "4" {
232            Some(Commands::Command4)
233        } else if input == "5" {
234            Some(Commands::Command5)
235        } else if input == "6" {
236            Some(Commands::Command6)
237        } else {
238            None
239        }
240    }
241
242    fn keep_asking_for_valid_user_command() -> Commands {
243        let command: Commands;
244        loop {
245            command = match Commands::ask_for_user_command() {
246                Some(command) => command,
247                None => {
248                    println!("unrecognized command");
249                    continue;
250                }
251            };
252            break;
253        }
254        command
255    }
256}
257
258use std::fmt;
259impl fmt::Display for Commands {
260    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
261        write!(
262            f,
263            "{}",
264            Purple.paint(match &self {
265                Commands::Help => "(h)elp",
266                Commands::Back => "(b)ack/[Enter]",
267                Commands::Start => "(s)tart",
268                Commands::Quit => "(q)uit",
269                Commands::Restart => "(r)estart",
270                Commands::FrontPage => "(f)rontpage",
271                Commands::Command1 => "(1)",
272                Commands::Command2 => "(2)",
273                Commands::Command3 => "(3)",
274                Commands::Command4 => "(4)",
275                Commands::Command5 => "(5)",
276                Commands::Command6 => "(6)",
277            })
278        )
279    }
280}
281
282use super::game::Players;
283struct CommandCache {
284    current_page: Page,
285    available_commands: Vec<Commands>,
286    current_player: Players,
287    winner: Option<Players>,
288}
289
290impl CommandCache {
291    fn new() -> CommandCache {
292        CommandCache {
293            current_page: Page::FrontPage,
294            available_commands: FRONT_PAGE_AVAILABLE_COMMANDS.to_vec(),
295            current_player: Players::Player1,
296            winner: None,
297        }
298    }
299
300    fn change_player(&mut self) {
301        if self.current_player == Players::Player1 {
302            self.current_player = Players::Player2;
303        } else {
304            self.current_player = Players::Player1;
305        }
306    }
307
308    fn print_current_page(&self, config: &Config, board: &Board) {
309        match self.current_page {
310            Page::FrontPage => {
311                print!(
312                    "{}
313Welcome to tictactoe-rust!
314
315Current configuration: 
316",
317                    Cyan.paint(TICTACTOE_LOGO)
318                );
319                config.text_print_config(false);
320                println!();
321                self.print_available_commands();
322            }
323            Page::FrontPageHelp => {
324                println!("{}", GAME_RULE);
325                println!();
326                self.print_available_commands();
327            }
328            Page::ConfigMode => {
329                println!(
330                    "Please enter mode:
331  [1] PVP : User1 vs User2
332  [2] PVC : User1 vs AI1
333  [3] CVC : AI1   vs AI2
334"
335                );
336                self.print_available_commands();
337            }
338            Page::ConfigPlayer => {
339                println!("Configuration for name and piece character:");
340                config.text_print_player_page_config();
341                println!();
342                self.print_available_commands();
343            }
344            Page::EnterUserName1 => {
345                println!("Please enter new user name for Player1");
346            }
347            Page::EnterUserName2 => {
348                println!("Please enter new user name for Player2");
349            }
350            Page::EnterUserPiece1 => {
351                println!("Please enter new 1-len string for piece for Player1");
352                println!();
353                print_table(PIECE_TABLE, 2);
354            }
355            Page::EnterUserPiece2 => {
356                println!("Please enter new 1-len string for piece for Player1");
357                println!();
358                print_table(PIECE_TABLE, 2);
359            }
360            Page::ConfigFirst => {
361                println!(
362                    "Please enter who will play first:
363  [1] Player1
364  [2] Player2
365"
366                );
367                self.print_available_commands();
368            }
369            Page::ConfigAiSmartness => {
370                println!(
371                    "Choose the smartness level of Ai.
372
373  [1] Kindergarden (random choose where is empty)
374  [2] Elementary   (get match line if it can and step in your match line)
375  [3] Graduate     (as smart as elementary)
376  [4] God          (as smart as elementary)
377"
378                );
379            }
380            Page::ConfigEmptyPiece => {
381                println!(
382                    "Please enter the 1-len string for empty piece
383Or choose one of the following:
384"
385                );
386                print_table(EMPTY_PIECE_TABLE, 2);
387            }
388            Page::Playing => {
389                super::game::Game::text_print_board(board, config);
390            }
391            Page::Winner => {
392                super::game::Game::text_print_board(board, config);
393                if self.winner.is_some() {
394                    let winner = self.winner.unwrap();
395                    let name = if Players::Player1 == winner {
396                        config.players.0.name.as_str()
397                    } else {
398                        config.players.1.name.as_str()
399                    };
400                    println!("The winner is {}", name);
401                } else {
402                    println!("The game is a draw.");
403                }
404                board.print_matches(2);
405                self.print_available_commands();
406            }
407        }
408    }
409
410    fn execute_command_and_update_command_cache(
411        &mut self,
412        command: Commands,
413        config: &mut Config,
414        board: &mut Board,
415    ) -> Result<(), &str> {
416        use super::game::{Mode, Players};
417        match self.current_page {
418            Page::FrontPage => match command {
419                Commands::Help => {
420                    self.current_page = Page::FrontPageHelp;
421                    self.available_commands = FRONT_PAGE_HELP_AVAILABLE_COMMANDS.to_vec();
422                    Ok(())
423                }
424                Commands::Start => {
425                    self.current_page = Page::Playing;
426                    self.available_commands = vec![];
427                    Ok(())
428                }
429                Commands::Command1 => {
430                    self.current_page = Page::ConfigMode;
431                    self.available_commands = vec![
432                        Commands::Command1,
433                        Commands::Command2,
434                        Commands::Command3,
435                        Commands::Back,
436                        Commands::Quit,
437                    ];
438                    Ok(())
439                }
440                Commands::Command2 | Commands::Command3 => {
441                    self.current_page = Page::ConfigPlayer;
442                    self.available_commands = CONFG_PLAYER_AVAILABLE_COMMANDS.to_vec();
443                    Ok(())
444                }
445                Commands::Command4 => {
446                    self.current_page = Page::ConfigFirst;
447                    self.available_commands = vec![
448                        Commands::Command1,
449                        Commands::Command2,
450                        Commands::Back,
451                        Commands::Quit,
452                    ];
453                    Ok(())
454                }
455                Commands::Command5 => {
456                    self.current_page = Page::ConfigAiSmartness;
457                    self.available_commands = vec![
458                        Commands::Command1,
459                        Commands::Command2,
460                        Commands::Command3,
461                        Commands::Command4,
462                        Commands::Back,
463                        Commands::Quit,
464                    ];
465                    Ok(())
466                }
467                Commands::Command6 => {
468                    self.current_page = Page::ConfigEmptyPiece;
469                    self.available_commands = vec![];
470                    Ok(())
471                }
472                Commands::Quit => {
473                    panic!("Should not reach here");
474                }
475                _ => Err("Does not support this command on this page"),
476            },
477            Page::FrontPageHelp => match command {
478                Commands::Back => {
479                    self.current_page = Page::FrontPage;
480                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
481                    Ok(())
482                }
483                Commands::Start => {
484                    self.current_page = Page::Playing;
485                    self.available_commands = vec![];
486                    Ok(())
487                }
488                Commands::Quit => {
489                    panic!("Should not reach here");
490                }
491                _ => Err("Does not support this command on this page"),
492            },
493            Page::ConfigMode => match command {
494                Commands::Command1 => {
495                    self.current_page = Page::FrontPage;
496                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
497                    config.mode = Mode::PVP;
498                    config.players.0.is_ai = false;
499                    config.players.0.name = String::from("User1");
500                    config.players.1.name = String::from("User2");
501                    config.players.1.is_ai = false;
502                    Ok(())
503                }
504                Commands::Command2 => {
505                    self.current_page = Page::FrontPage;
506                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
507                    config.mode = Mode::PVC;
508                    config.players.0.is_ai = false;
509                    config.players.1.is_ai = true;
510                    config.players.0.name = String::from("User1");
511                    config.players.1.name = String::from("AI1");
512                    Ok(())
513                }
514                Commands::Command3 => {
515                    self.current_page = Page::FrontPage;
516                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
517                    config.mode = Mode::CVC;
518                    config.players.0.is_ai = true;
519                    config.players.1.is_ai = true;
520                    config.players.0.name = String::from("AI1");
521                    config.players.1.name = String::from("AI2");
522                    Ok(())
523                }
524                Commands::Back => {
525                    self.current_page = Page::FrontPage;
526                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
527                    Ok(())
528                }
529                Commands::Quit => {
530                    panic!("Should not reach here");
531                }
532                _ => Err("Does not support this command on this page"),
533            },
534            Page::ConfigPlayer => match command {
535                Commands::Command1 => {
536                    self.current_page = Page::EnterUserName1;
537                    self.available_commands = vec![];
538                    Ok(())
539                }
540                Commands::Command2 => {
541                    self.current_page = Page::EnterUserName2;
542                    self.available_commands = vec![];
543                    Ok(())
544                }
545                Commands::Command3 => {
546                    self.current_page = Page::EnterUserPiece1;
547                    self.available_commands = vec![];
548                    Ok(())
549                }
550                Commands::Command4 => {
551                    self.current_page = Page::EnterUserPiece2;
552                    self.available_commands = vec![];
553                    Ok(())
554                }
555                Commands::Back => {
556                    self.current_page = Page::FrontPage;
557                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
558                    Ok(())
559                }
560                Commands::Quit => {
561                    panic!("Should not reach here");
562                }
563                _ => Err("Does not support this command on this page"),
564            },
565            Page::EnterUserName1 => {
566                self.current_page = Page::ConfigPlayer;
567                self.available_commands = CONFG_PLAYER_AVAILABLE_COMMANDS.to_vec();
568                config.players.0.name = ask_for_user_name();
569                Ok(())
570            }
571            Page::EnterUserName2 => {
572                self.current_page = Page::ConfigPlayer;
573                self.available_commands = CONFG_PLAYER_AVAILABLE_COMMANDS.to_vec();
574                config.players.1.name = ask_for_user_name();
575                Ok(())
576            }
577            Page::EnterUserPiece1 => {
578                self.current_page = Page::ConfigPlayer;
579                self.available_commands = CONFG_PLAYER_AVAILABLE_COMMANDS.to_vec();
580                let result = ask_for_user_string(PIECE_TABLE);
581                if result.is_ok() {
582                    let user_string = result.unwrap();
583                    config.players.0.piece = user_string;
584                    Ok(())
585                } else {
586                    Err(result.unwrap_err())
587                }
588            }
589            Page::EnterUserPiece2 => {
590                self.current_page = Page::ConfigPlayer;
591                self.available_commands = CONFG_PLAYER_AVAILABLE_COMMANDS.to_vec();
592                let result = ask_for_user_string(PIECE_TABLE);
593                if result.is_ok() {
594                    let user_string = result.unwrap();
595                    config.players.1.piece = user_string;
596                    Ok(())
597                } else {
598                    Err(result.unwrap_err())
599                }
600            }
601            Page::ConfigFirst => match command {
602                Commands::Command1 => {
603                    self.current_page = Page::FrontPage;
604                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
605                    config.first = Players::Player1;
606                    self.current_player = Players::Player1;
607                    Ok(())
608                }
609                Commands::Command2 => {
610                    self.current_page = Page::FrontPage;
611                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
612                    config.first = Players::Player2;
613                    self.current_player = Players::Player2;
614                    Ok(())
615                }
616                Commands::Back => {
617                    self.current_page = Page::FrontPage;
618                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
619                    Ok(())
620                }
621                Commands::Quit => {
622                    panic!("Should not reach here");
623                }
624                _ => Err("Does not support this command on this page"),
625            },
626            Page::ConfigAiSmartness => {
627                use super::game::SmartLevel;
628                match command {
629                    Commands::Command1 => {
630                        config.ai_smartness = SmartLevel::Kindergarden;
631                        self.current_page = Page::FrontPage;
632                        self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
633                        Ok(())
634                    }
635                    Commands::Command2 => {
636                        config.ai_smartness = SmartLevel::Elementary;
637                        self.current_page = Page::FrontPage;
638                        self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
639                        Ok(())
640                    }
641                    Commands::Command3 => {
642                        config.ai_smartness = SmartLevel::Graduate;
643                        self.current_page = Page::FrontPage;
644                        self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
645                        Ok(())
646                    }
647                    Commands::Command4 => {
648                        config.ai_smartness = SmartLevel::God;
649                        self.current_page = Page::FrontPage;
650                        self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
651                        Ok(())
652                    }
653                    Commands::Back => {
654                        self.current_page = Page::FrontPage;
655                        self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
656                        Ok(())
657                    }
658                    Commands::Quit => {
659                        panic!("Should not reach here");
660                    }
661                    _ => Err("Does not support this command on this page"),
662                }
663            }
664            Page::ConfigEmptyPiece => {
665                let result = ask_for_user_string(EMPTY_PIECE_TABLE);
666                if result.is_ok() {
667                    let user_string = result.unwrap();
668                    config.empty_piece = user_string;
669                    self.current_page = Page::FrontPage;
670                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
671                    Ok(())
672                } else {
673                    Err(result.unwrap_err())
674                }
675            }
676            Page::Playing => {
677                let mut coordinates: (usize, usize);
678                use super::ai;
679                if self.current_player == Players::Player1 && config.players.0.is_ai {
680                    let coordinates =
681                        ai::Ai::ask_ai_move_input(board, self.current_player, config.ai_smartness);
682                    println!(
683                        "{}'s input is ({}, {})",
684                        config.players.0.name, coordinates.0, coordinates.1
685                    );
686                    board
687                        .set_coordinate(coordinates.0, coordinates.1, self.current_player)
688                        .unwrap();
689                } else if self.current_player == Players::Player2 && config.players.1.is_ai {
690                    let coordinates =
691                        ai::Ai::ask_ai_move_input(board, self.current_player, config.ai_smartness);
692                    println!(
693                        "{}'s input is ({}, {})",
694                        config.players.1.name, coordinates.0, coordinates.1
695                    );
696                    board
697                        .set_coordinate(coordinates.0, coordinates.1, self.current_player)
698                        .unwrap();
699                } else {
700                    loop {
701                        let result = ask_user_move_input();
702                        if result.is_ok() {
703                            coordinates = result.unwrap();
704                            let tmp_result = board.set_coordinate(
705                                coordinates.0,
706                                coordinates.1,
707                                self.current_player,
708                            );
709                            if tmp_result.is_err() {
710                                println!("{}", tmp_result.unwrap_err());
711                                continue;
712                            } else {
713                                break;
714                            }
715                        } else {
716                            let err = result.unwrap_err();
717                            println!("{}", err);
718                            if err == "Quit" {
719                                std::process::exit(0);
720                            }
721                            continue;
722                        }
723                    }
724                }
725
726                board.update_matches();
727                let player = board.has_match_line();
728                if player.is_some() {
729                    self.winner = player;
730                    self.current_page = Page::Winner;
731                    self.available_commands =
732                        vec![Commands::Restart, Commands::FrontPage, Commands::Quit];
733                } else if board.is_full() {
734                    self.winner = None;
735                    self.current_page = Page::Winner;
736                    self.available_commands =
737                        vec![Commands::Restart, Commands::FrontPage, Commands::Quit];
738                } else {
739                    self.change_player();
740                }
741                Ok(())
742            }
743            Page::Winner => match command {
744                Commands::Restart => {
745                    self.current_page = Page::Playing;
746                    self.available_commands = vec![];
747                    board.clear();
748                    Ok(())
749                }
750                Commands::FrontPage => {
751                    self.current_page = Page::FrontPage;
752                    self.available_commands = FRONT_PAGE_AVAILABLE_COMMANDS.to_vec();
753                    board.clear();
754                    Ok(())
755                }
756                Commands::Quit => {
757                    panic!("Should not reach here");
758                }
759                _ => Err("Does not support this command on this page"),
760            },
761        }
762    }
763
764    fn print_available_commands(&self) {
765        use ansi_term::Colour::*;
766        println!("{}", Blue.paint("Available commands:"));
767        let mut iter = self.available_commands.iter();
768        print!("{}", iter.next().unwrap());
769        for c in iter {
770            print!(" {}", c);
771        }
772        println!();
773    }
774}
775
776#[cfg(test)]
777mod tests;