├── .gitignore ├── assets ├── rust-15-puzzle.gif ├── rust-15-puzzle-playing.png ├── rust-15-puzzle-finishing.png └── rust-15-puzzle-lightmode.png ├── src ├── helper │ ├── mod.rs │ ├── event.rs │ ├── draw.rs │ └── util.rs └── main.rs ├── .github └── workflows │ └── rust.yml ├── LICENSE ├── Cargo.toml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | debug_only.log 3 | -------------------------------------------------------------------------------- /assets/rust-15-puzzle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24seconds/rust-15-puzzle-cli/HEAD/assets/rust-15-puzzle.gif -------------------------------------------------------------------------------- /assets/rust-15-puzzle-playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24seconds/rust-15-puzzle-cli/HEAD/assets/rust-15-puzzle-playing.png -------------------------------------------------------------------------------- /src/helper/mod.rs: -------------------------------------------------------------------------------- 1 | mod draw; 2 | mod event; 3 | mod util; 4 | 5 | pub use draw::*; 6 | pub use event::*; 7 | pub use util::*; 8 | -------------------------------------------------------------------------------- /assets/rust-15-puzzle-finishing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24seconds/rust-15-puzzle-cli/HEAD/assets/rust-15-puzzle-finishing.png -------------------------------------------------------------------------------- /assets/rust-15-puzzle-lightmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24seconds/rust-15-puzzle-cli/HEAD/assets/rust-15-puzzle-lightmode.png -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: cargo build --verbose 18 | - name: Run tests 19 | run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ---------------------------------------------------------------------------- 3 | * "THE BEER-WARE LICENSE" (Revision 42): 4 | * <24seconds> wrote this file. As long as you retain this notice you 5 | * can do whatever you want with this stuff. If we meet some day, and you think 6 | * this stuff is worth it, you can buy me a beer in return. 7 | * ---------------------------------------------------------------------------- 8 | */ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-15-puzzle-cli" 3 | version = "0.2.0" 4 | authors = ["24seconds <24crazyoung@gmail.com>"] 5 | description = """ 6 | rust-15-puzzle-cli is 15puzzle terminal game written in Rust! 7 | """ 8 | homepage = "https://github.com/24seconds/rust-15-puzzle-cli" 9 | repository = "https://github.com/24seconds/rust-15-puzzle-cli" 10 | readme = "README.md" 11 | keywords = ["15-puzzle", "terminal-app"] 12 | categories = ["command-line-utilities", "games"] 13 | edition = "2018" 14 | license = "Beerware" 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | rand = "0.7.3" 20 | termion = "1.5.5" 21 | tui="0.9.1" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧩 rust-15puzzle-cli 2 | 3 | [`15 puzzle`](https://en.wikipedia.org/wiki/15_puzzle) terminal game written in Rust! 4 | 5 | 6 | ### Demo 7 | 8 | 9 | 10 | 11 | #### Game playing screenshot 12 | 13 | 14 | ### DarkMode 15 | 16 | 17 | 18 | ### LightMode 19 | 20 | 21 | 22 | #### Finishing screenshot 23 | 24 | 25 | 26 | 27 | -------------------- 28 | 29 | ### 🎮 How to use? 30 | 31 | #### Cargo run! 32 | ``` 33 | $ cargo run --release 34 | ``` 35 | 36 | Commands 37 | 38 | ```md 39 | Move: ↑,↓,←,→ or w,s,a,d 40 | Quit : q 41 | New game : r 42 | Pause : p 43 | ``` 44 | 45 | -------------- 46 | 47 | #### Installation 48 | 49 | For rust users 50 | 51 | ``` 52 | $ cargo install rust-15-puzzle-cli 53 | ``` 54 | -------------------------------------------------------------------------------- /src/helper/event.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::sync::mpsc; 3 | use std::sync::{ 4 | atomic::{AtomicBool, Ordering}, 5 | Arc, 6 | }; 7 | use std::thread; 8 | use std::time::Duration; 9 | 10 | use termion::event::Key; 11 | use termion::input::TermRead; 12 | 13 | pub enum Event { 14 | Input(I), 15 | Tick, 16 | } 17 | 18 | /// A small event handler that wrap termion input and tick events. Each event 19 | /// type is handled in its own thread and returned to a common `Receiver` 20 | #[allow(dead_code)] 21 | pub struct Events { 22 | rx: mpsc::Receiver>, 23 | 24 | tick_handle: thread::JoinHandle<()>, 25 | } 26 | 27 | #[derive(Debug, Clone, Copy)] 28 | pub struct Config { 29 | pub exit_key: Key, 30 | pub tick_rate: Duration, 31 | } 32 | 33 | impl Default for Config { 34 | fn default() -> Config { 35 | Config { 36 | exit_key: Key::Char('q'), 37 | tick_rate: Duration::from_millis(250), 38 | } 39 | } 40 | } 41 | 42 | impl Events { 43 | pub fn new() -> Events { 44 | Events::with_config(Config::default()) 45 | } 46 | 47 | pub fn with_config(config: Config) -> Events { 48 | let (tx, rx) = mpsc::channel(); 49 | let ignore_exit_key = Arc::new(AtomicBool::new(false)); 50 | let _input_handle = { 51 | let tx = tx.clone(); 52 | let ignore_exit_key = ignore_exit_key.clone(); 53 | thread::spawn(move || { 54 | let stdin = io::stdin(); 55 | for evt in stdin.keys() { 56 | match evt { 57 | Ok(key) => { 58 | if let Err(_) = tx.send(Event::Input(key)) { 59 | return; 60 | } 61 | if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { 62 | return; 63 | } 64 | } 65 | Err(_) => {} 66 | } 67 | } 68 | }) 69 | }; 70 | let tick_handle = { 71 | let tx = tx.clone(); 72 | thread::spawn(move || { 73 | let tx = tx.clone(); 74 | loop { 75 | tx.send(Event::Tick).unwrap(); 76 | thread::sleep(config.tick_rate); 77 | } 78 | }) 79 | }; 80 | Events { rx, tick_handle } 81 | } 82 | 83 | pub fn next(&self) -> Result, mpsc::RecvError> { 84 | self.rx.recv() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/helper/draw.rs: -------------------------------------------------------------------------------- 1 | use crate::helper::{GameState, ThemeSystem}; 2 | use std::error::Error; 3 | use tui::{ 4 | backend::Backend, 5 | layout::{Alignment, Rect}, 6 | style::{Color, Modifier, Style}, 7 | widgets::{Block, BorderType, Borders, Paragraph, Text}, 8 | Frame, 9 | }; 10 | 11 | pub fn draw_board( 12 | arr: &[u16; 16], 13 | frame: &mut Frame, 14 | area: &Rect, 15 | length: u16, 16 | theme_system: &ThemeSystem, 17 | ) -> Result<(), Box> 18 | where 19 | B: Backend, 20 | { 21 | let board = [ 22 | (0, 0), 23 | (1, 0), 24 | (2, 0), 25 | (3, 0), 26 | (0, 1), 27 | (1, 1), 28 | (2, 1), 29 | (3, 1), 30 | (0, 2), 31 | (1, 2), 32 | (2, 2), 33 | (3, 2), 34 | (0, 3), 35 | (1, 3), 36 | (2, 3), 37 | (3, 3), 38 | ]; 39 | 40 | let color_tile_default_border = theme_system.get_color_tile_default_border(); 41 | let color_tile_text = theme_system.get_color_tile_text(); 42 | let color_tile_selected_border = theme_system.get_color_tile_selected_border(); 43 | 44 | board.iter().zip(arr.iter()).enumerate().for_each(|x| { 45 | let (index, (multiplier, number)) = x; 46 | let width = length + 3; 47 | let height = length; 48 | let area = Rect::new( 49 | area.x + width * multiplier.0, 50 | area.y + length * multiplier.1, 51 | width, 52 | height, 53 | ); 54 | 55 | let style_selected = Style::default().fg(if index as u16 + 1 == *number && *number != 0 { 56 | color_tile_selected_border 57 | } else { 58 | color_tile_default_border 59 | }); 60 | 61 | let block = Block::default() 62 | .borders(Borders::ALL) 63 | .border_type(BorderType::Rounded) 64 | .border_style(style_selected); 65 | 66 | let number_string = if *number == 0 { 67 | String::from("") 68 | } else { 69 | format!("\n{}", number) 70 | }; 71 | 72 | let text = [Text::styled( 73 | number_string, 74 | style_selected.modifier(Modifier::BOLD).fg(color_tile_text), 75 | )]; 76 | let paragraph = Paragraph::new(text.iter()) 77 | .block(block) 78 | .alignment(Alignment::Center); 79 | 80 | frame.render_widget(paragraph, area); 81 | frame.render_widget(block, area); 82 | }); 83 | 84 | Ok(()) 85 | } 86 | 87 | pub fn draw_guide(frame: &mut Frame, area: &Rect) -> Result<(), Box> 88 | where 89 | B: Backend, 90 | { 91 | let guide = r#" 92 | 93 | Commands 94 | Move: ↑,↓,←,→ or w,s,a,d 95 | Quit : q 96 | New game : r 97 | Pause : p 98 | Change ColorTheme: c 99 | "#; 100 | 101 | let block = Block::default() 102 | .borders(Borders::NONE) 103 | .title("rust-15-puzzle : v0.1.0") 104 | .title_style(Style::default().modifier(Modifier::BOLD)); 105 | let text = [Text::styled( 106 | guide, 107 | Style::default() 108 | .fg(Color::LightBlue) 109 | .modifier(Modifier::BOLD), 110 | )]; 111 | let paragraph = Paragraph::new(text.iter()) 112 | .block(block) 113 | .alignment(Alignment::Left); 114 | 115 | frame.render_widget(paragraph, *area); 116 | 117 | Ok(()) 118 | } 119 | 120 | pub fn draw_header( 121 | frame: &mut Frame, 122 | area: &Rect, 123 | game_state: &GameState, 124 | ) -> Result<(), Box> 125 | where 126 | B: Backend, 127 | { 128 | let block = Block::default() 129 | .borders(Borders::NONE) 130 | .border_style(Style::default().fg(Color::Yellow)); 131 | 132 | let data = match game_state { 133 | GameState::INIT => { 134 | "\n To start, press move key! \n If you can't see the board, press 'c' to change Theme!" 135 | } 136 | GameState::PAUSED => "\n PAUSED", 137 | GameState::DONE => "\n Excellent! Press 'r' to start new game!", 138 | _ => "", 139 | }; 140 | 141 | let text = [Text::styled( 142 | data, 143 | Style::default() 144 | .fg(Color::Yellow) 145 | .modifier(if game_state == &GameState::DONE { 146 | Modifier::SLOW_BLINK | Modifier::BOLD 147 | } else { 148 | Modifier::empty() | Modifier::BOLD 149 | }), 150 | )]; 151 | let paragraph = Paragraph::new(text.iter()) 152 | .block(block) 153 | .alignment(Alignment::Left); 154 | 155 | frame.render_widget(paragraph, *area); 156 | 157 | Ok(()) 158 | } 159 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "1.2.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 8 | 9 | [[package]] 10 | name = "cassowary" 11 | version = "0.3.0" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 14 | 15 | [[package]] 16 | name = "cfg-if" 17 | version = "0.1.10" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 20 | 21 | [[package]] 22 | name = "either" 23 | version = "1.5.3" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 26 | 27 | [[package]] 28 | name = "getrandom" 29 | version = "0.1.14" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 32 | dependencies = [ 33 | "cfg-if", 34 | "libc", 35 | "wasi", 36 | ] 37 | 38 | [[package]] 39 | name = "itertools" 40 | version = "0.9.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" 43 | dependencies = [ 44 | "either", 45 | ] 46 | 47 | [[package]] 48 | name = "libc" 49 | version = "0.2.69" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" 52 | 53 | [[package]] 54 | name = "numtoa" 55 | version = "0.1.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 58 | 59 | [[package]] 60 | name = "ppv-lite86" 61 | version = "0.2.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 64 | 65 | [[package]] 66 | name = "rand" 67 | version = "0.7.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 70 | dependencies = [ 71 | "getrandom", 72 | "libc", 73 | "rand_chacha", 74 | "rand_core", 75 | "rand_hc", 76 | ] 77 | 78 | [[package]] 79 | name = "rand_chacha" 80 | version = "0.2.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 83 | dependencies = [ 84 | "ppv-lite86", 85 | "rand_core", 86 | ] 87 | 88 | [[package]] 89 | name = "rand_core" 90 | version = "0.5.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 93 | dependencies = [ 94 | "getrandom", 95 | ] 96 | 97 | [[package]] 98 | name = "rand_hc" 99 | version = "0.2.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 102 | dependencies = [ 103 | "rand_core", 104 | ] 105 | 106 | [[package]] 107 | name = "redox_syscall" 108 | version = "0.1.56" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 111 | 112 | [[package]] 113 | name = "redox_termios" 114 | version = "0.1.1" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 117 | dependencies = [ 118 | "redox_syscall", 119 | ] 120 | 121 | [[package]] 122 | name = "rust-15-puzzle-cli" 123 | version = "0.2.0" 124 | dependencies = [ 125 | "rand", 126 | "termion", 127 | "tui", 128 | ] 129 | 130 | [[package]] 131 | name = "termion" 132 | version = "1.5.5" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" 135 | dependencies = [ 136 | "libc", 137 | "numtoa", 138 | "redox_syscall", 139 | "redox_termios", 140 | ] 141 | 142 | [[package]] 143 | name = "tui" 144 | version = "0.9.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b7de74b91c6cb83119a2140e7c215d95d9e54db27b58a500a2cbdeec4987b0a2" 147 | dependencies = [ 148 | "bitflags", 149 | "cassowary", 150 | "either", 151 | "itertools", 152 | "termion", 153 | "unicode-segmentation", 154 | "unicode-width", 155 | ] 156 | 157 | [[package]] 158 | name = "unicode-segmentation" 159 | version = "1.6.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 162 | 163 | [[package]] 164 | name = "unicode-width" 165 | version = "0.1.7" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 168 | 169 | [[package]] 170 | name = "wasi" 171 | version = "0.9.0+wasi-snapshot-preview1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 174 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod helper; 2 | use helper::{ 3 | draw_board, draw_header, handle_game_state, handle_move_operation, move_tile, 4 | update_elapsed_time, Event, Events, GameData, GameState, Operation, ThemeMode, ThemeSystem, 5 | }; 6 | 7 | use std::{error::Error, io, time::Instant}; 8 | use termion::{event::Key, raw::IntoRawMode, screen::AlternateScreen}; 9 | use tui::{ 10 | backend::TermionBackend, 11 | layout::{Constraint, Direction, Layout, Margin}, 12 | style::{Color, Modifier, Style}, 13 | widgets::{Block, Borders}, 14 | Terminal, 15 | }; 16 | 17 | fn main() -> Result<(), Box> { 18 | // Terminal initialization 19 | let stdout = io::stdout().into_raw_mode()?; 20 | let stdout = AlternateScreen::from(stdout); 21 | let backend = TermionBackend::new(stdout); 22 | let mut terminal = Terminal::new(backend)?; 23 | terminal.hide_cursor()?; 24 | 25 | // Setup event handlers 26 | let events = Events::new(); 27 | let mut rng = rand::thread_rng(); 28 | 29 | let mut game_data = GameData::new(&mut rng); 30 | let mut theme_system = ThemeSystem::new(ThemeMode::DarkMode); 31 | 32 | loop { 33 | terminal.draw(|mut f| { 34 | let layout_chunks = Layout::default() 35 | .direction(Direction::Vertical) 36 | .constraints( 37 | [ 38 | Constraint::Length(4), 39 | Constraint::Min(0), // main render 40 | ] 41 | .as_ref(), 42 | ) 43 | .split(f.size()); 44 | 45 | let chunks = Layout::default() 46 | .direction(Direction::Horizontal) 47 | .constraints( 48 | [ 49 | Constraint::Length(10), 50 | Constraint::Length(40), 51 | Constraint::Min(0), 52 | ] 53 | .as_ref(), 54 | ) 55 | .split(layout_chunks[1]); 56 | 57 | let footer_chunks = Layout::default() 58 | .direction(Direction::Vertical) 59 | .constraints([Constraint::Length(23), Constraint::Min(0)].as_ref()) 60 | .split(chunks[1]); 61 | 62 | { 63 | draw_header( 64 | &mut f, 65 | &layout_chunks[0].inner(&Margin { 66 | horizontal: 10, 67 | vertical: 0, 68 | }), 69 | &game_data.game_state, 70 | ) 71 | .unwrap(); 72 | } 73 | 74 | { 75 | let time = match game_data.game_state { 76 | GameState::INIT => { 77 | game_data.start_time = Instant::now(); 78 | 79 | 0 80 | } 81 | GameState::PLAYING => { 82 | game_data.base_time + game_data.start_time.elapsed().as_secs() 83 | } 84 | GameState::PAUSED => { 85 | game_data.start_time = Instant::now(); 86 | 87 | game_data.base_time 88 | } 89 | GameState::DONE => game_data.base_time, 90 | }; 91 | 92 | let title_string = format!(" Time: {}s Moves: {}", time, &game_data.move_count); 93 | let title_string = title_string.as_str(); 94 | 95 | let block = Block::default() 96 | .borders(Borders::NONE) 97 | .title(title_string) 98 | .title_style(Style::default().modifier(Modifier::BOLD)); 99 | f.render_widget(block, chunks[1]); 100 | 101 | draw_board( 102 | &game_data.arr_state, 103 | &mut f, 104 | &chunks[1].inner(&Margin { 105 | horizontal: 1, 106 | vertical: 2, 107 | }), 108 | 5, 109 | &theme_system, 110 | ) 111 | .unwrap(); 112 | } 113 | 114 | { 115 | helper::draw_guide(&mut f, &chunks[2]).unwrap(); 116 | } 117 | 118 | { 119 | let footer = "🍺 Github: 24seconds/rust-15-puzzle-cli"; 120 | let block = Block::default() 121 | .borders(Borders::NONE) 122 | .border_style(Style::default().fg(Color::Yellow)) 123 | .title(footer); 124 | f.render_widget(block, footer_chunks[1]); 125 | } 126 | })?; 127 | 128 | match events.next()? { 129 | Event::Input(key) => match key { 130 | Key::Char('q') => { 131 | break; 132 | } 133 | Key::Char('w') | Key::Up => { 134 | let next_arr_state = move_tile(&game_data.arr_state, Operation::UP)?; 135 | handle_move_operation(&mut game_data, next_arr_state, 'w'); 136 | } 137 | Key::Char('a') | Key::Left => { 138 | let next_arr_state = move_tile(&game_data.arr_state, Operation::LEFT)?; 139 | handle_move_operation(&mut game_data, next_arr_state, 'a'); 140 | } 141 | Key::Char('s') | Key::Down => { 142 | let next_arr_state = move_tile(&game_data.arr_state, Operation::DOWN)?; 143 | handle_move_operation(&mut game_data, next_arr_state, 's'); 144 | } 145 | Key::Char('d') | Key::Right => { 146 | let next_arr_state = move_tile(&game_data.arr_state, Operation::RIGHT)?; 147 | handle_move_operation(&mut game_data, next_arr_state, 'd'); 148 | } 149 | Key::Char('p') => { 150 | let next_game_state = handle_game_state(&game_data, 'p'); 151 | 152 | game_data.base_time = update_elapsed_time(&game_data, &next_game_state); 153 | game_data.game_state = next_game_state; 154 | } 155 | Key::Char('r') => { 156 | game_data = GameData::new(&mut rng); 157 | let next_game_state = handle_game_state(&game_data, 'r'); 158 | game_data.game_state = next_game_state; 159 | } 160 | Key::Char('c') => { 161 | theme_system = theme_system.change_theme(); 162 | } 163 | _ => {} 164 | }, 165 | _ => {} 166 | } 167 | } 168 | Ok(()) 169 | } 170 | -------------------------------------------------------------------------------- /src/helper/util.rs: -------------------------------------------------------------------------------- 1 | use rand::{rngs::ThreadRng, seq::SliceRandom}; 2 | use std::{error::Error, time::Instant}; 3 | use tui::style::Color; 4 | 5 | #[derive(PartialEq)] 6 | pub enum GameState { 7 | INIT, 8 | PLAYING, 9 | PAUSED, 10 | DONE, 11 | } 12 | 13 | pub struct GameData { 14 | pub game_state: GameState, 15 | pub move_count: i32, 16 | pub base_time: u64, 17 | pub arr_state: [u16; 16], 18 | pub start_time: Instant, 19 | } 20 | 21 | impl GameData { 22 | pub fn new(rng: &mut ThreadRng) -> Self { 23 | GameData { 24 | game_state: GameState::INIT, 25 | move_count: 0, 26 | base_time: 0, 27 | arr_state: shuffle_arr(rng).unwrap(), 28 | start_time: Instant::now(), 29 | } 30 | } 31 | } 32 | 33 | pub fn handle_move_operation(game_data: &mut GameData, next_arr_state: [u16; 16], key: char) { 34 | if !is_state_same(game_data.arr_state, next_arr_state) 35 | && game_data.game_state != GameState::DONE 36 | { 37 | game_data.move_count += 1; 38 | game_data.arr_state = next_arr_state; 39 | } 40 | 41 | let next_game_state = handle_game_state(&game_data, key); 42 | 43 | game_data.base_time = update_elapsed_time(&game_data, &next_game_state); 44 | game_data.game_state = next_game_state; 45 | } 46 | 47 | pub fn handle_game_state(game_data: &GameData, char: char) -> GameState { 48 | let curren_state = &game_data.game_state; 49 | let arr_state = &game_data.arr_state; 50 | 51 | match curren_state { 52 | GameState::INIT => { 53 | if ['w', 'a', 's', 'd'].contains(&char) { 54 | GameState::PLAYING 55 | } else { 56 | GameState::INIT 57 | } 58 | } 59 | GameState::PLAYING => { 60 | let is_done = is_done(arr_state); 61 | 62 | if char == 'p' { 63 | GameState::PAUSED 64 | } else if is_done { 65 | GameState::DONE 66 | } else { 67 | GameState::PLAYING 68 | } 69 | } 70 | GameState::PAUSED => GameState::PLAYING, 71 | GameState::DONE => { 72 | if char == 'r' { 73 | GameState::INIT 74 | } else { 75 | GameState::DONE 76 | } 77 | } 78 | } 79 | } 80 | 81 | pub fn update_elapsed_time(game_data: &GameData, next_game_state: &GameState) -> u64 { 82 | let game_state = &game_data.game_state; 83 | let base_time = game_data.base_time; 84 | let start_time = &game_data.start_time; 85 | 86 | let mut updated_base_time = base_time; 87 | 88 | if game_state == &GameState::PLAYING 89 | && (next_game_state == &GameState::PAUSED || next_game_state == &GameState::DONE) 90 | { 91 | updated_base_time = base_time + start_time.elapsed().as_secs(); 92 | } 93 | 94 | updated_base_time 95 | } 96 | 97 | fn is_state_same(arr1: [u16; 16], arr2: [u16; 16]) -> bool { 98 | for i in 0..arr1.len() { 99 | if arr1[i] != arr2[i] { 100 | return false; 101 | } 102 | } 103 | 104 | true 105 | } 106 | 107 | fn shuffle_arr(rng: &mut ThreadRng) -> Result<[u16; 16], Box> { 108 | let mut arr = [0; 16]; 109 | 110 | (0..16).into_iter().enumerate().for_each(|args| { 111 | let (index, number) = args; 112 | 113 | arr[index] = number; 114 | }); 115 | 116 | loop { 117 | arr.shuffle(rng); 118 | 119 | if is_solvable(&arr)? { 120 | break; 121 | } 122 | } 123 | 124 | Ok(arr) 125 | } 126 | 127 | fn is_solvable(arr: &[u16; 16]) -> Result> { 128 | // solvable : blank even row (count from bottom, count start from 1) and odd count inversions 129 | // solvable : blank odd row (count from bottom, count start from 1) and even count inversions 130 | 131 | let blank_index = arr 132 | .iter() 133 | .position(|x| *x == 0) 134 | .ok_or("There is no blank!")?; 135 | let blank_row = 4 - blank_index / 4; 136 | let inversion_count = count_inversion(&arr); 137 | 138 | let solvable = if blank_row % 2 == 0 { 139 | // blank row is even 140 | 141 | inversion_count % 2 == 1 142 | } else { 143 | // blank row is odd 144 | 145 | inversion_count % 2 == 0 146 | }; 147 | 148 | Ok(solvable) 149 | } 150 | 151 | fn count_inversion(arr: &[u16; 16]) -> u16 { 152 | let mut count = 0; 153 | 154 | let length = arr.len(); 155 | 156 | for i in 0..length { 157 | for j in 0..length { 158 | if i == j { 159 | continue; 160 | } 161 | 162 | if arr[i] == 0 || arr[j] == 0 { 163 | continue; 164 | } 165 | 166 | if arr[i] > arr[j] && i < j { 167 | count += 1; 168 | } 169 | } 170 | } 171 | 172 | count 173 | } 174 | 175 | pub enum Operation { 176 | UP, 177 | DOWN, 178 | LEFT, 179 | RIGHT, 180 | } 181 | 182 | pub fn move_tile(arr: &[u16; 16], operation: Operation) -> Result<[u16; 16], Box> { 183 | let mut next_arr = [0; 16]; 184 | 185 | arr.iter().enumerate().for_each(|args| { 186 | let (index, number) = args; 187 | next_arr[index] = *number; 188 | }); 189 | 190 | let index_blank = arr 191 | .iter() 192 | .position(|x| *x == 0) 193 | .ok_or("There is no blank!")?; 194 | let arr_length = arr.len(); 195 | 196 | match operation { 197 | Operation::UP => { 198 | let index_to_swap = index_blank + 4; 199 | 200 | if index_to_swap < arr_length { 201 | let temp = next_arr[index_blank]; 202 | next_arr[index_blank] = next_arr[index_to_swap]; 203 | next_arr[index_to_swap] = temp; 204 | } 205 | } 206 | Operation::DOWN => { 207 | let index_to_swap = index_blank as i32 - 4; 208 | 209 | if index_to_swap >= 0 { 210 | let index_to_swap = index_to_swap as usize; 211 | let temp = next_arr[index_blank]; 212 | 213 | next_arr[index_blank] = next_arr[index_to_swap]; 214 | next_arr[index_to_swap] = temp; 215 | } 216 | } 217 | Operation::LEFT => { 218 | let index_to_swap = index_blank + 1; 219 | 220 | if index_to_swap < arr_length && index_blank % 4 != 3 { 221 | let temp = next_arr[index_blank]; 222 | next_arr[index_blank] = next_arr[index_to_swap]; 223 | next_arr[index_to_swap] = temp; 224 | } 225 | } 226 | Operation::RIGHT => { 227 | let index_to_swap = index_blank as i32 - 1; 228 | 229 | if index_to_swap >= 0 && index_blank % 4 != 0 { 230 | let index_to_swap = index_to_swap as usize; 231 | let temp = next_arr[index_blank]; 232 | 233 | next_arr[index_blank] = next_arr[index_to_swap]; 234 | next_arr[index_to_swap] = temp; 235 | } 236 | } 237 | }; 238 | 239 | Ok(next_arr) 240 | } 241 | 242 | fn is_done(arr_state: &[u16; 16]) -> bool { 243 | let result = (0..16).into_iter().all(|x| { 244 | if x == 15 { 245 | arr_state[x as usize] == 0 246 | } else { 247 | x + 1 == arr_state[x as usize] 248 | } 249 | }); 250 | 251 | result 252 | } 253 | 254 | pub enum ThemeMode { 255 | LightMode, 256 | DarkMode, 257 | } 258 | 259 | pub struct ThemeSystem { 260 | mode: ThemeMode, 261 | } 262 | 263 | impl ThemeSystem { 264 | pub fn new(mode: ThemeMode) -> ThemeSystem { 265 | ThemeSystem { mode } 266 | } 267 | 268 | pub fn change_theme(self) -> ThemeSystem { 269 | match self.mode { 270 | ThemeMode::LightMode => ThemeSystem { 271 | mode: ThemeMode::DarkMode, 272 | }, 273 | ThemeMode::DarkMode => ThemeSystem { 274 | mode: ThemeMode::LightMode, 275 | }, 276 | } 277 | } 278 | 279 | pub fn get_color_tile_text(&self) -> Color { 280 | match self.mode { 281 | ThemeMode::LightMode => Color::Black, 282 | ThemeMode::DarkMode => Color::White, 283 | } 284 | } 285 | 286 | pub fn get_color_tile_default_border(&self) -> Color { 287 | match self.mode { 288 | ThemeMode::LightMode => Color::Black, 289 | ThemeMode::DarkMode => Color::White, 290 | } 291 | } 292 | 293 | pub fn get_color_tile_selected_border(&self) -> Color { 294 | match self.mode { 295 | ThemeMode::LightMode => Color::LightRed, 296 | ThemeMode::DarkMode => Color::Green, 297 | } 298 | } 299 | } 300 | 301 | #[cfg(test)] 302 | mod tests { 303 | use super::*; 304 | use rand::Rng; 305 | 306 | #[test] 307 | fn count_inversion_should_correct() { 308 | { 309 | let arr = [2, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0]; 310 | let count = count_inversion(&arr); 311 | assert_eq!(count, 1); 312 | } 313 | 314 | { 315 | let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; 316 | let count = count_inversion(&arr); 317 | assert_eq!(count, 0); 318 | } 319 | 320 | { 321 | let arr = [13, 2, 10, 3, 1, 12, 8, 4, 5, 0, 9, 6, 15, 14, 11, 7]; 322 | let count = count_inversion(&arr); 323 | assert_eq!(count, 41); 324 | } 325 | 326 | { 327 | let arr = [6, 13, 7, 10, 8, 9, 11, 0, 15, 2, 12, 5, 14, 3, 1, 4]; 328 | let count = count_inversion(&arr); 329 | assert_eq!(count, 62); 330 | } 331 | 332 | { 333 | let arr = [3, 9, 1, 15, 14, 11, 4, 6, 13, 0, 10, 12, 2, 7, 8, 5]; 334 | let count = count_inversion(&arr); 335 | assert_eq!(count, 56); 336 | } 337 | } 338 | 339 | #[test] 340 | fn is_solvable_should_correct() -> Result<(), Box> { 341 | { 342 | let arr = [13, 2, 10, 3, 1, 12, 8, 4, 5, 0, 9, 6, 15, 14, 11, 7]; 343 | let is_solvable = is_solvable(&arr)?; 344 | assert_eq!(is_solvable, true); 345 | } 346 | 347 | { 348 | let arr = [6, 13, 7, 10, 8, 9, 11, 0, 15, 2, 12, 5, 14, 3, 1, 4]; 349 | let is_solvable = is_solvable(&arr)?; 350 | assert_eq!(is_solvable, true); 351 | } 352 | 353 | { 354 | let arr = [3, 9, 1, 15, 14, 11, 4, 6, 13, 0, 10, 12, 2, 7, 8, 5]; 355 | let is_solvable = is_solvable(&arr)?; 356 | assert_eq!(is_solvable, false); 357 | } 358 | 359 | { 360 | let test_set = [ 361 | [13, 10, 11, 6, 5, 3, 1, 4, 8, 0, 12, 2, 14, 7, 9, 15], 362 | [9, 2, 15, 13, 7, 4, 12, 6, 8, 1, 0, 14, 5, 10, 3, 11], 363 | [4, 8, 7, 12, 5, 0, 13, 15, 9, 1, 6, 3, 11, 14, 10, 2], 364 | [7, 10, 11, 1, 0, 9, 3, 4, 5, 8, 13, 2, 14, 6, 12, 15], 365 | [14, 11, 8, 15, 12, 5, 13, 3, 6, 2, 9, 0, 1, 7, 10, 4], 366 | [4, 12, 15, 9, 2, 13, 14, 3, 5, 7, 8, 6, 11, 1, 10, 0], 367 | [12, 14, 2, 11, 1, 7, 0, 10, 6, 5, 13, 4, 8, 9, 15, 3], 368 | [4, 13, 8, 7, 10, 6, 2, 9, 5, 0, 14, 11, 12, 15, 1, 3], 369 | [4, 5, 11, 13, 3, 7, 8, 12, 0, 14, 2, 6, 10, 15, 1, 9], 370 | [15, 4, 3, 6, 2, 7, 5, 1, 8, 11, 0, 14, 13, 9, 10, 12], 371 | [7, 10, 13, 5, 6, 8, 11, 0, 1, 2, 12, 14, 3, 4, 15, 9], 372 | [10, 9, 0, 11, 1, 6, 15, 7, 4, 5, 2, 12, 14, 13, 3, 8], 373 | [4, 15, 14, 8, 10, 9, 3, 12, 7, 6, 13, 0, 2, 11, 1, 5], 374 | [9, 7, 15, 12, 8, 6, 13, 5, 14, 2, 11, 1, 4, 0, 10, 3], 375 | [3, 5, 14, 4, 0, 10, 12, 7, 15, 9, 6, 11, 2, 1, 13, 8], 376 | [5, 15, 3, 10, 9, 8, 7, 14, 4, 13, 12, 2, 0, 1, 6, 11], 377 | [5, 8, 7, 2, 14, 15, 12, 10, 0, 6, 9, 1, 4, 11, 13, 3], 378 | [3, 6, 15, 14, 7, 9, 11, 10, 2, 1, 13, 5, 0, 12, 8, 4], 379 | [4, 8, 13, 1, 11, 7, 12, 10, 2, 3, 0, 14, 6, 5, 9, 15], 380 | [3, 12, 0, 11, 10, 5, 7, 14, 6, 13, 2, 15, 8, 9, 4, 1], 381 | ]; 382 | for test in test_set.iter() { 383 | let is_solvable = is_solvable(test)?; 384 | assert_eq!(is_solvable, true); 385 | } 386 | } 387 | 388 | Ok(()) 389 | } 390 | 391 | #[test] 392 | fn move_tile_should_generate_correct_arr() -> Result<(), Box> { 393 | let mut rng = rand::thread_rng(); 394 | let mut arr = shuffle_arr(&mut rng)?; 395 | 396 | for _ in 0..10_000 { 397 | assert_eq!(is_solvable(&arr)?, true); 398 | 399 | let random_number = rng.gen_range(0, 4); 400 | let operation = match random_number { 401 | 0 => Operation::UP, 402 | 1 => Operation::DOWN, 403 | 2 => Operation::LEFT, 404 | 3 => Operation::RIGHT, 405 | _ => Operation::UP, 406 | }; 407 | 408 | arr = move_tile(&arr, operation)?; 409 | } 410 | 411 | Ok(()) 412 | } 413 | } 414 | --------------------------------------------------------------------------------