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]
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 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<SoundEffect>) {
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<SoundEffect>) {
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<SoundEffect>) {
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<SoundEffect>) {
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<SoundEffect> {
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<SoundEffect>) {
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(&current.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<SoundEffect>) {
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],

View File

@ -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(&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() {
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));
}