basic snake movement implementation
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
14
Cargo.lock
generated
Normal file
14
Cargo.lock
generated
Normal 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
3
Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["game_core", "game_cli_frontend"]
|
||||
1
game_cli_frontend/.gitignore
vendored
Normal file
1
game_cli_frontend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
7
game_cli_frontend/Cargo.toml
Normal file
7
game_cli_frontend/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "game_cli_frontend"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
game_core = { path = "../game_core" }
|
||||
5
game_cli_frontend/src/main.rs
Normal file
5
game_cli_frontend/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use game_core;
|
||||
|
||||
fn main() {
|
||||
game_core::init();
|
||||
}
|
||||
1
game_core/.gitignore
vendored
Normal file
1
game_core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
6
game_core/Cargo.toml
Normal file
6
game_core/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "game_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
1
game_core/src/common/mod.rs
Normal file
1
game_core/src/common/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod position;
|
||||
33
game_core/src/common/position.rs
Normal file
33
game_core/src/common/position.rs
Normal 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
32
game_core/src/inputs.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
1
game_core/src/interfaces/mod.rs
Normal file
1
game_core/src/interfaces/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod state;
|
||||
7
game_core/src/interfaces/state.rs
Normal file
7
game_core/src/interfaces/state.rs
Normal 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
25
game_core/src/lib.rs
Normal 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
82
game_core/src/loading.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
1
game_core/src/snake/mod.rs
Normal file
1
game_core/src/snake/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod snake;
|
||||
135
game_core/src/snake/snake.rs
Normal file
135
game_core/src/snake/snake.rs
Normal 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});
|
||||
}
|
||||
}
|
||||
1
game_core/src/traits/mod.rs
Normal file
1
game_core/src/traits/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod number;
|
||||
14
game_core/src/traits/number.rs
Normal file
14
game_core/src/traits/number.rs
Normal 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
|
||||
{}
|
||||
Reference in New Issue
Block a user