├── unblocked.rc ├── star.ico ├── assets ├── arrows.png ├── bricks.png ├── numbers.png ├── rules.png ├── solved.png ├── throws.png ├── attempts.png ├── level_no.png ├── progress.png ├── all_plates.png ├── background.png ├── level-0000.rpl ├── menu_arrow.png ├── menu_items.png └── std_puzzles ├── images ├── unblocked_shot.png └── unblocked_shot_25.jpg ├── .gitignore ├── changelog ├── rustfmt.toml ├── src ├── main.rs ├── consts.rs ├── scenes.rs ├── textnum.rs ├── common.rs ├── play.rs ├── demo.rs ├── replay.rs ├── scores.rs ├── loader.rs ├── mainmenu.rs └── field.rs ├── Cargo.toml ├── LICENSE ├── README.md ├── docs.md └── Cargo.lock /unblocked.rc: -------------------------------------------------------------------------------- 1 | 1 ICON "star.ico" -------------------------------------------------------------------------------- /star.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/star.ico -------------------------------------------------------------------------------- /assets/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/arrows.png -------------------------------------------------------------------------------- /assets/bricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/bricks.png -------------------------------------------------------------------------------- /assets/numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/numbers.png -------------------------------------------------------------------------------- /assets/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/rules.png -------------------------------------------------------------------------------- /assets/solved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/solved.png -------------------------------------------------------------------------------- /assets/throws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/throws.png -------------------------------------------------------------------------------- /assets/attempts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/attempts.png -------------------------------------------------------------------------------- /assets/level_no.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/level_no.png -------------------------------------------------------------------------------- /assets/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/progress.png -------------------------------------------------------------------------------- /assets/all_plates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/all_plates.png -------------------------------------------------------------------------------- /assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/background.png -------------------------------------------------------------------------------- /assets/level-0000.rpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/level-0000.rpl -------------------------------------------------------------------------------- /assets/menu_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/menu_arrow.png -------------------------------------------------------------------------------- /assets/menu_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/assets/menu_items.png -------------------------------------------------------------------------------- /images/unblocked_shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/images/unblocked_shot.png -------------------------------------------------------------------------------- /images/unblocked_shot_25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VladimirMarkelov/unblocked/HEAD/images/unblocked_shot_25.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /_res 4 | /replays 5 | *.zip 6 | *.7z 7 | *.rar 8 | *.dll 9 | *.exe 10 | *.a -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | unblocked (1.0.0) unstable; urgency=medium 2 | 3 | * First release 4 | 5 | -- Vladimir Markelov Tue, 21 May 2019 19:46:29 -0700 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | newline_style = "Unix" 2 | tab_spaces = 4 3 | # single_line_if_else = true 4 | # combine_control_expr = true 5 | # control_brace_style = "AlwaysSameLine" 6 | max_width = 120 7 | use_small_heuristics = "max" -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | 3 | use tetra::ContextBuilder; 4 | 5 | mod common; 6 | mod consts; 7 | mod demo; 8 | mod field; 9 | mod loader; 10 | mod mainmenu; 11 | mod play; 12 | mod replay; 13 | mod scenes; 14 | mod scores; 15 | mod textnum; 16 | 17 | use crate::scenes::SceneManager; 18 | 19 | fn main() -> tetra::Result { 20 | ContextBuilder::new("Unblocked", consts::SCR_W as i32, consts::SCR_H as i32) 21 | .resizable(true) 22 | .quit_on_escape(false) 23 | .show_mouse(true) 24 | .build()? 25 | .run(SceneManager::new) 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unblock-it" 3 | version = "1.1.0" 4 | authors = ["Vladimir Markelov "] 5 | edition = "2021" 6 | keywords = ["puzzle", "game", "2d"] 7 | license = "MIT" 8 | description = "Unblocked is a puzzle game inspired by Flipull" 9 | readme = "README.md" 10 | repository = "https://github.com/VladimirMarkelov/unblocked" 11 | categories = ["games"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | tetra = { version = "0.7", default-features = false, features = ["font_ttf", "texture_png"] } 17 | dirs = "2.0" 18 | toml = "^0.4" 19 | serde = "1" 20 | serde_derive = "1" 21 | chrono = "^0.4" 22 | bincode = "1" 23 | 24 | [target.'cfg(windows)'.build-dependencies] 25 | windres = "0.2" 26 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | // Screen sizes 2 | pub const SCR_W: f32 = 1024.0; 3 | pub const SCR_H: f32 = 768.0; 4 | 5 | pub const BRICK_SIZE: f32 = 48.0; // Width and height of a block 6 | pub const WIDTH: usize = 21; // width of the window in blocks 7 | pub const INFO_WIDTH: usize = 5; // width of the info window at the right in blocks 8 | pub const HEIGHT: usize = 16; // height of the window in blocks 9 | pub const MAX_SIZE: usize = 7; // puzzle max dimension: 7x7 10 | 11 | pub const NUM_STATES: i32 = 4; // number of states 12 | 13 | // number of the level used to show demo from main menu 14 | // this level must be inaccessible in normal game 15 | pub const DEMO_LEVEL: usize = 0; 16 | 17 | // Plates ordinal number in a sprite 18 | pub const PLATE_LEVEL_SOLVED: f32 = 0.0; 19 | pub const PLATE_NO_MOVES: f32 = 1.0; 20 | pub const PLATE_GAME_COMPLETED: f32 = 2.0; 21 | pub const PLATE_REPLAY_COMPLETED: f32 = 3.0; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vladimir Markelov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unblocked 2 | 3 | A puzzle game inspired by NES game "Flipull" with a bit different mechanics. 4 | 5 | Game screenshot 6 | 7 | As of version 1.0, it contains 56 puzzles (and one demo level that is unplayable). 8 | 9 | For detailed information about hot keys, built-in help and replays, please see [documentation](docs.md). 10 | 11 | ## Installation 12 | 13 | Before compiling the application from sources you may need to install extra developer libraries beforehand. Building it on Ubuntu 18 required to install the following libraries(ALSA, SDL2, and pkg-config): 14 | 15 | ```shell 16 | $ sudo apt-get install libsdl2-dev libasound2-dev pkg-config 17 | ``` 18 | 19 | The application can be compiled from source, or installed using cargo: 20 | 21 | ```shell 22 | $ cargo install unblock-it 23 | ``` 24 | 25 | You need Rust compiler that supports Rust 2018 edition (Rust 1.36 or newer) to do it. If you want to upgrade, execute the following command: 26 | 27 | ```shell 28 | $ cargo install unblock-it --force 29 | ``` 30 | 31 | ### Precompiled binaries 32 | 33 | For Windows you can download precompiled binaries from [Release page](https://github.com/VladimirMarkelov/unblocked/releases). 34 | 35 | Windows binary works on Windows 7 or newer Windows. 36 | 37 | ### Contact info 38 | 39 | Suggestions, ideas, bugs, and better replays are very welcome. Send all of them to vmatroskin (at) gmail.com. Bugs can be submitted at [github](https://github.com/VladimirMarkelov/unblocked/issues) 40 | 41 | ## License 42 | 43 | MIT 44 | -------------------------------------------------------------------------------- /src/scenes.rs: -------------------------------------------------------------------------------- 1 | use tetra::graphics::scaling::{ScalingMode, ScreenScaler}; 2 | use tetra::graphics::{self, Color}; 3 | use tetra::window; 4 | use tetra::{Context, Event, State}; 5 | 6 | use crate::consts::{SCR_H, SCR_W}; 7 | use crate::mainmenu::TitleScene; 8 | 9 | pub trait Scene { 10 | fn update(&mut self, ctx: &mut Context) -> tetra::Result; 11 | fn draw(&mut self, ctx: &mut Context) -> tetra::Result; 12 | } 13 | 14 | pub enum Transition { 15 | None, 16 | Push(Box), 17 | Pop, 18 | } 19 | 20 | pub struct SceneManager { 21 | scaler: ScreenScaler, 22 | scenes: Vec>, 23 | } 24 | 25 | impl SceneManager { 26 | pub fn new(ctx: &mut Context) -> tetra::Result { 27 | let ts = TitleScene::new(ctx)?; 28 | Ok(SceneManager { 29 | // with this scaling the drawn area is scaled with the window. 30 | // So a user can make game window fullscreen and all sprites are scaled as well 31 | scaler: ScreenScaler::with_window_size(ctx, SCR_W as i32, SCR_H as i32, ScalingMode::ShowAll)?, 32 | scenes: vec![Box::new(ts)], 33 | }) 34 | } 35 | } 36 | 37 | impl State for SceneManager { 38 | fn update(&mut self, ctx: &mut Context) -> tetra::Result { 39 | match self.scenes.last_mut() { 40 | Some(active_scene) => match active_scene.update(ctx)? { 41 | Transition::None => {} 42 | Transition::Push(s) => { 43 | self.scenes.push(s); 44 | } 45 | Transition::Pop => { 46 | self.scenes.pop(); 47 | } 48 | }, 49 | None => window::quit(ctx), 50 | } 51 | 52 | Ok(()) 53 | } 54 | 55 | fn draw(&mut self, ctx: &mut Context) -> tetra::Result { 56 | match self.scenes.last_mut() { 57 | Some(active_scene) => { 58 | graphics::set_canvas(ctx, self.scaler.canvas()); 59 | match active_scene.draw(ctx)? { 60 | Transition::None => {} 61 | Transition::Push(s) => { 62 | self.scenes.push(s); 63 | } 64 | Transition::Pop => { 65 | self.scenes.pop(); 66 | } 67 | } 68 | graphics::reset_canvas(ctx); 69 | graphics::clear(ctx, Color::BLACK); 70 | self.scaler.draw(ctx); 71 | } 72 | None => window::quit(ctx), 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | fn event(&mut self, _: &mut Context, event: Event) -> tetra::Result { 79 | if let Event::Resized { width, height } = event { 80 | self.scaler.set_outer_size(width, height); 81 | } 82 | Ok(()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/textnum.rs: -------------------------------------------------------------------------------- 1 | use tetra::graphics::{Color, DrawParams, Rectangle, Texture}; 2 | use tetra::math::Vec2; 3 | use tetra::Context; 4 | 5 | // A struct that can draw a number digit by digit using a texture with 10 digits 6 | pub struct TextNumber { 7 | digits: Texture, //texture with 10 digits (0..9) 8 | digit_w: f32, // width of a digit (all digits have the same width) 9 | digit_h: f32, // height of a digit 10 | } 11 | 12 | // Parameters to display a number 13 | #[derive(Clone)] 14 | pub struct TextParams { 15 | // how many numbers to show (used when displaying right-aligned numbers or numbers with 16 | // leading zeroes 17 | width: u8, 18 | // Show zeroes or spaces when width is bigger than the number of digits in the number 19 | leading_zeroes: bool, 20 | // used only if width is set and leading_zeroes is off 21 | right_align: bool, 22 | // optional color tint 23 | color: Option, 24 | } 25 | 26 | impl TextParams { 27 | pub fn new() -> Self { 28 | TextParams { width: 0, leading_zeroes: false, right_align: false, color: None } 29 | } 30 | pub fn with_width(self, w: u8) -> Self { 31 | TextParams { width: w, ..self } 32 | } 33 | pub fn with_right_align(self) -> Self { 34 | TextParams { right_align: true, ..self } 35 | } 36 | pub fn with_leading_zeroes(self) -> Self { 37 | TextParams { leading_zeroes: true, ..self } 38 | } 39 | pub fn with_color(self, c: Color) -> Self { 40 | TextParams { color: Some(c), ..self } 41 | } 42 | } 43 | 44 | impl TextNumber { 45 | pub fn new(ctx: &mut Context, bytes: &[u8]) -> tetra::Result { 46 | let mut tx = TextNumber { digits: Texture::from_encoded(ctx, bytes)?, digit_w: 0.0, digit_h: 0.0 }; 47 | tx.digit_w = (tx.digits.width() / 10) as f32; 48 | tx.digit_h = tx.digits.height() as f32; 49 | Ok(tx) 50 | } 51 | 52 | pub fn digit_size(&self) -> Vec2 { 53 | Vec2::new(self.digit_w, self.digit_h) 54 | } 55 | 56 | pub fn draw(&mut self, ctx: &mut Context, start_pos: Vec2, n: u32, param: TextParams) { 57 | let mut d: Vec = Vec::new(); 58 | 59 | // split a number into its digits 60 | if n == 0 { 61 | d.push(0); 62 | } else { 63 | let mut n = n; 64 | while n > 0 { 65 | let m = n % 10; 66 | n /= 10; 67 | d.insert(0, m); 68 | } 69 | } 70 | 71 | // add extra zeroes if required 72 | if param.width != 0 && param.leading_zeroes { 73 | while d.len() < param.width as usize { 74 | d.insert(0, 0); 75 | } 76 | } 77 | 78 | // fix starting position if the number is right aligned 79 | let mut p: Vec2 = start_pos; 80 | if param.width != 0 && param.right_align && d.len() < param.width as usize { 81 | p = Vec2::new(p.x + ((param.width as usize) - d.len()) as f32 * self.digit_w, p.y); 82 | } 83 | 84 | // show digits one by one 85 | for digit in d { 86 | let clip = Rectangle::new(digit as f32 * self.digit_w, 0.0, self.digit_w, self.digit_h); 87 | let mut dp = DrawParams::new().position(p); 88 | if let Some(c) = param.color { 89 | dp = dp.color(c); 90 | } 91 | self.digits.draw_region(ctx, clip, dp); 92 | p = Vec2::new(p.x + self.digit_w, p.y); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_exe; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | 5 | use tetra::math::Vec2; 6 | 7 | use crate::consts::{BRICK_SIZE, INFO_WIDTH, SCR_H, SCR_W}; 8 | 9 | const CONF_FILE: &str = "config.toml"; 10 | const SCORE_FILE: &str = "hiscores.toml"; 11 | const DEV_NAME: &str = "rionnag"; 12 | const GAME_NAME: &str = "unblocked"; 13 | const REPLAY_DIR: &str = "replays"; 14 | 15 | // Returns the number of digits in a number. 16 | // Used for small numbers like level number or the number of throws 17 | pub fn digits(n: usize) -> u8 { 18 | if n > 999_999 { 19 | panic!("Number too big") 20 | } else if n > 99_999 { 21 | 6 22 | } else if n > 9_999 { 23 | 5 24 | } else if n > 999 { 25 | 4 26 | } else if n > 99 { 27 | 3 28 | } else if n > 9 { 29 | 2 30 | } else { 31 | 1 32 | } 33 | } 34 | 35 | // Returns the directory where the game binary is 36 | fn exe_path() -> PathBuf { 37 | match current_exe() { 38 | Ok(mut p) => { 39 | p.pop(); 40 | p 41 | } 42 | Err(_) => unreachable!(), 43 | } 44 | } 45 | 46 | // Returns current user's directory for configuration files 47 | // For Windows it is %USER%/Appdata/Roaming 48 | // For Linux it is ~/.config 49 | fn user_config_path() -> PathBuf { 50 | match dirs::config_dir() { 51 | Some(p) => p, 52 | None => unreachable!(), 53 | } 54 | } 55 | 56 | // Returns the directory where the application save to/loads from all its configs/hiscores etc 57 | // In normal mode: 58 | // For Windows it is %USER%/Appdata/Roaming/DEV_NAME/GAME_NAME/ 59 | // For Linux it is ~/.config/DEV_NAME/GAME_NAME/ 60 | // In portable mode: 61 | // For all OSes it is directory where the application binary is 62 | fn base_path() -> PathBuf { 63 | if is_portable() { 64 | exe_path() 65 | } else { 66 | // exe_path() // TODO: 67 | let mut path = user_config_path(); 68 | path.push(DEV_NAME); 69 | path.push(GAME_NAME); 70 | ensure_path_exists(&path); 71 | path 72 | } 73 | } 74 | 75 | // Returns if the application works in portable mode. 76 | // If there is CONF_FILE file in the directory where the application binary, it means the 77 | // portable mode is on 78 | fn is_portable() -> bool { 79 | let mut p = exe_path(); 80 | p.push(CONF_FILE); 81 | p.exists() 82 | } 83 | 84 | // there is no config yet, so comment the function out for now 85 | // pub fn config_path() -> PathBuf { 86 | // let mut p = base_path(); 87 | // p.push(CONF_FILE); 88 | // p 89 | // } 90 | 91 | // Returns path to the file with hiscores 92 | pub fn score_path() -> PathBuf { 93 | let mut p = base_path(); 94 | p.push(SCORE_FILE); 95 | p 96 | } 97 | 98 | // Returns path to the directory where replays are 99 | pub fn replay_path() -> PathBuf { 100 | let mut path = base_path(); 101 | path.push(REPLAY_DIR); 102 | ensure_path_exists(&path); 103 | path 104 | } 105 | 106 | // Creates all path's intermediate directories to make sure that the `p` exists. 107 | // Returns false if it failed to create required directories (may happen, e.g, on read-only media 108 | pub fn ensure_path_exists(p: &Path) -> bool { 109 | if p.exists() { 110 | return true; 111 | } 112 | fs::create_dir_all(p).is_ok() 113 | } 114 | 115 | // Returns position for an object to put it in the center of the screen 116 | pub fn center_screen(width: f32, height: f32) -> Vec2 { 117 | let x = (SCR_W - width) / 2.0; 118 | let y = (SCR_H - height) / 2.0; 119 | Vec2::new(x, y) 120 | } 121 | 122 | // Returns position for an object to put it in the center of the play area 123 | pub fn center_play_area(width: f32, height: f32) -> Vec2 { 124 | let info_width = INFO_WIDTH as f32 * BRICK_SIZE; 125 | let x = (SCR_W - info_width - width) / 2.0; 126 | let y = (SCR_H - height) / 2.0; 127 | Vec2::new(x, y) 128 | } 129 | 130 | // return max value if the value exceeds max 131 | pub fn clamp(value: u32, max: u32) -> u32 { 132 | if value > max { 133 | max 134 | } else { 135 | value 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/play.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use tetra::graphics::{DrawParams, Rectangle, Texture}; 5 | use tetra::input::{self, Key}; 6 | use tetra::Context; 7 | 8 | use crate::common::center_screen; 9 | use crate::consts::{NUM_STATES, PLATE_GAME_COMPLETED, PLATE_LEVEL_SOLVED, PLATE_NO_MOVES}; 10 | use crate::demo::DemoScene; 11 | use crate::field::{GameField, GameState}; 12 | use crate::loader::Loader; 13 | use crate::replay::ReplayEngine; 14 | use crate::scenes::{Scene, Transition}; 15 | use crate::scores::Scores; 16 | 17 | // interrupting a game after making this many throws is considered a fail 18 | const MIN_THROWS: u32 = 3; 19 | 20 | pub struct PlayScene { 21 | field: GameField, 22 | state_tx: Texture, 23 | loader: Rc, 24 | scores: Rc>, 25 | replay: ReplayEngine, 26 | tick: u64, // internal tick counter for replays 27 | } 28 | 29 | impl PlayScene { 30 | pub fn new(ctx: &mut Context, ld: Rc, sc: Rc>) -> tetra::Result { 31 | let s = sc.clone(); 32 | let l = ld.clone(); 33 | let lvl = sc.borrow().curr_level(); 34 | let state_image = include_bytes!("../assets/all_plates.png"); 35 | let mut p = PlayScene { 36 | loader: l, 37 | scores: s, 38 | field: GameField::new(ctx, ld, sc, false)?, 39 | state_tx: Texture::from_encoded(ctx, state_image)?, 40 | replay: ReplayEngine::new(), 41 | tick: 0, 42 | }; 43 | p.field.load(lvl); 44 | p.replay.rec_start(); 45 | Ok(p) 46 | } 47 | 48 | fn draw_deco(&mut self, ctx: &mut Context) { 49 | let w = self.state_tx.width() as f32; 50 | let h = (self.state_tx.height() / NUM_STATES) as f32; 51 | 52 | // draw a plate that describes game state (if the game is over) 53 | let clip_rect = match self.field.state { 54 | GameState::Unfinished => return, 55 | GameState::Winner => Rectangle::new(0.0, h * PLATE_LEVEL_SOLVED, w, h), 56 | GameState::Looser => Rectangle::new(0.0, h * PLATE_NO_MOVES, w, h), 57 | GameState::Completed => Rectangle::new(0.0, h * PLATE_GAME_COMPLETED, w, h), 58 | }; 59 | 60 | let pos = center_screen(w, h); 61 | let dp = DrawParams::new().position(pos); 62 | self.state_tx.draw_region(ctx, clip_rect, dp); 63 | } 64 | } 65 | 66 | impl Scene for PlayScene { 67 | fn update(&mut self, ctx: &mut Context) -> tetra::Result { 68 | if input::is_key_pressed(ctx, Key::Escape) { 69 | // Escape is pressed after the level is solved or failed - must save info anyway 70 | if self.field.state == GameState::Completed || self.field.state == GameState::Winner { 71 | let mut sc = self.field.scores.borrow_mut(); 72 | sc.set_win(self.field.level, self.field.score); 73 | } else if self.field.state == GameState::Looser 74 | || (self.field.score >= MIN_THROWS && self.field.state == GameState::Unfinished) 75 | { 76 | let mut sc = self.field.scores.borrow_mut(); 77 | sc.set_fail(self.field.level); 78 | } 79 | return Ok(Transition::Pop); 80 | } 81 | self.tick += 1; 82 | if self.field.is_interactive() { 83 | if input::is_key_pressed(ctx, Key::Space) { 84 | self.replay.add_action(self.tick, Key::Space); 85 | self.field.throw_brick(); 86 | return Ok(Transition::None); 87 | } else if input::is_key_pressed(ctx, Key::Up) { 88 | self.replay.add_action(self.tick, Key::Up); 89 | self.field.player_up(); 90 | } else if input::is_key_pressed(ctx, Key::Down) { 91 | self.replay.add_action(self.tick, Key::Down); 92 | self.field.player_down(); 93 | } else if input::is_key_pressed(ctx, Key::F1) { 94 | // try to load a replay for the level. If there is no replay, do nothing 95 | let mut replay = ReplayEngine::new(); 96 | replay.load(self.field.level); 97 | if replay.is_loaded() { 98 | { 99 | // save info that replay was called for the level 100 | let mut sc = self.field.scores.borrow_mut(); 101 | sc.set_help_used(self.field.level); 102 | } 103 | return Ok(Transition::Push(Box::new(DemoScene::new( 104 | ctx, 105 | self.loader.clone(), 106 | self.scores.clone(), 107 | self.field.level, 108 | )?))); 109 | } 110 | } 111 | } 112 | 113 | assert!(!self.field.demoing); 114 | // save replay. It rewrites any previously saved replay for this level 115 | if input::is_key_pressed(ctx, Key::F5) { 116 | self.replay.save(self.field.level); 117 | } 118 | 119 | // if the level is failed, reset replay recorder 120 | let field_res = self.field.update(ctx); 121 | if self.field.state == GameState::Looser { 122 | self.replay.rec_start(); 123 | self.tick = 0; 124 | } 125 | field_res 126 | } 127 | 128 | fn draw(&mut self, ctx: &mut Context) -> tetra::Result { 129 | let _ = self.field.draw(ctx)?; 130 | self.draw_deco(ctx); 131 | Ok(Transition::None) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/demo.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use tetra::graphics::{DrawParams, Rectangle, Texture}; 5 | use tetra::input::{self, Key}; 6 | use tetra::math::Vec2; 7 | use tetra::Context; 8 | 9 | use crate::common::{center_play_area, center_screen}; 10 | use crate::consts::{BRICK_SIZE, DEMO_LEVEL, INFO_WIDTH, NUM_STATES, PLATE_REPLAY_COMPLETED, WIDTH}; 11 | use crate::field::{GameField, GameState}; 12 | use crate::loader::Loader; 13 | use crate::replay::{Action, ReplayEngine}; 14 | use crate::scenes::{Scene, Transition}; 15 | use crate::scores::Scores; 16 | 17 | pub struct DemoScene { 18 | field: GameField, 19 | state_tx: Texture, 20 | progress_tx: Texture, 21 | info_tx: Texture, 22 | replay: ReplayEngine, 23 | tick: u64, // internal ticker counter for displaying replays correctly 24 | rules_shown: bool, // true if replay must pause before start and show the game rules 25 | } 26 | 27 | impl DemoScene { 28 | pub fn new(ctx: &mut Context, ld: Rc, sc: Rc>, lvl: usize) -> tetra::Result { 29 | let lvl = if lvl == 0 { DEMO_LEVEL } else { lvl }; 30 | let state_image = include_bytes!("../assets/all_plates.png"); 31 | let progress_image = include_bytes!("../assets/progress.png"); 32 | let info_image = include_bytes!("../assets/rules.png"); 33 | let mut p = DemoScene { 34 | field: GameField::new(ctx, ld, sc, true)?, 35 | state_tx: Texture::from_encoded(ctx, state_image)?, 36 | progress_tx: Texture::from_encoded(ctx, progress_image)?, 37 | info_tx: Texture::from_encoded(ctx, info_image)?, 38 | replay: ReplayEngine::new(), 39 | tick: 0, 40 | rules_shown: lvl == DEMO_LEVEL, 41 | }; 42 | p.field.load(lvl); 43 | p.replay.load(lvl); 44 | p.replay.replay_start(); 45 | println!("Replay for level {} loaded. {} moves.", lvl, p.replay.action_count()); 46 | Ok(p) 47 | } 48 | 49 | // the only decoration is a plate that shows that the replay has finished 50 | fn draw_deco(&mut self, ctx: &mut Context) { 51 | let w = self.state_tx.width() as f32; 52 | let h = (self.state_tx.height() / NUM_STATES) as f32; 53 | 54 | let clip_rect = match self.field.state { 55 | GameState::Unfinished => return, 56 | _ => Rectangle::new(0.0, h * PLATE_REPLAY_COMPLETED, w, h), 57 | }; 58 | 59 | let dp = DrawParams::new().position(center_screen(w, h)); 60 | self.state_tx.draw_region(ctx, clip_rect, dp); 61 | } 62 | 63 | // show progress bar for replay 64 | fn draw_progress(&mut self, ctx: &mut Context) { 65 | let progress = if self.replay.replay_percent() > 100 { 100 } else { self.replay.replay_percent() }; 66 | if progress == 0 { 67 | return; 68 | } 69 | 70 | let x = ((WIDTH - INFO_WIDTH) as f32 + 0.5) * BRICK_SIZE; 71 | let y = BRICK_SIZE * 1.0; 72 | let w = self.progress_tx.width() * progress / 100; 73 | let h = self.progress_tx.height() as f32; 74 | let clip_rect = Rectangle::new(0.0, 0.0, w as f32, h); 75 | let dp = DrawParams::new().position(Vec2::new(x, y)); 76 | self.progress_tx.draw_region(ctx, clip_rect, dp); 77 | } 78 | } 79 | 80 | impl Scene for DemoScene { 81 | fn update(&mut self, ctx: &mut Context) -> tetra::Result { 82 | // take a break while game rules are displayed 83 | if self.rules_shown { 84 | if input::is_key_pressed(ctx, Key::Space) || input::is_key_pressed(ctx, Key::Escape) { 85 | self.rules_shown = false; 86 | } 87 | return Ok(Transition::None); 88 | } 89 | 90 | self.tick += 1; 91 | while let Some(act) = self.replay.next_replay_action(self.tick) { 92 | match act { 93 | Action::Up => { 94 | println!("{} - UP", self.tick); 95 | self.field.player_up(); 96 | } 97 | Action::Down => { 98 | println!("{} - DOWN", self.tick); 99 | self.field.player_down(); 100 | } 101 | Action::Throw => { 102 | println!("{} - THROW", self.tick); 103 | self.field.throw_brick(); 104 | } 105 | } 106 | } 107 | 108 | // if replay ends, consider this as the level is solved 109 | if !self.replay.is_playing() { 110 | self.field.state = GameState::Winner; 111 | } 112 | 113 | if input::is_key_pressed(ctx, Key::Escape) { 114 | return Ok(Transition::Pop); 115 | } 116 | 117 | if input::is_key_pressed(ctx, Key::Space) 118 | || input::is_key_pressed(ctx, Key::Enter) 119 | || input::is_key_pressed(ctx, Key::NumPadEnter) && self.field.state == GameState::Winner 120 | { 121 | return Ok(Transition::Pop); 122 | } 123 | 124 | // field.update is always considered to return None because DEMO mode 125 | // contols itself 126 | let _ = self.field.update(ctx); 127 | Ok(Transition::None) 128 | } 129 | 130 | fn draw(&mut self, ctx: &mut Context) -> tetra::Result { 131 | let _ = self.field.draw(ctx)?; 132 | self.draw_deco(ctx); 133 | self.draw_progress(ctx); 134 | 135 | if self.rules_shown { 136 | let w = self.info_tx.width() as f32; 137 | let h = self.info_tx.width() as f32; 138 | let pos = center_play_area(w, h); 139 | self.info_tx.draw(ctx, DrawParams::new().position(pos)); 140 | } 141 | 142 | Ok(Transition::None) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /assets/std_puzzles: -------------------------------------------------------------------------------- 1 | #00 - demo level 2 | start:? 3 | $$$$$ 4 | $$$$$ 5 | $$O$$ 6 | $$$$$ 7 | $$$$$ 8 | 9 | #01 10 | start:$ 11 | +=== 12 | o+++ 13 | $$$$ 14 | =ooo 15 | 16 | #02 17 | oooo 18 | ===o 19 | oooo 20 | ===o 21 | 22 | #03 23 | +ooo 24 | o+++ 25 | =ooo 26 | o+++ 27 | 28 | #04 29 | $$$$$ 30 | $$$O$ 31 | $$$$$ 32 | $O$$$ 33 | $$$$$ 34 | 35 | #05 36 | $=== 37 | o$$$ 38 | :::: 39 | =ooo 40 | 41 | #06 42 | OOOO 43 | $$$O 44 | OOOO 45 | $=$O 46 | 47 | #07 48 | +o+o 49 | +o+o 50 | o+o+ 51 | $oo+ 52 | 53 | #08 54 | ****** 55 | ***** 56 | **** 57 | ** 58 | ** 59 | * 60 | 61 | ::$$ 62 | oo:: 63 | :+++ 64 | o$:+ 65 | 66 | #09 67 | $$$$$ 68 | $===$ 69 | $===$ 70 | $===$ 71 | $$$$$ 72 | 73 | #10 74 | $:oo+ 75 | +++++ 76 | o:o:: 77 | ::ooo 78 | $$$$$ 79 | 80 | #11 81 | ::++:: 82 | ::++:: 83 | ++++++ 84 | ++++++ 85 | ::++:: 86 | ::++:: 87 | 88 | #12 89 | +$$: 90 | :++: 91 | :::$ 92 | o+o: 93 | 94 | #13 95 | $$++ 96 | :$$: 97 | ::oo 98 | o:$$ 99 | 100 | #14 101 | start:$ 102 | ::$+ 103 | o:+: 104 | :o$o 105 | $+:o 106 | 107 | #15 108 | ****** 109 | ***** 110 | **** 111 | *** 112 | * 113 | * 114 | 115 | +o+o 116 | o$$o 117 | =+oo 118 | +$=o 119 | 120 | #16 121 | ****** 122 | ***** 123 | **** 124 | ** 125 | ** 126 | * 127 | 128 | o+++ 129 | o++= 130 | oo== 131 | =ooo 132 | 133 | #17 134 | start:: 135 | o+:$ 136 | :+:+ 137 | :o$+ 138 | +++: 139 | 140 | #18 141 | :=:= 142 | ==== 143 | :ooo 144 | :=oo 145 | 146 | #19 147 | o::+ 148 | o+$: 149 | :o$+ 150 | $:+: 151 | 152 | #20 153 | =:=o 154 | ==== 155 | $=o$ 156 | $==$ 157 | 158 | #21 159 | :o$+ 160 | $+++ 161 | o:$: 162 | $o:o 163 | 164 | #22 165 | start:+ 166 | +==+ 167 | O+OO 168 | $=o$ 169 | $$$= 170 | 171 | #23 172 | start:o 173 | o:o+ 174 | oo:$ 175 | +oo$ 176 | $$:: 177 | 178 | #24 179 | ooo= 180 | ==o= 181 | :=o: 182 | ==== 183 | 184 | #25 185 | +++++ 186 | +++=+ 187 | o+==+ 188 | =+=++ 189 | =+oo+ 190 | 191 | #26 192 | +++++ 193 | +$$++ 194 | +oo++ 195 | +++++ 196 | oooo+ 197 | 198 | #27 199 | ****** 200 | ***** 201 | **** 202 | *** 203 | * 204 | * 205 | 206 | :o$$$ 207 | :+ooo 208 | :++++ 209 | :o+++ 210 | :$+++ 211 | 212 | #28 213 | ===== 214 | ===+= 215 | =+==$ 216 | =++== 217 | $==== 218 | 219 | #29 220 | start:o 221 | oo:+o 222 | ++o:+ 223 | :+oo: 224 | $$$o$ 225 | $$oo+ 226 | 227 | #30 228 | start:+ 229 | =+o$+ 230 | =$+o$ 231 | =+o+o 232 | =$$o+ 233 | ==o+$ 234 | 235 | #31 236 | start:: 237 | +::o= 238 | o=oo= 239 | o:+++ 240 | +:::: 241 | =:=++ 242 | 243 | #32 244 | ****** 245 | ***** 246 | **** 247 | *** 248 | * 249 | * 250 | 251 | start:$ 252 | ++o=o 253 | o++o$ 254 | =$+=$ 255 | $ooo= 256 | =$$+= 257 | 258 | #33 259 | +:=:+ 260 | =+:=: 261 | ::++: 262 | oooo: 263 | +=++o 264 | 265 | #34 266 | start:+ 267 | ****** 268 | ***** 269 | **** 270 | *** 271 | * 272 | * 273 | 274 | o:::+ 275 | +:o:: 276 | $:$o: 277 | :+oo: 278 | $:oo: 279 | 280 | #35 281 | o+:$: 282 | ::::: 283 | $o+:$ 284 | $$$$$ 285 | :$$o: 286 | 287 | #36 288 | start:+ 289 | ****** 290 | ***** 291 | ** 292 | ** 293 | ** 294 | * 295 | 296 | +$=+= 297 | $o=++ 298 | o$$oo 299 | +$=++ 300 | =+=oo 301 | 302 | #37 303 | start:+ 304 | o$o:+ 305 | ++:o: 306 | $$::+ 307 | :o$$+ 308 | ::+o: 309 | 310 | #38 311 | start:= 312 | =o$=: 313 | $:o$o 314 | $=:o= 315 | o=o=o 316 | $$$:: 317 | 318 | #39 319 | $$o++ 320 | $$o$$ 321 | ++o$o 322 | ooo$+ 323 | $+o$+ 324 | 325 | #40 326 | start:o 327 | ****** 328 | ***** 329 | **** 330 | ** 331 | ** 332 | * 333 | 334 | :o:+= 335 | =oo+= 336 | o+::o 337 | ===++ 338 | +:+++ 339 | 340 | #41 341 | start:$ 342 | :===o 343 | $:o=: 344 | :+$=$ 345 | $::=: 346 | +$$$$ 347 | 348 | #42 349 | $oo+: 350 | +$oo+ 351 | o+:o$ 352 | :$$o+ 353 | +$$:: 354 | 355 | #43 356 | start:: 357 | $+o+o 358 | +o:o: 359 | $$:+: 360 | oo$o+ 361 | o+:o: 362 | 363 | #44 364 | start:+ 365 | +:::o 366 | :o$$: 367 | :+++$ 368 | $:$:$ 369 | o$+++ 370 | 371 | #45 372 | start:$ 373 | ****** 374 | ***** 375 | ** 376 | ** 377 | ** 378 | * 379 | 380 | ==o$$$ 381 | ++oooo 382 | ++o$$$ 383 | ++:::: 384 | $:$$$$ 385 | $+$$$$ 386 | 387 | #46 388 | start:$ 389 | ****** 390 | ***** 391 | *** 392 | ** 393 | ** 394 | * 395 | 396 | $$++== 397 | ++$=== 398 | +ooooo 399 | =++ooo 400 | $$$+++ 401 | oo+$$$ 402 | 403 | #47 404 | start:+ 405 | ****** 406 | ***** 407 | ** 408 | ** 409 | ** 410 | * 411 | 412 | :oo$:+ 413 | ++++$+ 414 | ::$$++ 415 | $++++$ 416 | :$$+$$ 417 | o:::$$ 418 | 419 | #48 420 | start:+ 421 | ==+++o 422 | $+=o+o 423 | =++o=+ 424 | +===+o 425 | +o$$$= 426 | +ooo+$ 427 | 428 | #49 429 | start:+ 430 | o:+:++ 431 | $+o+$+ 432 | oooo+: 433 | +$:o$$ 434 | ::+::$ 435 | oo$o:$ 436 | 437 | #50-51 438 | oo$o+ 439 | oo+:$ 440 | oo++: 441 | $$:++ 442 | ::$$: 443 | 444 | #51 445 | $+o$o+ 446 | ++o:o$ 447 | ++o$o: 448 | +:o::+ 449 | $oo:$: 450 | ::$$$: 451 | 452 | #52 453 | ****** 454 | ***** 455 | *** 456 | ** 457 | ** 458 | * 459 | 460 | oo:=: 461 | +:+++ 462 | +o=:= 463 | :ooo+ 464 | =:==+ 465 | 466 | #53 467 | ****** 468 | ***** 469 | **** 470 | *** 471 | ** 472 | 473 | +$:$$ 474 | o::+o 475 | oo:o: 476 | +$:++ 477 | $$$++ 478 | 479 | #54 480 | $o:o++ 481 | o:$o:$ 482 | ooo+o+ 483 | o::++: 484 | $:+++: 485 | :$$$$$ 486 | 487 | #55 488 | ****** 489 | ***** 490 | **** 491 | *** 492 | ** 493 | 494 | o==oo$ 495 | =$$+++ 496 | =+o+o$ 497 | +=+$$$ 498 | o+o$o+ 499 | ++==$+ 500 | 501 | #56 502 | ****** 503 | ***** 504 | **** 505 | *** 506 | ** 507 | 508 | $$o$+ 509 | $=oo= 510 | o==o= 511 | ++++= 512 | =$$$+ 513 | -------------------------------------------------------------------------------- /src/replay.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | use tetra::input::Key; 3 | 4 | use std::fmt; 5 | use std::fs::{read, File}; 6 | use std::io::Write; 7 | use std::path::PathBuf; 8 | 9 | use crate::common::replay_path; 10 | use crate::consts::DEMO_LEVEL; 11 | 12 | const REPLAY_VERSION: u32 = 1; 13 | // the first replay action must be no later than MAX_DELAY ticks 14 | const MAX_DELAY: u64 = 60 * 3; 15 | 16 | #[derive(Copy, Clone, Serialize, Deserialize)] 17 | pub enum Action { 18 | Up, 19 | Down, 20 | Throw, 21 | } 22 | 23 | #[derive(PartialEq)] 24 | enum State { 25 | Idle, 26 | Recording, 27 | Replaying, 28 | } 29 | 30 | fn key_to_action(k: Key) -> Action { 31 | match k { 32 | Key::Up => Action::Up, 33 | Key::Down => Action::Down, 34 | Key::Space => Action::Throw, 35 | _ => unreachable!(), 36 | } 37 | } 38 | 39 | #[derive(Serialize, Deserialize)] 40 | pub struct Move { 41 | tick: u64, 42 | act: Action, 43 | } 44 | 45 | impl fmt::Display for Move { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | write!(f, "{} => ", self.tick)?; 48 | match self.act { 49 | Action::Up => write!(f, "Player UP"), 50 | Action::Down => write!(f, "Player DOWN"), 51 | Action::Throw => write!(f, "THROW"), 52 | } 53 | } 54 | } 55 | 56 | #[derive(Serialize, Deserialize)] 57 | pub struct Replay { 58 | version: u32, 59 | moves: Vec, 60 | } 61 | 62 | impl Default for Replay { 63 | fn default() -> Self { 64 | Replay { version: REPLAY_VERSION, moves: Vec::new() } 65 | } 66 | } 67 | 68 | pub struct ReplayEngine { 69 | replay: Replay, 70 | state: State, 71 | idx: usize, 72 | shift: u64, 73 | } 74 | 75 | impl ReplayEngine { 76 | pub fn new() -> Self { 77 | ReplayEngine { replay: Replay::default(), state: State::Idle, shift: 0, idx: 0 } 78 | } 79 | 80 | fn replay_filename(lvl: usize) -> PathBuf { 81 | PathBuf::from(&format!("level-{:04}.rpl", lvl)) 82 | } 83 | 84 | pub fn rec_start(&mut self) { 85 | self.state = State::Recording; 86 | self.replay.moves.clear(); 87 | // reassign because previous load call can load old version of replay 88 | self.replay.version = REPLAY_VERSION; 89 | } 90 | 91 | pub fn load(&mut self, lvl: usize) { 92 | let bytes: Vec; 93 | if lvl == DEMO_LEVEL { 94 | bytes = include_bytes!("../assets/level-0000.rpl").to_vec(); 95 | } else { 96 | let mut rpath = replay_path(); 97 | rpath.push(Self::replay_filename(lvl)); 98 | if !rpath.is_file() { 99 | return; 100 | } 101 | if let Ok(v) = read(rpath) { 102 | bytes = v; 103 | } else { 104 | return; 105 | } 106 | } 107 | 108 | let replay: Replay = bincode::deserialize(&bytes).unwrap(); 109 | if replay.version == REPLAY_VERSION { 110 | self.replay = replay; 111 | self.idx = 0; 112 | if !self.replay.moves.is_empty() { 113 | self.shift = 114 | if self.replay.moves[0].tick > MAX_DELAY { self.replay.moves[0].tick - MAX_DELAY } else { 0 } 115 | } 116 | } else { 117 | eprintln!("Unsupported version: {}, can replay only version {}", replay.version, REPLAY_VERSION); 118 | } 119 | } 120 | 121 | pub fn save(&mut self, lvl: usize) { 122 | if self.replay.moves.is_empty() { 123 | return; 124 | } 125 | 126 | // make breaks between actions no longer than MAX_DELAY 127 | let mut shift = 0u64; 128 | let mut last_delay = 0u64; 129 | for v in self.replay.moves.iter_mut() { 130 | let delay = v.tick - shift - last_delay; 131 | if delay > MAX_DELAY { 132 | shift += delay - MAX_DELAY; 133 | } 134 | if shift != 0 { 135 | v.tick -= shift; 136 | } 137 | last_delay = v.tick; 138 | } 139 | 140 | let encoded: Vec = bincode::serialize(&self.replay).unwrap(); 141 | let mut rpath = replay_path(); 142 | rpath.push(Self::replay_filename(lvl)); 143 | if let Ok(mut f) = File::create(rpath) { 144 | let _ = f.write_all(&encoded); 145 | } 146 | } 147 | 148 | pub fn is_loaded(&self) -> bool { 149 | !self.replay.moves.is_empty() 150 | } 151 | 152 | pub fn replay_start(&mut self) { 153 | assert!(self.state == State::Idle); 154 | if self.replay.moves.is_empty() { 155 | return; 156 | } 157 | self.state = State::Replaying; 158 | self.idx = 0; 159 | } 160 | 161 | pub fn is_playing(&self) -> bool { 162 | self.state == State::Replaying && self.idx < self.replay.moves.len() 163 | } 164 | 165 | pub fn replay_percent(&self) -> i32 { 166 | let l = self.replay.moves.len() as i32; 167 | if l == 0 || self.state != State::Replaying { 168 | 0 169 | } else { 170 | self.idx as i32 * 100 / l 171 | } 172 | } 173 | 174 | pub fn next_replay_action(&mut self, tick: u64) -> Option { 175 | if !self.is_playing() || self.idx >= self.replay.moves.len() { 176 | return None; 177 | } 178 | 179 | if self.idx >= self.replay.moves.len() { 180 | return None; 181 | } 182 | 183 | let next_ticks = self.replay.moves[self.idx].tick - self.shift; 184 | if tick < next_ticks { 185 | return None; 186 | } 187 | 188 | self.idx += 1; 189 | Some(self.replay.moves[self.idx - 1].act) 190 | } 191 | 192 | pub fn add_action(&mut self, ticks: u64, key: Key) { 193 | assert!(self.state == State::Recording); 194 | let m = Move { tick: ticks, act: key_to_action(key) }; 195 | self.replay.moves.push(m); 196 | } 197 | 198 | pub fn action_count(&mut self) -> usize { 199 | self.replay.moves.len() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/scores.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | use std::fs::{read_to_string, write}; 3 | use std::path::PathBuf; 4 | 5 | use chrono::prelude::*; 6 | use chrono::NaiveDateTime; 7 | 8 | use crate::common::score_path; 9 | 10 | // a lever score info 11 | #[derive(Copy, Clone, Serialize, Deserialize, Default)] 12 | pub struct Score { 13 | pub attempts: u32, // attempts to solve the puzzle 14 | pub wins: u32, // puzzle solved N times 15 | pub hiscore: u32, // best score 16 | pub first_win: i32, // date of the first win 17 | pub help_used: bool, // help was used before any win 18 | } 19 | 20 | #[derive(Clone, Serialize, Deserialize)] 21 | pub struct ScoreVec { 22 | max_level: usize, 23 | levels: Vec, 24 | } 25 | 26 | pub struct Scores { 27 | scores: ScoreVec, 28 | curr_level: usize, // current level a player plays (used by main menu and play scene) 29 | lvl_cnt: usize, // total number of levels 30 | file_path: PathBuf, // file path to save/load hiscores 31 | } 32 | 33 | impl Scores { 34 | pub fn new(lvl_cnt: usize) -> Scores { 35 | let mut sc = Scores { 36 | scores: ScoreVec { levels: Vec::new(), max_level: 1 }, 37 | curr_level: 1, 38 | lvl_cnt, 39 | file_path: score_path(), 40 | }; 41 | sc.load(); 42 | sc 43 | } 44 | 45 | pub fn load(&mut self) { 46 | if !self.file_path.exists() { 47 | // first start - no file, so initialize the scores with a default score info 48 | self.scores.levels.push(Score::default()); 49 | return; 50 | } 51 | 52 | let data = match read_to_string(self.file_path.clone()) { 53 | Ok(s) => s, 54 | Err(_) => return, 55 | }; 56 | 57 | let scores: ScoreVec = match toml::from_str(&data) { 58 | Ok(v) => v, 59 | Err(e) => { 60 | eprintln!("Failed to parse config file: {:?}", e); 61 | return; 62 | } 63 | }; 64 | 65 | self.scores = scores; 66 | // Set the current level to the maximum level a user has reached 67 | self.curr_level = self.scores.max_level; 68 | if self.scores.levels.is_empty() { 69 | self.scores.levels.push(Score::default()); 70 | } 71 | } 72 | 73 | pub fn save(&self) { 74 | let tml = toml::to_string(&self.scores).unwrap(); 75 | let name = score_path(); 76 | let _ = write(name, tml); 77 | } 78 | 79 | pub fn level_info(&self, lvl_no: usize) -> Score { 80 | if self.scores.levels.len() > lvl_no { 81 | self.scores.levels[lvl_no] 82 | } else { 83 | Score::default() 84 | } 85 | } 86 | 87 | // save info about winning the level by a user. If it is the first time, save the date as well 88 | pub fn set_win(&mut self, lvl_no: usize, throws: u32) { 89 | if self.lvl_cnt <= lvl_no || self.scores.levels.len() + 1 < lvl_no { 90 | unreachable!() 91 | } 92 | 93 | // if the level was played for the first time, it may not have corresponding score info, 94 | // so fill the hiscore list with default one beforehand 95 | while self.scores.levels.len() <= lvl_no { 96 | self.scores.levels.push(Score::default()); 97 | } 98 | 99 | let mut curr = self.scores.levels[lvl_no]; 100 | curr.wins += 1; 101 | curr.attempts += 1; 102 | if curr.wins == 1 { 103 | // first win - remember the date 104 | let dt: NaiveDateTime = Local::now().naive_local(); 105 | let days = dt.num_days_from_ce(); 106 | curr.first_win = days; 107 | } 108 | if curr.hiscore == 0 || curr.hiscore > throws { 109 | curr.hiscore = if throws > 999 { 999 } else { throws }; 110 | } 111 | self.scores.levels[lvl_no] = curr; 112 | 113 | if lvl_no < self.lvl_cnt - 1 { 114 | if lvl_no + 1 > self.scores.max_level { 115 | self.scores.max_level = lvl_no + 1; 116 | } 117 | self.curr_level = lvl_no + 1; 118 | } 119 | 120 | self.save(); 121 | } 122 | 123 | // save info about the level failed 124 | pub fn set_fail(&mut self, lvl_no: usize) { 125 | if self.lvl_cnt <= lvl_no || self.scores.levels.len() + 1 < lvl_no { 126 | unreachable!() 127 | } 128 | 129 | while self.scores.levels.len() <= lvl_no { 130 | self.scores.levels.push(Score::default()); 131 | } 132 | 133 | let mut curr = self.scores.levels[lvl_no]; 134 | curr.attempts += 1; 135 | self.scores.levels[lvl_no] = curr; 136 | self.save(); 137 | } 138 | 139 | // mark a level as being solved using help. 140 | // The detect is simple: if level has not solved and a user requests its replay, then 141 | // mark the level as help-used one 142 | pub fn set_help_used(&mut self, lvl_no: usize) { 143 | if self.scores.levels.len() <= lvl_no { 144 | unreachable!() 145 | } 146 | 147 | let mut curr = self.scores.levels[lvl_no]; 148 | if curr.wins == 0 { 149 | curr.help_used = true; 150 | self.scores.levels[lvl_no] = curr; 151 | self.save(); 152 | } 153 | } 154 | 155 | pub fn max_avail_level(&self) -> usize { 156 | self.scores.max_level 157 | } 158 | 159 | pub fn curr_level(&self) -> usize { 160 | self.curr_level 161 | } 162 | 163 | // used by main menu 164 | pub fn inc_curr_level(&mut self, delta: usize) -> usize { 165 | if self.curr_level + delta < self.scores.max_level { 166 | self.curr_level += delta 167 | } else { 168 | self.curr_level = self.scores.max_level; 169 | } 170 | self.curr_level 171 | } 172 | 173 | // used by main menu 174 | pub fn dec_curr_level(&mut self, delta: usize) -> usize { 175 | if self.curr_level - 1 > delta { 176 | self.curr_level -= delta 177 | } else { 178 | self.curr_level = 1; 179 | } 180 | self.curr_level 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | # Unblocked 2 | 3 | A puzzle game inspired by NES game "Flipull" with a bit different mechanics. 4 | 5 | Game screenshot 6 | 7 | As of version 1.0, it contains 56 puzzles (and one demo level that is unplayable). 8 | 9 | # Table of Contents 10 | 11 | - [Unblocked](#unblocked) 12 | - [Table of Contents](#table-of-contents) 13 | - [Where the application stores its data files](#where-the-application-stores-its-data-files) 14 | - [Making the game portable](#making-the-game-portable) 15 | - [Running from a read-only location](#running-from-a-read-only-location) 16 | - [Game rules](#game-rules) 17 | - [Hotkeys](#hotkeys) 18 | - [Main menu](#main-menu) 19 | - [Demo mode](#demo-mode) 20 | - [While playing](#while-playing) 21 | - [Replays](#replays) 22 | - [How to use replays from release page](#how-to-use-replays-from-release-page) 23 | - [How to record a replay](#how-to-record-a-replay) 24 | - [FAQ](#faq) 25 | 26 | ## Where the application stores its data files 27 | 28 | To play the game, you need only its binary. But the game may create files to track your progress. Please read below what files the games may create and how to turn the game to portable version. 29 | 30 | The root game data directory depends on operation system and portability. In portable mode the data directory is the directory where the game's binary is. Otherwise, the root data directory is the current user's configuration directory: 31 | 32 | * Linux: `~/.config/rionnag/unblocked` 33 | * Windows: `c:\Users\{username}\AppData\Roaming\rionnag\unblocked` 34 | * OSX: `/Users/{username}/Library/Preferences/rionnag/unblocked` 35 | 36 | After you win(or fail) the first level, the game creates `hiscores.toml` in its data directory to keep your progress. 37 | 38 | If you save any of your replays, the game creates subdirectory `replays` in its root data directory, and saves the replay into it. The name of replay file is `level-<4 digits level number>.rpl`. 39 | 40 | ### Making the game portable 41 | 42 | To make the game portable, create an empty file `config.toml` in the same directory where the game's binary is. Since next start, the game will save and read all its data from the binary's directory. 43 | 44 | Note: Windows distribution is already portable. You have to delete `config.toml` to make the game using the current user's configuration directory. 45 | 46 | ### Running from a read-only location 47 | 48 | The game is playable even if it is launched from read-only location(e.g. from CD). It will save its data to user's configuration directory. 49 | 50 | You even can make it portable on CD by burn both the game binary and `config.toml` to the same directory. In this case the game does not save your progress and you have to start playing from the first level every game launch. So, it may be a good idea to complete the game before putting it to read-only location and add third file `hiscores.toml` to the game package. 51 | 52 | ## Game rules 53 | 54 | The game goal is to remove all blocks from the screen. 55 | 56 | You can throw your block only if the first block it hits is a matching block. The block `?` is a "joker" block - it matches any block. 57 | 58 | After the block is thrown, it annihilates all matching blocks and the first unmatched one becomes the new player's block. 59 | 60 | ## Hotkeys 61 | 62 | ### Main menu 63 | 64 | * up and down - select menu item 65 | * left and right - if the selected menu item is level number it decreases and increased the number 66 | * shift+left and shift+right - if the selected menu item is level number it decreases and increased the number by 10 67 | * enter or space - execute the selected menu item 68 | 69 | ### Demo mode 70 | 71 | * esc - interrupt the replay and return to main menu or to the moment you stopped playing 72 | 73 | ### While playing 74 | 75 | * up and down - move player's block up and down 76 | * space - throw player's block if it is possible 77 | * esc - exit to main menu (if you have made a few throws before pressing esc, the game counts the attempt failed) 78 | * f5 - save replay (if you have made no throws or failed, nothing is saved) 79 | * f1 - show saved replays (the hotkey works only if there is corresponding replay file for the level in `replays` directory) 80 | 81 | ## Replays 82 | 83 | Distribution does not include any replays(except built-in one for the demo level). You have to copy them from somewhere, or download from game releases page. 84 | 85 | ### How to use replays from release page 86 | 87 | 1. Locate where the game stores its [data](#where-the-application-stores-its-data-files) 88 | 2. Create directory `replays` inside the data directory if it does not exist yet 89 | 3. Unpack all replays or one replay from the archive into `replays` directory 90 | 4. Start the game 91 | 5. Open the level you want to watch replay 92 | 6. Press f1, if everything has been done correctly the replay starts immediately 93 | 94 | It is possible that a replay does not start even if everything has been done right. It is possible if the game and your replay are not compatible: every replay includes its version. In this case, it prints to stderr message `Unsupported version`. Solution: download replay pack of supported version from release page. At of version 1.0, there is the only one replay version. So, if you see `Unsupported version` it means that the replay file is damaged. 95 | 96 | Another sign of invalid replay is player's brick is moving chaotically without making throws and taking a long pauses. It may mean that the replay file is for different level or invalid. 97 | 98 | Note: replay format does not change if the game version changes. You should not re-download a replay pack every time you update the game. 99 | 100 | ### How to record a replay 101 | 102 | Every time you start or restart a level, the game starts recording a replay. But it does not save anything automatically. You have to press f5 to save the recorded replay. Pressing the key saves the recording to a file only if there is anything to save. If you just started or you failed the level, the recording is reset. So, do not try to save a replay after the game shows `no moves` - it won\'t save anything. 103 | 104 | Do not hurry while recording a replay. Take your time and do not worry. When the game saves the replay to a file, it squeezes the replay so the longest pause between two actions turns to 3 seconds. 105 | 106 | Warning: saving a new replay for a level overwrites previous one in the game [replay directory](#how-to-use-replays-from-release-page). So, if you want to save a few different replays for the same level, copy replays manually to safe location. 107 | 108 | ## FAQ 109 | 110 | **Q. Why does my hiscore color change?** 111 | 112 | A. When I was testing all the levels I wrote down my best results. And now the game shows how well you have done: white color means your hiscore equals mine; blue color means you made more throws than I did; and green color means you have beaten my hiscore. My results are not optimal: when I was watching replay, at least for 3 of them, I notices that the result can be improved by one throw. 113 | 114 | 115 | **Q. And I spotted that the date when the level was solved successfully for the first time changes its color as well. Why?** 116 | 117 | A. Yes, this date can be displayed in two different colors: white does not mean anything special, but blue color means that someone was cheating :) - the game detected that the level replay had been watched before the level was solved for the first time. 118 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::MAX_SIZE; 2 | use crate::field::BrickKind; 3 | 4 | // file name with all levels 5 | const STD_LEVELS: &str = include_str!("../assets/std_puzzles"); 6 | // if starting block is not set for a level in the file, use this one 7 | const DEFAULT_KIND: BrickKind = BrickKind::Joker; 8 | 9 | // convert character to type of a block 10 | fn c2brick(c: char) -> BrickKind { 11 | match c { 12 | 'S' | 's' | '$' | '1' => BrickKind::K1, 13 | 'X' | 'x' | '%' | '2' => BrickKind::K2, 14 | 'O' | 'o' | '@' | '3' => BrickKind::K3, 15 | 'T' | 't' | '=' | '4' => BrickKind::K4, 16 | 'Z' | 'z' | '+' | '5' => BrickKind::K5, 17 | 'W' | 'w' | ':' | '6' => BrickKind::K6, 18 | '?' => BrickKind::Joker, 19 | _ => BrickKind::None, 20 | } 21 | } 22 | 23 | // a single level 24 | #[derive(Clone)] 25 | pub struct Level { 26 | pub corner: Vec, // pattern of the top left corner 27 | pub puzzle: Vec>, // initial block positions 28 | pub first: BrickKind, // player's starting block 29 | } 30 | 31 | impl Default for Level { 32 | fn default() -> Self { 33 | Level { corner: Vec::new(), puzzle: Vec::new(), first: DEFAULT_KIND } 34 | } 35 | } 36 | 37 | pub struct Loader { 38 | levels: Vec, // all levels 39 | } 40 | 41 | impl Loader { 42 | pub fn new() -> Loader { 43 | let mut loader = Loader { levels: Vec::new() }; 44 | loader.load_from_string(STD_LEVELS); 45 | loader 46 | } 47 | 48 | // returns a level info by its number. 49 | // Panics if the level number is invalid (that should never happen without 50 | // manual modification of hiscores file) 51 | pub fn level(&self, level_no: usize) -> Level { 52 | self.levels[level_no].clone() 53 | } 54 | 55 | pub fn level_count(&self) -> usize { 56 | self.levels.len() 57 | } 58 | 59 | // Validate level and fail early - in any case the game in not playable 60 | fn validate_level(&self, level: &Level, lvl_num: usize) { 61 | let max_size: u8 = MAX_SIZE as u8; 62 | // 1. Corner pattern must be: 63 | // - Either missing 64 | // - Or contain less than MAX_SIZE-1 lines 65 | // 2. No corner line length can exceed MAX_SIZE 66 | if level.corner.len() > MAX_SIZE - 1 || level.corner.len() == 1 { 67 | panic!( 68 | "Level {}: corner pattern must be omitted or has between 2 and {} lines, found {} lines", 69 | lvl_num, 70 | max_size - 1, 71 | level.corner.len() 72 | ); 73 | } 74 | for l in level.corner.iter() { 75 | if *l > max_size { 76 | panic!("Level {}: corner line exceeds {} blocks = {} blocks", lvl_num, max_size, *l); 77 | } 78 | } 79 | 80 | // A puzzle must have: 81 | // 1. Width and height less than or equal to MAX_SIZE 82 | // 2. Both width and height at least 2 blocks 83 | // 3. No holes in any column 84 | if level.puzzle.len() > MAX_SIZE || level.puzzle.len() < 2 { 85 | panic!( 86 | "Level {}: puzzle must has between 2 and {} lines, found {} lines", 87 | lvl_num, 88 | max_size, 89 | level.puzzle.len() 90 | ); 91 | } 92 | let max_w: usize = level.puzzle.iter().fold(0, |mx, x| if mx < x.len() { x.len() } else { mx }); 93 | if !(2..=MAX_SIZE).contains(&max_w) { 94 | panic!("Level {}: puzzle must has between 2 and {} columns, found {} columns", lvl_num, max_size, max_w); 95 | } 96 | for i in 0..max_w { 97 | let mut found: bool = false; 98 | for l in level.puzzle.iter() { 99 | if l.len() < i { 100 | continue; 101 | } 102 | if l[i] == BrickKind::None && found { 103 | panic!("Level {} contains a hole in a puzzle", lvl_num); 104 | } 105 | if l[i] != BrickKind::None { 106 | found = true; 107 | } 108 | } 109 | } 110 | } 111 | 112 | // Load all levels from a string 113 | // Level file restrictions: 114 | // - No leading whitespaces 115 | // - No whitespaces between 'start:' and the following block type 116 | // - Mandatory empty line after a corner pattern if it is set for a level 117 | // - You should not change the very first level in `std_puzzles` file - it is DEMO level: 118 | // a) inaccessible for a player to play 119 | // b) its replay is built-in in the binary 120 | // So, if you ever change the first `DEMO` level you have to do the following as well: 121 | // a) solve this and record your solution 122 | // b) replace existing `assets/level-0000.rpl` with your new replay 123 | // Otherwise `DEMO` in main menu would be broken 124 | // Format: 125 | // `;` - comment line. All lines starting with `;` are ignoerd 126 | // `#` at the first position means that from the next line the next level starts 127 | // you can write any text after `#` (e.g, I write level numbers for easier debugging) 128 | // `start:BLOCK_TYPE` 129 | // Optional line. 130 | // It should be the first line of level description. The line defines the player's 131 | // block at game start. If the line is missing, the player's first block is `?` 132 | // `*****` 133 | // If line starts from `*` it means that it is corner pattern. After `*` any characters 134 | // can follow because loader only reads the line of the length and does not parse it. 135 | // It results in that you cannot create a corner with holes - it is always filled with 136 | // icy blocks 137 | // The last part of the level is its puzzle: lines of blocks. How to encode a block with 138 | // characters you can see in function `c2brick` 139 | // Full level example: 140 | // # level 01 141 | // start:? 142 | // ****** 143 | // **** 144 | // **** 145 | // *** 146 | // ** 147 | // * 148 | // 149 | // $%= 150 | // %%% 151 | // %=$ 152 | fn load_from_string(&mut self, pset: &str) { 153 | let mut in_corner: bool = false; 154 | let mut in_puzzle: bool = false; 155 | let mut lvl: Level = Default::default(); 156 | self.levels.clear(); 157 | 158 | for s in pset.lines() { 159 | let s = s.trim_end(); 160 | // empty line found. If previous section was corner pattern, switch to puzzle mode 161 | if s.is_empty() { 162 | if in_corner { 163 | in_corner = false; 164 | in_puzzle = true; 165 | } 166 | continue; 167 | } 168 | // skip comment lines 169 | if s.starts_with(';') { 170 | continue; 171 | } 172 | // sets the first block used by a player 173 | if s.starts_with("start:") { 174 | let s1 = s.trim_start_matches("start:"); 175 | let s1 = s1.trim_start(); 176 | if s1.is_empty() { 177 | continue; 178 | } 179 | lvl.first = c2brick(s1.chars().next().unwrap()); 180 | continue; 181 | } 182 | // new level starts. Save previous level and continue 183 | if s.starts_with('#') { 184 | if !lvl.puzzle.is_empty() { 185 | self.validate_level(&lvl, self.levels.len()); 186 | self.levels.push(lvl); 187 | lvl = Default::default(); 188 | } 189 | continue; 190 | } 191 | // corner pattern starts 192 | if s.starts_with('*') { 193 | if !in_corner { 194 | in_corner = true; 195 | } 196 | lvl.corner.push(s.len() as u8); 197 | continue; 198 | } 199 | if !in_puzzle { 200 | in_puzzle = true; 201 | } 202 | let puzzle_line: Vec = s.chars().map(c2brick).collect(); 203 | lvl.puzzle.push(puzzle_line); 204 | } 205 | // save the last level - there is no `#` after it 206 | if !lvl.puzzle.is_empty() { 207 | self.validate_level(&lvl, self.levels.len()); 208 | self.levels.push(lvl); 209 | } 210 | println!("Loaded {} levels", self.levels.len()); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/mainmenu.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | use std::time::Duration; 4 | 5 | use tetra::graphics::{self, animation, Color, DrawParams, Rectangle, Texture}; 6 | use tetra::input::{self, Key}; 7 | use tetra::math::Vec2; 8 | use tetra::Context; 9 | 10 | use crate::common::digits; 11 | use crate::consts::{DEMO_LEVEL, SCR_H, SCR_W}; 12 | use crate::demo::DemoScene; 13 | use crate::loader::Loader; 14 | use crate::play::PlayScene; 15 | use crate::scenes::{Scene, Transition}; 16 | use crate::scores::Scores; 17 | use crate::textnum::{TextNumber, TextParams}; 18 | 19 | // height of a menu item sprite 20 | const LBL_HEIGHT: f32 = 32.0; 21 | // sizes of main menu arrow 22 | const POINTER_W: f32 = 36.0; 23 | const POINTER_H: f32 = 40.0; 24 | // number of frames in main menu arrow animation 25 | const POINTER_FRAMES: usize = 6; 26 | // shift to draw the main menu arrow centered for the menu item 27 | const POINTER_SHIFT: f32 = (LBL_HEIGHT - POINTER_H) * 0.5; 28 | // menu items to manually select a level to start from 29 | const LVL_MENU_ITEM: usize = 1; 30 | 31 | pub struct TitleScene { 32 | item_pos: [Vec2; 4], // positions of all 4 menu items 33 | animation: animation::Animation, // arrow 34 | menu_tx: Texture, 35 | menu_id: usize, 36 | txt_num: TextNumber, 37 | 38 | lbl_width: [f32; 4], // width of menu items (at this moment it is hardcoded) 39 | lbl_gap: [f32; 4], // extra space between menu item and arrow 40 | lbl_ext_width: [f32; 4], // full menu item width (include level number) 41 | 42 | loader: Rc, 43 | scores: Rc>, 44 | } 45 | 46 | impl TitleScene { 47 | pub fn new(ctx: &mut Context) -> tetra::Result { 48 | // hardcoded menu item widths (change it if you replace main menu sprites) 49 | let widths: [f32; 4] = [100.0, 98.0, 80.0, 80.0]; 50 | let mut ext_widths: [f32; 4] = [0.0, 0.0, 0.0, 0.0]; 51 | let mut lbl_gap: [f32; 4] = [0.0, 0.0, 0.0, 0.0]; 52 | 53 | let line_gap = LBL_HEIGHT * 0.5; // vertical space between items 54 | 55 | // calculate menu item positions so they all are shown in the middle of the screen 56 | // Positions can be hardcoded but I was not sure that 4 menu items is permanent count. 57 | let item_cnt = 4; // number of menu items 58 | let half_cnt = (item_cnt / 2) as f32; 59 | let first = if item_cnt % 2 == 0 { 60 | half_cnt * LBL_HEIGHT + (half_cnt - 1.0) * line_gap + line_gap * 0.5 61 | } else { 62 | half_cnt * LBL_HEIGHT + half_cnt * line_gap + LBL_HEIGHT * 0.5 63 | }; 64 | let half_scr_h = SCR_H * 0.5; 65 | let first = half_scr_h - first; // vertical position of the first menu item 66 | let mut v = [Vec2::new(0.0, 0.0); 4]; 67 | 68 | let loader = Rc::new(Loader::new()); 69 | let scores = Rc::new(RefCell::new(Scores::new(loader.level_count()))); 70 | 71 | // calculates extra horizontal gaps - now it makes sense only for menu item 72 | // that allows a user manually select level to start from. 73 | // Extra space depends on width of one digit 74 | let number_image = include_bytes!("../assets/numbers.png"); 75 | let txt = TextNumber::new(ctx, number_image)?; 76 | let lvl_cnt = loader.level_count(); 77 | let sz = txt.digit_size(); 78 | let digs = digits(lvl_cnt); 79 | let lvl_width = f32::from(digs) * sz.x; 80 | ext_widths[LVL_MENU_ITEM] += lvl_width; 81 | lbl_gap[LVL_MENU_ITEM] += sz.x; 82 | 83 | let half_scr_w = SCR_W * 0.5; 84 | for i in 0..4 { 85 | v[i] = 86 | Vec2::new(half_scr_w - (widths[i] + ext_widths[i]) * 0.5, first + i as f32 * (LBL_HEIGHT + line_gap)); 87 | } 88 | 89 | let arrow_image = include_bytes!("../assets/menu_arrow.png"); 90 | let menu_image = include_bytes!("../assets/menu_items.png"); 91 | 92 | Ok(TitleScene { 93 | item_pos: v, 94 | animation: animation::Animation::new( 95 | Texture::from_encoded(ctx, arrow_image)?, 96 | Rectangle::row(0.0, 0.0, POINTER_W, POINTER_H).take(POINTER_FRAMES).collect(), 97 | Duration::from_millis(100), 98 | ), 99 | 100 | menu_tx: Texture::from_encoded(ctx, menu_image)?, 101 | menu_id: 0, 102 | txt_num: txt, 103 | 104 | lbl_width: widths, 105 | lbl_gap, 106 | lbl_ext_width: ext_widths, 107 | 108 | loader, 109 | scores, 110 | }) 111 | } 112 | } 113 | 114 | impl Scene for TitleScene { 115 | fn update(&mut self, ctx: &mut Context) -> tetra::Result { 116 | self.animation.advance(ctx); 117 | // Key processing: 118 | // - Up and Down to select a menu item 119 | // - Space and Return to execute the selected menu item 120 | // - Left and Right to increase and decrease the starting level number by `1` 121 | // if the menu item `LVL_MENU_ITEM` is selected 122 | // - Shift+Left and Shift+Right to increase and decrease the starting level number by `10` 123 | // if the menu item `LVL_MENU_ITEM` is selected 124 | if input::is_key_pressed(ctx, Key::Up) { 125 | if self.menu_id == 0 { 126 | self.menu_id = 3; 127 | } else { 128 | self.menu_id -= 1; 129 | } 130 | Ok(Transition::None) 131 | } else if input::is_key_pressed(ctx, Key::Down) { 132 | if self.menu_id == 3 { 133 | self.menu_id = 0; 134 | } else { 135 | self.menu_id += 1; 136 | } 137 | Ok(Transition::None) 138 | } else if input::is_key_pressed(ctx, Key::Left) && self.menu_id == 1 { 139 | let diff = if input::is_key_down(ctx, Key::RightShift) || input::is_key_down(ctx, Key::LeftShift) { 140 | 10usize 141 | } else { 142 | 1usize 143 | }; 144 | { 145 | let mut sc = self.scores.borrow_mut(); 146 | sc.dec_curr_level(diff); 147 | } 148 | Ok(Transition::None) 149 | } else if input::is_key_pressed(ctx, Key::Right) && self.menu_id == 1 { 150 | let diff = if input::is_key_down(ctx, Key::RightShift) || input::is_key_down(ctx, Key::LeftShift) { 151 | 10usize 152 | } else { 153 | 1usize 154 | }; 155 | { 156 | let mut sc = self.scores.borrow_mut(); 157 | sc.inc_curr_level(diff); 158 | } 159 | Ok(Transition::None) 160 | } else if input::is_key_pressed(ctx, Key::Space) 161 | || input::is_key_pressed(ctx, Key::Enter) 162 | || input::is_key_pressed(ctx, Key::NumPadEnter) 163 | { 164 | if self.menu_id == 3 { 165 | Ok(Transition::Pop) 166 | } else if self.menu_id == 0 || self.menu_id == 1 { 167 | Ok(Transition::Push(Box::new(PlayScene::new(ctx, self.loader.clone(), self.scores.clone())?))) 168 | } else if self.menu_id == 2 { 169 | Ok(Transition::Push(Box::new(DemoScene::new( 170 | ctx, 171 | self.loader.clone(), 172 | self.scores.clone(), 173 | DEMO_LEVEL, 174 | )?))) 175 | } else { 176 | Ok(Transition::None) 177 | } 178 | } else { 179 | Ok(Transition::None) 180 | } 181 | } 182 | 183 | fn draw(&mut self, ctx: &mut Context) -> tetra::Result { 184 | graphics::clear(ctx, Color::rgb(0.094, 0.11, 0.16)); 185 | 186 | let mut start: f32 = 0.0; 187 | 188 | // show main menu items 189 | for i in 0..4 { 190 | let clip = Rectangle::new(start, 0.0, self.lbl_width[i], LBL_HEIGHT); 191 | let dp = DrawParams::new().position(self.item_pos[i]); 192 | self.menu_tx.draw_region(ctx, clip, dp); 193 | start += self.lbl_width[i]; 194 | } 195 | 196 | // show "arrows" to the left and to the right from the selected menu item 197 | let pos = 198 | Vec2::new(self.item_pos[self.menu_id].x - POINTER_W - 5.0, self.item_pos[self.menu_id].y + POINTER_SHIFT); 199 | self.animation.draw(ctx, DrawParams::new().position(pos).color(Color::rgb(0.0, 1.0, 1.0))); 200 | let wdth = self.lbl_width[self.menu_id] + self.lbl_ext_width[self.menu_id] + self.lbl_gap[self.menu_id]; 201 | let pos = Vec2::new(self.item_pos[self.menu_id].x + wdth + 5.0, self.item_pos[self.menu_id].y + POINTER_SHIFT); 202 | self.animation.draw(ctx, DrawParams::new().position(pos).color(Color::rgb(0.0, 1.0, 1.0))); 203 | 204 | // show the level number to start playing from 205 | let digits = digits(self.scores.borrow().max_avail_level()); 206 | let mx = self.item_pos[LVL_MENU_ITEM].x + self.lbl_gap[LVL_MENU_ITEM] + self.lbl_width[LVL_MENU_ITEM]; 207 | self.txt_num.draw( 208 | ctx, 209 | Vec2::new(mx, self.item_pos[LVL_MENU_ITEM].y), 210 | self.scores.borrow().curr_level() as u32, 211 | TextParams::new().with_width(digits).with_leading_zeroes(), 212 | ); 213 | 214 | Ok(Transition::None) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ab_glyph" 7 | version = "0.2.19" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e5568a4aa5ba8adf5175c5c460b030e27d8893412976cc37bef0e4fbc16cfbba" 10 | dependencies = [ 11 | "ab_glyph_rasterizer", 12 | "owned_ttf_parser", 13 | ] 14 | 15 | [[package]] 16 | name = "ab_glyph_rasterizer" 17 | version = "0.1.8" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" 20 | 21 | [[package]] 22 | name = "adler" 23 | version = "1.0.2" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 26 | 27 | [[package]] 28 | name = "ahash" 29 | version = "0.7.6" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 32 | dependencies = [ 33 | "getrandom", 34 | "once_cell", 35 | "version_check", 36 | ] 37 | 38 | [[package]] 39 | name = "android_system_properties" 40 | version = "0.1.5" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 43 | dependencies = [ 44 | "libc", 45 | ] 46 | 47 | [[package]] 48 | name = "approx" 49 | version = "0.5.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" 52 | dependencies = [ 53 | "num-traits", 54 | ] 55 | 56 | [[package]] 57 | name = "arrayvec" 58 | version = "0.5.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 61 | 62 | [[package]] 63 | name = "autocfg" 64 | version = "1.1.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 67 | 68 | [[package]] 69 | name = "bincode" 70 | version = "1.3.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 73 | dependencies = [ 74 | "serde", 75 | ] 76 | 77 | [[package]] 78 | name = "bitflags" 79 | version = "1.3.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 82 | 83 | [[package]] 84 | name = "bumpalo" 85 | version = "3.12.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 88 | 89 | [[package]] 90 | name = "bytemuck" 91 | version = "1.12.3" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" 94 | dependencies = [ 95 | "bytemuck_derive", 96 | ] 97 | 98 | [[package]] 99 | name = "bytemuck_derive" 100 | version = "1.3.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "5fe233b960f12f8007e3db2d136e3cb1c291bfd7396e384ee76025fc1a3932b4" 103 | dependencies = [ 104 | "proc-macro2", 105 | "quote", 106 | "syn", 107 | ] 108 | 109 | [[package]] 110 | name = "byteorder" 111 | version = "1.4.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 114 | 115 | [[package]] 116 | name = "cc" 117 | version = "1.0.78" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 120 | 121 | [[package]] 122 | name = "cfg-if" 123 | version = "0.1.10" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 126 | 127 | [[package]] 128 | name = "cfg-if" 129 | version = "1.0.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 132 | 133 | [[package]] 134 | name = "chrono" 135 | version = "0.4.23" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" 138 | dependencies = [ 139 | "iana-time-zone", 140 | "js-sys", 141 | "num-integer", 142 | "num-traits", 143 | "time", 144 | "wasm-bindgen", 145 | "winapi", 146 | ] 147 | 148 | [[package]] 149 | name = "codespan-reporting" 150 | version = "0.11.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 153 | dependencies = [ 154 | "termcolor", 155 | "unicode-width", 156 | ] 157 | 158 | [[package]] 159 | name = "color_quant" 160 | version = "1.1.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 163 | 164 | [[package]] 165 | name = "concat-string" 166 | version = "1.0.1" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" 169 | 170 | [[package]] 171 | name = "core-foundation-sys" 172 | version = "0.8.3" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 175 | 176 | [[package]] 177 | name = "crc32fast" 178 | version = "1.3.2" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 181 | dependencies = [ 182 | "cfg-if 1.0.0", 183 | ] 184 | 185 | [[package]] 186 | name = "cxx" 187 | version = "1.0.86" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" 190 | dependencies = [ 191 | "cc", 192 | "cxxbridge-flags", 193 | "cxxbridge-macro", 194 | "link-cplusplus", 195 | ] 196 | 197 | [[package]] 198 | name = "cxx-build" 199 | version = "1.0.86" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" 202 | dependencies = [ 203 | "cc", 204 | "codespan-reporting", 205 | "once_cell", 206 | "proc-macro2", 207 | "quote", 208 | "scratch", 209 | "syn", 210 | ] 211 | 212 | [[package]] 213 | name = "cxxbridge-flags" 214 | version = "1.0.86" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" 217 | 218 | [[package]] 219 | name = "cxxbridge-macro" 220 | version = "1.0.86" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" 223 | dependencies = [ 224 | "proc-macro2", 225 | "quote", 226 | "syn", 227 | ] 228 | 229 | [[package]] 230 | name = "dirs" 231 | version = "2.0.2" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 234 | dependencies = [ 235 | "cfg-if 0.1.10", 236 | "dirs-sys", 237 | ] 238 | 239 | [[package]] 240 | name = "dirs-sys" 241 | version = "0.3.7" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 244 | dependencies = [ 245 | "libc", 246 | "redox_users", 247 | "winapi", 248 | ] 249 | 250 | [[package]] 251 | name = "euclid" 252 | version = "0.22.7" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "b52c2ef4a78da0ba68fbe1fd920627411096d2ac478f7f4c9f3a54ba6705bade" 255 | dependencies = [ 256 | "num-traits", 257 | ] 258 | 259 | [[package]] 260 | name = "find-winsdk" 261 | version = "0.2.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "a8cbf17b871570c1f8612b763bac3e86290602bcf5dc3c5ce657e0e1e9071d9e" 264 | dependencies = [ 265 | "serde", 266 | "serde_derive", 267 | "winreg", 268 | ] 269 | 270 | [[package]] 271 | name = "flate2" 272 | version = "1.0.25" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 275 | dependencies = [ 276 | "crc32fast", 277 | "miniz_oxide", 278 | ] 279 | 280 | [[package]] 281 | name = "float_next_after" 282 | version = "0.1.5" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632" 285 | dependencies = [ 286 | "num-traits", 287 | ] 288 | 289 | [[package]] 290 | name = "getrandom" 291 | version = "0.2.8" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 294 | dependencies = [ 295 | "cfg-if 1.0.0", 296 | "libc", 297 | "wasi 0.11.0+wasi-snapshot-preview1", 298 | ] 299 | 300 | [[package]] 301 | name = "glow" 302 | version = "0.11.2" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "d8bd5877156a19b8ac83a29b2306fe20537429d318f3ff0a1a2119f8d9c61919" 305 | dependencies = [ 306 | "js-sys", 307 | "slotmap", 308 | "wasm-bindgen", 309 | "web-sys", 310 | ] 311 | 312 | [[package]] 313 | name = "half" 314 | version = "1.8.2" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 317 | dependencies = [ 318 | "bytemuck", 319 | ] 320 | 321 | [[package]] 322 | name = "hashbrown" 323 | version = "0.12.3" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 326 | dependencies = [ 327 | "ahash", 328 | ] 329 | 330 | [[package]] 331 | name = "iana-time-zone" 332 | version = "0.1.53" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 335 | dependencies = [ 336 | "android_system_properties", 337 | "core-foundation-sys", 338 | "iana-time-zone-haiku", 339 | "js-sys", 340 | "wasm-bindgen", 341 | "winapi", 342 | ] 343 | 344 | [[package]] 345 | name = "iana-time-zone-haiku" 346 | version = "0.1.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 349 | dependencies = [ 350 | "cxx", 351 | "cxx-build", 352 | ] 353 | 354 | [[package]] 355 | name = "image" 356 | version = "0.24.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" 359 | dependencies = [ 360 | "bytemuck", 361 | "byteorder", 362 | "color_quant", 363 | "num-rational", 364 | "num-traits", 365 | "png", 366 | ] 367 | 368 | [[package]] 369 | name = "js-sys" 370 | version = "0.3.60" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" 373 | dependencies = [ 374 | "wasm-bindgen", 375 | ] 376 | 377 | [[package]] 378 | name = "lazy_static" 379 | version = "1.4.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 382 | 383 | [[package]] 384 | name = "libc" 385 | version = "0.2.139" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" 388 | 389 | [[package]] 390 | name = "link-cplusplus" 391 | version = "1.0.8" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 394 | dependencies = [ 395 | "cc", 396 | ] 397 | 398 | [[package]] 399 | name = "log" 400 | version = "0.4.17" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 403 | dependencies = [ 404 | "cfg-if 1.0.0", 405 | ] 406 | 407 | [[package]] 408 | name = "lyon_geom" 409 | version = "0.17.7" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "71d89ccbdafd83d259403e22061be27bccc3254bba65cdc5303250c4227c8c8e" 412 | dependencies = [ 413 | "arrayvec", 414 | "euclid", 415 | "num-traits", 416 | ] 417 | 418 | [[package]] 419 | name = "lyon_path" 420 | version = "0.17.7" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "5b0a59fdf767ca0d887aa61d1b48d4bbf6a124c1a45503593f7d38ab945bfbc0" 423 | dependencies = [ 424 | "lyon_geom", 425 | ] 426 | 427 | [[package]] 428 | name = "lyon_tessellation" 429 | version = "0.17.10" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "7230e08dd0638048e46f387f255dbe7a7344a3e6705beab53242b5af25635760" 432 | dependencies = [ 433 | "float_next_after", 434 | "lyon_path", 435 | ] 436 | 437 | [[package]] 438 | name = "miniz_oxide" 439 | version = "0.6.2" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 442 | dependencies = [ 443 | "adler", 444 | ] 445 | 446 | [[package]] 447 | name = "num-integer" 448 | version = "0.1.45" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 451 | dependencies = [ 452 | "autocfg", 453 | "num-traits", 454 | ] 455 | 456 | [[package]] 457 | name = "num-rational" 458 | version = "0.4.1" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" 461 | dependencies = [ 462 | "autocfg", 463 | "num-integer", 464 | "num-traits", 465 | ] 466 | 467 | [[package]] 468 | name = "num-traits" 469 | version = "0.2.15" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 472 | dependencies = [ 473 | "autocfg", 474 | ] 475 | 476 | [[package]] 477 | name = "once_cell" 478 | version = "1.17.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" 481 | 482 | [[package]] 483 | name = "owned_ttf_parser" 484 | version = "0.18.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "2a5f3c7ca08b6879e7965fb25e24d1f5eeb32ea73f9ad99b3854778a38c57e93" 487 | dependencies = [ 488 | "ttf-parser", 489 | ] 490 | 491 | [[package]] 492 | name = "png" 493 | version = "0.17.7" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" 496 | dependencies = [ 497 | "bitflags", 498 | "crc32fast", 499 | "flate2", 500 | "miniz_oxide", 501 | ] 502 | 503 | [[package]] 504 | name = "proc-macro2" 505 | version = "1.0.50" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" 508 | dependencies = [ 509 | "unicode-ident", 510 | ] 511 | 512 | [[package]] 513 | name = "quote" 514 | version = "1.0.23" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 517 | dependencies = [ 518 | "proc-macro2", 519 | ] 520 | 521 | [[package]] 522 | name = "redox_syscall" 523 | version = "0.2.16" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 526 | dependencies = [ 527 | "bitflags", 528 | ] 529 | 530 | [[package]] 531 | name = "redox_users" 532 | version = "0.4.3" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 535 | dependencies = [ 536 | "getrandom", 537 | "redox_syscall", 538 | "thiserror", 539 | ] 540 | 541 | [[package]] 542 | name = "rustc_version" 543 | version = "0.4.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 546 | dependencies = [ 547 | "semver", 548 | ] 549 | 550 | [[package]] 551 | name = "scratch" 552 | version = "1.0.3" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" 555 | 556 | [[package]] 557 | name = "sdl2" 558 | version = "0.35.2" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" 561 | dependencies = [ 562 | "bitflags", 563 | "lazy_static", 564 | "libc", 565 | "sdl2-sys", 566 | ] 567 | 568 | [[package]] 569 | name = "sdl2-sys" 570 | version = "0.35.2" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" 573 | dependencies = [ 574 | "cfg-if 1.0.0", 575 | "libc", 576 | "version-compare", 577 | ] 578 | 579 | [[package]] 580 | name = "semver" 581 | version = "1.0.16" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" 584 | 585 | [[package]] 586 | name = "serde" 587 | version = "1.0.152" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 590 | 591 | [[package]] 592 | name = "serde_derive" 593 | version = "1.0.152" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 596 | dependencies = [ 597 | "proc-macro2", 598 | "quote", 599 | "syn", 600 | ] 601 | 602 | [[package]] 603 | name = "slotmap" 604 | version = "1.0.6" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" 607 | dependencies = [ 608 | "version_check", 609 | ] 610 | 611 | [[package]] 612 | name = "syn" 613 | version = "1.0.107" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 616 | dependencies = [ 617 | "proc-macro2", 618 | "quote", 619 | "unicode-ident", 620 | ] 621 | 622 | [[package]] 623 | name = "termcolor" 624 | version = "1.2.0" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 627 | dependencies = [ 628 | "winapi-util", 629 | ] 630 | 631 | [[package]] 632 | name = "tetra" 633 | version = "0.7.0" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "86978e7ae9c09ada19a354a4651a6c42c7167ed01b900014ba806c2eb532b847" 636 | dependencies = [ 637 | "ab_glyph", 638 | "bytemuck", 639 | "glow", 640 | "half", 641 | "hashbrown", 642 | "image", 643 | "lyon_tessellation", 644 | "num-traits", 645 | "sdl2", 646 | "vek", 647 | "xi-unicode", 648 | ] 649 | 650 | [[package]] 651 | name = "thiserror" 652 | version = "1.0.38" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 655 | dependencies = [ 656 | "thiserror-impl", 657 | ] 658 | 659 | [[package]] 660 | name = "thiserror-impl" 661 | version = "1.0.38" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 664 | dependencies = [ 665 | "proc-macro2", 666 | "quote", 667 | "syn", 668 | ] 669 | 670 | [[package]] 671 | name = "time" 672 | version = "0.1.45" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 675 | dependencies = [ 676 | "libc", 677 | "wasi 0.10.0+wasi-snapshot-preview1", 678 | "winapi", 679 | ] 680 | 681 | [[package]] 682 | name = "toml" 683 | version = "0.4.10" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" 686 | dependencies = [ 687 | "serde", 688 | ] 689 | 690 | [[package]] 691 | name = "ttf-parser" 692 | version = "0.18.1" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" 695 | 696 | [[package]] 697 | name = "unblock-it" 698 | version = "1.1.0" 699 | dependencies = [ 700 | "bincode", 701 | "chrono", 702 | "dirs", 703 | "serde", 704 | "serde_derive", 705 | "tetra", 706 | "toml", 707 | "windres", 708 | ] 709 | 710 | [[package]] 711 | name = "unicode-ident" 712 | version = "1.0.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 715 | 716 | [[package]] 717 | name = "unicode-width" 718 | version = "0.1.10" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 721 | 722 | [[package]] 723 | name = "vek" 724 | version = "0.15.9" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "02eeed9a6ab91448e2aea59ffa21e644e22dc373ab0a187ac34ada660c2cb9de" 727 | dependencies = [ 728 | "approx", 729 | "num-integer", 730 | "num-traits", 731 | "rustc_version", 732 | ] 733 | 734 | [[package]] 735 | name = "version-compare" 736 | version = "0.1.1" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 739 | 740 | [[package]] 741 | name = "version_check" 742 | version = "0.9.4" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 745 | 746 | [[package]] 747 | name = "wasi" 748 | version = "0.10.0+wasi-snapshot-preview1" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 751 | 752 | [[package]] 753 | name = "wasi" 754 | version = "0.11.0+wasi-snapshot-preview1" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 757 | 758 | [[package]] 759 | name = "wasm-bindgen" 760 | version = "0.2.83" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" 763 | dependencies = [ 764 | "cfg-if 1.0.0", 765 | "wasm-bindgen-macro", 766 | ] 767 | 768 | [[package]] 769 | name = "wasm-bindgen-backend" 770 | version = "0.2.83" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" 773 | dependencies = [ 774 | "bumpalo", 775 | "log", 776 | "once_cell", 777 | "proc-macro2", 778 | "quote", 779 | "syn", 780 | "wasm-bindgen-shared", 781 | ] 782 | 783 | [[package]] 784 | name = "wasm-bindgen-macro" 785 | version = "0.2.83" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" 788 | dependencies = [ 789 | "quote", 790 | "wasm-bindgen-macro-support", 791 | ] 792 | 793 | [[package]] 794 | name = "wasm-bindgen-macro-support" 795 | version = "0.2.83" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" 798 | dependencies = [ 799 | "proc-macro2", 800 | "quote", 801 | "syn", 802 | "wasm-bindgen-backend", 803 | "wasm-bindgen-shared", 804 | ] 805 | 806 | [[package]] 807 | name = "wasm-bindgen-shared" 808 | version = "0.2.83" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 811 | 812 | [[package]] 813 | name = "web-sys" 814 | version = "0.3.60" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" 817 | dependencies = [ 818 | "js-sys", 819 | "wasm-bindgen", 820 | ] 821 | 822 | [[package]] 823 | name = "winapi" 824 | version = "0.3.9" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 827 | dependencies = [ 828 | "winapi-i686-pc-windows-gnu", 829 | "winapi-x86_64-pc-windows-gnu", 830 | ] 831 | 832 | [[package]] 833 | name = "winapi-i686-pc-windows-gnu" 834 | version = "0.4.0" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 837 | 838 | [[package]] 839 | name = "winapi-util" 840 | version = "0.1.5" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 843 | dependencies = [ 844 | "winapi", 845 | ] 846 | 847 | [[package]] 848 | name = "winapi-x86_64-pc-windows-gnu" 849 | version = "0.4.0" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 852 | 853 | [[package]] 854 | name = "windres" 855 | version = "0.2.2" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "82115619221b2b66001a39088b8059d171b1f9005a00d6a10c6e8a71a30a4cdc" 858 | dependencies = [ 859 | "concat-string", 860 | "find-winsdk", 861 | ] 862 | 863 | [[package]] 864 | name = "winreg" 865 | version = "0.5.1" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" 868 | dependencies = [ 869 | "serde", 870 | "winapi", 871 | ] 872 | 873 | [[package]] 874 | name = "xi-unicode" 875 | version = "0.3.0" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" 878 | -------------------------------------------------------------------------------- /src/field.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Datelike, Local, NaiveDate}; 2 | use std::cell::RefCell; 3 | use std::f32::consts::PI; 4 | use std::fmt; 5 | use std::rc::Rc; 6 | use std::time::Duration; 7 | 8 | use tetra::graphics::{self, animation, Color, DrawParams, Rectangle, Texture}; 9 | use tetra::input::{self, Key}; 10 | use tetra::math::Vec2; 11 | use tetra::Context; 12 | 13 | use crate::common::{clamp, digits}; 14 | use crate::consts::{BRICK_SIZE, HEIGHT, INFO_WIDTH, MAX_SIZE, SCR_H, SCR_W, WIDTH}; 15 | use crate::loader::Loader; 16 | use crate::scenes::Transition; 17 | use crate::scores::{Score, Scores}; 18 | use crate::textnum::{TextNumber, TextParams}; 19 | 20 | const TICKS: u32 = 1; 21 | const BRICK_DEF_SPEED: f32 = 48.0; 22 | const BRICK_FALL_SPEED: f32 = 16.0; 23 | const ARROW_FRAMES: usize = 4; 24 | 25 | // developer best results - I know some of them can be improved 26 | static RECORDS: &[u32] = &[ 27 | 4, // demo level 28 | 4, 5, 4, 7, 4, 5, 6, 6, 9, 10, // 1-10 29 | 11, 6, 6, 8, 8, 8, 8, 6, 7, 7, // 11-20 30 | 8, 9, 8, 7, 9, 8, 7, 10, 12, 12, // 21-30 31 | 10, 12, 11, 11, 10, 10, 11, 11, 10, 12, // 31-40 32 | 12, 12, 14, 11, 10, 11, 15, 15, 17, 12, // 41-50 33 | 15, 12, 12, 14, 16, 12, // 51-56 34 | ]; 35 | const RECORD_LEN: usize = 57; 36 | 37 | #[derive(Debug, Copy, Clone, PartialEq)] 38 | pub enum GameState { 39 | Unfinished, // keep playing 40 | Winner, // level cleared 41 | Looser, // no moves available 42 | Completed, // last level cleared 43 | } 44 | 45 | impl fmt::Display for GameState { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | match self { 48 | GameState::Unfinished => write!(f, "playing..."), 49 | GameState::Winner => write!(f, "level solved"), 50 | GameState::Looser => write!(f, "level failed"), 51 | GameState::Completed => write!(f, "game completed"), 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug, Copy, Clone, PartialEq)] 57 | pub enum BrickKind { 58 | None, 59 | K1, 60 | K2, 61 | K3, 62 | K4, 63 | K5, 64 | K6, 65 | Joker, 66 | } 67 | 68 | impl fmt::Display for BrickKind { 69 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 70 | match self { 71 | BrickKind::K1 => write!(f, "'S'"), 72 | BrickKind::K2 => write!(f, "'X'"), 73 | BrickKind::K3 => write!(f, "'O'"), 74 | BrickKind::K4 => write!(f, "'T'"), 75 | BrickKind::K5 => write!(f, "'Z'"), 76 | BrickKind::K6 => write!(f, "'W'"), 77 | BrickKind::Joker => write!(f, "'?'"), 78 | _ => write!(f, "???"), 79 | } 80 | } 81 | } 82 | 83 | fn brick2shift(k: BrickKind) -> f32 { 84 | match k { 85 | BrickKind::K1 => BRICK_SIZE, 86 | BrickKind::K2 => BRICK_SIZE * 2.0, 87 | BrickKind::K3 => BRICK_SIZE * 3.0, 88 | BrickKind::K4 => BRICK_SIZE * 4.0, 89 | BrickKind::K5 => BRICK_SIZE * 5.0, 90 | BrickKind::K6 => BRICK_SIZE * 6.0, 91 | BrickKind::Joker => BRICK_SIZE * 7.0, 92 | BrickKind::None => 0.0, 93 | } 94 | } 95 | 96 | #[derive(Debug, Clone)] 97 | struct Brick { 98 | // position in whole blocks 99 | x: usize, 100 | y: usize, 101 | scr_pos: Vec2, // exact position in points 102 | kind: BrickKind, // kind of block 103 | vel: Vec2, // velocity 104 | ticks: u32, // ticks for moving (shift a block by velocity every N ticks) 105 | limit: Vec2, // stop moving the block when it reaches the limit 106 | } 107 | 108 | impl Brick { 109 | fn new(x: usize, y: usize, kind: BrickKind) -> Self { 110 | Brick { 111 | x, 112 | y, 113 | kind, 114 | scr_pos: Vec2::new(BRICK_SIZE * x as f32, BRICK_SIZE * y as f32), 115 | vel: Vec2::new(0.0, 0.0), 116 | ticks: 0, 117 | limit: Vec2::new(0.0, 0.0), 118 | } 119 | } 120 | fn start_moving(&mut self, vel: Vec2, limit: Vec2) { 121 | self.vel = vel; 122 | self.limit = limit; 123 | self.ticks = TICKS; 124 | } 125 | // A block must start falling when: 126 | // - thrown block hits the right wall 127 | // - thrown block annihilates a block and a block at the top of it must fall now 128 | // A velocity for both cases differs 129 | fn fall(&mut self, speed: f32) { 130 | if !self.is_moving() { 131 | self.vel = Vec2::new(0.0, speed); 132 | self.limit = Vec2::new(self.scr_pos.x, self.scr_pos.y + BRICK_SIZE); 133 | self.ticks = TICKS; 134 | } else { 135 | self.limit.y += BRICK_SIZE; 136 | } 137 | } 138 | fn is_moving(&self) -> bool { 139 | self.vel.x.abs() > 0.1 || self.vel.y.abs() > 0.1 140 | } 141 | fn is_moving_down(&self) -> bool { 142 | self.vel.y.abs() > 0.1 143 | } 144 | // stop moving 145 | fn stop(&mut self) { 146 | self.vel = Vec2::new(0.0, 0.0); 147 | self.x = (self.scr_pos.x / BRICK_SIZE) as usize; 148 | self.y = (self.scr_pos.y / BRICK_SIZE) as usize; 149 | } 150 | fn update(&mut self) { 151 | if !self.is_moving() { 152 | return; 153 | } 154 | 155 | self.ticks -= 1; 156 | if self.ticks != 0 { 157 | return; 158 | } 159 | 160 | // time to move the block 161 | self.ticks = TICKS; 162 | self.scr_pos.x += self.vel.x; 163 | self.scr_pos.y += self.vel.y; 164 | 165 | if (self.scr_pos.x > self.limit.x && self.vel.x > 0.0) || (self.scr_pos.x < self.limit.x && self.vel.x < 0.0) { 166 | self.scr_pos.x = self.limit.x; 167 | } 168 | if (self.scr_pos.y > self.limit.y && self.vel.y > 0.0) || (self.scr_pos.y < self.limit.y && self.vel.y < 0.0) { 169 | self.scr_pos.y = self.limit.y; 170 | } 171 | 172 | // convert current exact screen position into whole blocks when the block reaches the limit 173 | if (self.scr_pos.x - self.limit.x).abs() < 0.1 && (self.scr_pos.y - self.limit.y).abs() < 0.1 { 174 | let xx = (self.scr_pos.x / BRICK_SIZE).round() as usize; 175 | let yy = (self.scr_pos.y / BRICK_SIZE).round() as usize; 176 | self.x = xx; 177 | self.y = yy; 178 | self.vel = Vec2::new(0.0, 0.0); 179 | } 180 | } 181 | } 182 | 183 | // convert coordinate in whole blocks into screen coordinates 184 | fn b2s>(x: T, y: T) -> Vec2 { 185 | Vec2::new(x.into() as f32 * BRICK_SIZE, y.into() as f32 * BRICK_SIZE) 186 | } 187 | // puzzle is a one-dimensional array, the function converts X,Y coordinate into 188 | // position inside the puzzle array 189 | fn pos2puz>(x: T, y: T) -> usize { 190 | x.into() + y.into() * WIDTH 191 | } 192 | 193 | pub struct GameField { 194 | puzzle: [u32; HEIGHT * WIDTH], 195 | bricks: Vec, 196 | pub level: usize, // current level No inside puzzle_set 197 | pub state: GameState, 198 | 199 | player: Brick, 200 | player_row: usize, 201 | going_back: bool, // the player's block is flying back after throw 202 | pub score: u32, // the number of throws so far 203 | lvl_score: Score, // info about level hiscores 204 | pub demoing: bool, // is in demo mode(for demo mode some things are not displayed) 205 | 206 | // calculated and orientation of an arrow that shows the first block that 207 | // player's block would hit after throwing 208 | arrow_pos: Vec2, 209 | arrow_down: bool, 210 | arrow_animation: animation::Animation, 211 | 212 | // kind of a block that player's block would hit after throwing 213 | first_brick: BrickKind, 214 | 215 | brick_tx: Texture, 216 | back_tx: Texture, 217 | 218 | level_no_tx: Texture, 219 | throws_tx: Texture, 220 | attempts_tx: Texture, 221 | solved_tx: Texture, 222 | 223 | txt_num: TextNumber, 224 | loader: Rc, 225 | pub scores: Rc>, 226 | } 227 | 228 | impl GameField { 229 | pub fn new(ctx: &mut Context, loader: Rc, scores: Rc>, demo: bool) -> tetra::Result { 230 | let lvl_curr = scores.borrow().curr_level(); 231 | let lvl_info = scores.borrow().level_info(lvl_curr); 232 | let arrow_image = include_bytes!("../assets/arrows.png"); 233 | let brick_image = include_bytes!("../assets/bricks.png"); 234 | let background_image = include_bytes!("../assets/background.png"); 235 | let number_image = include_bytes!("../assets/numbers.png"); 236 | let level_no_image = include_bytes!("../assets/level_no.png"); 237 | let throws_image = include_bytes!("../assets/throws.png"); 238 | let attempts_image = include_bytes!("../assets/attempts.png"); 239 | let solved_image = include_bytes!("../assets/solved.png"); 240 | Ok(GameField { 241 | bricks: Vec::new(), 242 | puzzle: [0; HEIGHT * WIDTH], 243 | level: lvl_curr, 244 | state: GameState::Unfinished, 245 | player: Brick::new(WIDTH - INFO_WIDTH - 1, HEIGHT - 2, BrickKind::Joker), 246 | player_row: HEIGHT - 2, 247 | going_back: false, 248 | lvl_score: lvl_info, 249 | score: 0, 250 | demoing: demo, 251 | 252 | arrow_down: false, 253 | arrow_pos: Vec2::new(0.0, 0.0), 254 | first_brick: BrickKind::None, 255 | 256 | txt_num: TextNumber::new(ctx, number_image)?, 257 | loader, 258 | scores, 259 | 260 | brick_tx: Texture::from_encoded(ctx, brick_image)?, 261 | back_tx: Texture::from_encoded(ctx, background_image)?, 262 | level_no_tx: Texture::from_encoded(ctx, level_no_image)?, 263 | throws_tx: Texture::from_encoded(ctx, throws_image)?, 264 | attempts_tx: Texture::from_encoded(ctx, attempts_image)?, 265 | solved_tx: Texture::from_encoded(ctx, solved_image)?, 266 | 267 | arrow_animation: animation::Animation::new( 268 | Texture::from_encoded(ctx, arrow_image)?, 269 | Rectangle::row(0.0, 0.0, BRICK_SIZE, BRICK_SIZE).take(ARROW_FRAMES).collect(), 270 | Duration::from_millis(150), // 60HZ to a frame per 250ms 271 | ), 272 | }) 273 | } 274 | 275 | // are user key strokes processed? 276 | // All key presses are ignored if the player's block in moving or game is over 277 | pub fn is_interactive(&self) -> bool { 278 | !self.going_back && self.state == GameState::Unfinished 279 | } 280 | 281 | // start moving player's block back after hitting the floor or an non-matching block 282 | fn go_back(&mut self) { 283 | self.going_back = true; 284 | let xlimit = (WIDTH - INFO_WIDTH - 1) as f32 * BRICK_SIZE; 285 | let ylimit = self.player_row as f32 * BRICK_SIZE; 286 | let xn = (xlimit - self.player.scr_pos.x) / BRICK_DEF_SPEED; 287 | let dy = (self.player_row as f32 * BRICK_SIZE - self.player.scr_pos.y) / xn; 288 | self.player.start_moving(Vec2::new(BRICK_DEF_SPEED, dy), Vec2::new(xlimit, ylimit)); 289 | } 290 | 291 | fn update_player(&mut self) { 292 | // detect that anything should be updated: player's block must be moving 293 | // or just has stopped 294 | let moved = self.player.is_moving(); 295 | let moved_down = self.player.is_moving_down(); 296 | self.player.update(); 297 | let stopped = moved && !self.player.is_moving(); 298 | if !moved || !stopped { 299 | return; 300 | } 301 | 302 | // player's block returned back after throw 303 | if self.going_back && stopped { 304 | self.going_back = false; 305 | self.recalc_arrow(); 306 | self.state = self.calc_state(); 307 | return; 308 | } 309 | 310 | // below this line it is the case when player's block is still moving 311 | 312 | if moved_down { 313 | // player's block is falling 314 | // 315 | // hit the floor 316 | if self.player.y == HEIGHT - 2 { 317 | self.player.stop(); 318 | self.go_back(); 319 | return; 320 | } 321 | 322 | // calculate the new kind of player's block 323 | let bricks = self.bricks.iter().filter(|b| b.x == self.player.x && b.y == self.player.y + 1); 324 | let mut removed: bool = false; 325 | let mut exists: bool = false; 326 | let mut new_kind = self.player.kind; 327 | for brick in bricks { 328 | exists = true; 329 | if brick.kind == new_kind || new_kind == BrickKind::Joker { 330 | removed = true; 331 | } 332 | new_kind = brick.kind; 333 | } 334 | 335 | // annihilate matched blocks and drop block that were on top of them 336 | if !removed && exists { 337 | self.player.kind = new_kind; 338 | let x = self.player.x; 339 | let y = self.player.y; 340 | self.bricks.retain(|b| b.x != x || b.y != y + 1); 341 | self.bricks.iter_mut().for_each(|b| { 342 | if b.x == x && b.y < y { 343 | b.fall(BRICK_FALL_SPEED); 344 | } 345 | }); 346 | self.player.stop(); 347 | self.go_back(); 348 | return; 349 | } 350 | self.player.kind = new_kind; 351 | self.player.fall(BRICK_DEF_SPEED); 352 | 353 | let x = self.player.x; 354 | let y = self.player.y; 355 | self.bricks.retain(|b| b.x != x || b.y != y + 1); 356 | self.bricks.iter_mut().for_each(|b| { 357 | if b.x == x && b.y < y { 358 | b.fall(BRICK_FALL_SPEED); 359 | } 360 | }); 361 | } else { 362 | // player's block is moving horizontally 363 | let x = self.player.x; 364 | let y = self.player.y; 365 | 366 | let (dx, dy) = if self.puzzle[pos2puz(x - 1, y)] == 1 { 367 | //hit wall -> block falls down 368 | (0i32, 1i32) 369 | } else { 370 | (-1i32, 0i32) 371 | }; 372 | 373 | let bricks = 374 | self.bricks.iter().filter(|b| b.x == (x as i32 + dx) as usize && b.y == (y as i32 + dy) as usize); 375 | let mut removed: bool = false; 376 | let mut exists: bool = false; 377 | let mut new_kind = self.player.kind; 378 | for brick in bricks { 379 | exists = true; 380 | if brick.kind == new_kind || new_kind == BrickKind::Joker { 381 | removed = true; 382 | } 383 | new_kind = brick.kind; 384 | } 385 | 386 | if !removed && !exists && dx != 0 && self.puzzle[pos2puz(self.player.x - 1, self.player.y)] == 0 { 387 | self.player.vel = Vec2::new(-BRICK_DEF_SPEED, 0.0); 388 | self.player.limit = Vec2::new(BRICK_SIZE * (x as i32 + dx) as f32, BRICK_SIZE * y as f32); 389 | self.player.ticks = TICKS; 390 | return; 391 | } 392 | 393 | if removed { 394 | self.player.kind = new_kind; 395 | self.bricks.retain(|b| b.x != (x as i32 + dx) as usize || b.y != (y as i32 + dy) as usize); 396 | self.bricks.iter_mut().for_each(|b| { 397 | if b.x == (x as i32 + dx) as usize && b.y < y { 398 | b.fall(BRICK_FALL_SPEED); 399 | } 400 | }); 401 | if dx == 0 { 402 | if self.player.y == HEIGHT - 2 { 403 | self.player.stop(); 404 | self.go_back(); 405 | 406 | return; 407 | } 408 | self.player.fall(BRICK_DEF_SPEED); 409 | } else { 410 | self.player.vel = Vec2::new(-BRICK_DEF_SPEED, 0.0); 411 | self.player.limit = Vec2::new(BRICK_SIZE * (x as i32 + dx) as f32, BRICK_SIZE * y as f32); 412 | self.player.ticks = TICKS; 413 | } 414 | return; 415 | } 416 | if exists { 417 | self.player.stop(); 418 | self.player.kind = new_kind; 419 | self.bricks.retain(|b| b.x != (x as i32 + dx) as usize || b.y != (y as i32 + dy) as usize); 420 | self.bricks.iter_mut().for_each(|b| { 421 | if b.x == (x as i32 + dx) as usize && b.y < y { 422 | b.fall(BRICK_FALL_SPEED); 423 | } 424 | }); 425 | self.go_back(); 426 | 427 | return; 428 | } 429 | // hit the floor 430 | if self.player.y == HEIGHT - 2 { 431 | self.player.stop(); 432 | self.go_back(); 433 | 434 | return; 435 | } 436 | self.player.fall(BRICK_DEF_SPEED); 437 | } 438 | } 439 | 440 | pub fn update(&mut self, ctx: &mut Context) -> tetra::Result { 441 | self.arrow_animation.advance(ctx); 442 | for b in self.bricks.iter_mut() { 443 | b.update(); 444 | } 445 | 446 | self.update_player(); 447 | 448 | if self.going_back { 449 | return Ok(Transition::None); 450 | } 451 | 452 | if self.state == GameState::Unfinished { 453 | return Ok(Transition::None); 454 | } 455 | 456 | // reach here only if the level solved or failed or demo replay finished. 457 | // Update hiscores if it is not in DEMO mode 458 | if input::is_key_pressed(ctx, Key::Space) 459 | || input::is_key_pressed(ctx, Key::Enter) 460 | || input::is_key_pressed(ctx, Key::NumPadEnter) 461 | { 462 | match self.state { 463 | GameState::Completed => { 464 | if !self.demoing { 465 | let mut sc = self.scores.borrow_mut(); 466 | sc.set_win(self.level, self.score); 467 | } 468 | return Ok(Transition::Pop); 469 | } 470 | GameState::Looser => { 471 | { 472 | let mut sc = self.scores.borrow_mut(); 473 | sc.set_fail(self.level); 474 | } 475 | self.load(self.level); 476 | self.score = 0; 477 | } 478 | GameState::Winner => { 479 | if !self.demoing { 480 | { 481 | let mut sc = self.scores.borrow_mut(); 482 | sc.set_win(self.level, self.score); 483 | } 484 | self.level += 1; 485 | self.score = 0; 486 | self.load(self.level); 487 | } 488 | } 489 | _ => { 490 | dbg!(self.state); 491 | } 492 | } 493 | } 494 | Ok(Transition::None) 495 | } 496 | 497 | fn draw_background(&mut self, ctx: &mut Context) { 498 | let info_w = INFO_WIDTH as i32 * BRICK_SIZE as i32; 499 | let bw = self.back_tx.width(); 500 | let bh = self.back_tx.height(); 501 | let wn = (SCR_W as i32 - info_w + bw - 1) / bw; 502 | let hn = (SCR_H as i32 + bh - 1) / bh; 503 | for y in 0..hn { 504 | for x in 0..wn { 505 | let pos = Vec2::new((x * bw) as f32, (y * bh) as f32); 506 | self.back_tx.draw(ctx, DrawParams::new().position(pos)); 507 | } 508 | } 509 | } 510 | 511 | fn draw_static(&mut self, ctx: &mut Context) { 512 | for y in 0..HEIGHT { 513 | for x in 0..WIDTH { 514 | let t = self.puzzle[pos2puz(x, y)]; 515 | if t == 0 { 516 | continue; 517 | } 518 | let clip_rect = Rectangle::new(0.0, (t - 1) as f32 * BRICK_SIZE, BRICK_SIZE, BRICK_SIZE); 519 | let pos = b2s(x, y); 520 | let dp = DrawParams::new().position(pos); 521 | self.brick_tx.draw_region(ctx, clip_rect, dp); 522 | } 523 | } 524 | 525 | let first_num_pos = |x: f32, y: f32| -> Vec2 { Vec2::new(x + BRICK_SIZE * 0.25, y + 10.0) }; 526 | let second_num_pos = |x: f32, y: f32| -> Vec2 { Vec2::new(x + BRICK_SIZE * 2.0, y + 10.0) }; 527 | 528 | // score 529 | let x = ((WIDTH - INFO_WIDTH) as f32 + 0.5) * BRICK_SIZE; 530 | let y = BRICK_SIZE * 3.0; 531 | self.throws_tx.draw(ctx, DrawParams::new().position(Vec2::new(x, y))); 532 | let tp = TextParams::new().with_width(3).with_right_align(); 533 | let n = clamp(self.score, 999); 534 | self.txt_num.draw(ctx, first_num_pos(x, y), n, tp); 535 | #[allow(clippy::comparison_chain)] 536 | if self.lvl_score.hiscore != 0 { 537 | let dev_hiscore = 538 | if self.level >= RECORD_LEN || self.level == 0 { self.lvl_score.hiscore } else { RECORDS[self.level] }; 539 | let mut tp_hscore = TextParams::new().with_width(3).with_right_align(); 540 | if self.lvl_score.hiscore < dev_hiscore { 541 | tp_hscore = tp_hscore.with_color(Color::rgb(0.0, 0.8, 0.3)); 542 | } else if self.lvl_score.hiscore > dev_hiscore { 543 | tp_hscore = tp_hscore.with_color(Color::rgb(0.0, 0.3, 0.8)); 544 | } 545 | self.txt_num.draw(ctx, second_num_pos(x, y), self.lvl_score.hiscore, tp_hscore); 546 | } 547 | 548 | // level # in game, replay progress in demo 549 | let y = BRICK_SIZE * 1.0; 550 | self.level_no_tx.draw(ctx, DrawParams::new().position(Vec2::new(x, y))); 551 | 552 | if self.demoing { 553 | return; 554 | } 555 | 556 | let digit_size = self.txt_num.digit_size(); 557 | // level # 558 | let level_digits = digits(self.loader.level_count()); 559 | let w = (self.level_no_tx.width() / 2) as f32; 560 | let lw = f32::from(level_digits) * digit_size.x; 561 | let pos = Vec2::new(x + w - lw / 2.0, y + 10.0); 562 | self.txt_num.draw( 563 | ctx, 564 | pos, 565 | self.level as u32, 566 | TextParams::new().with_width(level_digits).with_leading_zeroes(), 567 | ); 568 | 569 | // attempts 570 | let y = BRICK_SIZE * 5.0; 571 | self.attempts_tx.draw(ctx, DrawParams::new().position(Vec2::new(x, y))); 572 | let tp = TextParams::new().with_width(3).with_right_align(); 573 | let att = clamp(self.lvl_score.attempts, 999); 574 | let win = clamp(self.lvl_score.wins, 999); 575 | self.txt_num.draw(ctx, first_num_pos(x, y), att, tp.clone()); 576 | self.txt_num.draw(ctx, second_num_pos(x, y), win, tp); 577 | 578 | // solved on 579 | let y = BRICK_SIZE * 7.0; 580 | self.solved_tx.draw(ctx, DrawParams::new().position(Vec2::new(x, y))); 581 | if self.lvl_score.first_win > 0 { 582 | let dw = digit_size.x; 583 | let dt: NaiveDate = NaiveDate::from_num_days_from_ce_opt(self.lvl_score.first_win) 584 | .unwrap_or_else(|| Local::now().date_naive()); 585 | let mut tp = TextParams::new().with_width(2).with_leading_zeroes(); 586 | // change color if help had been used before the level was solved 587 | if self.lvl_score.help_used { 588 | tp = tp.with_color(Color::rgb(0.0, 0.7, 0.7)); 589 | } 590 | let year = (dt.year() as u32) % 100; 591 | self.txt_num.draw(ctx, first_num_pos(x, y), year, tp.clone()); 592 | let month = dt.month(); 593 | self.txt_num.draw(ctx, first_num_pos(x + dw * 2.5, y), month, tp.clone()); 594 | let day = dt.day(); 595 | self.txt_num.draw(ctx, first_num_pos(x + dw * 5.0, y), day, tp); 596 | }; 597 | } 598 | 599 | fn draw_bricks(&mut self, ctx: &mut Context) { 600 | for b in self.bricks.iter() { 601 | let clip_rect = Rectangle::new(0.0, brick2shift(b.kind), BRICK_SIZE, BRICK_SIZE); 602 | let dp = DrawParams::new().position(b.scr_pos); 603 | self.brick_tx.draw_region(ctx, clip_rect, dp); 604 | } 605 | } 606 | 607 | fn draw_player(&mut self, ctx: &mut Context) { 608 | let clip_rect = Rectangle::new(0.0, brick2shift(self.player.kind), BRICK_SIZE, BRICK_SIZE); 609 | let dp = DrawParams::new().position(self.player.scr_pos); 610 | self.brick_tx.draw_region(ctx, clip_rect, dp); 611 | 612 | if !self.player.is_moving() { 613 | let color = if self.first_brick != BrickKind::None 614 | && (self.first_brick == self.player.kind || self.player.kind == BrickKind::Joker) 615 | { 616 | Color::rgb(0.0, 0.8, 0.2) 617 | } else { 618 | Color::rgb(0.8, 0.0, 0.0) 619 | }; 620 | let rotate: f32 = if self.arrow_down { 0.0 } else { PI / 2.0 }; 621 | 622 | self.arrow_animation.draw(ctx, DrawParams::new().position(self.arrow_pos).color(color).rotation(rotate)); 623 | } 624 | } 625 | 626 | pub fn draw(&mut self, ctx: &mut Context) -> tetra::Result { 627 | graphics::clear(ctx, Color::rgb(0.094, 0.11, 0.16)); 628 | self.draw_background(ctx); 629 | self.draw_static(ctx); 630 | self.draw_bricks(ctx); 631 | self.draw_player(ctx); 632 | 633 | Ok(Transition::None) 634 | } 635 | 636 | pub fn player_down(&mut self) { 637 | if self.player.is_moving() { 638 | return; 639 | } 640 | if self.player.y < HEIGHT - 2 { 641 | self.player.y += 1; 642 | self.player.scr_pos = b2s(self.player.x, self.player.y); 643 | } 644 | self.recalc_arrow(); 645 | } 646 | 647 | pub fn player_up(&mut self) { 648 | if self.player.is_moving() { 649 | return; 650 | } 651 | if self.player.y > 1 { 652 | self.player.y -= 1; 653 | self.player.scr_pos = b2s(self.player.x, self.player.y); 654 | } 655 | self.recalc_arrow(); 656 | } 657 | 658 | // should return error? 659 | pub fn load(&mut self, lvl_no: usize) { 660 | self.state = GameState::Unfinished; 661 | self.puzzle = [0u32; WIDTH * HEIGHT]; 662 | 663 | // top and bottom lines 664 | for i in 0..WIDTH { 665 | self.puzzle[pos2puz(i, 0)] = 1; 666 | self.puzzle[pos2puz(i, HEIGHT - 1)] = 1; 667 | } 668 | // info panel 669 | for i in 1..HEIGHT - 1 { 670 | self.puzzle[pos2puz(0, i)] = 1; 671 | for p in 0..INFO_WIDTH { 672 | self.puzzle[pos2puz(WIDTH - p - 1, i)] = 1; 673 | } 674 | } 675 | 676 | let lvl = self.loader.level(lvl_no); 677 | self.player = Brick::new(WIDTH - INFO_WIDTH - 1, HEIGHT - 2, lvl.first); 678 | 679 | // corner 680 | if lvl.corner.is_empty() { 681 | for i in 1..=MAX_SIZE { 682 | for j in 1..=(MAX_SIZE - i) { 683 | self.puzzle[pos2puz(j, i)] = 1; 684 | } 685 | } 686 | } else { 687 | let mut y = 1usize; 688 | for line_len in lvl.corner.iter() { 689 | for x in 1..=*line_len { 690 | self.puzzle[pos2puz(x, y as u8)] = 1; 691 | } 692 | y += 1; 693 | } 694 | } 695 | 696 | self.bricks.clear(); 697 | let cnt = lvl.puzzle.len(); 698 | for (yidx, bricks) in lvl.puzzle.iter().enumerate() { 699 | for (xidx, brick) in bricks.iter().enumerate() { 700 | if *brick == BrickKind::None { 701 | continue; 702 | } 703 | let y = HEIGHT - cnt + yidx - 1; 704 | let x = xidx + 1; 705 | self.bricks.push(Brick::new(x, y, *brick)); 706 | } 707 | } 708 | 709 | self.lvl_score = self.scores.borrow().level_info(self.level); 710 | self.recalc_arrow(); 711 | } 712 | 713 | fn can_throw(&self) -> bool { 714 | !self.player.is_moving() 715 | && self.state == GameState::Unfinished 716 | && self.first_brick != BrickKind::None 717 | && (self.player.kind == BrickKind::Joker || self.player.kind == self.first_brick) 718 | } 719 | 720 | pub fn throw_brick(&mut self) { 721 | if !self.can_throw() { 722 | return; 723 | } 724 | self.score += 1; 725 | self.player_row = self.player.y; 726 | let bricks = self.bricks.iter().filter(|b| b.y == self.player.y); 727 | let mut x = 0; 728 | for brick in bricks { 729 | if brick.x > x { 730 | x = brick.x 731 | } 732 | } 733 | if x == 0 { 734 | for i in 0..MAX_SIZE + 4 { 735 | if self.puzzle[self.player.y * WIDTH + i] != 0 { 736 | x = i; 737 | } 738 | } 739 | } 740 | x += 1; 741 | self.player.start_moving(Vec2::new(-BRICK_DEF_SPEED, 0.0), b2s(x, self.player.y)); 742 | } 743 | 744 | fn target(&self, row: usize) -> (bool, usize, usize, BrickKind) { 745 | let mut first = BrickKind::None; 746 | let mut bx: usize = 0; 747 | let mut by: usize = row; 748 | 749 | let down = if row < HEIGHT - 1 - MAX_SIZE { true } else { !self.bricks.iter().any(|b| b.y == row) }; 750 | 751 | if down { 752 | bx = if row >= HEIGHT - 1 - MAX_SIZE { 753 | 1 754 | } else { 755 | let mut n: usize = 0; 756 | for i in 0..MAX_SIZE + 4 { 757 | if self.puzzle[row * WIDTH + i] == 0 { 758 | n = i; 759 | break; 760 | } 761 | } 762 | if n == 0 { 763 | panic!("Nothing found"); 764 | }; 765 | n 766 | }; 767 | let bricks = self.bricks.iter().filter(|b| b.x == bx && b.y >= row); 768 | by = HEIGHT - 1; 769 | for brick in bricks { 770 | if brick.y < by { 771 | by = brick.y; 772 | first = brick.kind; 773 | } 774 | } 775 | by -= 1 776 | } else { 777 | let bricks = self.bricks.iter().filter(|b| b.y == row); 778 | for brick in bricks { 779 | if brick.x > bx { 780 | bx = brick.x; 781 | } 782 | } 783 | bx += 1; 784 | for brick in self.bricks.iter().filter(|b| b.y == row && b.x == bx - 1) { 785 | first = brick.kind; 786 | } 787 | } 788 | 789 | (down, bx, by, first) 790 | } 791 | 792 | fn recalc_arrow(&mut self) { 793 | let (is_down, x, y, brick) = self.target(self.player.y); 794 | if is_down { 795 | self.arrow_pos = b2s(x, y); 796 | } else { 797 | self.arrow_pos = b2s(x + 1, y); 798 | } 799 | self.first_brick = brick; 800 | self.arrow_down = is_down; 801 | } 802 | 803 | pub fn calc_state(&self) -> GameState { 804 | if self.bricks.is_empty() { 805 | if self.level + 1 >= self.loader.level_count() { 806 | return GameState::Completed; 807 | } 808 | return GameState::Winner; 809 | } 810 | if self.player.kind == BrickKind::Joker { 811 | return GameState::Unfinished; 812 | } 813 | 814 | for y in 1..HEIGHT - 1 { 815 | let (_d, _x, _y, kind) = self.target(y); 816 | if kind == self.player.kind { 817 | return GameState::Unfinished; 818 | } 819 | } 820 | 821 | GameState::Looser 822 | } 823 | } 824 | --------------------------------------------------------------------------------