├── .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 |
--------------------------------------------------------------------------------