From 34969cffa67fd6d4276f2d0d507296e4ce5e8655 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:10:53 +0000 Subject: [PATCH] Implement short-term roadmap goals: HUD feedback, mock serial fallback, integration tests Co-authored-by: MnemOnicE <170563909+MnemOnicE@users.noreply.github.com> --- Cargo.toml | 9 ++++ ROADMAP.md | 6 +-- src/lib.rs | 3 ++ src/main.rs | 27 +++++++----- src/serial_daemon.rs | 1 + src/state.rs | 21 +++++++++ tests/integration_tests.rs | 90 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 src/lib.rs create mode 100644 tests/integration_tests.rs diff --git a/Cargo.toml b/Cargo.toml index afc960a..47298be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,12 @@ rand = "0.8" tokio = { version = "1.35", features = ["full"] } warp = { version = "0.3", features = ["websocket"] } futures = "0.3" + + +[lib] +name = "grid_crawler_wsl" +path = "src/lib.rs" + +[[bin]] +name = "grid_crawler_wsl" +path = "src/main.rs" \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index 2ad0d40..ddbfc6b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,9 +15,9 @@ - [x] Support dynamic seed and map size selection - [x] Add mobile WebSocket interface and browser client - [x] Add firing actions and weapon mechanics -- [ ] Implement mock serial fallback for hardware development -- [ ] Improve in-game HUD and action feedback -- [ ] Add integration tests for movement, action points, and pickups +- [x] Implement mock serial fallback for hardware development +- [x] Improve in-game HUD and action feedback +- [x] Add integration tests for movement, action points, and pickups ## Medium-term goals diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7be9010 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod net; +pub mod serial_daemon; +pub mod state; diff --git a/src/main.rs b/src/main.rs index 1e971aa..78c24c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ -mod net; -mod serial_daemon; -mod state; - use crossterm::{ event::{self, Event, KeyCode}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; +use grid_crawler_wsl::state::{ + AppPhase, GameState, fire_at_direction, initialize_state, move_player, regenerate_map, + spawn_drops, +}; use rand::random; use ratatui::{ Frame, Terminal, @@ -16,10 +16,6 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Borders, Gauge, Paragraph}, }; -use state::{ - AppPhase, GameState, fire_at_direction, initialize_state, move_player, regenerate_map, - spawn_drops, -}; use std::env; use std::io; use std::sync::Arc; @@ -37,8 +33,11 @@ fn main() -> Result<(), Box> { } let game_state = initialize_state(); - let _tx_port = serial_daemon::init_hardware_bridge(Arc::clone(&game_state), "/dev/ttyACM0"); - net::start_ws_server(Arc::clone(&game_state), "127.0.0.1:9001"); + let _tx_port = grid_crawler_wsl::serial_daemon::init_hardware_bridge( + Arc::clone(&game_state), + "/dev/ttyACM0", + ); + grid_crawler_wsl::net::start_ws_server(Arc::clone(&game_state), "127.0.0.1:9001"); enable_raw_mode()?; let mut stdout = io::stdout(); @@ -195,6 +194,7 @@ fn draw_combat_ui(f: &mut Frame, state: &GameState, aiming: bool) { Constraint::Length(3), // HP Constraint::Length(3), // AR Constraint::Length(3), // AP + Constraint::Length(3), // SYSTEM LOG Constraint::Min(0), ] .as_ref(), @@ -301,6 +301,13 @@ fn draw_combat_ui(f: &mut Frame, state: &GameState, aiming: bool) { ) .percent(ap_percent.min(100)); f.render_widget(ap_gauge, chunks[4]); + + // --- System Log (Feedback) --- + let log_widget = Paragraph::new(state.feedback.clone()) + .block(Block::default().borders(Borders::ALL).title(" SYSTEM LOG ")) + .alignment(Alignment::Center) + .style(Style::default().fg(Color::LightGreen)); + f.render_widget(log_widget, chunks[5]); } fn draw_game_over(f: &mut Frame) { diff --git a/src/serial_daemon.rs b/src/serial_daemon.rs index 9e9bcef..f2fb518 100644 --- a/src/serial_daemon.rs +++ b/src/serial_daemon.rs @@ -55,6 +55,7 @@ pub fn init_hardware_bridge(state: SharedState, port_name: &str) -> Box>; @@ -136,6 +137,7 @@ pub fn initialize_state() -> SharedState { height, seed, player_idx, + feedback: "Neural link established. Awaiting input.".to_string(), }; Arc::new(Mutex::new(initial_state)) @@ -150,6 +152,7 @@ pub fn regenerate_map(state: &mut GameState, seed: u64, size: usize) { state.height = size; state.seed = seed; state.player_idx = new_player_idx; + state.feedback = "Map regenerated.".to_string(); } /// Move the player by dx,dy if there is enough AP and no wall. Returns true if moved. @@ -161,13 +164,16 @@ pub fn move_player(state: &mut GameState, dx: isize, dy: isize) -> bool { let nx = x as isize + dx; let ny = y as isize + dy; if nx < 0 || ny < 0 || nx >= state.width as isize || ny >= state.height as isize { + state.feedback = "Cannot move outside map boundaries.".to_string(); return false; } let nidx = (ny as usize) * state.width + (nx as usize); if state.map_matrix[nidx] == Tile::Wall as u8 || state.map_matrix[nidx] == Tile::Enemy as u8 { + state.feedback = "Path blocked.".to_string(); return false; } if state.stats.ap == 0 { + state.feedback = "Not enough AP to move.".to_string(); return false; } state.stats.ap = state.stats.ap.saturating_sub(1); @@ -176,6 +182,9 @@ pub fn move_player(state: &mut GameState, dx: isize, dy: isize) -> bool { state.map_matrix[nidx] = Tile::Player as u8; state.player_idx = nidx; let _ = consume_tile_effect(state, target_tile); + if target_tile == Tile::Empty as u8 { + state.feedback = "Moved successfully.".to_string(); + } true } @@ -183,6 +192,7 @@ pub fn move_player(state: &mut GameState, dx: isize, dy: isize) -> bool { pub fn fire_at_direction(state: &mut GameState, dx: isize, dy: isize) -> bool { let idx = state.player_idx; if state.stats.ap < 2 { + state.feedback = "Not enough AP to fire (needs 2).".to_string(); return false; } state.stats.ap = state.stats.ap.saturating_sub(2); @@ -202,12 +212,15 @@ pub fn fire_at_direction(state: &mut GameState, dx: isize, dy: isize) -> bool { } if target == Tile::Enemy as u8 { state.map_matrix[nidx] = Tile::Wreck as u8; + state.feedback = "Target destroyed!".to_string(); return true; } else if target != Tile::Empty as u8 { state.map_matrix[nidx] = Tile::Empty as u8; + state.feedback = "Obstacle cleared.".to_string(); return true; } } + state.feedback = "Missed.".to_string(); false } @@ -215,23 +228,28 @@ fn consume_tile_effect(state: &mut GameState, tile: u8) -> bool { match tile { x if x == Tile::Health as u8 => { state.stats.health = state.stats.health.saturating_add(20).min(100); + state.feedback = "Picked up Health: +20 HP".to_string(); true } x if x == Tile::Smoke as u8 => { state.stats.active_item = 1; state.stats.item_charges = 1; + state.feedback = "Acquired Smoke charge.".to_string(); true } x if x == Tile::Resource as u8 => { state.stats.ap = state.stats.ap.saturating_add(3).min(12); + state.feedback = "Acquired Resource: +3 AP".to_string(); true } x if x == Tile::Mine as u8 => { state.stats.health = state.stats.health.saturating_sub(15); + state.feedback = "Hit a Mine! -15 HP".to_string(); true } x if x == Tile::Wreck as u8 => { state.stats.armor = state.stats.armor.saturating_sub(10); + state.feedback = "Scraped wreckage: -10 Armor".to_string(); true } _ => false, @@ -307,6 +325,7 @@ mod tests { height: 1, seed: 1, player_idx: 0, + feedback: "".to_string(), }; let applied = consume_tile_effect(&mut gs, Tile::Health as u8); gs.map_matrix[0] = Tile::Empty as u8; @@ -333,6 +352,7 @@ mod tests { height: 1, seed: 1, player_idx: 0, + feedback: "".to_string(), }; // try to move right into wall (should fail) assert!(!move_player(&mut gs, 1, 0)); @@ -372,6 +392,7 @@ mod fire_tests { height: 1, seed: 1, player_idx: 0, + feedback: "".to_string(), }; // Fire right diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..5e464bc --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,90 @@ +use grid_crawler_wsl::state::{ + AppPhase, GameState, PlayerStats, Tile, fire_at_direction, move_player, +}; + +#[test] +fn test_player_sequence_move_pickup_fire() { + let mut gs = GameState { + phase: AppPhase::Playing, + stats: PlayerStats { + health: 50, + armor: 10, + ap: 4, + is_supercharging: false, + has_shield: false, + active_item: 0, + item_charges: 0, + }, + map_matrix: vec![ + Tile::Player as u8, + Tile::Health as u8, + Tile::Empty as u8, + Tile::Enemy as u8, + ], + width: 4, + height: 1, + seed: 1, + player_idx: 0, + feedback: "".to_string(), + }; + + // Step 1: Move right onto Health + assert!(move_player(&mut gs, 1, 0)); + assert_eq!(gs.stats.ap, 3); // Cost 1 AP + assert_eq!(gs.stats.health, 70); // Health increased + assert_eq!(gs.player_idx, 1); + assert_eq!(gs.map_matrix[0], Tile::Empty as u8); // Old position empty + assert_eq!(gs.map_matrix[1], Tile::Player as u8); // New position player + + // Step 2: Fire right + assert!(fire_at_direction(&mut gs, 1, 0)); + assert_eq!(gs.stats.ap, 1); // Cost 2 AP + assert_eq!(gs.map_matrix[3], Tile::Wreck as u8); // Enemy turns to wreck + + // Step 3: Try to fire again (not enough AP) + assert!(!fire_at_direction(&mut gs, 1, 0)); + assert_eq!(gs.stats.ap, 1); // AP unchanged +} + +#[test] +fn test_player_sequence_move_obstacle_mine() { + let mut gs = GameState { + phase: AppPhase::Playing, + stats: PlayerStats { + health: 100, + armor: 50, + ap: 4, + is_supercharging: false, + has_shield: false, + active_item: 0, + item_charges: 0, + }, + map_matrix: vec![ + Tile::Player as u8, + Tile::Wall as u8, + Tile::Empty as u8, + Tile::Mine as u8, + ], + width: 2, + height: 2, + seed: 1, + player_idx: 0, + feedback: "".to_string(), + }; + + // Try moving right into wall + assert!(!move_player(&mut gs, 1, 0)); + assert_eq!(gs.stats.ap, 4); // AP unchanged + assert_eq!(gs.player_idx, 0); // Position unchanged + + // Move down into empty space + assert!(move_player(&mut gs, 0, 1)); + assert_eq!(gs.stats.ap, 3); // Cost 1 AP + assert_eq!(gs.player_idx, 2); + + // Move right into mine + assert!(move_player(&mut gs, 1, 0)); + assert_eq!(gs.stats.ap, 2); // Cost 1 AP + assert_eq!(gs.player_idx, 3); + assert_eq!(gs.stats.health, 85); // Hit mine +}