diff --git a/Cargo.toml b/Cargo.toml index d7f32ce..e73523f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,8 @@ edition = "2021" [dependencies] pancurses = "0.17.0" rand = "0.8.5" +serde = "1.0.136" +serde_derive = "1.0.136" +toml = "0.5.8" [features] diff --git a/README.md b/README.md index 59997c0..29f6c4f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ # tetris +Keybinds can be edited in `~/.config/tetris/binds.toml`. + +Default bindings: + + - Clockwise rotation: 'x' or up arrow + - Counterclockwise rotation: 'z' + - Hard drop: spacebar + - Soft drop: down arrow + - Hold: 'c' + - Move left: left arrow + - Move right: right arrow + - Quit: 'q' \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 368fb1b..743ee48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ -use std::{collections::VecDeque, time::{Instant, Duration}, thread}; +use std::{collections::VecDeque, time::{Instant, Duration}, thread, fs::{self, File, OpenOptions}, env, path::PathBuf, io::{Read, self, ErrorKind, Write}, process}; use data::{TetriminoData, WallkickTable, WALLKICK_TABLE_1, WALLKICK_TABLE_2, Rotation}; use pancurses::{initscr, noecho, Input, endwin, start_color, init_color, use_default_colors, init_pair, COLOR_RED, COLOR_CYAN, COLOR_YELLOW, COLOR_MAGENTA, COLOR_GREEN, COLOR_BLUE, Window, COLOR_WHITE, curs_set, cbreak}; use rand::prelude::*; +use serde::de::{Visitor, self}; +use serde_derive::{Deserialize, Serialize}; mod data; @@ -235,7 +237,142 @@ fn lock_tetrimino(tetrimino: Tetrimino, board: &mut [[Option::; 10]; 40], } } +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Binds { + cw_rot: Vec, + ccw_rot: Vec, + hard_drop: Vec, + soft_drop: Vec, + hold: Vec, + // pause: Vec, + left: Vec, + right: Vec, + quit: Vec, +} + +#[derive(Debug, Clone, Copy)] +enum Bind { + Char(char), + UpArrow, + DownArrow, + LeftArrow, + RightArrow, +} + +impl<'de> serde::Deserialize<'de> for Bind { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> { + deserializer.deserialize_str(BindVisitor) + } +} + +struct BindVisitor; + +impl<'de> Visitor<'de> for BindVisitor { + type Value = Bind; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + // doesnt appear this is ever called, still implementing it + write!(formatter, r#"expecting a char or one of "up", "down", "left", "right""#) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v.chars().count() == 1 { + Ok(Bind::Char(v.chars().next().unwrap())) + } else { + match v { + "up" => Ok(Bind::UpArrow), + "down" => Ok(Bind::DownArrow), + "left" => Ok(Bind::LeftArrow), + "right" => Ok(Bind::RightArrow), + _ => Err(de::Error::unknown_variant(v, &["", "up", "down", "left", "right"])) + } + } + } +} + +impl serde::Serialize for Bind { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer { + let s; + serializer.serialize_str(match self { + Bind::Char(c) => { + s = c.to_string(); + &s + }, + Bind::UpArrow => "up", + Bind::DownArrow => "down", + Bind::LeftArrow => "left", + Bind::RightArrow => "right", + }) + } +} + +impl Default for Binds { + fn default() -> Self { + Self { + cw_rot: vec![Bind::Char('x'), Bind::UpArrow], + ccw_rot: vec![Bind::Char('z')], + hard_drop: vec![Bind::Char(' ')], + soft_drop: vec![Bind::DownArrow], + hold: vec![Bind::Char('c')], + // pause: vec![Bind::Char('`')], + left: vec![Bind::LeftArrow], + right: vec![Bind::RightArrow], + quit: vec![Bind::Char('q')] + } + } +} + +impl PartialEq for Bind { + fn eq(&self, other: &Input) -> bool { + match (self, other) { + (Self::Char(l), Input::Character(f)) => l == f, + (Self::UpArrow, Input::KeyUp) => true, + (Self::DownArrow, Input::KeyDown) => true, + (Self::LeftArrow, Input::KeyLeft) => true, + (Self::RightArrow, Input::KeyRight) => true, + _ => false + } + } +} + +fn get_binds() -> io::Result { + let home = env::var("HOME"); + if let Ok(home) = home { + let mut path = PathBuf::from(home); + path.push(".config"); + path.push("tetris"); + fs::create_dir_all(&path)?; + path.push("binds.toml"); + let mut file = File::open(&path).or_else(|e| { + if e.kind() == ErrorKind::NotFound { + let mut file = File::create(&path)?; + file.write_all(toml::to_string_pretty(&Binds::default()).unwrap().as_bytes())?; + File::open(&path) + } else { + Err(e) + } + })?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(toml::from_str(&contents).unwrap_or_else(|e| { + eprintln!("Error parsing `{}`:\n{}", path.to_str().unwrap(), e); + process::exit(1); + })) + } else { + Ok(Default::default()) + } +} + fn main() { + let binds = get_binds().unwrap_or(Default::default()); + let mut board = [[None; 10]; 40]; let window = initscr(); @@ -288,25 +425,25 @@ fn main() { let key = window.getch(); if let Some(key) = key { - if key == Input::Character('q') { + if binds.quit.iter().any(|v| *v == key) { break } - if key == Input::KeyLeft { + if binds.left.iter().any(|v| *v == key) { if check_tetrimino_valid(tetrimino.into(), board, x - 1, y) { needs_refresh = true; x -= 1; } } - if key == Input::KeyRight { + if binds.right.iter().any(|v| *v == key) { if check_tetrimino_valid(tetrimino.into(), board, x + 1, y) { needs_refresh = true; x += 1; } } - if key == Input::KeyUp || key == Input::Character('x') { + if binds.cw_rot.iter().any(|v| *v == key) { for kick in tetrimino.wallkick_table().for_rotation_cw(tetrimino.rotation) { tetrimino.rotate_cw(); if check_tetrimino_valid(tetrimino.into(), board, x + kick.0, y - kick.1) { @@ -320,7 +457,7 @@ fn main() { } } - if key == Input::Character('z') { + if binds.ccw_rot.iter().any(|v| *v == key) { for kick in tetrimino.wallkick_table().for_rotation_ccw(tetrimino.rotation) { tetrimino.rotate_ccw(); if check_tetrimino_valid(tetrimino.into(), board, x + kick.0, y - kick.1) { @@ -334,7 +471,7 @@ fn main() { } } - if key == Input::KeyDown { + if binds.soft_drop.iter().any(|v| *v == key) { if check_tetrimino_valid(tetrimino.into(), board, x, y + 1) { needs_refresh = true; y += 1; @@ -346,7 +483,7 @@ fn main() { } } - if key == Input::Character(' ') { + if binds.hard_drop.iter().any(|v| *v == key) { // while !drop_tetrimino(tetrimino, &mut board, &mut x, &mut y) {} while check_tetrimino_valid(tetrimino.data(), board, x, y + 1) { y += 1; @@ -357,7 +494,7 @@ fn main() { reset!(next_fall, tetrimino, y, x, hold_used, rando, board); } - if key == Input::Character('c') { + if binds.hold.iter().any(|v| *v == key) { if !hold_used { tetrimino.rotation = Rotation::Spawn; let held = hold.replace(tetrimino);