From fc6ebcdbc265e338a4120eda9b2fda1b3859ba8d Mon Sep 17 00:00:00 2001 From: Theis Pieter Hollebeek Date: Mon, 3 Mar 2025 14:54:51 +0100 Subject: [PATCH] pause/game over --- Cargo.toml | 5 +- src/game.rs | 77 +++++++++++++------- src/sdl_impl.rs | 189 +++++++++++++++++++++++++++++++++--------------- 3 files changed, 185 insertions(+), 86 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d07594c..6610994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,7 @@ edition = "2021" [dependencies] rand = "0.9.0" -sdl2 = "0.37.0" + +[dependencies.sdl2] +version = "0.37.0" +features = ["ttf"] diff --git a/src/game.rs b/src/game.rs index fa60d9a..b571688 100644 --- a/src/game.rs +++ b/src/game.rs @@ -22,6 +22,7 @@ impl CurrentTetromino { } pub struct Game { + pub game_over: bool, pub board: Board, pub next_tetrominos: [Tetromino; 3], pub current_tetromino: CurrentTetromino, @@ -31,11 +32,18 @@ pub struct Game { pub ticks: usize, } -struct Score { - level: usize, - points: usize, - lines: usize, - combo: usize, +pub enum SoundEffect { + HardDrop, + LineClear(usize), + Move, + Rotation, +} + +pub struct Score { + pub level: usize, + pub points: usize, + pub lines: usize, + pub combo: usize, back_to_back: bool, } @@ -81,6 +89,7 @@ impl Score { impl Game { pub fn new() -> Self { Self { + game_over: false, board: Board::new(), next_tetrominos: std::array::from_fn(|_| Tetromino::random()), current_tetromino: CurrentTetromino::new(Tetromino::random()), @@ -97,7 +106,7 @@ impl Game { last } - fn hard_drop(&mut self, actions: &ActionsHeld) { + fn try_hard_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec) { if !actions.just_pressed(self.ticks, &Action::HardDrop) { return; } @@ -110,12 +119,12 @@ impl Game { self.current_tetromino.y -= 1; self.score.points += (self.current_tetromino.y - start_y) as usize * 2; self.place_current_tetromino(); - self.check_line_clears(); + self.check_line_clears(effects); break; } } - fn soft_drop(&mut self, actions: &ActionsHeld) { + fn soft_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec) { let mut delay = 32 - self.score.level * 2; if actions.contains_key(&Action::SoftDrop) { delay /= 10; @@ -129,13 +138,13 @@ impl Game { if self.board.colliding(&self.current_tetromino) { self.current_tetromino.y -= 1; self.place_current_tetromino(); - self.check_line_clears(); + self.check_line_clears(effects); } else if actions.contains_key(&Action::SoftDrop) { self.score.points += 1; } } - fn move_horizontally(&mut self, actions: &ActionsHeld) { + fn try_move_horizontally(&mut self, actions: &ActionsHeld, effects: &mut Vec) { for key in [Action::Left, Action::Right] { let just_pressed = actions.just_pressed(self.ticks, &key); let long_press = actions.held_for(self.ticks, &key, |held_for| held_for > 15); @@ -150,11 +159,13 @@ impl Game { self.current_tetromino.x += offset; if self.board.colliding(&self.current_tetromino) { self.current_tetromino.x -= offset; + } else { + effects.push(SoundEffect::Move); } } } - fn check_line_clears(&mut self) { + fn check_line_clears(&mut self, effects: &mut Vec) { let lines_cleared = self.board.lines_cleared(); self.score.level_up(lines_cleared); @@ -177,20 +188,24 @@ impl Game { if lines_cleared > 0 { self.score.combo += 1; - // play_line_clears_sound(); + effects.push(SoundEffect::LineClear(lines_cleared)); } else { self.score.combo = 0; - // play_hard_drop_sound(); + effects.push(SoundEffect::HardDrop); } } - pub fn step(&mut self, actions: &ActionsHeld) { - self.hard_drop(actions); - self.soft_drop(actions); - self.move_horizontally(actions); + pub fn step(&mut self, actions: &ActionsHeld) -> Vec { + if self.game_over { + panic!("should check if game is over before stepping"); + } + let mut effects = Vec::new(); + self.try_hard_drop(actions, &mut effects); + self.soft_drop(actions, &mut effects); + self.try_move_horizontally(actions, &mut effects); if actions.just_pressed(self.ticks, &Action::Swap) { - self.try_swap_tetromino(); + self.try_swap_tetromino(&mut effects); } for (control, direction) in [ @@ -200,16 +215,18 @@ impl Game { if !actions.just_pressed(self.ticks, &control) { continue; } - self.try_rotate(direction); + self.try_rotate(direction, &mut effects); } self.ticks += 1; + effects } - fn try_rotate(&mut self, diff: DirectionDiff) -> bool { + fn try_rotate(&mut self, diff: DirectionDiff, effects: &mut Vec) { let rotated = self.current_tetromino.direction.rotate(&diff); let old_direction = std::mem::replace(&mut self.current_tetromino.direction, rotated); if !self.board.colliding(&self.current_tetromino) { - return true; + effects.push(SoundEffect::Rotation); + return; } let wall_kicks = self .current_tetromino @@ -220,14 +237,14 @@ impl Game { self.current_tetromino.x += x; self.current_tetromino.y += y; if !(self.board.colliding(&self.current_tetromino)) { - return true; + effects.push(SoundEffect::Rotation); + return; } self.current_tetromino.x -= x; self.current_tetromino.y -= y; } self.current_tetromino.direction = old_direction; - false } fn place_current_tetromino(&mut self) { @@ -235,6 +252,10 @@ impl Game { let current = std::mem::replace(&mut self.current_tetromino, next); let pattern = current.tetromino.direction_pattern(¤t.direction); + if current.y <= 0 { + self.game_over = true; + } + for (y, row) in pattern.iter().enumerate() { for x in row .iter() @@ -242,7 +263,11 @@ impl Game { .filter(|(_, exists)| **exists) .map(|(x, _)| x) { - let y = (current.y + y as i8) as usize; + let y = current.y + y as i8; + if y < 0 { + continue; + } + let y = y as usize; let x = (current.x + x as i8) as usize; self.board[y][x] = Some(current.tetromino.clone()); } @@ -251,7 +276,7 @@ impl Game { self.has_swapped_held = false; } - fn try_swap_tetromino(&mut self) { + fn try_swap_tetromino(&mut self, effects: &mut Vec) { if self.has_swapped_held { return; } @@ -263,6 +288,7 @@ impl Game { let current_tetromino = CurrentTetromino::new(held_or_first_in_bag_tetromino); let old_tetromino = std::mem::replace(&mut self.current_tetromino, current_tetromino); self.held_tetromino.replace(old_tetromino.tetromino); + effects.push(SoundEffect::Rotation); } } @@ -273,6 +299,7 @@ mod test { #[test] fn advance_bag() { let mut game = Game { + game_over: false, board: Board::new(), score: Score::new(), next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O], diff --git a/src/sdl_impl.rs b/src/sdl_impl.rs index edcb86e..f69c391 100644 --- a/src/sdl_impl.rs +++ b/src/sdl_impl.rs @@ -1,31 +1,130 @@ use crate::actions::{Action, ActionsHeld}; use crate::board::Board; -use crate::game::Game; +use crate::game::{CurrentTetromino, Game}; use crate::Rgb; use sdl2::event::Event; use sdl2::keyboard::Keycode; use sdl2::pixels::Color; use sdl2::rect::Rect; -use sdl2::render::WindowCanvas; +use sdl2::render::{Canvas, RenderTarget, Texture, TextureCreator, WindowCanvas}; +use sdl2::rwops::RWops; +use sdl2::ttf::Sdl2TtfContext; use std::time::Duration; -fn draw_board(canvas: &mut WindowCanvas, width: usize, height: usize, color: Rgb) { +fn draw_board_tile(canvas: &mut WindowCanvas, width: usize, height: usize, color: Rgb) { canvas.set_draw_color(Color::RGB(color.0, color.1, color.2)); canvas .fill_rect(Rect::new( - (800 - 24 * Board::WIDTH as i32) / 2 + width as i32 * 24, - (600 - 24 * Board::HEIGHT as i32) / 2 + height as i32 * 24, + center(24 * Board::WIDTH as i32, 800) + width as i32 * 24, + center(24 * Board::HEIGHT as i32, 600) + height as i32 * 24, 24, 24, )) .unwrap(); } +fn center(length: i32, max: i32) -> i32 { + (max - length) / 2 +} + +fn draw_board(canvas: &mut WindowCanvas, board: &Board, current: &CurrentTetromino) { + canvas.set_draw_color(Color::WHITE); + canvas + .draw_rect(Rect::new( + center(24 * Board::WIDTH as i32, 800) - 1, + center(24 * Board::HEIGHT as i32, 600) - 1, + 24 * Board::WIDTH as u32 + 2, + 24 * Board::HEIGHT as u32 + 2, + )) + .unwrap(); + + for (y, row) in board.iter().enumerate() { + for (x, piece) in row.iter().enumerate() { + let color = match piece { + Some(t) => Rgb::from_tetromino(t), + None => Rgb(0, 0, 0), + }; + draw_board_tile(canvas, x, y, color) + } + } + + let pattern = current.tetromino.direction_pattern(¤t.direction); + + for (y, row) in pattern.iter().enumerate() { + for x in row + .iter() + .enumerate() + .filter(|(_, exists)| **exists) + .map(|(x, _)| x) + { + let x = x as i8 + current.x; + let y = y as i8 + current.y; + + if y < 0 { + continue; + } + + draw_board_tile( + canvas, + x as usize, + y as usize, + Rgb::from_tetromino(¤t.tetromino), + ) + } + } +} + +fn font_texture<'font, 'a, C>( + text: &'a str, + ttf_context: &'a Sdl2TtfContext, + texture_creator: &'font TextureCreator, +) -> Texture<'font> { + let font = ttf_context + .load_font_from_rwops( + RWops::from_bytes(include_bytes!("res/josenfin_sans_regular.ttf")).unwrap(), + 24, + ) + .unwrap(); + let game_over_text = font.render(text).solid(Color::RGB(255, 255, 255)).unwrap(); + let texture = texture_creator + .create_texture_from_surface(game_over_text) + .unwrap(); + + texture +} + +fn draw_important_text( + text: &str, + canvas: &mut WindowCanvas, + ttf_context: &Sdl2TtfContext, +) -> Result<(), String> { + let texture_creator = canvas.texture_creator(); + let texture = font_texture(text, &ttf_context, &texture_creator); + + let size = texture.query(); + let width = size.width; + let height = size.height; + + let x = center(width as i32, 800); + let y = center(height as i32, 600); + + canvas.set_draw_color(Color::WHITE); + canvas.draw_rect(Rect::new(x - 9, y - 9, width + 18, height + 18))?; + + canvas.set_draw_color(Color::RGB(16, 16, 16)); + canvas.fill_rect(Rect::new(x - 8, y - 8, width + 16, height + 16))?; + canvas.copy(&texture, None, Some(Rect::new(x, y, width, height)))?; + + Ok(()) +} + pub fn start_game() { let mut game = Game::new(); let mut actions = ActionsHeld::new(); + let mut paused = false; let sdl_context = sdl2::init().unwrap(); + let ttf_context = sdl2::ttf::init().unwrap(); let video_subsystem = sdl_context.video().unwrap(); let window = video_subsystem @@ -51,6 +150,14 @@ pub fn start_game() { .. } => { let keycode = match keycode { + Keycode::Return if !paused && game.game_over => { + game = Game::new(); + continue; + } + Keycode::P => { + paused = !paused; + continue; + } Keycode::Left | Keycode::A => Action::Left, Keycode::Right | Keycode::D => Action::Right, Keycode::Down | Keycode::S => Action::SoftDrop, @@ -82,64 +189,26 @@ pub fn start_game() { } } - canvas.set_draw_color(Color::WHITE); - canvas - .draw_rect(Rect::new( - (800 - 24 * Board::WIDTH as i32) / 2 - 1, - (600 - 24 * Board::HEIGHT as i32) / 2 - 1, - 24 * Board::WIDTH as u32 + 2, - 24 * Board::HEIGHT as u32 + 2, - )) + draw_board(&mut canvas, &game.board, &game.current_tetromino); + + if paused { + draw_important_text( + "game paused o_o... press [p] to unpause !!", + &mut canvas, + &ttf_context, + ) .unwrap(); - - for (y, row) in game.board.iter().enumerate() { - for (x, piece) in row.iter().enumerate() { - let color = match piece { - Some(t) => Rgb::from_tetromino(t), - None => Rgb(0, 0, 0), - }; - draw_board(&mut canvas, x, y, color) - } + } else if game.game_over { + draw_important_text( + "game over T_T... press [enter] 2 restart :D", + &mut canvas, + &ttf_context, + ) + .unwrap(); + } else { + game.step(&actions); } - let pattern = game - .current_tetromino - .tetromino - .direction_pattern(&game.current_tetromino.direction); - - for (y, row) in pattern.iter().enumerate() { - for x in row - .iter() - .enumerate() - .filter(|(_, exists)| **exists) - .map(|(x, _)| x) - { - let x = x as i8 + game.current_tetromino.x; - let y = y as i8 + game.current_tetromino.y; - - if y < 0 { - continue; - } - - if y >= Board::HEIGHT as i8 { - continue; - } - - if x < 0 || x >= Board::WIDTH as i8 { - continue; - } - - draw_board( - &mut canvas, - x as usize, - y as usize, - Rgb::from_tetromino(&game.current_tetromino.tetromino), - ) - } - } - - game.step(&actions); - canvas.present(); ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60)); }