From 8b6387737cf5bc9774278c4511a64512d5477af7 Mon Sep 17 00:00:00 2001 From: ElnuDev Date: Fri, 18 Mar 2022 20:37:52 -0700 Subject: [PATCH] Rewrite in Rust --- Cargo.lock | 93 ++++ Cargo.toml | 10 +- build/.gitignore | 3 +- build/debian.sh | 6 +- build/septadrop_1.1.0_amd64/DEBIAN/changelog | 5 + src/config.rs | 37 ++ src/main.rs | 492 ++++++++++++++++++- src/structs/block.rs | 61 +++ src/structs/block_type.rs | 151 ++++++ src/structs/mod.rs | 11 + src/structs/number_renderer.rs | 76 +++ src/structs/tile_type.rs | 15 + 12 files changed, 956 insertions(+), 4 deletions(-) create mode 100644 src/config.rs create mode 100644 src/structs/block.rs create mode 100644 src/structs/block_type.rs create mode 100644 src/structs/mod.rs create mode 100644 src/structs/number_renderer.rs create mode 100644 src/structs/tile_type.rs diff --git a/Cargo.lock b/Cargo.lock index d39f4c6..bef1c36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "const_format" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22bc6cd49b0ec407b680c3e380182b6ac63b73991cb7602de350352fc309b614" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef196d5d972878a48da7decb7686eded338b4858fbabeed513d63a7c98b2b82d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "csfml-audio-sys" version = "0.6.0" @@ -54,6 +74,15 @@ dependencies = [ "sfml-build", ] +[[package]] +name = "gcd" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37978dab2ca789938a83b2f8bc1ef32db6633af9051a6cd409eff72cbaaa79a" +dependencies = [ + "paste", +] + [[package]] name = "getrandom" version = "0.2.5" @@ -65,6 +94,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi", +] + [[package]] name = "libc" version = "0.2.120" @@ -77,12 +115,36 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +[[package]] +name = "paste" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" + [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.8.5" @@ -117,6 +179,9 @@ dependencies = [ name = "septadrop" version = "1.1.0" dependencies = [ + "const_format", + "gcd", + "home", "rand", "sfml", ] @@ -142,6 +207,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53e0893aaf18583de27202b17007258377d5c4be16e1d0b601fd6943bc36c98b" +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -153,3 +224,25 @@ name = "widestring" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index ac1bf6e..006cb4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.release] +strip = true +lto = true +codegen-units = 1 + [dependencies] sfml = "0.16.0" -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" +home = "0.5.3" +gcd = "2.1.0" +const_format = "0.2.22" \ No newline at end of file diff --git a/build/.gitignore b/build/.gitignore index a4c20f4..e82c8dc 100644 --- a/build/.gitignore +++ b/build/.gitignore @@ -1,2 +1,3 @@ septadrop -*.deb \ No newline at end of file +*.deb +DEBIAN/usr/games/septadrop \ No newline at end of file diff --git a/build/debian.sh b/build/debian.sh index 2c6b877..bc92793 100755 --- a/build/debian.sh +++ b/build/debian.sh @@ -1,7 +1,11 @@ TARGET=septadrop_1.1.0_amd64 mkdir -p ${TARGET}/usr/bin/ +rm -r ${TARGET}/usr/games/septadrop/ +mkdir -p ${TARGET}/usr/games/septadrop/ cd .. -cargo build --release +cargo rustc --release -- --cfg debian cp target/release/septadrop build/${TARGET}/usr/bin/ +upx --best --lzma build/${TARGET}/usr/bin/septadrop +cp -r res/* build/${TARGET}/usr/games/septadrop/ cd build dpkg-deb --build ${TARGET} \ No newline at end of file diff --git a/build/septadrop_1.1.0_amd64/DEBIAN/changelog b/build/septadrop_1.1.0_amd64/DEBIAN/changelog index dd47a26..e52dff0 100644 --- a/build/septadrop_1.1.0_amd64/DEBIAN/changelog +++ b/build/septadrop_1.1.0_amd64/DEBIAN/changelog @@ -1,3 +1,8 @@ +septadrop (1.1.0) impish; urgency=low + + * Rewrite in Rust + * Prevent block overlapping at top of screen; immediate game over + septadrop (1.0.1) impish; urgency=low * Fix performance issue in paused state diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..677d10b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,37 @@ +use const_format::formatcp; + +pub const TILE_SIZE: u32 = 20; + +pub const GRID_WIDTH: u32 = 14; +pub const GRID_HEIGHT: u32 = 20; + +pub const WINDOW_WIDTH: u32 = 500; +pub const WINDOW_HEIGHT: u32 = 440; + +pub const PLAYFIELD_X: u32 = 20; +pub const PLAYFIELD_Y: u32 = 20; + +pub const NEXT_X: u32 = 370; +pub const NEXT_Y: u32 = 70; +pub const NEXT_WIDTH: u32 = 5; +pub const NEXT_HEIGHT: u32 = 5; + +pub const LINES_PER_LEVEL: u32 = 5; +pub const POINTS_1_LINE: u32 = 40; +pub const POINTS_2_LINES: u32 = 100; +pub const POINTS_3_LINES: u32 = 300; +pub const POINTS_4_LINES: u32 = 1200; + +pub const MOVE_FRAME_INTERVAL: u32 = 125; +pub const MAX_FAST_FORWARD_INTERVAL: u32 = 125; + +pub const FPS: u32 = 60; + +#[cfg(not(debian))] +pub const RES_PATH: &str = "res"; + +#[cfg(debian)] +pub const RES_PATH: &str = "/usr/games/septadrop"; + +pub const RES_AUDIO_PATH: &str = formatcp!("{RES_PATH}/audio"); +pub const RES_TEXTURES_PATH: &str = formatcp!("{RES_PATH}/textures"); \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a9..f9861fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,493 @@ +use sfml::graphics::*; +use sfml::window::*; +use sfml::system::*; +use sfml::audio::*; +use rand::seq::SliceRandom; +use gcd::Gcd; +use std::io::Write; + +mod structs; +use structs::*; + +mod config; +use config::*; + +pub fn audio(name: &str) -> String { + format!("{RES_AUDIO_PATH}/{name}.wav") +} + +pub fn texture(name: &str) -> String { + format!("{RES_TEXTURES_PATH}/{name}.png") +} + +fn get_level(lines: u32) -> u32 { + return std::cmp::min(lines / LINES_PER_LEVEL, 15); +} + +fn get_update_interval(level: u32) -> u32 { + // From Tetris Worlds, see https://harddrop.com/wiki/Tetris_Worlds#Gravity + ((0.8 - (level as i32 - 1) as f32 * 0.007).powi(level as i32 - 1) * 1000.0) as u32 +} + fn main() { - println!("Hello, world!"); + let context_settings = ContextSettings::default(); + let mut window = RenderWindow::new( + VideoMode::new(WINDOW_WIDTH, WINDOW_HEIGHT, 16), + "septadrop", + Style::TITLEBAR | Style::CLOSE, + &context_settings + ); + window.set_framerate_limit(FPS); + window.set_key_repeat_enabled(false); + + let icon = Image::from_file(&texture("icon")).unwrap(); + { + let Vector2u { x: width, y: height } = icon.size(); + window.set_icon(width, height, icon.pixel_data()); + } + + let mut thread_rng = rand::thread_rng(); + let block_types = BlockType::init_list(); + + let mut random_block = || Block::new(block_types.choose(&mut thread_rng).unwrap()); + + let mut block = random_block(); + let mut next_block = random_block(); + + // Default::default() initializes all array cells to None (no block) + let mut grid: [[Option<&TileType>; GRID_WIDTH as usize]; GRID_HEIGHT as usize] = Default::default(); + + let blocks_texture = Texture::from_file(&texture("blocks")).unwrap(); + let mut sprite = Sprite::with_texture(&blocks_texture); + + let background_texture = Texture::from_file(&texture("background")).unwrap(); + let background = Sprite::with_texture(&background_texture); + + let number_renderer = NumberRenderer::default(); + + let mut paused_clear = RectangleShape::new(); + paused_clear.set_fill_color(Color::rgb(81, 62, 69)); + + let paused_texture = Texture::from_file(&texture("paused")).unwrap(); + let paused_text = { + let mut paused_text = Sprite::with_texture(&paused_texture); + let paused_texture_size = paused_texture.size(); + paused_text.set_position(Vector2f::new( + PLAYFIELD_X as f32 + (GRID_WIDTH as f32 * TILE_SIZE as f32 / 2.0) - paused_texture_size.x as f32 / 2.0, + PLAYFIELD_Y as f32 + (GRID_HEIGHT as f32 * TILE_SIZE as f32 / 2.0) - paused_texture_size.y as f32 / 2.0 + )); + paused_text + }; + + let highscore_file_path = home::home_dir().unwrap().join(".septadrop"); + + let mut highscore: u32; + + if highscore_file_path.exists() { + highscore = std::fs::read_to_string(&highscore_file_path).unwrap().trim().parse::().unwrap(); + let point_gcd = POINTS_1_LINE + .gcd(POINTS_2_LINES) + .gcd(POINTS_3_LINES) + .gcd(POINTS_4_LINES); + if highscore % point_gcd != 0 { + println!("It seems your system is misconfigured. Please see this guide for fixing the issue: https://www.youtube.com/watch?v=dQw4w9WgXcQ"); + return; + } + } else { + let mut highscore_file = std::fs::File::create(&highscore_file_path).unwrap(); + write!(&mut highscore_file, "0").unwrap(); + highscore = 0; + } + + let mut score: u32 = 0; + let mut lines: u32 = 0; + let mut blocks: u32 = 0; + let mut tiles: u32 = 0; + + let mut update_interval = get_update_interval(0); + + let mut rotate = false; + let mut move_left = false; + let mut move_right = false; + let mut move_left_immediate = false; + let mut move_right_immediate = false; + let mut fast_forward = false; + let mut snap = false; + let mut paused = false; + let mut paused_from_lost_focus = false; + let mut update_clock = Clock::default(); + let mut move_clock = Clock::default(); + let mut pause_clock = Clock::default(); + let mut pause_offset: u32 = 0; + let mut toggle_pause = false; + + // https://sfxr.me/#57uBnWWZeyDTsBRrJsAp2Vwd76cMVrdeRQ7DirNQW5XekKxcrCUNx47Zggh7Uqw4R5FdeUpyk362uhjWmpNHmqxE7JBp3EkxDxfJ1VjzMRpuSHieW6B5iyVFM + let rotate_buffer = SoundBuffer::from_file(&audio("rotate")).unwrap(); + let mut rotate_sound = Sound::with_buffer(&rotate_buffer); + + // https://sfxr.me/#57uBnWTMa2LUtaPa3P8xWZekiRxNwCPFWpRoPDVXDJM9KHkiGJcs6J62FRcjMY5oVNdT73MtmUf5rXCPvSZWL7AZuTRWWjKbPKTpZjT85AcZ6htUqTswkjksZ + let snap_buffer = SoundBuffer::from_file(&audio("snap")).unwrap(); + let mut snap_sound = Sound::with_buffer(&snap_buffer); + + // https://sfxr.me/#57uBnWbareN7MJJsWGD8eFCrqjikS9f8JXg8jvmKzMdVtqmRsb81eToSUpnkqgFhvxD2QoAjpw4SmGZHZjbhEiPQKetRSHCHXYFZzD7Q6RVVS9CRSeRAb6bZp + let game_over_buffer = SoundBuffer::from_file(&audio("game_over")).unwrap(); + let mut game_over_sound = Sound::with_buffer(&game_over_buffer); + + // https://sfxr.me/#7BMHBGMfGk8EHV8czJkUucUm8EMAnMNxiqYyTfKkMpHFJu44GEdD7xP6E8NM3K7RKRExTpagPBAiWf7BLtC52CEWJVGHh8hwDLygoEG86tcPth2UtmfdrXLoh + let row_clear_buffer = SoundBuffer::from_file(&audio("row_clear")).unwrap(); + let mut row_clear_sound = Sound::with_buffer(&row_clear_buffer); + + // https://sfxr.me/#57uBnWg8448kTPqWAxeDvZ5CP5JWbrfJGWuRcTjva5uX3vvBnEAZ6SfiH9oLKMXgsusuJwGWx6KPfvLfHtqnhLxr476ptGv4jPbfNhQaFMYeMHFdHk9SotQ4X + let level_up_buffer = SoundBuffer::from_file(&audio("level_up")).unwrap(); + let mut level_up_sound = Sound::with_buffer(&level_up_buffer); + + // https://sfxr.me/#34T6PkzvrkfdahGDBAh1uYGXTwZ8rG54kxfHpgdVCPxqG7yyK5UuqgiK9Z8Q5177itxbkSNfLSHm4zTkemT4iyxJpW89VJx82feaq8qxZeA5AJR2nWZZR59hq + let new_highscore_buffer = SoundBuffer::from_file(&audio("new_highscore")).unwrap(); + let mut new_highscore_sound = Sound::with_buffer(&new_highscore_buffer); + + while window.is_open() { + loop { + match window.poll_event() { + Some(event) => match event { + Event::Closed => { + window.close(); + break; + }, + Event::GainedFocus => { + if paused && paused_from_lost_focus { + toggle_pause = true; + } + }, + Event::LostFocus => { + if !paused { + toggle_pause = true; + paused_from_lost_focus = true; + } + }, + Event::KeyPressed { code, alt: _, ctrl: _, shift: _, system: _ } => { + match code { + Key::ESCAPE => toggle_pause = true, + Key::SPACE => snap = !paused, + Key::UP => rotate = !paused, + Key::DOWN => fast_forward = !paused, + Key::LEFT => { + move_left = !paused; + move_left_immediate = !paused; + move_clock.restart(); + }, + Key::RIGHT => { + move_right = !paused; + move_right_immediate = !paused; + move_clock.restart(); + } + _ => {} + } + }, + Event::KeyReleased { code, alt: _, ctrl: _, shift: _, system: _ } => { + match code { + Key::DOWN => fast_forward = false, + Key::LEFT => move_left = false, + Key::RIGHT => move_right = false, + _ => {} + } + } + _ => {}, + }, + None => break + } + } + + if toggle_pause { + paused = !paused; + if paused { + pause_clock.restart(); + paused_clear.set_position(Vector2f::new( + PLAYFIELD_X as f32, + PLAYFIELD_Y as f32 + )); + paused_clear.set_size(Vector2f::new( + (GRID_WIDTH * TILE_SIZE) as f32, + (GRID_HEIGHT * TILE_SIZE) as f32 + )); + window.draw(&paused_clear); + let size = Vector2f::new( + (NEXT_WIDTH * TILE_SIZE) as f32, + (NEXT_HEIGHT * TILE_SIZE) as f32 + ); + paused_clear.set_position(Vector2f::new( + NEXT_X as f32, + NEXT_Y as f32 + ) - size / 2.0); + paused_clear.set_size(size); + window.draw(&paused_clear); + window.draw(&paused_text); + window.display(); + } else { + pause_offset = pause_clock.elapsed_time().as_milliseconds() as u32; + } + toggle_pause = false; + } + + if paused { + // window.display() is where SFML implements frame rate limiting + // If we don't run this here, then when paused septadrop will max out the thread + window.display(); + continue; + } + + let is_update_frame = + update_clock.elapsed_time().as_milliseconds() - pause_offset as i32 > + if fast_forward { + std::cmp::min(update_interval, MAX_FAST_FORWARD_INTERVAL) + } else { + update_interval + } as i32; + if is_update_frame { + update_clock.restart(); + } + + let is_move_frame = + move_clock.elapsed_time().as_milliseconds() - pause_offset as i32 > + MOVE_FRAME_INTERVAL as i32; + if is_move_frame { + move_clock.restart(); + } + + pause_offset = 0; + + // Rotation + if rotate { + block.rotation_state += 1; + // Check to see if new rotation state is overlapping any tiles + let mut offset_required: i32 = 0; + for tile in block.get_tiles().iter() { + if grid[tile.y as usize][tile.x as usize].is_some() { + // Can't wall kick off of blocks + block.rotation_state -= 1; + break; + } + if tile.x <= 0 { + let potential_offset = -tile.x; + if potential_offset > offset_required.abs() { + offset_required = potential_offset; + } + } else if tile.x >= GRID_WIDTH as i32 { + let potential_offset = GRID_WIDTH as i32 - tile.x - 1; + if -potential_offset > offset_required.abs() { + offset_required = potential_offset; + } + } + } + block.position.x += offset_required; + rotate = false; + rotate_sound.play(); + } + + // Horizontal movement + { + let mut movement = 0; + if move_left_immediate || (is_move_frame && move_left) { + movement -= 1; + move_left_immediate = false; + } + if move_right_immediate || (is_move_frame && move_right) { + movement += 1; + move_right_immediate = false; + } + if movement != 0 { + for (i, tile) in block.get_tiles().iter().enumerate().rev() { + if tile.x + movement < 0 || tile.x + movement >= GRID_WIDTH as i32 || grid[tile.y as usize][(tile.x + movement) as usize].is_some() { + break; + } + if i == 0 { + /* + We're going through all the blocks backwards, + so the first element is last. + (Only for .enumerate().rev(), not .rev().enumerate(), + since .enumerate() is what adds indexes.) + If we managed to get through all of the tiles without breaking, + (haven't found anything that obstructs any tile), + we can finally add the movement. + */ + block.position.x += movement; + } + } + } + } + + // Snapping + let snap_offset = { + let mut snap_offset = 0; + 'outer: loop { + for tile in block.get_tiles().iter() { + let y = tile.y + snap_offset; + if y >= GRID_HEIGHT as i32 || grid[y as usize][tile.x as usize].is_some() { + snap_offset -= 1; + break 'outer; + } + } + snap_offset += 1; + } + snap_offset + }; + let mut landed = snap; + if snap { + block.position.y += snap_offset; + snap = false; + snap_sound.play(); + } else if is_update_frame { // Land checking + for tile in block.get_tiles().iter() { + if tile.y == GRID_HEIGHT as i32 - 1 || grid[tile.y as usize + 1][tile.x as usize].is_some() { + landed = true; + break; + } + } + } + let landed = landed; // remove mutability + + // Clear window + // Normally, one would run window.clear(), + // but the background image covers the entire window. + window.draw(&background); + + // Draw block + if !landed { + for tile in block.get_tiles().iter() { + let snap_y = tile.y + snap_offset; + sprite.set_texture_rect(&block.block_type.tile_type.texture_rect); + sprite.set_position(Vector2f::new( + (PLAYFIELD_X as i32 + tile.x * TILE_SIZE as i32) as f32, + (PLAYFIELD_Y as i32 + tile.y * TILE_SIZE as i32) as f32 + )); + window.draw(&sprite); + sprite.set_texture_rect(&block.block_type.tile_type.ghost_texture_rect); + sprite.set_position(Vector2f::new( + (PLAYFIELD_X as i32 + tile.x * TILE_SIZE as i32) as f32, + (PLAYFIELD_Y as i32 + snap_y * TILE_SIZE as i32) as f32 + )); + window.draw(&sprite); + } + } + + // Draw next block + { + let next_block_tiles = next_block.get_tiles(); + // This is assuming the next block spawns unrotated. + // Refactoring is needed if random rotations are added + let x_offset = next_block.block_type.width * TILE_SIZE / 2; + let y_offset = (next_block.block_type.height + next_block.block_type.starting_line * 2) * TILE_SIZE / 2; + for tile in next_block_tiles.iter() { + sprite.set_texture_rect(&next_block.block_type.tile_type.texture_rect); + sprite.set_position(Vector2f::new( + (NEXT_X + (tile.x - next_block.position.x) as u32 * TILE_SIZE - x_offset) as f32, + (NEXT_Y + (tile.y - next_block.position.y) as u32 * TILE_SIZE - y_offset) as f32 + )); + window.draw(&sprite); + } + } + + // Landing (transferring block to grid and reinitializing) + if landed { + tiles += block.get_tiles().len() as u32; + blocks += 1; + for tile in block.get_tiles().iter() { + grid[tile.y as usize][tile.x as usize] = Some(&block.block_type.tile_type); + } + let mut cleared_lines = 0; + for y in 0..GRID_HEIGHT { + let mut completed = true; + for x in 0..GRID_WIDTH { + if grid[y as usize][x as usize].is_none() { + completed = false; + break; + } + } + if !completed { + continue; + } + for z in (0..y).rev() { + let z = z as usize; + for x in 0..GRID_WIDTH { + let x = x as usize; + grid[z + 1][x] = grid[z][x]; + } + } + cleared_lines += 1; + } + let mut scored = match cleared_lines { + 0 => 0, + 1 => POINTS_1_LINE, + 2 => POINTS_2_LINES, + 3 => POINTS_3_LINES, + _ => POINTS_4_LINES + }; + if scored > 0 { + let level = get_level(lines); + scored *= level + 1; + if score + scored > highscore && score < highscore { + new_highscore_sound.play(); + } + score += scored; + lines += cleared_lines; + let new_level = get_level(lines); + if level != new_level { + level_up_sound.play(); + } + if score > highscore { + highscore = score; + let mut file = std::fs::OpenOptions::new() + .write(true) + .open(&highscore_file_path) + .unwrap(); + file.write_all(highscore.to_string().as_bytes()).unwrap(); + file.flush().unwrap(); + } + update_interval = get_update_interval(new_level); + row_clear_sound.play(); + } + for tile in next_block.get_tiles().iter() { + if grid[tile.y as usize][tile.x as usize].is_some() { + score = 0; + lines = 0; + blocks = 0; + tiles = 0; + grid = Default::default(); // reset grid + update_interval = get_update_interval(0); + game_over_sound.play(); + next_block = random_block(); + break; + } + } + block = next_block; + next_block = random_block(); + } else if is_update_frame { + block.position.y += 1; + } + + // Drawing grid + for y in 0..GRID_HEIGHT { + for x in 0..GRID_WIDTH { + let tile_type = grid[y as usize][x as usize]; + if tile_type.is_none() { + continue; + } + let tile_type = tile_type.unwrap(); + sprite.set_texture_rect(&tile_type.texture_rect); + sprite.set_position(Vector2f::new( + (PLAYFIELD_X + x * TILE_SIZE) as f32, + (PLAYFIELD_Y + y * TILE_SIZE) as f32 + )); + window.draw(&sprite); + } + } + + number_renderer.render(&mut window, score, 477, 162); + number_renderer.render(&mut window, highscore, 477, 202); + number_renderer.render(&mut window, lines, 477, 242); + number_renderer.render(&mut window, get_level(lines), 477, 282); + number_renderer.render(&mut window, blocks, 477, 322); + number_renderer.render(&mut window, tiles, 477, 362); + + window.display(); + } } diff --git a/src/structs/block.rs b/src/structs/block.rs new file mode 100644 index 0000000..b68360e --- /dev/null +++ b/src/structs/block.rs @@ -0,0 +1,61 @@ +use crate::structs::BlockType; +use crate::config::GRID_WIDTH; +use sfml::system::Vector2i; + +pub struct Block<'a> { + pub block_type: &'a BlockType, + pub position: Vector2i, + pub rotation_state: i32 +} + +impl<'a> Block<'a> { + pub fn new(block_type: &'a BlockType, ) -> Self { + Self { + block_type, + position: Vector2i::new( + GRID_WIDTH as i32 / 2 - block_type.grid[0].len() as i32 / 2, + 0 + ), + rotation_state: 0 + } + } + + pub fn get_tiles(&mut self) -> Vec { + let mut tiles: Vec = Vec::new(); + for (y, row) in self.block_type.grid.iter().enumerate() { + let y = y as i32; + for (x, cell) in row.iter().enumerate() { + let x = x as i32; + if !cell { + continue; + } + let mut rotated = Vector2i::new(x as i32, y as i32); + if self.block_type.rotate { + let center_x = row.len() as i32 / 2; + let center_y = self.block_type.grid.len() as i32 / 2; + let offset_x = x - center_x; + let offset_y = y - center_y; + match self.rotation_state { + 0 => {}, + 1 => { + rotated.x = center_x + offset_y; + rotated.y = center_y - offset_x; + }, + 2 => { + rotated.x = center_x - offset_x; + rotated.y = center_y - offset_y; + }, + 3 => { + rotated.x = center_x - offset_y; + rotated.y = center_y + offset_x; + } + _ => self.rotation_state %= 4 + } + } + let global = self.position + rotated; + tiles.push(global); + } + } + tiles + } +} \ No newline at end of file diff --git a/src/structs/block_type.rs b/src/structs/block_type.rs new file mode 100644 index 0000000..ebc6d3d --- /dev/null +++ b/src/structs/block_type.rs @@ -0,0 +1,151 @@ +use crate::structs::TileType; +use crate::config::TILE_SIZE; +use sfml::graphics::IntRect; + +pub struct BlockType { + pub tile_type: TileType, + pub grid: Vec>, + pub width: u32, + pub height: u32, + pub starting_line: u32, + pub rotate: bool +} + +impl BlockType { + pub fn new(tile_type: TileType, grid: Vec>, rotate: bool) -> Self { + let mut width: u32 = 0; + let mut height: u32 = 0; + let mut starting_line: u32 = 0; + for (y, row) in grid.iter().enumerate() { + let mut has_content = false; + for (x, cell) in row.iter().enumerate() { + if *cell { + width = std::cmp::max(width, x as u32 + 1); + has_content = true; + } + } + if has_content { + if height == 0 { + starting_line = y as u32; + } + height = y as u32 + 1 - starting_line; + } + } + Self { + tile_type, + grid, + width, + height, + starting_line, + rotate + } + } + + pub fn init_list() -> Vec { + let mut list = Vec::new(); + let tile_size = TILE_SIZE as i32; + + const Y: bool = true; + const N: bool = false; + + // I block + list.push(Self::new( + TileType::new( + IntRect::new(0, 0, tile_size, tile_size), + IntRect::new(0, tile_size, tile_size, tile_size) + ), + vec![ + vec![N, N, N, N], + vec![Y, Y, Y, Y], + vec![N, N, N, N], + vec![N, N, N, N] + ], + true + )); + + // J Block + list.push(Self::new( + TileType::new( + IntRect::new(tile_size, 0, tile_size, tile_size), + IntRect::new(tile_size, tile_size, tile_size, tile_size), + ), + vec![ + vec![Y, N, N], + vec![Y, Y, Y], + vec![N, N, N] + ], + true + )); + + // L Block + list.push(Self::new( + TileType::new( + IntRect::new(tile_size * 2, 0, tile_size, tile_size), + IntRect::new(tile_size * 2, tile_size, tile_size, tile_size), + ), + vec![ + vec![N, N, Y], + vec![Y, Y, Y], + vec![N, N, N] + ], + true + )); + + // O Block + list.push(Self::new( + TileType::new( + IntRect::new(tile_size * 3, 0, tile_size, tile_size), + IntRect::new(tile_size * 3, tile_size, tile_size, tile_size), + ), + vec![ + vec![Y, Y], + vec![Y, Y] + ], + false + )); + + // S Block + list.push(Self::new( + TileType::new( + IntRect::new(tile_size * 4, 0, tile_size, tile_size), + IntRect::new(tile_size * 4, tile_size, tile_size, tile_size), + ), + vec![ + vec![N, Y, Y], + vec![Y, Y, N], + vec![N, N, N] + ], + true + )); + + // T Block + list.push(Self::new( + TileType::new( + IntRect::new(tile_size * 5, 0, tile_size, tile_size), + IntRect::new(tile_size * 5, tile_size, tile_size, tile_size), + ), + vec![ + vec![N, Y, N], + vec![Y, Y, Y], + vec![N, N, N] + ], + true + )); + + // Z Block + list.push(Self::new( + TileType::new( + IntRect::new(tile_size * 6, 0, tile_size, tile_size), + IntRect::new(tile_size * 6, tile_size, tile_size, tile_size) + ), + vec![ + vec![Y, Y, N], + vec![N, Y, Y], + vec![N, N, N] + ], + true + )); + + list + } +} \ No newline at end of file diff --git a/src/structs/mod.rs b/src/structs/mod.rs new file mode 100644 index 0000000..0a5232e --- /dev/null +++ b/src/structs/mod.rs @@ -0,0 +1,11 @@ +mod block_type; +pub use block_type::BlockType; + +mod block; +pub use block::Block; + +mod tile_type; +pub use tile_type::TileType; + +mod number_renderer; +pub use number_renderer::NumberRenderer; \ No newline at end of file diff --git a/src/structs/number_renderer.rs b/src/structs/number_renderer.rs new file mode 100644 index 0000000..ed84517 --- /dev/null +++ b/src/structs/number_renderer.rs @@ -0,0 +1,76 @@ +use sfml::graphics::*; +use sfml::system::Vector2f; +use sfml::SfBox; + +pub struct NumberRenderer { + texture: SfBox, + comma_rect: IntRect, + numeral_rects: [IntRect; 10], +} + +impl NumberRenderer { + pub fn new(texture: SfBox, comma_rect: IntRect, numeral_rects: [IntRect; 10]) -> Self { + Self { + texture, + comma_rect, + numeral_rects + } + } + + pub fn default() -> Self { + Self::new( + Texture::from_file(&crate::texture("numerals")).unwrap(), + IntRect::new(134, 0, 10, 16), + [ + IntRect::new(0, 0, 14, 16), + IntRect::new(14, 0, 8, 16), + IntRect::new(22, 0, 14, 16), + IntRect::new(36, 0, 14, 16), + IntRect::new(50, 0, 14, 16), + IntRect::new(64, 0, 14, 16), + IntRect::new(78, 0, 14, 16), + IntRect::new(92, 0, 14, 16), + IntRect::new(106, 0, 14, 16), + IntRect::new(120, 0, 14, 16) + ] + ) + } + + pub fn render(&self, window: &mut RenderWindow, number: u32, x: u32, y: u32) { + let number_string = number.to_string(); + let get_numeral_rect = |numeral: char| self.numeral_rects[numeral.to_digit(10).unwrap() as usize]; + let mut numeral_position = Vector2f::new({ + let numeral = number_string.chars().last().unwrap(); + let numeral_rect = get_numeral_rect(numeral); + x as f32 - numeral_rect.width as f32 + }, y as f32); + let digits = number_string.len(); + let mut sprite = Sprite::new(); + sprite.set_texture(&self.texture, false); + // can't reverse .chars() directly since it doesn't implement std::iter::DoubleEndedIterator + // Instead, we must collect it to a Vec then iterate over that. + // For more info, see https://users.rust-lang.org/t/43401/2 + for (i, numeral) in number_string.chars().collect::>().iter().enumerate().rev() { + let numeral_rect = get_numeral_rect(*numeral); + if (digits - i) % 3 == 1 && i != digits - 1 { + sprite.set_texture_rect(&self.comma_rect); + sprite.set_position(numeral_position); + window.draw(&sprite); + numeral_position.x -= numeral_rect.width as f32; + } + sprite.set_texture_rect(&numeral_rect); + sprite.set_position(numeral_position); + window.draw(&sprite); + if i == 0 { + break; + } + if (digits - i) % 3 == 0 { + numeral_position.x -= self.comma_rect.width as f32; + continue; + } + let numeral = number_string.as_bytes()[i - 1] as char; + let numeral_rect = get_numeral_rect(numeral); + numeral_position.x -= numeral_rect.width as f32; + } + } +} \ No newline at end of file diff --git a/src/structs/tile_type.rs b/src/structs/tile_type.rs new file mode 100644 index 0000000..4ba7ead --- /dev/null +++ b/src/structs/tile_type.rs @@ -0,0 +1,15 @@ +use sfml::graphics::IntRect; + +pub struct TileType { + pub texture_rect: IntRect, + pub ghost_texture_rect: IntRect, +} + +impl TileType { + pub fn new(texture_rect: IntRect, ghost_texture_rect: IntRect) -> Self { + Self { + texture_rect, + ghost_texture_rect + } + } +} \ No newline at end of file