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 interfaces;
|
||||||
pub mod inputs;
|
pub mod inputs;
|
||||||
pub mod snake;
|
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::common::position::Position;
|
||||||
|
use crate::inputs::Inputs;
|
||||||
|
use crate::snake::states::SnakeState;
|
||||||
|
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
position: Position<isize>,
|
pub position: Position<isize>,
|
||||||
next: Option<Box<Node>>,
|
next: Option<Box<Node>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Snake {
|
pub struct Snake {
|
||||||
body: Node,
|
pub body: Node,
|
||||||
moving_direction: Position<isize>,
|
pub speed: f32,
|
||||||
|
pub moving_direction: Position<isize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Snake {
|
impl Snake {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
starting_position: Position<isize>,
|
starting_position: Position<isize>,
|
||||||
starting_moving_direction: Position<isize>,
|
starting_moving_direction: Position<isize>,
|
||||||
|
speed: f32, // units per second
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
speed: speed,
|
||||||
body: Node {
|
body: Node {
|
||||||
position: starting_position,
|
position: starting_position,
|
||||||
next: None,
|
next: None,
|
||||||
@@ -48,7 +54,7 @@ impl Snake {
|
|||||||
// system doesn't allow for that in an easy way
|
// system doesn't allow for that in an easy way
|
||||||
|
|
||||||
let mut last_position = self.body.position;
|
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();
|
let mut current_node = self.body.next.as_deref_mut();
|
||||||
while let Some(node) = current_node {
|
while let Some(node) = current_node {
|
||||||
let temp = node.position;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -69,10 +90,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_is_colliding_against_snake() {
|
fn test_is_colliding_against_snake() {
|
||||||
// Snake 1: Just the head at (5, 5)
|
// 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)
|
// 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 {
|
snake2.body.next = Some(Box::new(Node {
|
||||||
position: Position{x: 5, y: 6},
|
position: Position{x: 5, y: 6},
|
||||||
next: None,
|
next: None,
|
||||||
@@ -80,7 +101,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(snake1.is_colliding_against_snake(&snake2));
|
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));
|
assert!(!snake1.is_colliding_against_snake(&snake3));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +109,7 @@ mod tests {
|
|||||||
fn test_do_move() {
|
fn test_do_move() {
|
||||||
// Setup: Snake at (10, 10), moving Right (1, 0)
|
// Setup: Snake at (10, 10), moving Right (1, 0)
|
||||||
// Body: (10, 10) -> (9, 10) -> (8, 10)
|
// 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
|
// Build the tail manually
|
||||||
snake.body.next = Some(Box::new(Node {
|
snake.body.next = Some(Box::new(Node {
|
||||||
position: Position{x: 9, y: 10},
|
position: Position{x: 9, y: 10},
|
||||||
@@ -106,7 +127,7 @@ mod tests {
|
|||||||
|
|
||||||
// Move left
|
// Move left
|
||||||
// Start at (10, 10), moving Left (-1, 0)
|
// 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 {
|
snake.body.next = Some(Box::new(Node {
|
||||||
position: Position{x: 11, y: 10},
|
position: Position{x: 11, y: 10},
|
||||||
next: None,
|
next: None,
|
||||||
@@ -120,7 +141,7 @@ mod tests {
|
|||||||
|
|
||||||
// Move up
|
// Move up
|
||||||
// Start at (10, 10), moving Up (0, -1)
|
// 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 {
|
snake.body.next = Some(Box::new(Node {
|
||||||
position: Position{x: 10, y: 11},
|
position: Position{x: 10, y: 11},
|
||||||
next: None,
|
next: None,
|
||||||
|
|||||||
@@ -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