pause/game over

This commit is contained in:
Theis Pieter Hollebeek 2025-03-03 14:54:51 +01:00
parent 63a8c0bd78
commit fc6ebcdbc2
3 changed files with 185 additions and 86 deletions

View File

@ -5,4 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
rand = "0.9.0" rand = "0.9.0"
sdl2 = "0.37.0"
[dependencies.sdl2]
version = "0.37.0"
features = ["ttf"]

View File

@ -22,6 +22,7 @@ impl CurrentTetromino {
} }
pub struct Game { pub struct Game {
pub game_over: bool,
pub board: Board, pub board: Board,
pub next_tetrominos: [Tetromino; 3], pub next_tetrominos: [Tetromino; 3],
pub current_tetromino: CurrentTetromino, pub current_tetromino: CurrentTetromino,
@ -31,11 +32,18 @@ pub struct Game {
pub ticks: usize, pub ticks: usize,
} }
struct Score { pub enum SoundEffect {
level: usize, HardDrop,
points: usize, LineClear(usize),
lines: usize, Move,
combo: usize, Rotation,
}
pub struct Score {
pub level: usize,
pub points: usize,
pub lines: usize,
pub combo: usize,
back_to_back: bool, back_to_back: bool,
} }
@ -81,6 +89,7 @@ impl Score {
impl Game { impl Game {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
game_over: false,
board: Board::new(), board: Board::new(),
next_tetrominos: std::array::from_fn(|_| Tetromino::random()), next_tetrominos: std::array::from_fn(|_| Tetromino::random()),
current_tetromino: CurrentTetromino::new(Tetromino::random()), current_tetromino: CurrentTetromino::new(Tetromino::random()),
@ -97,7 +106,7 @@ impl Game {
last last
} }
fn hard_drop(&mut self, actions: &ActionsHeld) { fn try_hard_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) {
if !actions.just_pressed(self.ticks, &Action::HardDrop) { if !actions.just_pressed(self.ticks, &Action::HardDrop) {
return; return;
} }
@ -110,12 +119,12 @@ impl Game {
self.current_tetromino.y -= 1; self.current_tetromino.y -= 1;
self.score.points += (self.current_tetromino.y - start_y) as usize * 2; self.score.points += (self.current_tetromino.y - start_y) as usize * 2;
self.place_current_tetromino(); self.place_current_tetromino();
self.check_line_clears(); self.check_line_clears(effects);
break; break;
} }
} }
fn soft_drop(&mut self, actions: &ActionsHeld) { fn soft_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) {
let mut delay = 32 - self.score.level * 2; let mut delay = 32 - self.score.level * 2;
if actions.contains_key(&Action::SoftDrop) { if actions.contains_key(&Action::SoftDrop) {
delay /= 10; delay /= 10;
@ -129,13 +138,13 @@ impl Game {
if self.board.colliding(&self.current_tetromino) { if self.board.colliding(&self.current_tetromino) {
self.current_tetromino.y -= 1; self.current_tetromino.y -= 1;
self.place_current_tetromino(); self.place_current_tetromino();
self.check_line_clears(); self.check_line_clears(effects);
} else if actions.contains_key(&Action::SoftDrop) { } else if actions.contains_key(&Action::SoftDrop) {
self.score.points += 1; self.score.points += 1;
} }
} }
fn move_horizontally(&mut self, actions: &ActionsHeld) { fn try_move_horizontally(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) {
for key in [Action::Left, Action::Right] { for key in [Action::Left, Action::Right] {
let just_pressed = actions.just_pressed(self.ticks, &key); let just_pressed = actions.just_pressed(self.ticks, &key);
let long_press = actions.held_for(self.ticks, &key, |held_for| held_for > 15); 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; self.current_tetromino.x += offset;
if self.board.colliding(&self.current_tetromino) { if self.board.colliding(&self.current_tetromino) {
self.current_tetromino.x -= offset; 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<SoundEffect>) {
let lines_cleared = self.board.lines_cleared(); let lines_cleared = self.board.lines_cleared();
self.score.level_up(lines_cleared); self.score.level_up(lines_cleared);
@ -177,20 +188,24 @@ impl Game {
if lines_cleared > 0 { if lines_cleared > 0 {
self.score.combo += 1; self.score.combo += 1;
// play_line_clears_sound(); effects.push(SoundEffect::LineClear(lines_cleared));
} else { } else {
self.score.combo = 0; self.score.combo = 0;
// play_hard_drop_sound(); effects.push(SoundEffect::HardDrop);
} }
} }
pub fn step(&mut self, actions: &ActionsHeld) { pub fn step(&mut self, actions: &ActionsHeld) -> Vec<SoundEffect> {
self.hard_drop(actions); if self.game_over {
self.soft_drop(actions); panic!("should check if game is over before stepping");
self.move_horizontally(actions); }
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) { if actions.just_pressed(self.ticks, &Action::Swap) {
self.try_swap_tetromino(); self.try_swap_tetromino(&mut effects);
} }
for (control, direction) in [ for (control, direction) in [
@ -200,16 +215,18 @@ impl Game {
if !actions.just_pressed(self.ticks, &control) { if !actions.just_pressed(self.ticks, &control) {
continue; continue;
} }
self.try_rotate(direction); self.try_rotate(direction, &mut effects);
} }
self.ticks += 1; self.ticks += 1;
effects
} }
fn try_rotate(&mut self, diff: DirectionDiff) -> bool { fn try_rotate(&mut self, diff: DirectionDiff, effects: &mut Vec<SoundEffect>) {
let rotated = self.current_tetromino.direction.rotate(&diff); let rotated = self.current_tetromino.direction.rotate(&diff);
let old_direction = std::mem::replace(&mut self.current_tetromino.direction, rotated); let old_direction = std::mem::replace(&mut self.current_tetromino.direction, rotated);
if !self.board.colliding(&self.current_tetromino) { if !self.board.colliding(&self.current_tetromino) {
return true; effects.push(SoundEffect::Rotation);
return;
} }
let wall_kicks = self let wall_kicks = self
.current_tetromino .current_tetromino
@ -220,14 +237,14 @@ impl Game {
self.current_tetromino.x += x; self.current_tetromino.x += x;
self.current_tetromino.y += y; self.current_tetromino.y += y;
if !(self.board.colliding(&self.current_tetromino)) { if !(self.board.colliding(&self.current_tetromino)) {
return true; effects.push(SoundEffect::Rotation);
return;
} }
self.current_tetromino.x -= x; self.current_tetromino.x -= x;
self.current_tetromino.y -= y; self.current_tetromino.y -= y;
} }
self.current_tetromino.direction = old_direction; self.current_tetromino.direction = old_direction;
false
} }
fn place_current_tetromino(&mut self) { fn place_current_tetromino(&mut self) {
@ -235,6 +252,10 @@ impl Game {
let current = std::mem::replace(&mut self.current_tetromino, next); let current = std::mem::replace(&mut self.current_tetromino, next);
let pattern = current.tetromino.direction_pattern(&current.direction); let pattern = current.tetromino.direction_pattern(&current.direction);
if current.y <= 0 {
self.game_over = true;
}
for (y, row) in pattern.iter().enumerate() { for (y, row) in pattern.iter().enumerate() {
for x in row for x in row
.iter() .iter()
@ -242,7 +263,11 @@ impl Game {
.filter(|(_, exists)| **exists) .filter(|(_, exists)| **exists)
.map(|(x, _)| x) .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; let x = (current.x + x as i8) as usize;
self.board[y][x] = Some(current.tetromino.clone()); self.board[y][x] = Some(current.tetromino.clone());
} }
@ -251,7 +276,7 @@ impl Game {
self.has_swapped_held = false; self.has_swapped_held = false;
} }
fn try_swap_tetromino(&mut self) { fn try_swap_tetromino(&mut self, effects: &mut Vec<SoundEffect>) {
if self.has_swapped_held { if self.has_swapped_held {
return; return;
} }
@ -263,6 +288,7 @@ impl Game {
let current_tetromino = CurrentTetromino::new(held_or_first_in_bag_tetromino); let current_tetromino = CurrentTetromino::new(held_or_first_in_bag_tetromino);
let old_tetromino = std::mem::replace(&mut self.current_tetromino, current_tetromino); let old_tetromino = std::mem::replace(&mut self.current_tetromino, current_tetromino);
self.held_tetromino.replace(old_tetromino.tetromino); self.held_tetromino.replace(old_tetromino.tetromino);
effects.push(SoundEffect::Rotation);
} }
} }
@ -273,6 +299,7 @@ mod test {
#[test] #[test]
fn advance_bag() { fn advance_bag() {
let mut game = Game { let mut game = Game {
game_over: false,
board: Board::new(), board: Board::new(),
score: Score::new(), score: Score::new(),
next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O], next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O],

View File

@ -1,31 +1,130 @@
use crate::actions::{Action, ActionsHeld}; use crate::actions::{Action, ActionsHeld};
use crate::board::Board; use crate::board::Board;
use crate::game::Game; use crate::game::{CurrentTetromino, Game};
use crate::Rgb; use crate::Rgb;
use sdl2::event::Event; use sdl2::event::Event;
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use sdl2::pixels::Color; use sdl2::pixels::Color;
use sdl2::rect::Rect; 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; 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.set_draw_color(Color::RGB(color.0, color.1, color.2));
canvas canvas
.fill_rect(Rect::new( .fill_rect(Rect::new(
(800 - 24 * Board::WIDTH as i32) / 2 + width as i32 * 24, center(24 * Board::WIDTH as i32, 800) + width as i32 * 24,
(600 - 24 * Board::HEIGHT as i32) / 2 + height as i32 * 24, center(24 * Board::HEIGHT as i32, 600) + height as i32 * 24,
24, 24,
24, 24,
)) ))
.unwrap(); .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(&current.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(&current.tetromino),
)
}
}
}
fn font_texture<'font, 'a, C>(
text: &'a str,
ttf_context: &'a Sdl2TtfContext,
texture_creator: &'font TextureCreator<C>,
) -> 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() { pub fn start_game() {
let mut game = Game::new(); let mut game = Game::new();
let mut actions = ActionsHeld::new(); let mut actions = ActionsHeld::new();
let mut paused = false;
let sdl_context = sdl2::init().unwrap(); let sdl_context = sdl2::init().unwrap();
let ttf_context = sdl2::ttf::init().unwrap();
let video_subsystem = sdl_context.video().unwrap(); let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem let window = video_subsystem
@ -51,6 +150,14 @@ pub fn start_game() {
.. ..
} => { } => {
let keycode = match keycode { 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::Left | Keycode::A => Action::Left,
Keycode::Right | Keycode::D => Action::Right, Keycode::Right | Keycode::D => Action::Right,
Keycode::Down | Keycode::S => Action::SoftDrop, Keycode::Down | Keycode::S => Action::SoftDrop,
@ -82,64 +189,26 @@ pub fn start_game() {
} }
} }
canvas.set_draw_color(Color::WHITE); draw_board(&mut canvas, &game.board, &game.current_tetromino);
canvas
.draw_rect(Rect::new( if paused {
(800 - 24 * Board::WIDTH as i32) / 2 - 1, draw_important_text(
(600 - 24 * Board::HEIGHT as i32) / 2 - 1, "game paused o_o... press [p] to unpause !!",
24 * Board::WIDTH as u32 + 2, &mut canvas,
24 * Board::HEIGHT as u32 + 2, &ttf_context,
)) )
.unwrap(); .unwrap();
} else if game.game_over {
for (y, row) in game.board.iter().enumerate() { draw_important_text(
for (x, piece) in row.iter().enumerate() { "game over T_T... press [enter] 2 restart :D",
let color = match piece { &mut canvas,
Some(t) => Rgb::from_tetromino(t), &ttf_context,
None => Rgb(0, 0, 0), )
}; .unwrap();
draw_board(&mut canvas, x, y, color) } 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(); canvas.present();
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60)); ::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
} }