Compare commits
9 Commits
eddf9d81c0
...
1830aec847
Author | SHA1 | Date | |
---|---|---|---|
1830aec847 | |||
489c2814e2 | |||
ecbb7c6756 | |||
a19a9e9cc3 | |||
7d6822cd34 | |||
fc6ebcdbc2 | |||
63a8c0bd78 | |||
db57450452 | |||
f1877a1e8c |
911
Cargo.lock
generated
911
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,3 +5,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.9.0"
|
||||
rodio = "0.20.1"
|
||||
|
||||
[dependencies.sdl2]
|
||||
version = "0.37.0"
|
||||
features = ["ttf"]
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Hash, PartialEq, Eq)]
|
||||
pub enum Controls {
|
||||
pub enum Action {
|
||||
Left,
|
||||
Right,
|
||||
SoftDrop,
|
||||
@ -11,17 +11,20 @@ pub enum Controls {
|
||||
RotateCcw,
|
||||
}
|
||||
|
||||
pub struct ControlsHeld(HashMap<Controls, usize>);
|
||||
pub struct ActionsHeld(HashMap<Action, usize>);
|
||||
|
||||
impl ControlsHeld {
|
||||
pub fn just_pressed(&self, ticks: usize, control: &Controls) -> bool {
|
||||
impl ActionsHeld {
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
pub fn just_pressed(&self, ticks: usize, control: &Action) -> bool {
|
||||
self.held_for(ticks, control, |held_for| held_for == 0)
|
||||
}
|
||||
|
||||
pub fn held_for<F: Fn(usize) -> bool>(
|
||||
&self,
|
||||
ticks: usize,
|
||||
control: &Controls,
|
||||
control: &Action,
|
||||
functor: F,
|
||||
) -> bool {
|
||||
self.get(control)
|
||||
@ -30,15 +33,15 @@ impl ControlsHeld {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ControlsHeld {
|
||||
type Target = HashMap<Controls, usize>;
|
||||
impl std::ops::Deref for ActionsHeld {
|
||||
type Target = HashMap<Action, usize>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for ControlsHeld {
|
||||
impl std::ops::DerefMut for ActionsHeld {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
|
40
src/board.rs
40
src/board.rs
@ -1,6 +1,6 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crate::{CurrentTetromino, Tetromino};
|
||||
use crate::{game::CurrentTetromino, Tetromino};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct Board([[Option<Tetromino>; Self::WIDTH]; Self::HEIGHT]);
|
||||
@ -28,26 +28,35 @@ impl Board {
|
||||
Board(board)
|
||||
}
|
||||
|
||||
pub fn colliding(
|
||||
pub fn lowest_y(
|
||||
&self,
|
||||
CurrentTetromino {
|
||||
tetromino,
|
||||
direction,
|
||||
x: cur_x,
|
||||
y: cur_y,
|
||||
x,
|
||||
y,
|
||||
}: &CurrentTetromino,
|
||||
) -> bool {
|
||||
) -> i8 {
|
||||
let pattern = tetromino.direction_pattern(direction);
|
||||
let mut y = *y;
|
||||
loop {
|
||||
if self.pattern_and_position_colliding(pattern, *x, y) {
|
||||
break y - 1;
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (y, row) in pattern.iter().enumerate() {
|
||||
for x in row
|
||||
fn pattern_and_position_colliding(&self, pattern: [[bool; 4]; 4], x: i8, y: i8) -> bool {
|
||||
for (y_offset, row) in pattern.iter().enumerate() {
|
||||
for x_offset in row
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, exists)| **exists)
|
||||
.map(|(x, _)| x)
|
||||
{
|
||||
let x = x as i8 + cur_x;
|
||||
let y = y as i8 + cur_y;
|
||||
let x = x_offset as i8 + x;
|
||||
let y = y_offset as i8 + y;
|
||||
|
||||
if y < 0 {
|
||||
continue;
|
||||
@ -70,6 +79,19 @@ impl Board {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn colliding(
|
||||
&self,
|
||||
CurrentTetromino {
|
||||
tetromino,
|
||||
direction,
|
||||
x,
|
||||
y,
|
||||
}: &CurrentTetromino,
|
||||
) -> bool {
|
||||
let pattern = tetromino.direction_pattern(direction);
|
||||
self.pattern_and_position_colliding(pattern, *x, *y)
|
||||
}
|
||||
|
||||
pub fn lines_cleared(&mut self) -> usize {
|
||||
let line_clears: Vec<_> = self
|
||||
.iter()
|
||||
|
317
src/game.rs
Normal file
317
src/game.rs
Normal file
@ -0,0 +1,317 @@
|
||||
use crate::actions::{Action, ActionsHeld};
|
||||
use crate::board::Board;
|
||||
use crate::tetromino::{Direction, DirectionDiff, Tetromino};
|
||||
|
||||
pub struct CurrentTetromino {
|
||||
pub tetromino: Tetromino,
|
||||
pub direction: Direction,
|
||||
pub x: i8,
|
||||
pub y: i8,
|
||||
}
|
||||
|
||||
impl CurrentTetromino {
|
||||
fn new(tetromino: Tetromino) -> Self {
|
||||
const PIECE_WIDTH: i8 = 2;
|
||||
Self {
|
||||
tetromino,
|
||||
direction: Direction::Up,
|
||||
x: (Board::WIDTH as i8 - PIECE_WIDTH) / 2,
|
||||
y: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
pub game_over: bool,
|
||||
pub board: Board,
|
||||
pub next_tetrominos: [Tetromino; 3],
|
||||
pub current_tetromino: CurrentTetromino,
|
||||
pub held_tetromino: Option<Tetromino>,
|
||||
has_swapped_held: bool,
|
||||
pub score: Score,
|
||||
pub ticks: 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,
|
||||
}
|
||||
|
||||
impl Score {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
level: 0,
|
||||
points: 0,
|
||||
lines: 0,
|
||||
combo: 0,
|
||||
back_to_back: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn level_up(&mut self, lines_cleared: usize) {
|
||||
self.lines += lines_cleared;
|
||||
if self.lines > self.level * 5 {
|
||||
self.level += 1;
|
||||
self.lines = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn point_multiplier_from_lines_cleared(lines_cleared: usize) -> f32 {
|
||||
match lines_cleared {
|
||||
0 => 0.0,
|
||||
1 => 100.0,
|
||||
2 => 300.0,
|
||||
3 => 500.0,
|
||||
4 => 800.0,
|
||||
_ => unreachable!("we cannot clear more than 4 lines"),
|
||||
}
|
||||
}
|
||||
|
||||
fn combos(&self, lines_cleared: usize) -> usize {
|
||||
if lines_cleared > 0 {
|
||||
self.combo * 50 * self.level
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
held_tetromino: None,
|
||||
has_swapped_held: false,
|
||||
score: Score::new(),
|
||||
ticks: 0,
|
||||
}
|
||||
}
|
||||
fn take_next_in_bag(&mut self, mut last: Tetromino) -> Tetromino {
|
||||
for value in self.next_tetrominos.iter_mut().rev() {
|
||||
std::mem::swap(value, &mut last)
|
||||
}
|
||||
last
|
||||
}
|
||||
|
||||
fn try_hard_drop(&mut self, actions: &ActionsHeld, effects: &mut Vec<SoundEffect>) {
|
||||
if !actions.just_pressed(self.ticks, &Action::HardDrop) {
|
||||
return;
|
||||
}
|
||||
let start_y = self.current_tetromino.y;
|
||||
loop {
|
||||
self.current_tetromino.y += 1;
|
||||
if !self.board.colliding(&self.current_tetromino) {
|
||||
continue;
|
||||
}
|
||||
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(effects);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if self.ticks % delay != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.current_tetromino.y += 1;
|
||||
if self.board.colliding(&self.current_tetromino) {
|
||||
self.current_tetromino.y -= 1;
|
||||
self.place_current_tetromino();
|
||||
self.check_line_clears(effects);
|
||||
} else if actions.contains_key(&Action::SoftDrop) {
|
||||
self.score.points += 1;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if !just_pressed && !long_press {
|
||||
continue;
|
||||
}
|
||||
let offset = match key {
|
||||
Action::Left => -1,
|
||||
Action::Right => 1,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
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, effects: &mut Vec<SoundEffect>) {
|
||||
let lines_cleared = self.board.lines_cleared();
|
||||
|
||||
self.score.level_up(lines_cleared);
|
||||
|
||||
let mut points =
|
||||
self.score.level as f32 * Score::point_multiplier_from_lines_cleared(lines_cleared);
|
||||
|
||||
if self.score.back_to_back && lines_cleared == 4 {
|
||||
points *= 1.5;
|
||||
}
|
||||
points += self.score.combos(lines_cleared) as f32;
|
||||
|
||||
self.score.points += points as usize;
|
||||
|
||||
if lines_cleared == 4 {
|
||||
self.score.back_to_back = true;
|
||||
} else if lines_cleared > 0 {
|
||||
self.score.back_to_back = false;
|
||||
}
|
||||
|
||||
if lines_cleared > 0 {
|
||||
self.score.combo += 1;
|
||||
effects.push(SoundEffect::LineClear(lines_cleared));
|
||||
} else {
|
||||
self.score.combo = 0;
|
||||
effects.push(SoundEffect::HardDrop);
|
||||
}
|
||||
}
|
||||
|
||||
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(&mut effects);
|
||||
}
|
||||
|
||||
for (control, direction) in [
|
||||
(Action::RotateCw, DirectionDiff::Cw),
|
||||
(Action::RotateCcw, DirectionDiff::Ccw),
|
||||
] {
|
||||
if !actions.just_pressed(self.ticks, &control) {
|
||||
continue;
|
||||
}
|
||||
self.try_rotate(direction, &mut effects);
|
||||
}
|
||||
self.ticks += 1;
|
||||
effects
|
||||
}
|
||||
|
||||
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) {
|
||||
effects.push(SoundEffect::Rotation);
|
||||
return;
|
||||
}
|
||||
let wall_kicks = self
|
||||
.current_tetromino
|
||||
.tetromino
|
||||
.wall_kicks(&old_direction, &diff);
|
||||
|
||||
for (x, y) in wall_kicks {
|
||||
self.current_tetromino.x += x;
|
||||
self.current_tetromino.y += y;
|
||||
if !(self.board.colliding(&self.current_tetromino)) {
|
||||
effects.push(SoundEffect::Rotation);
|
||||
return;
|
||||
}
|
||||
self.current_tetromino.x -= x;
|
||||
self.current_tetromino.y -= y;
|
||||
}
|
||||
|
||||
self.current_tetromino.direction = old_direction;
|
||||
}
|
||||
|
||||
fn place_current_tetromino(&mut self) {
|
||||
let next = CurrentTetromino::new(self.take_next_in_bag(Tetromino::random()));
|
||||
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()
|
||||
.enumerate()
|
||||
.filter(|(_, exists)| **exists)
|
||||
.map(|(x, _)| x)
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
self.has_swapped_held = false;
|
||||
}
|
||||
|
||||
fn try_swap_tetromino(&mut self, effects: &mut Vec<SoundEffect>) {
|
||||
if self.has_swapped_held {
|
||||
return;
|
||||
}
|
||||
self.has_swapped_held = true;
|
||||
let held_or_first_in_bag_tetromino = self
|
||||
.held_tetromino
|
||||
.take()
|
||||
.unwrap_or_else(|| self.take_next_in_bag(Tetromino::random()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Board, CurrentTetromino, Game, Score, Tetromino};
|
||||
|
||||
#[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],
|
||||
current_tetromino: CurrentTetromino::new(Tetromino::J),
|
||||
held_tetromino: None,
|
||||
has_swapped_held: false,
|
||||
ticks: 0,
|
||||
};
|
||||
assert_eq!(game.take_next_in_bag(Tetromino::S), Tetromino::I);
|
||||
assert_eq!(
|
||||
game.next_tetrominos,
|
||||
[Tetromino::J, Tetromino::O, Tetromino::S]
|
||||
);
|
||||
}
|
||||
}
|
5
src/gui.rs
Normal file
5
src/gui.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod audio;
|
||||
mod sdl;
|
||||
mod ui;
|
||||
|
||||
pub use sdl::start_game;
|
99
src/gui/audio.rs
Normal file
99
src/gui/audio.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use std::{fs::File, io::BufReader, sync::mpsc};
|
||||
|
||||
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink, Source};
|
||||
|
||||
use crate::game::SoundEffect;
|
||||
|
||||
fn source_from_path<P: AsRef<std::path::Path>>(path: P) -> Decoder<BufReader<File>> {
|
||||
let file = BufReader::new(File::open(path).unwrap());
|
||||
let source = Decoder::new(file).unwrap();
|
||||
source
|
||||
}
|
||||
|
||||
fn play_audio<P: AsRef<std::path::Path>>(
|
||||
stream_handle: &OutputStreamHandle,
|
||||
sink: &mut Option<Sink>,
|
||||
path: P,
|
||||
volume: f32,
|
||||
) {
|
||||
let source = source_from_path(path);
|
||||
*sink = Sink::try_new(&stream_handle).ok();
|
||||
if let Some(sink) = sink {
|
||||
sink.set_volume(volume);
|
||||
sink.append(source);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Command {
|
||||
ToggleMuted,
|
||||
PlayEffect(SoundEffect),
|
||||
}
|
||||
|
||||
pub fn audio_thread() -> mpsc::Sender<Command> {
|
||||
let (sender, receiver) = mpsc::channel::<Command>();
|
||||
|
||||
let _ = std::thread::spawn(move || {
|
||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
let music_sink = Sink::try_new(&stream_handle).unwrap();
|
||||
let mut hard_drop_sink = None;
|
||||
let mut line_clear_sink = None;
|
||||
let mut move_sink = None;
|
||||
let mut rotation_sink = None;
|
||||
let mut muted = false;
|
||||
|
||||
music_sink.append(source_from_path("resources/music.ogg").repeat_infinite());
|
||||
|
||||
loop {
|
||||
let Ok(cmd) = receiver.recv() else {
|
||||
break;
|
||||
};
|
||||
|
||||
let effect = match cmd {
|
||||
Command::ToggleMuted => {
|
||||
muted = !muted;
|
||||
if muted {
|
||||
music_sink.pause();
|
||||
} else {
|
||||
music_sink.play();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Command::PlayEffect(effect) => effect,
|
||||
};
|
||||
|
||||
if muted {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base_volume = 0.5;
|
||||
match effect {
|
||||
SoundEffect::HardDrop => play_audio(
|
||||
&stream_handle,
|
||||
&mut hard_drop_sink,
|
||||
"resources/hard_drop.ogg",
|
||||
base_volume,
|
||||
),
|
||||
SoundEffect::LineClear(lines_cleared) => play_audio(
|
||||
&stream_handle,
|
||||
&mut line_clear_sink,
|
||||
"resources/line_clear.ogg",
|
||||
base_volume + (lines_cleared as f32 - 1.0) * 0.5,
|
||||
),
|
||||
SoundEffect::Move => play_audio(
|
||||
&stream_handle,
|
||||
&mut move_sink,
|
||||
"resources/move.ogg",
|
||||
base_volume,
|
||||
),
|
||||
SoundEffect::Rotation => play_audio(
|
||||
&stream_handle,
|
||||
&mut rotation_sink,
|
||||
"resources/rotation.ogg",
|
||||
base_volume,
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
sender
|
||||
}
|
223
src/gui/sdl.rs
Normal file
223
src/gui/sdl.rs
Normal file
@ -0,0 +1,223 @@
|
||||
use crate::actions::{Action, ActionsHeld};
|
||||
use crate::game::Game;
|
||||
use sdl2::event::Event;
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::pixels::Color;
|
||||
use sdl2::rect::Rect;
|
||||
use sdl2::render::{Texture, TextureCreator, WindowCanvas};
|
||||
use sdl2::ttf::Sdl2TtfContext;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::audio::{self};
|
||||
use super::ui::{GameUiCtx, Rgb, UiCtx};
|
||||
|
||||
const WIDTH: i32 = 1000;
|
||||
const HEIGHT: i32 = 800;
|
||||
|
||||
fn font_texture<'font, 'a, P: AsRef<std::path::Path>, Text: AsRef<str>, C>(
|
||||
font: P,
|
||||
text: Text,
|
||||
ttf_context: &'a Sdl2TtfContext,
|
||||
texture_creator: &'font TextureCreator<C>,
|
||||
) -> Result<Texture<'font>, String> {
|
||||
let font = ttf_context.load_font(font, 24)?;
|
||||
let game_over_text = font
|
||||
.render(text.as_ref())
|
||||
.solid(Color::RGB(255, 255, 255))
|
||||
.map_err(|err| err.to_string())?;
|
||||
let texture = texture_creator
|
||||
.create_texture_from_surface(game_over_text)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
struct SdlUiCtx {
|
||||
canvas: WindowCanvas,
|
||||
ttf: Sdl2TtfContext,
|
||||
}
|
||||
|
||||
impl SdlUiCtx {
|
||||
fn present(&mut self) {
|
||||
self.canvas.present();
|
||||
}
|
||||
}
|
||||
|
||||
impl UiCtx<String> for SdlUiCtx {
|
||||
fn window_size(&self) -> Result<(i32, i32), String> {
|
||||
let (width, height) = self.canvas.window().size();
|
||||
Ok((width as i32, height as i32))
|
||||
}
|
||||
|
||||
fn fill_rect(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
rgb: &super::ui::Rgb,
|
||||
) -> Result<(), String> {
|
||||
self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2));
|
||||
self.canvas
|
||||
.fill_rect(Rect::new(x, y, width as u32, height as u32))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn outline_rect(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
rgb: &super::ui::Rgb,
|
||||
) -> Result<(), String> {
|
||||
self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2));
|
||||
self.canvas
|
||||
.draw_rect(Rect::new(x, y, width as u32, height as u32))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn text_size<P: AsRef<std::path::Path>, Text: AsRef<str>>(
|
||||
&mut self,
|
||||
font: P,
|
||||
text: Text,
|
||||
) -> Result<(i32, i32), String> {
|
||||
let texture_creator = self.canvas.texture_creator();
|
||||
let texture = font_texture(font, text, &self.ttf, &texture_creator)?;
|
||||
let query = texture.query();
|
||||
Ok((query.width as i32, query.height as i32))
|
||||
}
|
||||
|
||||
fn fill_text<P: AsRef<std::path::Path>, Text: AsRef<str>>(
|
||||
&mut self,
|
||||
font: P,
|
||||
text: Text,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<(), String> {
|
||||
let texture_creator = self.canvas.texture_creator();
|
||||
let texture = font_texture(font, text, &self.ttf, &texture_creator)?;
|
||||
self.canvas.copy(
|
||||
&texture,
|
||||
None,
|
||||
Some(Rect::new(x, y, width as u32, height as u32)),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self, rgb: &Rgb) -> Result<(), String> {
|
||||
self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2));
|
||||
self.canvas.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_game() -> Result<(), String> {
|
||||
let mut game = Game::new();
|
||||
let mut actions = ActionsHeld::new();
|
||||
let mut paused = false;
|
||||
|
||||
let audio_thread = audio::audio_thread();
|
||||
|
||||
let sdl_context = sdl2::init()?;
|
||||
let ttf_context = sdl2::ttf::init().unwrap();
|
||||
let video_subsystem = sdl_context.video()?;
|
||||
|
||||
let window = video_subsystem
|
||||
.window("reimtris2", WIDTH as u32, HEIGHT as u32)
|
||||
.resizable()
|
||||
.position_centered()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let canvas = window.into_canvas().build().unwrap();
|
||||
let mut ctx = SdlUiCtx {
|
||||
canvas,
|
||||
ttf: ttf_context,
|
||||
};
|
||||
let mut event_pump = sdl_context.event_pump()?;
|
||||
'running: loop {
|
||||
ctx.clear(&Rgb(16, 16, 16))?;
|
||||
for event in event_pump.poll_iter() {
|
||||
match event {
|
||||
Event::Quit { .. }
|
||||
| Event::KeyDown {
|
||||
keycode: Some(Keycode::Escape),
|
||||
..
|
||||
} => break 'running Ok(()),
|
||||
Event::KeyDown {
|
||||
keycode: Some(keycode),
|
||||
..
|
||||
} => {
|
||||
let keycode = match keycode {
|
||||
Keycode::Return if !paused && game.game_over => {
|
||||
game = Game::new();
|
||||
continue;
|
||||
}
|
||||
Keycode::M => {
|
||||
audio_thread.send(audio::Command::ToggleMuted).unwrap();
|
||||
continue;
|
||||
}
|
||||
|
||||
Keycode::P => {
|
||||
paused = !paused;
|
||||
continue;
|
||||
}
|
||||
Keycode::Left | Keycode::A => Action::Left,
|
||||
Keycode::Right | Keycode::D => Action::Right,
|
||||
Keycode::Down | Keycode::S => Action::SoftDrop,
|
||||
Keycode::Space => Action::HardDrop,
|
||||
Keycode::Z => Action::RotateCcw,
|
||||
Keycode::X => Action::RotateCw,
|
||||
Keycode::C => Action::Swap,
|
||||
_ => continue,
|
||||
};
|
||||
actions.insert(keycode, game.ticks);
|
||||
}
|
||||
Event::KeyUp {
|
||||
keycode: Some(keycode),
|
||||
..
|
||||
} => {
|
||||
let keycode = match keycode {
|
||||
Keycode::Left | Keycode::A => Action::Left,
|
||||
Keycode::Right | Keycode::D => Action::Right,
|
||||
Keycode::Down | Keycode::S => Action::SoftDrop,
|
||||
Keycode::Space => Action::HardDrop,
|
||||
Keycode::Z => Action::RotateCcw,
|
||||
Keycode::X => Action::RotateCw,
|
||||
Keycode::C => Action::Swap,
|
||||
_ => continue,
|
||||
};
|
||||
actions.remove(&keycode);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.draw_board(&game.board, &game.current_tetromino)?;
|
||||
|
||||
if paused {
|
||||
ctx.draw_important_text(
|
||||
"resources/josenfin_sans_regular.ttf",
|
||||
"game paused o_o... press [p] to unpause !!",
|
||||
)?;
|
||||
} else if game.game_over {
|
||||
ctx.draw_important_text(
|
||||
"resources/josenfin_sans_regular.ttf",
|
||||
"game over T_T... press [enter] 2 restart :D",
|
||||
)?;
|
||||
} else {
|
||||
let effects = game.step(&actions);
|
||||
effects.into_iter().for_each(|effect| {
|
||||
audio_thread
|
||||
.send(audio::Command::PlayEffect(effect))
|
||||
.unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
ctx.present();
|
||||
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
|
||||
}
|
||||
}
|
156
src/gui/ui.rs
Normal file
156
src/gui/ui.rs
Normal file
@ -0,0 +1,156 @@
|
||||
use crate::{board::Board, game::CurrentTetromino, tetromino::Tetromino};
|
||||
|
||||
pub trait UiCtx<Err> {
|
||||
fn window_size(&self) -> Result<(i32, i32), Err>;
|
||||
fn fill_rect(&mut self, x: i32, y: i32, width: i32, height: i32, rgb: &Rgb) -> Result<(), Err>;
|
||||
fn outline_rect(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
rgb: &Rgb,
|
||||
) -> Result<(), Err>;
|
||||
fn text_size<P: AsRef<std::path::Path>, Text: AsRef<str>>(
|
||||
&mut self,
|
||||
font: P,
|
||||
text: Text,
|
||||
) -> Result<(i32, i32), Err>;
|
||||
fn fill_text<P: AsRef<std::path::Path>, Text: AsRef<str>>(
|
||||
&mut self,
|
||||
font: P,
|
||||
text: Text,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<(), Err>;
|
||||
fn clear(&mut self, rgb: &Rgb) -> Result<(), Err>;
|
||||
}
|
||||
|
||||
pub trait GameUiCtx<Err>: UiCtx<Err> {
|
||||
fn draw_tetromino_from_parts(
|
||||
&mut self,
|
||||
x: i8,
|
||||
y: i8,
|
||||
color: Rgb,
|
||||
pattern: [[bool; 4]; 4],
|
||||
filled: bool,
|
||||
) -> Result<(), Err> {
|
||||
for (y_offset, row) in pattern.iter().enumerate() {
|
||||
for x_offset in row
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, exists)| **exists)
|
||||
.map(|(x, _)| x)
|
||||
{
|
||||
let x = x_offset as i8 + x;
|
||||
let y = y_offset as i8 + y;
|
||||
|
||||
if y < 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.draw_board_tile(x as i32, y as i32, &color, filled)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_board_tile(&mut self, x: i32, y: i32, color: &Rgb, filled: bool) -> Result<(), Err> {
|
||||
let tile_size = 24;
|
||||
let (win_width, win_height) = self.window_size()?;
|
||||
let x = center(tile_size * Board::WIDTH as i32, win_width) + x * tile_size;
|
||||
let y = center(tile_size * Board::HEIGHT as i32, win_height) + y * tile_size;
|
||||
if filled {
|
||||
self.fill_rect(x, y, 24, 24, color)?;
|
||||
} else {
|
||||
self.outline_rect(x, y, 24, 24, color)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_board(&mut self, board: &Board, current: &CurrentTetromino) -> Result<(), Err> {
|
||||
let (win_width, win_height) = self.window_size()?;
|
||||
self.outline_rect(
|
||||
center(24 * Board::WIDTH as i32, win_width) - 1,
|
||||
center(24 * Board::HEIGHT as i32, win_height) - 1,
|
||||
24 * Board::WIDTH as i32 + 2,
|
||||
24 * Board::HEIGHT as i32 + 2,
|
||||
&Rgb(255, 255, 255),
|
||||
)?;
|
||||
|
||||
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),
|
||||
};
|
||||
self.draw_board_tile(x as i32, y as i32, &color, true)?
|
||||
}
|
||||
}
|
||||
|
||||
let pattern = current.tetromino.direction_pattern(¤t.direction);
|
||||
|
||||
self.draw_tetromino_from_parts(
|
||||
current.x,
|
||||
board.lowest_y(¤t),
|
||||
Rgb(255, 255, 255),
|
||||
pattern,
|
||||
false,
|
||||
)?;
|
||||
|
||||
self.draw_tetromino_from_parts(
|
||||
current.x,
|
||||
current.y,
|
||||
Rgb::from_tetromino(¤t.tetromino),
|
||||
pattern,
|
||||
true,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_important_text<P: AsRef<std::path::Path>, Text: AsRef<str>>(
|
||||
&mut self,
|
||||
font: P,
|
||||
text: Text,
|
||||
) -> Result<(), Err> {
|
||||
let (win_width, win_height) = self.window_size()?;
|
||||
let size = self.text_size(font.as_ref(), text.as_ref())?;
|
||||
let width = size.0;
|
||||
let height = size.1;
|
||||
|
||||
let x = center(width, win_width);
|
||||
let y = center(height, win_height);
|
||||
|
||||
self.outline_rect(x - 9, y - 9, width + 18, height + 18, &Rgb(255, 255, 255))?;
|
||||
|
||||
self.fill_rect(x - 8, y - 8, width + 16, height + 16, &Rgb(16, 16, 16))?;
|
||||
self.fill_text(font, text, x, y, width, height)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Err> GameUiCtx<Err> for T where T: UiCtx<Err> {}
|
||||
|
||||
pub struct Rgb(pub u8, pub u8, pub u8);
|
||||
|
||||
impl Rgb {
|
||||
pub fn from_tetromino(tetromino: &Tetromino) -> Self {
|
||||
match tetromino {
|
||||
Tetromino::I => Self(0, 255, 255),
|
||||
Tetromino::J => Self(0, 0, 255),
|
||||
Tetromino::L => Self(255, 128, 0),
|
||||
Tetromino::O => Self(255, 255, 0),
|
||||
Tetromino::S => Self(0, 255, 0),
|
||||
Tetromino::T => Self(255, 0, 255),
|
||||
Tetromino::Z => Self(255, 0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn center(length: i32, max: i32) -> i32 {
|
||||
(max - length) / 2
|
||||
}
|
287
src/main.rs
287
src/main.rs
@ -1,288 +1,11 @@
|
||||
use actions::{Controls, ControlsHeld};
|
||||
use board::Board;
|
||||
use tetromino::{Direction, DirectionDiff, Tetromino};
|
||||
use tetromino::Tetromino;
|
||||
|
||||
mod actions;
|
||||
mod board;
|
||||
mod game;
|
||||
mod gui;
|
||||
mod tetromino;
|
||||
|
||||
struct Rgb(u8, u8, u8);
|
||||
|
||||
struct CurrentTetromino {
|
||||
tetromino: Tetromino,
|
||||
direction: Direction,
|
||||
x: i8,
|
||||
y: i8,
|
||||
}
|
||||
|
||||
impl CurrentTetromino {
|
||||
fn new(tetromino: Tetromino) -> Self {
|
||||
const PIECE_WIDTH: i8 = 2;
|
||||
Self {
|
||||
tetromino,
|
||||
direction: Direction::Up,
|
||||
x: (Board::WIDTH as i8 - PIECE_WIDTH) / 2,
|
||||
y: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Game {
|
||||
board: Board,
|
||||
next_tetrominos: [Tetromino; 3],
|
||||
current_tetromino: CurrentTetromino,
|
||||
held_tetromino: Option<Tetromino>,
|
||||
has_swapped_held: bool,
|
||||
score: Score,
|
||||
ticks: usize,
|
||||
}
|
||||
|
||||
struct Score {
|
||||
level: usize,
|
||||
points: usize,
|
||||
lines: usize,
|
||||
combo: usize,
|
||||
back_to_back: bool,
|
||||
}
|
||||
|
||||
impl Score {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
level: 0,
|
||||
points: 0,
|
||||
lines: 0,
|
||||
combo: 0,
|
||||
back_to_back: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn level_up(&mut self, lines_cleared: usize) {
|
||||
self.lines += lines_cleared;
|
||||
if self.lines > self.level * 5 {
|
||||
self.level += 1;
|
||||
self.lines = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn point_multiplier_from_lines_cleared(lines_cleared: usize) -> f32 {
|
||||
match lines_cleared {
|
||||
0 => 0.0,
|
||||
1 => 100.0,
|
||||
2 => 300.0,
|
||||
3 => 500.0,
|
||||
4 => 800.0,
|
||||
_ => unreachable!("we cannot clear more than 4 lines"),
|
||||
}
|
||||
}
|
||||
|
||||
fn combos(&self, lines_cleared: usize) -> usize {
|
||||
if lines_cleared > 0 {
|
||||
self.combo * 50 * self.level
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn take_next_in_bag(&mut self, mut last: Tetromino) -> Tetromino {
|
||||
for value in self.next_tetrominos.iter_mut().rev() {
|
||||
std::mem::swap(value, &mut last)
|
||||
}
|
||||
last
|
||||
}
|
||||
|
||||
fn hard_drop(&mut self, controls: &ControlsHeld) {
|
||||
if !controls.just_pressed(self.ticks, &Controls::HardDrop) {
|
||||
return;
|
||||
}
|
||||
let start_y = self.current_tetromino.y;
|
||||
loop {
|
||||
self.current_tetromino.y += 1;
|
||||
if !self.board.colliding(&self.current_tetromino) {
|
||||
continue;
|
||||
}
|
||||
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();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fn soft_drop(&mut self, controls: &ControlsHeld) {
|
||||
let mut delay = 32 - self.score.level * 2;
|
||||
if controls.contains_key(&Controls::SoftDrop) {
|
||||
delay /= 10;
|
||||
}
|
||||
|
||||
if self.ticks % delay != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.current_tetromino.y += 1;
|
||||
if self.board.colliding(&self.current_tetromino) {
|
||||
self.current_tetromino.y -= 1;
|
||||
self.place_current_tetromino();
|
||||
self.check_line_clears();
|
||||
} else if controls.contains_key(&Controls::SoftDrop) {
|
||||
self.score.points += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn move_horizontally(&mut self, controls: &ControlsHeld) {
|
||||
for key in [Controls::Left, Controls::Right] {
|
||||
let just_pressed = controls.just_pressed(self.ticks, &key);
|
||||
let long_press = controls.held_for(self.ticks, &key, |held_for| held_for > 15);
|
||||
if !just_pressed && !long_press {
|
||||
continue;
|
||||
}
|
||||
let offset = match key {
|
||||
Controls::Left => -1,
|
||||
Controls::Right => 1,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
self.current_tetromino.x += offset;
|
||||
if self.board.colliding(&self.current_tetromino) {
|
||||
self.current_tetromino.x -= offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_line_clears(&mut self) {
|
||||
let lines_cleared = self.board.lines_cleared();
|
||||
|
||||
self.score.level_up(lines_cleared);
|
||||
|
||||
let mut points =
|
||||
self.score.level as f32 * Score::point_multiplier_from_lines_cleared(lines_cleared);
|
||||
|
||||
if self.score.back_to_back && lines_cleared == 4 {
|
||||
points *= 1.5;
|
||||
}
|
||||
points += self.score.combos(lines_cleared) as f32;
|
||||
|
||||
self.score.points += points as usize;
|
||||
|
||||
if lines_cleared == 4 {
|
||||
self.score.back_to_back = true;
|
||||
} else if lines_cleared > 0 {
|
||||
self.score.back_to_back = false;
|
||||
}
|
||||
|
||||
if lines_cleared > 0 {
|
||||
self.score.combo += 1;
|
||||
// play_line_clears_sound();
|
||||
} else {
|
||||
self.score.combo = 0;
|
||||
// play_hard_drop_sound();
|
||||
}
|
||||
}
|
||||
|
||||
fn step(&mut self, controls: &ControlsHeld) {
|
||||
// TODO: ensure game is running at 60 ticks per second? (`if !check_update_time(context, 60) { return; }`)
|
||||
self.hard_drop(controls);
|
||||
self.soft_drop(controls);
|
||||
self.move_horizontally(controls);
|
||||
|
||||
if controls.just_pressed(self.ticks, &Controls::Swap) {
|
||||
self.try_swap_tetromino();
|
||||
}
|
||||
|
||||
for (control, direction) in [
|
||||
(Controls::RotateCw, DirectionDiff::Cw),
|
||||
(Controls::RotateCcw, DirectionDiff::Ccw),
|
||||
] {
|
||||
if !controls.just_pressed(self.ticks, &control) {
|
||||
continue;
|
||||
}
|
||||
self.try_rotate(direction);
|
||||
}
|
||||
self.ticks += 1;
|
||||
}
|
||||
|
||||
fn try_rotate(&mut self, diff: DirectionDiff) -> bool {
|
||||
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;
|
||||
}
|
||||
let wall_kicks = self
|
||||
.current_tetromino
|
||||
.tetromino
|
||||
.wall_kicks(&old_direction, &diff);
|
||||
|
||||
for (x, y) in wall_kicks {
|
||||
self.current_tetromino.x += x;
|
||||
self.current_tetromino.y += y;
|
||||
if !(self.board.colliding(&self.current_tetromino)) {
|
||||
return true;
|
||||
}
|
||||
self.current_tetromino.x -= x;
|
||||
self.current_tetromino.y -= y;
|
||||
}
|
||||
|
||||
self.current_tetromino.direction = old_direction;
|
||||
false
|
||||
}
|
||||
|
||||
fn place_current_tetromino(&mut self) {
|
||||
let next = CurrentTetromino::new(self.take_next_in_bag(Tetromino::random()));
|
||||
let current = std::mem::replace(&mut self.current_tetromino, next);
|
||||
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 y = (current.y + y as i8) as usize;
|
||||
let x = (current.x + x as i8) as usize;
|
||||
self.board[y][x] = Some(current.tetromino.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.has_swapped_held = false;
|
||||
}
|
||||
|
||||
fn try_swap_tetromino(&mut self) {
|
||||
if self.has_swapped_held {
|
||||
return;
|
||||
}
|
||||
self.has_swapped_held = true;
|
||||
let held_or_first_in_bag_tetromino = self
|
||||
.held_tetromino
|
||||
.take()
|
||||
.unwrap_or_else(|| self.take_next_in_bag(Tetromino::random()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{Board, CurrentTetromino, Game, Score, Tetromino};
|
||||
|
||||
#[test]
|
||||
fn advance_bag() {
|
||||
let mut game = Game {
|
||||
board: Board::new(),
|
||||
score: Score::new(),
|
||||
next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O],
|
||||
current_tetromino: CurrentTetromino::new(Tetromino::J),
|
||||
held_tetromino: None,
|
||||
has_swapped_held: false,
|
||||
ticks: 0,
|
||||
};
|
||||
assert_eq!(game.take_next_in_bag(Tetromino::S), Tetromino::I);
|
||||
assert_eq!(
|
||||
game.next_tetrominos,
|
||||
[Tetromino::J, Tetromino::O, Tetromino::S]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
gui::start_game().unwrap();
|
||||
}
|
||||
|
196
src/tetromino.rs
196
src/tetromino.rs
@ -1,5 +1,3 @@
|
||||
use crate::Rgb;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Tetromino {
|
||||
I,
|
||||
@ -53,65 +51,173 @@ impl Tetromino {
|
||||
}
|
||||
}
|
||||
|
||||
const fn color(&self) -> Rgb {
|
||||
match self {
|
||||
Self::I => Rgb(0, 255, 255),
|
||||
Self::J => Rgb(0, 0, 255),
|
||||
Self::L => Rgb(255, 128, 0),
|
||||
Self::O => Rgb(255, 255, 0),
|
||||
Self::S => Rgb(0, 255, 0),
|
||||
Self::T => Rgb(255, 0, 255),
|
||||
Self::Z => Rgb(255, 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn direction_pattern(&self, direction: &Direction) -> [[bool; 4]; 4] {
|
||||
let dir = match self {
|
||||
Self::I => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]],
|
||||
Direction::Left => [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]],
|
||||
Direction::Up => [
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '#', '#'],
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Right => [
|
||||
['-', '-', '#', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
],
|
||||
Direction::Down => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '#', '#'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Left => [
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
},
|
||||
Self::J => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 0, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], [0, 0, 1, 0]],
|
||||
Direction::Left => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0]],
|
||||
Direction::Up => [
|
||||
['-', '-', '-', '-'],
|
||||
['#', '-', '-', '-'],
|
||||
['#', '#', '#', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Right => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
Direction::Down => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '#', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
],
|
||||
Direction::Left => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
],
|
||||
},
|
||||
Self::L => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0]],
|
||||
Direction::Left => [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]],
|
||||
},
|
||||
Self::O => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Left => [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Up => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
['#', '#', '#', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Right => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
],
|
||||
Direction::Down => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '#', '-'],
|
||||
['#', '-', '-', '-'],
|
||||
],
|
||||
Direction::Left => [
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
},
|
||||
Self::S => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0]],
|
||||
Direction::Left => [[0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0]],
|
||||
Direction::Up => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Right => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
],
|
||||
Direction::Down => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
],
|
||||
Direction::Left => [
|
||||
['-', '-', '-', '-'],
|
||||
['#', '-', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
},
|
||||
Self::T => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 0], [0, 1, 0, 0]],
|
||||
Direction::Left => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0]],
|
||||
Direction::Up => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['#', '#', '#', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Right => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
Direction::Down => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '#', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
Direction::Left => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
},
|
||||
Self::Z => match direction {
|
||||
Direction::Up => [[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||
Direction::Right => [[0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0]],
|
||||
Direction::Down => [[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0]],
|
||||
Direction::Left => [[0, 0, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0]],
|
||||
Direction::Up => [
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
Direction::Right => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '#', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
],
|
||||
Direction::Down => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
],
|
||||
Direction::Left => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '-', '-'],
|
||||
['#', '#', '-', '-'],
|
||||
['#', '-', '-', '-'],
|
||||
],
|
||||
},
|
||||
Self::O => [
|
||||
['-', '-', '-', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '#', '#', '-'],
|
||||
['-', '-', '-', '-'],
|
||||
],
|
||||
};
|
||||
|
||||
dir.map(|row| row.map(|v| v != 0))
|
||||
dir.map(|row| row.map(|v| v != '-'))
|
||||
}
|
||||
|
||||
pub const fn wall_kicks(&self, direction: &Direction, diff: &DirectionDiff) -> [(i8, i8); 5] {
|
||||
|
Loading…
Reference in New Issue
Block a user