diff --git a/README.md b/README.md index efbb5e6..372981b 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ Classic Tetris (NES Tetris) written in BEVY/RUST. | ------: | :-------------------------------------- | | System | System default random number generator. | +**SCORING** + +| Options | | 123 | 1,234,567 | 3,704,567 | 39,504,567 | +| ------: | :---------------------------------------------------------------- | -------: | --------: | --------: | ---------: | +| Decimal | Display the score in decimal up to `2^64`. | `000123` | `1234567` | `3704567` | `39504567` | +| Classic | Display the score in decimal up to `999999`. | `000123` | `999999` | `999999` | `999999` | +| Base36 | Apply `base36` encoding for the 6th digit and above of the score. | `000123` | `C34567` | `1104567` | `AZ04567` | + **TV SYSTEM** | Options | | diff --git a/src/game/board.rs b/src/game/board.rs index 619d71c..04c5952 100644 --- a/src/game/board.rs +++ b/src/game/board.rs @@ -2,7 +2,6 @@ use std::collections::VecDeque; use super::{ piece::{Piece, Square}, - score::get_score, seed::Seed, transition::Transition, }; @@ -142,7 +141,7 @@ impl Board { self.squares.remove(*row); }); - self.score += get_score(rows.len(), self.level()); + self.score += Self::get_score(rows.len(), self.level()); self.lines += rows.len(); match rows.len() { 1 => self.single += 1, @@ -273,6 +272,18 @@ impl Board { fn is_inside(x: i32, y: i32) -> bool { x >= 0 && x < Self::BOARD_COLS as i32 && y >= 0 && y < Self::INTERNAL_BOARD_ROWS as i32 } + + fn get_score(lines: usize, level: usize) -> usize { + (level + 1) + * match lines { + 0 => 0, + 1 => 40, + 2 => 100, + 3 => 300, + 4 => 1200, + _ => panic!("can only clear lines between 1-4"), + } + } } impl Default for Board { diff --git a/src/game/game.rs b/src/game/game.rs index 951a7ac..bcfad52 100644 --- a/src/game/game.rs +++ b/src/game/game.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use super::{ das_counter::DASCounter, gravity::Gravity, linecap::Linecap, next_piece_hint::NextPieceHint, - seed::Seed, transition::Transition, tv_system::TVSystem, + score::Scoring, seed::Seed, transition::Transition, tv_system::TVSystem, }; #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, States)] @@ -20,6 +20,7 @@ pub struct GameConfig { pub linecap: Linecap, pub gravity: Gravity, pub seed: Seed, + pub scoring: Scoring, pub tv_system: TVSystem, pub next_piece_hint: NextPieceHint, pub das_counter: DASCounter, @@ -33,6 +34,7 @@ impl Default for GameConfig { transition: Transition::default(), gravity: Gravity::default(), seed: Seed::default(), + scoring: Scoring::default(), tv_system: TVSystem::default(), next_piece_hint: NextPieceHint::default(), das_counter: DASCounter::default(), diff --git a/src/game/plugin.rs b/src/game/plugin.rs index d0ebabb..7fe2bb6 100644 --- a/src/game/plugin.rs +++ b/src/game/plugin.rs @@ -684,7 +684,7 @@ fn update_statistics_system( text.sections[1].value = format!("{:03}", player_data.board.lines()); } if let Ok(mut text) = query.p0().p1().get_single_mut() { - text.sections[1].value = format!("{:07}", player_data.board.score()); + text.sections[1].value = game_config.scoring.format(player_data.board.score()); } if let Ok(mut text) = query.p0().p2().get_single_mut() { text.sections[1].value = format!("{:02}", player_data.board.level()); diff --git a/src/game/score.rs b/src/game/score.rs index bc94c2a..e714ebd 100644 --- a/src/game/score.rs +++ b/src/game/score.rs @@ -1,11 +1,124 @@ -pub fn get_score(lines: usize, level: usize) -> usize { - (level + 1) - * match lines { - 0 => 0, - 1 => 40, - 2 => 100, - 3 => 300, - 4 => 1200, - _ => panic!("can only clear lines between 1-4"), +use std::fmt::Display; + +use num_traits::FromPrimitive; + +#[derive(Default, Clone, Copy, PartialEq, Eq, FromPrimitive)] +pub enum Scoring { + #[default] + Decimal, + Classic, + Base36, +} + +impl Scoring { + pub fn enum_has_prev(&self) -> bool { + ::from_i64(*self as i64 - 1).is_some() + } + + pub fn enum_has_next(&self) -> bool { + ::from_i64(*self as i64 + 1).is_some() + } + + pub fn enum_prev(&mut self) -> bool { + match FromPrimitive::from_i64(*self as i64 - 1) { + Some(n) => { + *self = n; + true + } + None => false, + } + } + + pub fn enum_next(&mut self) -> bool { + match FromPrimitive::from_i64(*self as i64 + 1) { + Some(n) => { + *self = n; + true + } + None => false, + } + } + + pub fn format(&self, score: usize) -> String { + match self { + Scoring::Decimal => format!("{:06}", score), + Scoring::Classic => { + if score > 999999 { + "999999".into() + } else { + format!("{:06}", score) + } + } + Scoring::Base36 => format!( + "{}{:05}", + Self::format_base36(score / 100000), + score % 100000 + ), + } + } + + pub fn format_base36(mut value: usize) -> String { + let mut s = String::new(); + + while value > 0 { + s.push(char::from_digit((value % 36) as u32, 36).unwrap()); + value /= 36; } + + if s.is_empty() { + s.push_str("0"); + } + + s.chars().map(|c| c.to_ascii_uppercase()).rev().collect() + } +} + +impl Display for Scoring { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Scoring::Decimal => f.write_str("DECIMAL"), + Scoring::Classic => f.write_str("CLASSIC"), + Scoring::Base36 => f.write_str("BASE36"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decimal() { + let scoring = Scoring::Decimal; + assert_eq!(scoring.format(123), "000123"); + assert_eq!(scoring.format(123456), "123456"); + assert_eq!(scoring.format(999999), "999999"); + assert_eq!(scoring.format(1000000), "1000000"); + } + + #[test] + fn test_classic() { + let scoring = Scoring::Classic; + assert_eq!(scoring.format(123), "000123"); + assert_eq!(scoring.format(123456), "123456"); + assert_eq!(scoring.format(999999), "999999"); + assert_eq!(scoring.format(1000000), "999999"); + } + + #[test] + fn test_base36() { + let scoring = Scoring::Base36; + assert_eq!(scoring.format(123), "000123"); + assert_eq!(scoring.format(123456), "123456"); + assert_eq!(scoring.format(999999), "999999"); + assert_eq!(scoring.format(1000000), "A00000"); + assert_eq!(scoring.format(1100000), "B00000"); + assert_eq!(scoring.format(1500000), "F00000"); + assert_eq!(scoring.format(1600000), "G00000"); + assert_eq!(scoring.format(3500000), "Z00000"); + assert_eq!(scoring.format(3600000), "1000000"); + assert_eq!(scoring.format(3700000), "1100000"); + assert_eq!(scoring.format(36000000), "A000000"); + assert_eq!(scoring.format(39500000), "AZ00000"); + } } diff --git a/src/game_option_menu/plugin.rs b/src/game_option_menu/plugin.rs index b4ef875..a8fd1ce 100644 --- a/src/game_option_menu/plugin.rs +++ b/src/game_option_menu/plugin.rs @@ -61,6 +61,7 @@ enum GameOptionMenuSelection { Linecap, Gravity, Seed, + Scoring, TVSystem, NextPieceHint, DASCounter, @@ -78,9 +79,9 @@ enum GameOptionMenuSelection { impl GameOptionMenuSelection { pub fn iter() -> std::slice::Iter<'static, GameOptionMenuSelection> { #[cfg(not(target_arch = "wasm32"))] - type ArrayType = [GameOptionMenuSelection; 18]; + type ArrayType = [GameOptionMenuSelection; 19]; #[cfg(target_arch = "wasm32")] - type ArrayType = [GameOptionMenuSelection; 16]; + type ArrayType = [GameOptionMenuSelection; 17]; const STATES: ArrayType = [ GameOptionMenuSelection::Tetris, GameOptionMenuSelection::BlankLine0, @@ -90,6 +91,7 @@ impl GameOptionMenuSelection { GameOptionMenuSelection::Linecap, GameOptionMenuSelection::Gravity, GameOptionMenuSelection::Seed, + GameOptionMenuSelection::Scoring, GameOptionMenuSelection::TVSystem, GameOptionMenuSelection::NextPieceHint, GameOptionMenuSelection::DASCounter, @@ -311,6 +313,14 @@ fn update_ui_system( game_config.seed.enum_has_next(), ); } + GameOptionMenuSelection::Scoring => { + text.sections[0].value = fname_opt("SCORING"); + text.sections[1].value = fopt( + game_config.scoring.to_string(), + game_config.scoring.enum_has_prev(), + game_config.scoring.enum_has_next(), + ); + } GameOptionMenuSelection::TVSystem => { text.sections[0].value = fname_opt("TV SYSTEM"); text.sections[1].value = fopt( @@ -509,7 +519,7 @@ fn handle_input_system( game_option_menu_data.selection = GameOptionMenuSelection::Gravity; selection_changed = true; } else if player_inputs.down.just_pressed { - game_option_menu_data.selection = GameOptionMenuSelection::TVSystem; + game_option_menu_data.selection = GameOptionMenuSelection::Scoring; selection_changed = true; } @@ -523,10 +533,29 @@ fn handle_input_system( } } } - GameOptionMenuSelection::TVSystem => { + GameOptionMenuSelection::Scoring => { if player_inputs.up.just_pressed { game_option_menu_data.selection = GameOptionMenuSelection::Seed; selection_changed = true; + } else if player_inputs.down.just_pressed { + game_option_menu_data.selection = GameOptionMenuSelection::TVSystem; + selection_changed = true; + } + + if player_inputs.right.just_pressed { + if game_config.scoring.enum_next() { + option_changed = true; + } + } else if player_inputs.left.just_pressed { + if game_config.scoring.enum_prev() { + option_changed = true; + } + } + } + GameOptionMenuSelection::TVSystem => { + if player_inputs.up.just_pressed { + game_option_menu_data.selection = GameOptionMenuSelection::Scoring; + selection_changed = true; } else if player_inputs.down.just_pressed { game_option_menu_data.selection = GameOptionMenuSelection::NextPieceHint; selection_changed = true;