Compare commits
2 Commits
91c18ff16f
...
e11653d8dc
Author | SHA1 | Date | |
---|---|---|---|
e11653d8dc | |||
7caef1ce4a |
213
src/config.rs
Normal file
213
src/config.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "key")]
|
||||||
|
pub enum Key {
|
||||||
|
Zero,
|
||||||
|
One,
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
Five,
|
||||||
|
Six,
|
||||||
|
Seven,
|
||||||
|
Eight,
|
||||||
|
Nine,
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
F,
|
||||||
|
G,
|
||||||
|
H,
|
||||||
|
I,
|
||||||
|
J,
|
||||||
|
K,
|
||||||
|
L,
|
||||||
|
M,
|
||||||
|
N,
|
||||||
|
O,
|
||||||
|
P,
|
||||||
|
Q,
|
||||||
|
R,
|
||||||
|
S,
|
||||||
|
T,
|
||||||
|
U,
|
||||||
|
V,
|
||||||
|
W,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Enter,
|
||||||
|
Backspace,
|
||||||
|
Space,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Key {
|
||||||
|
pub fn from_sdl2_keycode(keycode: sdl2::keyboard::Keycode) -> Option<Key> {
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
let v = match keycode {
|
||||||
|
Keycode::Num0 | Keycode::Kp0 => Key::Zero,
|
||||||
|
Keycode::Num1 | Keycode::Kp1 => Key::One,
|
||||||
|
Keycode::Num2 | Keycode::Kp2 => Key::Two,
|
||||||
|
Keycode::Num3 | Keycode::Kp3 => Key::Three,
|
||||||
|
Keycode::Num4 | Keycode::Kp4 => Key::Four,
|
||||||
|
Keycode::Num5 | Keycode::Kp5 => Key::Five,
|
||||||
|
Keycode::Num6 | Keycode::Kp6 => Key::Six,
|
||||||
|
Keycode::Num7 | Keycode::Kp7 => Key::Seven,
|
||||||
|
Keycode::Num8 | Keycode::Kp8 => Key::Eight,
|
||||||
|
Keycode::Num9 | Keycode::Kp9 => Key::Nine,
|
||||||
|
Keycode::A => Key::A,
|
||||||
|
Keycode::B => Key::B,
|
||||||
|
Keycode::C => Key::C,
|
||||||
|
Keycode::D => Key::D,
|
||||||
|
Keycode::E => Key::E,
|
||||||
|
Keycode::F => Key::F,
|
||||||
|
Keycode::G => Key::G,
|
||||||
|
Keycode::H => Key::H,
|
||||||
|
Keycode::I => Key::I,
|
||||||
|
Keycode::J => Key::J,
|
||||||
|
Keycode::K => Key::K,
|
||||||
|
Keycode::L => Key::L,
|
||||||
|
Keycode::M => Key::M,
|
||||||
|
Keycode::N => Key::N,
|
||||||
|
Keycode::O => Key::O,
|
||||||
|
Keycode::P => Key::P,
|
||||||
|
Keycode::Q => Key::Q,
|
||||||
|
Keycode::R => Key::R,
|
||||||
|
Keycode::S => Key::S,
|
||||||
|
Keycode::T => Key::T,
|
||||||
|
Keycode::U => Key::U,
|
||||||
|
Keycode::V => Key::V,
|
||||||
|
Keycode::W => Key::W,
|
||||||
|
Keycode::X => Key::X,
|
||||||
|
Keycode::Y => Key::Y,
|
||||||
|
Keycode::Z => Key::Z,
|
||||||
|
Keycode::Up => Key::Up,
|
||||||
|
Keycode::Down => Key::Down,
|
||||||
|
Keycode::Left => Key::Left,
|
||||||
|
Keycode::Right => Key::Right,
|
||||||
|
Keycode::Return => Key::Enter,
|
||||||
|
Keycode::Backspace => Key::Backspace,
|
||||||
|
Keycode::Space => Key::Space,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Key {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let val = match self {
|
||||||
|
Key::Zero => "0",
|
||||||
|
Key::One => "1",
|
||||||
|
Key::Two => "2",
|
||||||
|
Key::Three => "3",
|
||||||
|
Key::Four => "4",
|
||||||
|
Key::Five => "5",
|
||||||
|
Key::Six => "6",
|
||||||
|
Key::Seven => "7",
|
||||||
|
Key::Eight => "8",
|
||||||
|
Key::Nine => "9",
|
||||||
|
Key::A => "A",
|
||||||
|
Key::B => "B",
|
||||||
|
Key::C => "C",
|
||||||
|
Key::D => "D",
|
||||||
|
Key::E => "E",
|
||||||
|
Key::F => "F",
|
||||||
|
Key::G => "G",
|
||||||
|
Key::H => "H",
|
||||||
|
Key::I => "I",
|
||||||
|
Key::J => "J",
|
||||||
|
Key::K => "K",
|
||||||
|
Key::L => "L",
|
||||||
|
Key::M => "M",
|
||||||
|
Key::N => "N",
|
||||||
|
Key::O => "O",
|
||||||
|
Key::P => "P",
|
||||||
|
Key::Q => "Q",
|
||||||
|
Key::R => "R",
|
||||||
|
Key::S => "S",
|
||||||
|
Key::T => "T",
|
||||||
|
Key::U => "U",
|
||||||
|
Key::V => "V",
|
||||||
|
Key::W => "W",
|
||||||
|
Key::X => "X",
|
||||||
|
Key::Y => "Y",
|
||||||
|
Key::Z => "Z",
|
||||||
|
Key::Up => "Up",
|
||||||
|
Key::Down => "Down",
|
||||||
|
Key::Left => "Left",
|
||||||
|
Key::Right => "Right",
|
||||||
|
Key::Enter => "Enter",
|
||||||
|
Key::Backspace => "Backspace",
|
||||||
|
Key::Space => "Space",
|
||||||
|
};
|
||||||
|
write!(f, "{val}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub reimtris1_feature_parity: bool,
|
||||||
|
pub restart: Vec<Key>,
|
||||||
|
pub left: Vec<Key>,
|
||||||
|
pub right: Vec<Key>,
|
||||||
|
pub rotate_cw: Vec<Key>,
|
||||||
|
pub rotate_ccw: Vec<Key>,
|
||||||
|
pub soft_drop: Vec<Key>,
|
||||||
|
pub hard_drop: Vec<Key>,
|
||||||
|
pub swap: Vec<Key>,
|
||||||
|
pub pause: Vec<Key>,
|
||||||
|
pub toggle_mute: Vec<Key>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
reimtris1_feature_parity: false,
|
||||||
|
restart: vec![Key::Enter, Key::Space],
|
||||||
|
left: vec![Key::Left],
|
||||||
|
right: vec![Key::Right],
|
||||||
|
rotate_cw: vec![Key::X],
|
||||||
|
rotate_ccw: vec![Key::Z],
|
||||||
|
soft_drop: vec![Key::Down],
|
||||||
|
hard_drop: vec![Key::Space],
|
||||||
|
swap: vec![Key::C],
|
||||||
|
pause: vec![Key::P],
|
||||||
|
toggle_mute: vec![Key::M],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Config, String> {
|
||||||
|
let Some(config) = fs::read_to_string(path.as_ref()).ok() else {
|
||||||
|
let config = Config::default();
|
||||||
|
{
|
||||||
|
println!("could not get config! creating default...");
|
||||||
|
let config = toml::to_string(&config).map_err(|err| err.to_string())?;
|
||||||
|
fs::write(path.as_ref(), config).map_err(|err| err.to_string())?;
|
||||||
|
println!("created config at '{}'", path.as_ref().display());
|
||||||
|
}
|
||||||
|
return Ok(config);
|
||||||
|
};
|
||||||
|
let Some(config) = toml::from_str(&config).ok() else {
|
||||||
|
println!("womp womp, config contains an invalid config, resetting...");
|
||||||
|
let config = Config::default();
|
||||||
|
{
|
||||||
|
let config = toml::to_string(&config).map_err(|err| err.to_string())?;
|
||||||
|
fs::write(path.as_ref(), config).map_err(|err| err.to_string())?;
|
||||||
|
println!("created config at '{}'", path.as_ref().display());
|
||||||
|
}
|
||||||
|
return Ok(config);
|
||||||
|
};
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
}
|
159
src/game.rs
159
src/game.rs
@ -2,6 +2,13 @@ use crate::actions::{Action, ActionsHeld};
|
|||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::tetromino::{Direction, DirectionDiff, Tetromino};
|
use crate::tetromino::{Direction, DirectionDiff, Tetromino};
|
||||||
|
|
||||||
|
pub enum SoundEffect {
|
||||||
|
HardDrop,
|
||||||
|
LineClear(usize),
|
||||||
|
Move,
|
||||||
|
Rotation,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CurrentTetromino {
|
pub struct CurrentTetromino {
|
||||||
pub tetromino: Tetromino,
|
pub tetromino: Tetromino,
|
||||||
@ -42,6 +49,7 @@ pub struct Game {
|
|||||||
pub game_over: bool,
|
pub game_over: bool,
|
||||||
pub board: Board,
|
pub board: Board,
|
||||||
pub next_tetrominos: [Tetromino; 3],
|
pub next_tetrominos: [Tetromino; 3],
|
||||||
|
bag: Bag,
|
||||||
pub current_tetromino: CurrentTetromino,
|
pub current_tetromino: CurrentTetromino,
|
||||||
pub held_tetromino: Option<Tetromino>,
|
pub held_tetromino: Option<Tetromino>,
|
||||||
has_swapped_held: bool,
|
has_swapped_held: bool,
|
||||||
@ -49,74 +57,68 @@ pub struct Game {
|
|||||||
pub ticks: usize,
|
pub ticks: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum SoundEffect {
|
struct Bag {
|
||||||
HardDrop,
|
inner: [Tetromino; 7],
|
||||||
LineClear(usize),
|
idx: usize,
|
||||||
Move,
|
|
||||||
Rotation,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Score {
|
impl Bag {
|
||||||
pub level: usize,
|
fn new() -> Self {
|
||||||
pub points: usize,
|
|
||||||
pub lines: usize,
|
|
||||||
pub combo: usize,
|
|
||||||
back_to_back: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Score {
|
|
||||||
const fn new() -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
level: 0,
|
inner: Self::random_tetrominos(),
|
||||||
points: 0,
|
idx: 0,
|
||||||
lines: 0,
|
|
||||||
combo: 0,
|
|
||||||
back_to_back: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn random_tetrominos() -> [Tetromino; 7] {
|
||||||
|
use rand::seq::IndexedRandom;
|
||||||
|
let sample = [
|
||||||
|
Tetromino::I,
|
||||||
|
Tetromino::J,
|
||||||
|
Tetromino::L,
|
||||||
|
Tetromino::O,
|
||||||
|
Tetromino::S,
|
||||||
|
Tetromino::T,
|
||||||
|
Tetromino::Z,
|
||||||
|
];
|
||||||
|
|
||||||
fn level_up(&mut self, lines_cleared: usize) {
|
debug_assert_eq!(sample.len(), 7, "each piece should only appear once");
|
||||||
self.lines += lines_cleared;
|
|
||||||
if self.lines > self.level * 5 {
|
sample
|
||||||
self.level += 1;
|
.choose_multiple_array(&mut rand::rng())
|
||||||
self.lines = 0;
|
.expect("both arrays should have a length of 7")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fn take_next(&mut self) -> Tetromino {
|
||||||
fn point_multiplier_from_lines_cleared(lines_cleared: usize) -> f32 {
|
if self.idx >= self.inner.len() {
|
||||||
match lines_cleared {
|
self.idx = 0;
|
||||||
0 => 0.0,
|
self.inner = Self::random_tetrominos();
|
||||||
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 {
|
let uninitialized_tetromino = Tetromino::I;
|
||||||
if lines_cleared > 0 {
|
let current = std::mem::replace(&mut self.inner[self.idx], uninitialized_tetromino);
|
||||||
self.combo * 50 * self.level
|
self.idx += 1;
|
||||||
} else {
|
current
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let mut bag = Bag::new();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
game_over: false,
|
game_over: false,
|
||||||
board: Board::new(),
|
board: Board::new(),
|
||||||
next_tetrominos: std::array::from_fn(|_| Tetromino::random()),
|
next_tetrominos: std::array::from_fn(|_| bag.take_next()),
|
||||||
current_tetromino: CurrentTetromino::new(Tetromino::random()),
|
current_tetromino: CurrentTetromino::new(bag.take_next()),
|
||||||
held_tetromino: None,
|
held_tetromino: None,
|
||||||
|
bag,
|
||||||
has_swapped_held: false,
|
has_swapped_held: false,
|
||||||
score: Score::new(),
|
score: Score::new(),
|
||||||
ticks: 0,
|
ticks: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn take_next_in_bag(&mut self, mut last: Tetromino) -> Tetromino {
|
|
||||||
|
fn take_next_up(&mut self) -> Tetromino {
|
||||||
|
let mut last = self.bag.take_next();
|
||||||
for value in self.next_tetrominos.iter_mut().rev() {
|
for value in self.next_tetrominos.iter_mut().rev() {
|
||||||
std::mem::swap(value, &mut last)
|
std::mem::swap(value, &mut last)
|
||||||
}
|
}
|
||||||
@ -265,7 +267,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn place_current_tetromino(&mut self) {
|
fn place_current_tetromino(&mut self) {
|
||||||
let next = CurrentTetromino::new(self.take_next_in_bag(Tetromino::random()));
|
let next = CurrentTetromino::new(self.take_next_up());
|
||||||
let current = std::mem::replace(&mut self.current_tetromino, next);
|
let current = std::mem::replace(&mut self.current_tetromino, next);
|
||||||
let pattern = current.tetromino.pattern(¤t.direction);
|
let pattern = current.tetromino.pattern(¤t.direction);
|
||||||
|
|
||||||
@ -301,7 +303,7 @@ impl Game {
|
|||||||
let held_or_first_in_bag_tetromino = self
|
let held_or_first_in_bag_tetromino = self
|
||||||
.held_tetromino
|
.held_tetromino
|
||||||
.take()
|
.take()
|
||||||
.unwrap_or_else(|| self.take_next_in_bag(Tetromino::random()));
|
.unwrap_or_else(|| self.take_next_up());
|
||||||
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);
|
||||||
@ -309,26 +311,49 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub struct Score {
|
||||||
mod test {
|
pub level: usize,
|
||||||
use super::{Board, CurrentTetromino, Game, Score, Tetromino};
|
pub points: usize,
|
||||||
|
pub lines: usize,
|
||||||
|
pub combo: usize,
|
||||||
|
back_to_back: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
impl Score {
|
||||||
fn advance_bag() {
|
const fn new() -> Self {
|
||||||
let mut game = Game {
|
Self {
|
||||||
game_over: false,
|
level: 0,
|
||||||
board: Board::new(),
|
points: 0,
|
||||||
score: Score::new(),
|
lines: 0,
|
||||||
next_tetrominos: [Tetromino::I, Tetromino::J, Tetromino::O],
|
combo: 0,
|
||||||
current_tetromino: CurrentTetromino::new(Tetromino::J),
|
back_to_back: false,
|
||||||
held_tetromino: None,
|
}
|
||||||
has_swapped_held: false,
|
}
|
||||||
ticks: 0,
|
|
||||||
};
|
fn level_up(&mut self, lines_cleared: usize) {
|
||||||
assert_eq!(game.take_next_in_bag(Tetromino::S), Tetromino::I);
|
self.lines += lines_cleared;
|
||||||
assert_eq!(
|
if self.lines > self.level * 5 {
|
||||||
game.next_tetrominos,
|
self.level += 1;
|
||||||
[Tetromino::J, Tetromino::O, Tetromino::S]
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,20 +38,6 @@ pub enum DirectionDiff {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tetromino {
|
impl Tetromino {
|
||||||
pub fn random() -> Self {
|
|
||||||
let v: u8 = rand::random();
|
|
||||||
match v % 7 {
|
|
||||||
0 => Self::I,
|
|
||||||
1 => Self::J,
|
|
||||||
2 => Self::L,
|
|
||||||
3 => Self::O,
|
|
||||||
4 => Self::S,
|
|
||||||
5 => Self::T,
|
|
||||||
6 => Self::Z,
|
|
||||||
_ => unreachable!("v%7 is always in range 0..=6"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pattern(&self, direction: &Direction) -> Vec<(usize, usize)> {
|
pub fn pattern(&self, direction: &Direction) -> Vec<(usize, usize)> {
|
||||||
self.raw_pattern(direction)
|
self.raw_pattern(direction)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
Loading…
Reference in New Issue
Block a user