snake movement

This commit is contained in:
2026-03-08 17:17:32 -05:00
parent 149e1e0381
commit 23b7e1ad23
7 changed files with 130 additions and 102 deletions

View File

@@ -1 +0,0 @@
pub mod state;

View File

@@ -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>>>;
}

View File

@@ -1,4 +1,3 @@
pub mod loading;
pub mod interfaces;
pub mod inputs;
pub mod snake;

View File

@@ -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());
}
}

View File

@@ -1 +1,2 @@
pub mod snake;
pub mod snake;
pub mod states;

View File

@@ -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,

View 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 });
}
}