snake movement
This commit is contained in:
@@ -1 +0,0 @@
|
||||
pub mod state;
|
||||
@@ -1,7 +0,0 @@
|
||||
use std::time::Duration;
|
||||
use crate::inputs::Inputs;
|
||||
|
||||
pub trait State<T> {
|
||||
fn update(&mut self, delta: Duration, inputs: &Inputs, context: &mut T) -> Option<Box<dyn State<T>>>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod loading;
|
||||
pub mod interfaces;
|
||||
pub mod inputs;
|
||||
pub mod snake;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
use std::time::Duration;
|
||||
use crate::{inputs::{InputKey, Inputs}, interfaces::state::State};
|
||||
|
||||
pub struct LoadingState {
|
||||
elapsed: Duration,
|
||||
target: Duration,
|
||||
}
|
||||
pub struct ReadyState;
|
||||
|
||||
pub struct LoadingContext;
|
||||
|
||||
impl LoadingState {
|
||||
pub fn new(target: Duration) -> Self {
|
||||
Self {
|
||||
elapsed: Duration::ZERO,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State<LoadingContext> for LoadingState {
|
||||
fn update(&mut self, delta: Duration, _inputs: &Inputs, _context: &mut LoadingContext) -> Option<Box<dyn State<LoadingContext>>> {
|
||||
self.elapsed += delta;
|
||||
|
||||
if self.elapsed >= self.target {
|
||||
return Some(Box::new(ReadyState{}));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl State<LoadingContext> for ReadyState {
|
||||
fn update(&mut self, _delta: Duration, inputs: &Inputs, _context: &mut LoadingContext) -> Option<Box<dyn State<LoadingContext>>> {
|
||||
if inputs.is_pressed(InputKey::Start) {
|
||||
return Some(Box::new(ReadyState{}));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_loading_stays_in_loading_state() {
|
||||
let mut state = LoadingState::new(Duration::from_secs(2));
|
||||
let inputs = Inputs::new();
|
||||
let mut context = LoadingContext;
|
||||
|
||||
let result = state.update(Duration::from_secs(1), &inputs, &mut context);
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loading_transitions_to_ready() {
|
||||
let mut state = LoadingState::new(Duration::from_secs(2));
|
||||
let inputs = Inputs::new();
|
||||
let mut context = LoadingContext;
|
||||
|
||||
let result = state.update(Duration::from_secs(3), &inputs, &mut context);
|
||||
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ready_state_transition() {
|
||||
let mut state = ReadyState;
|
||||
let mut inputs = Inputs::new();
|
||||
let mut context = LoadingContext;
|
||||
|
||||
assert!(state.update(Duration::from_millis(16), &inputs, &mut context).is_none());
|
||||
|
||||
inputs.set_pressed(InputKey::Start, true);
|
||||
assert!(inputs.is_pressed(InputKey::Start));
|
||||
|
||||
let result = state.update(Duration::from_millis(16), &inputs, &mut context);
|
||||
assert!(result.is_some());
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod snake;
|
||||
pub mod snake;
|
||||
pub mod states;
|
||||
@@ -1,21 +1,27 @@
|
||||
use std::time::Duration;
|
||||
use crate::common::position::Position;
|
||||
use crate::inputs::Inputs;
|
||||
use crate::snake::states::SnakeState;
|
||||
|
||||
pub struct Node {
|
||||
position: Position<isize>,
|
||||
pub position: Position<isize>,
|
||||
next: Option<Box<Node>>,
|
||||
}
|
||||
|
||||
pub struct Snake {
|
||||
body: Node,
|
||||
moving_direction: Position<isize>,
|
||||
pub body: Node,
|
||||
pub speed: f32,
|
||||
pub moving_direction: Position<isize>,
|
||||
}
|
||||
|
||||
impl Snake {
|
||||
pub fn new(
|
||||
starting_position: Position<isize>,
|
||||
starting_moving_direction: Position<isize>,
|
||||
speed: f32, // units per second
|
||||
) -> Self {
|
||||
Self {
|
||||
speed: speed,
|
||||
body: Node {
|
||||
position: starting_position,
|
||||
next: None,
|
||||
@@ -48,7 +54,7 @@ impl Snake {
|
||||
// system doesn't allow for that in an easy way
|
||||
|
||||
let mut last_position = self.body.position;
|
||||
self.body.position += self.moving_direction;
|
||||
self.body.position += self.moving_direction; // the first node in body is always the head
|
||||
let mut current_node = self.body.next.as_deref_mut();
|
||||
while let Some(node) = current_node {
|
||||
let temp = node.position;
|
||||
@@ -61,6 +67,21 @@ impl Snake {
|
||||
|
||||
}
|
||||
|
||||
pub struct SnakeHandler {
|
||||
snake: Snake,
|
||||
state: Box<dyn SnakeState>,
|
||||
|
||||
}
|
||||
|
||||
impl SnakeHandler {
|
||||
pub fn update(&mut self, delta: Duration, inputs: &Inputs) {
|
||||
let new_state = self.state.update(delta, inputs, &mut self.snake);
|
||||
if let Some(new_state) = new_state {
|
||||
self.state = new_state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -69,10 +90,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_is_colliding_against_snake() {
|
||||
// Snake 1: Just the head at (5, 5)
|
||||
let snake1 = Snake::new(Position{x: 5, y: 5}, Position{x: 0, y: 1});
|
||||
let snake1 = Snake::new(Position{x: 5, y: 5}, Position{x: 0, y: 1}, 0.5);
|
||||
|
||||
// Snake 2: Body at (5, 5) and (5, 6)
|
||||
let mut snake2 = Snake::new(Position{x: 5, y: 5}, Position{x: 0, y: 1});
|
||||
let mut snake2 = Snake::new(Position{x: 5, y: 5}, Position{x: 0, y: 1}, 0.5);
|
||||
snake2.body.next = Some(Box::new(Node {
|
||||
position: Position{x: 5, y: 6},
|
||||
next: None,
|
||||
@@ -80,7 +101,7 @@ mod tests {
|
||||
|
||||
assert!(snake1.is_colliding_against_snake(&snake2));
|
||||
|
||||
let snake3 = Snake::new(Position{x: 10, y: 10}, Position{x: 0, y: 1});
|
||||
let snake3 = Snake::new(Position{x: 10, y: 10}, Position{x: 0, y: 1}, 0.5);
|
||||
assert!(!snake1.is_colliding_against_snake(&snake3));
|
||||
}
|
||||
|
||||
@@ -88,7 +109,7 @@ mod tests {
|
||||
fn test_do_move() {
|
||||
// Setup: Snake at (10, 10), moving Right (1, 0)
|
||||
// Body: (10, 10) -> (9, 10) -> (8, 10)
|
||||
let mut snake = Snake::new(Position{x: 10, y: 10}, Position{x: 1, y: 0});
|
||||
let mut snake = Snake::new(Position{x: 10, y: 10}, Position{x: 1, y: 0}, 0.5);
|
||||
// Build the tail manually
|
||||
snake.body.next = Some(Box::new(Node {
|
||||
position: Position{x: 9, y: 10},
|
||||
@@ -106,7 +127,7 @@ mod tests {
|
||||
|
||||
// Move left
|
||||
// Start at (10, 10), moving Left (-1, 0)
|
||||
let mut snake = Snake::new(Position{x: 10, y: 10}, Position{x: -1, y: 0});
|
||||
let mut snake = Snake::new(Position{x: 10, y: 10}, Position{x: -1, y: 0}, 0.5);
|
||||
snake.body.next = Some(Box::new(Node {
|
||||
position: Position{x: 11, y: 10},
|
||||
next: None,
|
||||
@@ -120,7 +141,7 @@ mod tests {
|
||||
|
||||
// Move up
|
||||
// Start at (10, 10), moving Up (0, -1)
|
||||
let mut snake = Snake::new(Position{x: 10, y: 10}, Position{x: 0, y: -1});
|
||||
let mut snake = Snake::new(Position{x: 10, y: 10}, Position{x: 0, y: -1}, 0.5);
|
||||
snake.body.next = Some(Box::new(Node {
|
||||
position: Position{x: 10, y: 11},
|
||||
next: None,
|
||||
|
||||
97
game_core/src/snake/states.rs
Normal file
97
game_core/src/snake/states.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use std::time::Duration;
|
||||
use crate::common::position::Position;
|
||||
use crate::{inputs::{InputKey, Inputs}, snake::snake::Snake};
|
||||
|
||||
pub trait SnakeState {
|
||||
fn update(&mut self, delta: Duration, inputs: &Inputs, snake: &mut Snake) -> Option<Box<dyn SnakeState>>;
|
||||
}
|
||||
|
||||
pub struct MovingState {
|
||||
dt_accumulator: Duration,
|
||||
}
|
||||
pub struct StoppedState;
|
||||
|
||||
|
||||
impl MovingState {
|
||||
pub fn new() -> Self {
|
||||
Self { dt_accumulator: Duration::ZERO }
|
||||
}
|
||||
|
||||
pub fn handle_direction_change(&mut self, inputs: &Inputs, snake: &mut Snake) {
|
||||
let new_dir = if inputs.is_pressed(InputKey::Up) { Some(Position { x: 0, y: -1 }) }
|
||||
else if inputs.is_pressed(InputKey::Down) { Some(Position { x: 0, y: 1 }) }
|
||||
else if inputs.is_pressed(InputKey::Left) { Some(Position { x: -1, y: 0 }) }
|
||||
else if inputs.is_pressed(InputKey::Right) { Some(Position { x: 1, y: 0 }) }
|
||||
else { None };
|
||||
|
||||
if let Some(dir) = new_dir {
|
||||
// Prevent 180-degree turns
|
||||
// If current is (1, 0), opposite is (-1, 0)
|
||||
let opposite = Position { x: -snake.moving_direction.x, y: -snake.moving_direction.y };
|
||||
|
||||
if dir != opposite {
|
||||
snake.moving_direction = dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SnakeState for MovingState {
|
||||
fn update(&mut self, delta: Duration, inputs: &Inputs, snake: &mut Snake) -> Option<Box<dyn SnakeState>> {
|
||||
self.handle_direction_change(inputs, snake);
|
||||
let interval = Duration::from_secs_f32(1.0 / snake.speed);
|
||||
self.dt_accumulator += delta;
|
||||
if self.dt_accumulator >= interval {
|
||||
snake.do_move();
|
||||
self.dt_accumulator -= interval;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl SnakeState for StoppedState {
|
||||
fn update(&mut self, _delta: Duration, _inputs: &Inputs, _snake: &mut Snake) -> Option<Box<dyn SnakeState>> {
|
||||
Some(Box::new(Self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::inputs::Inputs;
|
||||
|
||||
#[test]
|
||||
fn test_moving_state_triggers_movement() {
|
||||
let mut snake = Snake::new(Position { x: 0, y: 0 }, Position { x: 1, y: 0 }, 1.0);
|
||||
let mut state = MovingState::new();
|
||||
let inputs = Inputs::new();
|
||||
|
||||
// Should not move
|
||||
state.update(Duration::from_millis(500), &inputs, &mut snake);
|
||||
assert_eq!(snake.body.position, Position { x: 0, y: 0 });
|
||||
|
||||
// Should move
|
||||
state.update(Duration::from_millis(500), &inputs, &mut snake);
|
||||
assert_eq!(snake.body.position, Position { x: 1, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_direction_change() {
|
||||
let mut snake = Snake::new(Position { x: 0, y: 0 }, Position { x: 1, y: 0 }, 1.0);
|
||||
let mut state = MovingState::new();
|
||||
let mut inputs = Inputs::new();
|
||||
// Try to move left (opposite of current right movement)
|
||||
inputs.set_pressed(InputKey::Left, true);
|
||||
state.handle_direction_change(&inputs, &mut snake);
|
||||
// Should still be moving Right
|
||||
assert_eq!(snake.moving_direction, Position { x: 1, y: 0 });
|
||||
// Try to move Up (valid)
|
||||
inputs.set_pressed(InputKey::Left, false); // clear previous
|
||||
inputs.set_pressed(InputKey::Up, true);
|
||||
state.handle_direction_change(&inputs, &mut snake);
|
||||
|
||||
// Should be moving Up
|
||||
assert_eq!(snake.moving_direction, Position { x: 0, y: -1 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user