basic snake movement implementation

This commit is contained in:
2026-03-08 13:18:28 -05:00
commit 149e1e0381
19 changed files with 370 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

14
Cargo.lock generated Normal file
View File

@@ -0,0 +1,14 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "game_cli_frontend"
version = "0.1.0"
dependencies = [
"game_core",
]
[[package]]
name = "game_core"
version = "0.1.0"

3
Cargo.toml Normal file
View File

@@ -0,0 +1,3 @@
[workspace]
resolver = "3"
members = ["game_core", "game_cli_frontend"]

1
game_cli_frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,7 @@
[package]
name = "game_cli_frontend"
version = "0.1.0"
edition = "2024"
[dependencies]
game_core = { path = "../game_core" }

View File

@@ -0,0 +1,5 @@
use game_core;
fn main() {
game_core::init();
}

1
game_core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

6
game_core/Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "game_core"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

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

View File

@@ -0,0 +1,33 @@
use std::ops::{Add, AddAssign};
use crate::traits::number::Number;
#[derive(PartialEq, Copy, Clone, Debug)]
pub struct Position<T: Number> {
pub x: T,
pub y: T,
}
impl<T> AddAssign for Position<T>
where
T: Number + AddAssign,
{
fn add_assign(&mut self, rhs: Self) {
self.x += rhs.x;
self.y += rhs.y;
}
}
impl<T> Add for Position<T>
where
T: Number + Add,
{
type Output = Position<T>;
fn add(self, rhs: Self) -> Self::Output {
Self {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}

32
game_core/src/inputs.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::collections::HashMap;
#[derive(Eq, Hash, PartialEq, Copy, Clone)]
pub enum InputKey {
Up,
Down,
Left,
Right,
Select,
Back,
Start,
}
pub struct Inputs {
inputs_state: HashMap<InputKey, bool>,
}
impl Inputs {
pub fn new() -> Self {
Self {
inputs_state: HashMap::new(),
}
}
pub fn is_pressed(&self, input: InputKey) -> bool {
*self.inputs_state.get(&input).unwrap_or(&false)
}
pub fn set_pressed(&mut self, input: InputKey, value: bool) {
self.inputs_state.insert(input, value);
}
}

View File

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

View File

@@ -0,0 +1,7 @@
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>>>;
}

25
game_core/src/lib.rs Normal file
View File

@@ -0,0 +1,25 @@
pub mod loading;
pub mod interfaces;
pub mod inputs;
pub mod snake;
pub mod common;
pub mod traits;
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
pub fn init() {
println!("init game engine");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

82
game_core/src/loading.rs Normal file
View File

@@ -0,0 +1,82 @@
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

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

View File

@@ -0,0 +1,135 @@
use crate::common::position::Position;
pub struct Node {
position: Position<isize>,
next: Option<Box<Node>>,
}
pub struct Snake {
body: Node,
moving_direction: Position<isize>,
}
impl Snake {
pub fn new(
starting_position: Position<isize>,
starting_moving_direction: Position<isize>,
) -> Self {
Self {
body: Node {
position: starting_position,
next: None,
},
moving_direction: starting_moving_direction,
}
}
pub fn is_colliding_against_snake(&self, other_snake: &Snake) -> bool {
let head = &self.body; // the head is always the first node in the list
let mut current_node = Some(&other_snake.body);
while let Some(node) = current_node {
if node.position == head.position {
return true;
}
current_node = node.next.as_deref();
}
false
}
// TODO: implement this once we have wall data
// pub fn is_colliding_against_wall(other_snake: &Snake) {
// }
pub fn do_move(&mut self) {
// probably a more optimal way of approaching this is just
// moving the tail node to head.next, but Rust's borrow checking
// system doesn't allow for that in an easy way
let mut last_position = self.body.position;
self.body.position += self.moving_direction;
let mut current_node = self.body.next.as_deref_mut();
while let Some(node) = current_node {
let temp = node.position;
node.position = last_position;
last_position = temp;
current_node = node.next.as_deref_mut();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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});
// Snake 2: Body at (5, 5) and (5, 6)
let mut snake2 = Snake::new(Position{x: 5, y: 5}, Position{x: 0, y: 1});
snake2.body.next = Some(Box::new(Node {
position: Position{x: 5, y: 6},
next: None,
}));
assert!(snake1.is_colliding_against_snake(&snake2));
let snake3 = Snake::new(Position{x: 10, y: 10}, Position{x: 0, y: 1});
assert!(!snake1.is_colliding_against_snake(&snake3));
}
#[test]
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});
// Build the tail manually
snake.body.next = Some(Box::new(Node {
position: Position{x: 9, y: 10},
next: Some(Box::new(Node {
position: Position{x: 8, y: 10},
next: None,
})),
}));
snake.do_move();
assert_eq!(snake.body.position, Position{x: 11, y: 10});
let first_body = snake.body.next.as_ref().unwrap();
assert_eq!(first_body.position, Position{x: 10, y: 10});
let second_body = first_body.next.as_ref().unwrap();
assert_eq!(second_body.position, Position{x: 9, y: 10});
// 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});
snake.body.next = Some(Box::new(Node {
position: Position{x: 11, y: 10},
next: None,
}));
snake.do_move();
assert_eq!(snake.body.position, Position{x: 9, y: 10});
let first_body = snake.body.next.as_ref().unwrap();
assert_eq!(first_body.position, Position{x: 10, y: 10});
// 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});
snake.body.next = Some(Box::new(Node {
position: Position{x: 10, y: 11},
next: None,
}));
snake.do_move();
assert_eq!(snake.body.position, Position{x: 10, y: 9});
let first_body = snake.body.next.as_ref().unwrap();
assert_eq!(first_body.position, Position{x: 10, y: 10});
}
}

View File

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

View File

@@ -0,0 +1,14 @@
use std::ops::{Add, Sub, Mul, Div};
pub trait Number:
Add<Output = Self> +
Sub<Output = Self> +
Mul<Output = Self> +
Div<Output = Self> +
Copy +
Sized
{}
impl<T> Number for T where
T: Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Div<Output = T> + Copy + Sized
{}