-
Notifications
You must be signed in to change notification settings - Fork 0
Implement short-term roadmap goals #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| pub mod net; | ||
| pub mod serial_daemon; | ||
| pub mod state; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ pub struct GameState { | |
| pub height: usize, | ||
| pub seed: u64, | ||
| pub player_idx: usize, | ||
| pub feedback: String, | ||
| } | ||
|
|
||
| pub type SharedState = Arc<Mutex<GameState>>; | ||
|
|
@@ -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; | ||
|
Comment on lines
164
to
165
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If if dx == 0 && dy == 0 {
return false;
}
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,13 +182,17 @@ 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 | ||
| } | ||
|
|
||
| /// Fire into an adjacent tile, consuming AP and resolving any tile effect. | ||
| pub fn fire_at_direction(state: &mut GameState, dx: isize, dy: isize) -> bool { | ||
| let idx = state.player_idx; | ||
| if state.stats.ap < 2 { | ||
|
Comment on lines
193
to
194
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If if dx == 0 && dy == 0 {
return false;
}
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,36 +212,44 @@ 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 | ||
| } | ||
|
|
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mock serial daemon background thread runs every second and sets
lock.feedback = "Hardware mock: Regenerated 1 AP". This happens even if the player's AP is already at the maximum of 12 (so no AP is actually regenerated). This constantly overwrites any other active feedback in the system log (like "Moved successfully", "Target destroyed!", etc.) with a misleading message, making the system log feature almost useless when using the mock fallback.