mirror of
https://github.com/lucaspalomodevelop/binbreak.git
synced 2026-03-13 00:07:28 +00:00
commit
9a19822aaf
@ -33,9 +33,9 @@ There is one file for linux and one for windows (.exe).
|
||||
- run the game: `./binbreak-linux`
|
||||
|
||||
## Controls
|
||||
- use the arrow keys for navigation
|
||||
- use the arrow or vim keys for navigation
|
||||
- press Enter to confirm choices
|
||||
- press Esc to exit a game mode or the game. CTRL+C also works to exit the game.
|
||||
- press Esc or Q to exit a game mode or the game. CTRL+C also works to exit the game.
|
||||
|
||||
## Recommended terminals
|
||||
The game should run fine in any terminal. If you want retro CRT effects, here are some recommendations:
|
||||
|
||||
53
src/app.rs
53
src/app.rs
@ -1,4 +1,5 @@
|
||||
use crate::binary_numbers::{BinaryNumbersGame, Bits};
|
||||
use crate::keybinds;
|
||||
use crate::main_screen_widget::MainScreenWidget;
|
||||
use crate::utils::{AsciiArtWidget, AsciiCells};
|
||||
use crossterm::event;
|
||||
@ -37,16 +38,16 @@ enum AppState {
|
||||
}
|
||||
|
||||
fn handle_start_input(state: &mut StartMenuState, key: KeyEvent) -> Option<AppState> {
|
||||
match key.code {
|
||||
KeyCode::Up => state.select_previous(),
|
||||
KeyCode::Down => state.select_next(),
|
||||
KeyCode::Enter => {
|
||||
match key {
|
||||
x if keybinds::is_up(x) => state.select_previous(),
|
||||
x if keybinds::is_down(x) => state.select_next(),
|
||||
x if keybinds::is_select(x) => {
|
||||
let bits = state.selected_bits();
|
||||
// Store the current selection before entering the game
|
||||
set_last_selected_index(state.selected_index());
|
||||
return Some(AppState::Playing(BinaryNumbersGame::new(bits)));
|
||||
}
|
||||
KeyCode::Esc => return Some(AppState::Exit),
|
||||
x if keybinds::is_exit(x) => return Some(AppState::Exit),
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
@ -127,29 +128,29 @@ fn render_start_screen(state: &mut StartMenuState, area: Rect, buf: &mut Buffer)
|
||||
}
|
||||
|
||||
fn handle_crossterm_events(app_state: &mut AppState) -> color_eyre::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
// global exit via Ctrl+C
|
||||
KeyCode::Char('c') | KeyCode::Char('C')
|
||||
if key.modifiers == KeyModifiers::CONTROL =>
|
||||
{
|
||||
*app_state = AppState::Exit;
|
||||
}
|
||||
if let Event::Key(key) = event::read()?
|
||||
&& key.kind == KeyEventKind::Press
|
||||
{
|
||||
match key.code {
|
||||
// global exit via Ctrl+C
|
||||
KeyCode::Char('c' | 'C')
|
||||
if key.modifiers == KeyModifiers::CONTROL =>
|
||||
{
|
||||
*app_state = AppState::Exit;
|
||||
}
|
||||
|
||||
// state-specific input handling
|
||||
_ => {
|
||||
*app_state = match std::mem::replace(app_state, AppState::Exit) {
|
||||
AppState::Start(mut menu) => {
|
||||
handle_start_input(&mut menu, key)
|
||||
.unwrap_or(AppState::Start(menu))
|
||||
}
|
||||
AppState::Playing(mut game) => {
|
||||
game.handle_input(key);
|
||||
AppState::Playing(game)
|
||||
}
|
||||
AppState::Exit => AppState::Exit,
|
||||
// state-specific input handling
|
||||
_ => {
|
||||
*app_state = match std::mem::replace(app_state, AppState::Exit) {
|
||||
AppState::Start(mut menu) => {
|
||||
handle_start_input(&mut menu, key)
|
||||
.unwrap_or(AppState::Start(menu))
|
||||
}
|
||||
AppState::Playing(mut game) => {
|
||||
game.handle_input(key);
|
||||
AppState::Playing(game)
|
||||
}
|
||||
AppState::Exit => AppState::Exit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use crate::keybinds;
|
||||
use crate::main_screen_widget::{MainScreenWidget, WidgetRef};
|
||||
use crate::utils::{center, When};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
@ -168,7 +169,7 @@ impl WidgetRef for BinaryNumbersPuzzle {
|
||||
Block::bordered().border_type(border_type).fg(border_color).render(area, buf);
|
||||
|
||||
let suggestion_str = format!("{suggestion}");
|
||||
Paragraph::new(format!("{}", suggestion_str))
|
||||
Paragraph::new(suggestion_str.to_string())
|
||||
.white()
|
||||
.when(show_correct_number && is_correct_number, |p| p.light_green().underlined())
|
||||
.alignment(Center)
|
||||
@ -241,7 +242,7 @@ impl WidgetRef for BinaryNumbersPuzzle {
|
||||
|
||||
Block::bordered().dark_gray().render(result_area, buf);
|
||||
|
||||
let instruction_spans: Vec<Span> = vec![
|
||||
let instruction_spans: Vec<Span> = [
|
||||
hotkey_span("Left Right", "select "),
|
||||
hotkey_span("Enter", "confirm "),
|
||||
hotkey_span("S", "skip "),
|
||||
@ -293,7 +294,7 @@ impl MainScreenWidget for BinaryNumbersGame {
|
||||
self.refresh_stats_snapshot();
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, input: KeyEvent) -> () { self.handle_game_input(input); }
|
||||
fn handle_input(&mut self, input: KeyEvent) { self.handle_game_input(input); }
|
||||
fn is_exit_intended(&self) -> bool { self.exit_intended }
|
||||
}
|
||||
|
||||
@ -394,7 +395,7 @@ impl BinaryNumbersGame {
|
||||
}
|
||||
|
||||
pub fn handle_game_input(&mut self, input: KeyEvent) {
|
||||
if input.code == KeyCode::Esc { self.exit_intended = true; return; }
|
||||
if keybinds::is_exit(input) { self.exit_intended = true; return; }
|
||||
|
||||
if self.game_state == GameState::GameOver { self.handle_game_over_input(input); return; }
|
||||
match self.puzzle.guess_result {
|
||||
@ -403,10 +404,10 @@ impl BinaryNumbersGame {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_game_over_input(&mut self, input: KeyEvent) {
|
||||
match input.code {
|
||||
KeyCode::Enter => { self.reset_game_state(); }
|
||||
KeyCode::Esc => { self.exit_intended = true; }
|
||||
fn handle_game_over_input(&mut self, key: KeyEvent) {
|
||||
match key {
|
||||
x if keybinds::is_select(x) => { self.reset_game_state(); }
|
||||
x if keybinds::is_exit(x) => { self.exit_intended = true; }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -426,8 +427,8 @@ impl BinaryNumbersGame {
|
||||
}
|
||||
|
||||
fn handle_no_result_yet(&mut self, input: KeyEvent) {
|
||||
match input.code {
|
||||
KeyCode::Right => {
|
||||
match input {
|
||||
x if keybinds::is_right(x) => {
|
||||
// select the next suggestion
|
||||
if let Some(selected) = self.puzzle.selected_suggestion {
|
||||
let current_index = self.puzzle.suggestions.iter().position(|&x| x == selected);
|
||||
@ -440,7 +441,7 @@ impl BinaryNumbersGame {
|
||||
self.puzzle.selected_suggestion = Some(self.puzzle.suggestions[0]);
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
x if keybinds::is_left(x) => {
|
||||
// select the previous suggestion
|
||||
if let Some(selected) = self.puzzle.selected_suggestion {
|
||||
let current_index = self.puzzle.suggestions.iter().position(|&x| x == selected);
|
||||
@ -454,7 +455,7 @@ impl BinaryNumbersGame {
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
x if keybinds::is_select(x) => {
|
||||
if let Some(selected) = self.puzzle.selected_suggestion {
|
||||
if self.puzzle.is_correct_guess(selected) {
|
||||
self.puzzle.guess_result = Some(GuessResult::Correct);
|
||||
@ -464,7 +465,7 @@ impl BinaryNumbersGame {
|
||||
self.finalize_round();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') | KeyCode::Char('S') => {
|
||||
KeyEvent { code: KeyCode::Char('s' | 'S'), .. } => {
|
||||
// Skip puzzle counts as timeout
|
||||
self.puzzle.guess_result = Some(GuessResult::Timeout);
|
||||
self.finalize_round();
|
||||
@ -473,9 +474,9 @@ impl BinaryNumbersGame {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_result_available(&mut self, input: KeyEvent) {
|
||||
match input.code {
|
||||
KeyCode::Enter => {
|
||||
fn handle_result_available(&mut self, key: KeyEvent) {
|
||||
match key {
|
||||
x if keybinds::is_select(x) => {
|
||||
match self.game_state {
|
||||
GameState::PendingGameOver => {
|
||||
// reveal summary
|
||||
@ -491,7 +492,7 @@ impl BinaryNumbersGame {
|
||||
GameState::Active => { /* shouldn't be here */ }
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => self.exit_intended = true,
|
||||
x if keybinds::is_exit(x) => self.exit_intended = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -632,8 +633,7 @@ impl Widget for &mut BinaryNumbersGame {
|
||||
|
||||
// Simple ASCII gauge renderer to avoid variable glyph heights from Unicode block elements
|
||||
fn render_ascii_gauge(area: Rect, buf: &mut Buffer, ratio: f64, color: Color) {
|
||||
let clamped = if ratio < 0.0 { 0.0 } else if ratio > 1.0 { 1.0 } else { ratio };
|
||||
let fill_width = ((area.width as f64) * clamped).round().min(area.width as f64) as u16;
|
||||
let fill_width = ((area.width as f64) * ratio.clamp(0.0, 1.0)).round().min(area.width as f64) as u16;
|
||||
if area.height == 0 { return; }
|
||||
for x in 0..area.width {
|
||||
let filled = x < fill_width;
|
||||
@ -664,10 +664,11 @@ impl HighScores {
|
||||
let mut contents = String::new();
|
||||
if file.read_to_string(&mut contents).is_ok() {
|
||||
for line in contents.lines() {
|
||||
if let Some((k,v)) = line.split_once('=') {
|
||||
if let (Ok(bits), Ok(score)) = (k.trim().parse::<u32>(), v.trim().parse::<u32>()) {
|
||||
hs.scores.insert(bits, score);
|
||||
}
|
||||
if let Some((k,v)) = line.split_once('=')
|
||||
&& let Ok(bits) = k.trim().parse::<u32>()
|
||||
&& let Ok(score) = v.trim().parse::<u32>()
|
||||
{
|
||||
hs.scores.insert(bits, score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
src/keybinds.rs
Normal file
25
src/keybinds.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
pub(crate) fn is_up(key: KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Up | KeyCode::Char('k'))
|
||||
}
|
||||
|
||||
pub(crate) fn is_down(key: KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Down | KeyCode::Char('j'))
|
||||
}
|
||||
|
||||
pub(crate) fn is_left(key: KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Left | KeyCode::Char('h'))
|
||||
}
|
||||
|
||||
pub(crate) fn is_right(key: KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Right | KeyCode::Char('l'))
|
||||
}
|
||||
|
||||
pub(crate) fn is_select(key: KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Enter)
|
||||
}
|
||||
|
||||
pub(crate) fn is_exit(key: KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Esc | KeyCode::Char('q' | 'Q'))
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod binary_numbers;
|
||||
mod keybinds;
|
||||
mod main_screen_widget;
|
||||
mod utils;
|
||||
|
||||
|
||||
@ -89,8 +89,8 @@ impl Widget for AsciiArtWidget {
|
||||
|
||||
pub fn center(area: Rect, horizontal: Constraint) -> Rect {
|
||||
let [area] = Layout::horizontal([horizontal]).flex(Flex::Center).areas(area);
|
||||
let area = vertically_center(area);
|
||||
area
|
||||
|
||||
vertically_center(area)
|
||||
}
|
||||
|
||||
pub fn vertically_center(area: Rect) -> Rect {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user