Compare commits

...

6 Commits

Author SHA1 Message Date
FranLMSP b4166d9e63 logic to keep snake within boundaries 2026-03-10 17:09:48 -05:00
FranLMSP f75ab1ac47 snake can now move, eat food and grow 2026-03-10 16:39:40 -05:00
FranLMSP 33db0eaaef draw snake 2026-03-10 12:13:21 -05:00
FranLMSP e57300e673 basic field drawer and basic timer implementation 2026-03-09 23:57:59 -05:00
FranLMSP 9856fd5f41 field loader 2026-03-09 20:30:49 -05:00
FranLMSP 085c3ab62f implement some of the main game logic 2026-03-09 18:05:08 -05:00
27 changed files with 1028 additions and 41 deletions
+45
View File
@@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug unit tests in library 'game_core'",
"type": "lldb",
"request": "launch",
"cargo": {
"args": [
"test",
"--package=game_core"
]
}
},
{
"name": "Debug executable 'game_cli_frontend'",
"type": "lldb",
"request": "launch",
"cargo": {
"args": [
"run",
"--bin=game_cli_frontend",
"--package=game_cli_frontend"
]
},
"args": []
},
{
"name": "Debug unit tests in executable 'game_cli_frontend'",
"type": "lldb",
"request": "launch",
"cargo": {
"args": [
"test",
"--bin=game_cli_frontend",
"--package=game_cli_frontend"
]
}
}
]
}
Generated
+421
View File
@@ -2,13 +2,434 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "game_cli_frontend"
version = "0.1.0"
dependencies = [
"game_core",
"termion",
]
[[package]]
name = "game_core"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "numtoa"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"chacha20",
"getrandom",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termion"
version = "4.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb"
dependencies = [
"libc",
"numtoa",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+1
View File
@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
game_core = { path = "../game_core" }
termion = "4.0.6"
+6
View File
@@ -0,0 +1,6 @@
pub fn reset_cursor() {
println!(
"{}",
termion::cursor::Goto(1, 1),
);
}
+29
View File
@@ -0,0 +1,29 @@
use game_core::field::field::{Field, Tile};
const BOUNDARY_TILE: &'static str = "#";
const WALL_TILE: &'static str = "";
const EMPTY_TILE : &'static str = " ";
const FOOD_TILE : &'static str = "o";
pub fn draw_field(field: &Field, color: &dyn termion::color::Color) {
print!(
"{}",
termion::cursor::Goto(1, 1),
);
print!("{}{}\r\n", termion::color::Fg(color), BOUNDARY_TILE.repeat(field.width + 2));
for y in 0..field.height {
print!("{}{}", termion::color::Fg(color), BOUNDARY_TILE);
for x in 0..field.width {
let tile = field.tiles[y * field.width + x];
let char = match tile {
Tile::Empty => EMPTY_TILE,
Tile::Wall => WALL_TILE,
Tile::Food => FOOD_TILE,
};
print!("{}{}", termion::color::Fg(color), char);
}
print!("{}{}\r\n", termion::color::Fg(color), BOUNDARY_TILE);
}
println!("{}{}\r\n", termion::color::Fg(color), BOUNDARY_TILE.repeat(field.width + 2));
}
+3
View File
@@ -0,0 +1,3 @@
pub mod field;
pub mod snake;
pub mod common;
+29
View File
@@ -0,0 +1,29 @@
use game_core::snake::snake::Snake;
const HEAD_TILE: &'static str = "@";
const BODY_TILE: &'static str = "@";
pub fn draw_snake(snake: &Snake, color: &dyn termion::color::Color, x_offset: isize, y_offset: isize) {
println!(
"{}{}{}",
termion::cursor::Goto(
(x_offset + snake.body.position.x).try_into().unwrap(),
(y_offset + snake.body.position.y).try_into().unwrap()
),
termion::color::Fg(color),
HEAD_TILE,
);
let mut current_node = snake.body.next.as_deref();
while let Some(node) = current_node {
println!(
"{}{}{}",
termion::cursor::Goto(
(x_offset + node.position.x).try_into().unwrap(),
(y_offset + node.position.y).try_into().unwrap()
),
termion::color::Fg(color),
BODY_TILE,
);
current_node = node.next.as_deref();
}
}
+33
View File
@@ -0,0 +1,33 @@
use game_core::inputs::{InputKey, Inputs};
use std::io::{Write, Read, stdout, stdin};
extern crate termion;
use termion::event::{Key, Event, MouseEvent};
use termion::input::{TermRead, MouseTerminal};
use termion::raw::IntoRawMode;
fn map_to_input_key(key_event: termion::event::Key) -> Option<InputKey> {
match key_event {
termion::event::Key::Up => Some(InputKey::Up),
termion::event::Key::Down => Some(InputKey::Down),
termion::event::Key::Left => Some(InputKey::Left),
termion::event::Key::Right => Some(InputKey::Right),
_ => None,
}
}
pub fn process_events<R: Read>(reader: &mut R, inputs: &mut Inputs) {
inputs.clear();
for c in reader.events() {
if let Ok(Event::Key(key_pressed)) = c {
if let Some(input_key) = map_to_input_key(key_pressed) {
inputs.set_pressed(input_key, true);
}
} else {
break;
}
}
}
+7 -2
View File
@@ -1,5 +1,10 @@
use game_core;
mod drawers;
mod renderers;
mod input_mapping;
use crate::renderers::renderer::main_loop;
fn main() {
game_core::init();
main_loop();
}
+1
View File
@@ -0,0 +1 @@
pub mod renderer;
+106
View File
@@ -0,0 +1,106 @@
use std::io::{self, Write};
use std::env;
use std::time::Duration;
use game_core::common::position::Position;
use game_core::field::loader::loader::FieldLoader;
use game_core::field::loader::plain_text_loader::PlainTextFieldLoader;
use game_core::game_handler::game_handler::{GameData, GameManager};
use game_core::game_handler::states::{PausedState, PlayingState};
use game_core::inputs::Inputs;
use game_core::snake::snake::{Node, Snake, SnakeHandler};
use game_core::snake::states::{MovingState, StoppedState};
use game_core::timer::Timer;
use termion::async_stdin;
use termion::raw::IntoRawMode;
use crate::drawers::common::reset_cursor;
use crate::drawers::field::draw_field;
use crate::drawers::snake::draw_snake;
use crate::input_mapping::process_events;
use std::io::{Read, stdout, stdin};
fn clear_screen() {
// Clear the entire screen and move cursor to top-left
print!("\x1B[2J\x1B[1;1H");
// Flush to ensure the escape codes are sent immediately
io::stdout().flush().unwrap();
}
pub fn initialize_main_game() -> GameManager {
let path = env::current_dir().unwrap();
let field_loader = PlainTextFieldLoader{};
let full_path = format!("{}{}", path.display(), "/game_data/fields/maze1");
let field = field_loader.load(full_path);
let snake = Snake {
speed: 5.0,
moving_direction: Position { x: 1, y: 0 },
body: Node {
position: Position {
x: 4, y: 4,
},
next: Some(Box::new(Node {
position: Position {
x: 4, y: 3,
},
next: Some(Box::new(Node {
position: Position {
x: 4, y: 2,
},
next: None,
})),
})),
},
};
let snake_handler = SnakeHandler {
snake: snake,
state: Box::new(MovingState::new()),
};
let mut game_data = GameData {
field: field,
snakes: vec![snake_handler],
};
let mut playing_state = PlayingState::new();
playing_state.spawn_food(&mut game_data);
let game_manager = GameManager {
game_data: game_data,
state: Box::new(playing_state),
};
return game_manager;
}
pub fn main_loop() {
let mut game_manager = initialize_main_game();
let mut stdout = stdout().into_raw_mode().unwrap();
let mut reader = async_stdin();
let mut timer = Timer::new(120);
let mut inputs = Inputs::new();
loop {
timer.start();
process_events(&mut reader, &mut inputs);
game_manager.update(timer.get_delta_time(), &inputs);
// TODO: define a "Draw" function?
clear_screen();
reset_cursor();
draw_field(&game_manager.game_data.field, &termion::color::White);
for snake_handler in &game_manager.game_data.snakes {
draw_snake(&snake_handler.snake, &termion::color::Cyan, 2, 2);
}
reset_cursor();
stdout.flush().unwrap();
timer.limit_fps();
timer.end();
}
}
+1
View File
@@ -4,3 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.10.0"
+8 -1
View File
@@ -1,4 +1,4 @@
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Tile {
Empty,
Wall,
@@ -27,4 +27,11 @@ impl Field {
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
self.tiles[y * self.width + x] = tile;
}
pub fn clear_food(&mut self, x: usize, y: usize) {
let current_tile = self.tiles[y * self.width + x];
if current_tile == Tile::Food {
self.tiles[y * self.width + x] = Tile::Empty;
}
}
}
+5
View File
@@ -0,0 +1,5 @@
use crate::field::field::Field;
pub trait FieldLoader {
fn load(&self, field_id: String) -> Field;
}
+2
View File
@@ -0,0 +1,2 @@
pub mod loader;
pub mod plain_text_loader;
@@ -0,0 +1,57 @@
use crate::field::field::{Field, Tile};
use crate::field::loader::loader::FieldLoader;
use std::fs;
pub struct PlainTextFieldLoader;
impl FieldLoader for PlainTextFieldLoader {
fn load(&self, field_id: String) -> Field {
let content = fs::read_to_string(&field_id)
.expect("Could not read field file");
let lines: Vec<&str> = content.lines().filter(|l| !l.is_empty()).collect();
let height = lines.len();
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
let mut tiles = vec![Tile::Empty; width * height];
for (y, line) in lines.iter().enumerate() {
println!("y: {}", y);
for (x, ch) in line.chars().enumerate() {
println!("x: {}", x);
let tile = match ch {
'x' => Tile::Wall,
_ => Tile::Empty,
};
tiles[y * width + x] = tile;
}
}
Field { width, height, tiles }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_load_from_file() {
let test_file = "test_level_tmp.txt";
let level_data = "xxxx\nx x\nxxxx";
fs::write(test_file, level_data).expect("Could not write test file");
let field = PlainTextFieldLoader{}.load(test_file.to_string());
let _ = fs::remove_file(test_file);
assert_eq!(field.width, 4);
assert_eq!(field.height, 3);
assert_eq!(field.tiles[0], Tile::Wall);
assert_eq!(field.tiles[5], Tile::Empty);
}
}
+2 -1
View File
@@ -1 +1,2 @@
pub mod field;
pub mod field;
pub mod loader;
+27 -3
View File
@@ -1,8 +1,12 @@
use crate::{field::field::Field, snake::snake::SnakeHandler};
use std::time::Duration;
use crate::{field::field::{Field, Tile}, game_handler::states::GameManagerState, inputs::Inputs, snake::snake::{Snake, SnakeHandler}};
pub struct GameData {
field: Field,
snakes: Vec<SnakeHandler>,
pub field: Field,
pub snakes: Vec<SnakeHandler>,
}
impl GameData {
@@ -12,4 +16,24 @@ impl GameData {
snakes,
}
}
pub fn get_food_count(&self) -> usize {
self.field.tiles.iter().filter(|tile| **tile == Tile::Food).count()
}
}
pub struct GameManager {
pub game_data: GameData,
pub state: Box<dyn GameManagerState>,
}
impl GameManager {
pub fn update(&mut self, delta: Duration, inputs: &Inputs) {
let new_state = self.state.update(delta, inputs, &mut self.game_data);
if let Some(new_state) = new_state {
self.state = new_state;
}
}
}
+65 -2
View File
@@ -1,6 +1,8 @@
use std::time::Duration;
use crate::{game_handler::game_handler::GameData, inputs::Inputs};
use rand::prelude::*;
use crate::{field::field::Tile, game_handler::game_handler::GameData, inputs::Inputs, snake::snake::Snake};
pub trait GameManagerState {
@@ -16,10 +18,63 @@ impl PlayingState {
pub fn new() -> Self {
Self { dt_accumulator: Duration::ZERO }
}
pub fn spawn_food(&mut self, game_data: &mut GameData) {
let mut empty_positions = Vec::new();
// Get all empty tiles from the field
for y in 0..game_data.field.height {
for x in 0..game_data.field.width {
if game_data.field.get_tile(x, y) == Tile::Empty {
empty_positions.push((x, y));
}
}
}
// Filter out positions occupied by snakes
for snake_handler in &game_data.snakes {
let mut current = Some(&snake_handler.snake.body);
while let Some(node) = current {
empty_positions.retain(|&(x, y)| (x as isize, y as isize) != (node.position.x, node.position.y));
current = node.next.as_deref();
}
}
// Spawn food
if let Some(&(x, y)) = empty_positions.choose(&mut rand::rng()) {
game_data.field.set_tile(x, y, Tile::Food);
}
}
}
impl GameManagerState for PlayingState {
fn update(&mut self, delta: Duration, inputs: &Inputs, game_data: &mut GameData) -> Option<Box<dyn GameManagerState>> {
let mut should_spawn_food = false;
let mut is_food_available = game_data.get_food_count() >= 1;
let all_snakes: Vec<Snake> = game_data.snakes.iter().map(|sh| sh.snake.clone()).collect();
for (current_snake_index, snake_handler) in game_data.snakes.iter_mut().enumerate() {
if snake_handler.snake.is_eating(&game_data.field) {
snake_handler.snake.grow();
should_spawn_food = true;
game_data.field.clear_food(snake_handler.snake.body.position.x as usize, snake_handler.snake.body.position.y as usize);
is_food_available = false;
}
let other_snakes = all_snakes
.iter()
.enumerate()
.filter_map(|(i, s)| {
if i != current_snake_index {
return Some(s.clone())
}
None
})
.collect();
snake_handler.update(delta, inputs, &other_snakes, &game_data.field);
}
if should_spawn_food && !is_food_available {
self.spawn_food(game_data);
}
None
}
}
@@ -30,4 +85,12 @@ impl GameManagerState for PausedState {
fn update(&mut self, _delta: Duration, _inputs: &Inputs, _game_data: &mut GameData) -> Option<Box<dyn GameManagerState>> {
None
}
}
}
pub struct FinishedState;
impl GameManagerState for FinishedState {
fn update(&mut self, _delta: Duration, _inputs: &Inputs, _game_data: &mut GameData) -> Option<Box<dyn GameManagerState>> {
None
}
}
+4
View File
@@ -29,4 +29,8 @@ impl Inputs {
pub fn set_pressed(&mut self, input: InputKey, value: bool) {
self.inputs_state.insert(input, value);
}
pub fn clear(&mut self) {
self.inputs_state.clear();
}
}
+1 -19
View File
@@ -5,22 +5,4 @@ pub mod common;
pub mod traits;
pub mod field;
pub mod game_handler;
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);
}
}
pub mod timer;
+33 -7
View File
@@ -4,11 +4,13 @@ use crate::inputs::Inputs;
use crate::field::field::{Field, Tile};
use crate::snake::states::SnakeState;
#[derive(Clone)]
pub struct Node {
pub position: Position<isize>,
next: Option<Box<Node>>,
pub next: Option<Box<Node>>,
}
#[derive(Clone)]
pub struct Snake {
pub body: Node,
pub speed: f32,
@@ -44,10 +46,18 @@ impl Snake {
false
}
pub fn is_colliding_against_snakes(&self, other_snakes: &Vec<Snake>) -> bool {
for other_snake in other_snakes {
if self.is_colliding_against_snake(other_snake) {
return true;
}
}
false
}
pub fn is_colliding_against_itself(&self) -> bool {
let head_pos = self.body.position;
let mut current_node = self.body.next.as_deref();
while let Some(node) = current_node {
if node.position == head_pos {
return true;
@@ -85,13 +95,29 @@ impl Snake {
field.get_tile(pos.x as usize, pos.y as usize) == Tile::Food
}
pub fn keep_within_boundaries(&mut self, width: isize, height: isize) {
if self.body.position.x >= width {
self.body.position.x = 0;
} else if self.body.position.x < 0 {
self.body.position.x = width - 1;
}
if self.body.position.y >= height {
self.body.position.y = 0;
} else if self.body.position.y < 0 {
self.body.position.y = height - 1;
}
}
pub fn do_move(&mut self) {
println!("moving?");
// 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;
println!("prev pos: {:?}", self.body.position);
self.body.position += self.moving_direction; // the first node in body is always the head
println!("next pos: {:?}", self.body.position);
let mut current_node = self.body.next.as_deref_mut();
while let Some(node) = current_node {
let temp = node.position;
@@ -115,14 +141,14 @@ impl Snake {
}
pub struct SnakeHandler {
snake: Snake,
state: Box<dyn SnakeState>,
pub snake: Snake,
pub 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);
pub fn update(&mut self, delta: Duration, inputs: &Inputs, other_snakes: &Vec<Snake>, field: &Field) {
let new_state = self.state.update(delta, inputs, &mut self.snake, other_snakes, field);
if let Some(new_state) = new_state {
self.state = new_state;
}
+26 -6
View File
@@ -1,15 +1,17 @@
use std::time::Duration;
use crate::common::position::Position;
use crate::field::field::Field;
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>>;
fn update(&mut self, delta: Duration, inputs: &Inputs, snake: &mut Snake, other_snakes: &Vec<Snake>, field: &Field) -> Option<Box<dyn SnakeState>>;
}
pub struct MovingState {
dt_accumulator: Duration,
}
pub struct StoppedState;
pub struct DeadState;
impl MovingState {
@@ -37,7 +39,14 @@ impl MovingState {
}
impl SnakeState for MovingState {
fn update(&mut self, delta: Duration, inputs: &Inputs, snake: &mut Snake) -> Option<Box<dyn SnakeState>> {
fn update(&mut self, delta: Duration, inputs: &Inputs, snake: &mut Snake, other_snakes: &Vec<Snake>, field: &Field) -> Option<Box<dyn SnakeState>> {
if
snake.is_colliding_against_itself()
|| snake.is_colliding_against_snakes(other_snakes)
|| snake.is_colliding_against_wall(field)
{
return Some(Box::new(DeadState{}));
}
self.handle_direction_change(inputs, snake);
let interval = Duration::from_secs_f32(1.0 / snake.speed);
self.dt_accumulator += delta;
@@ -46,13 +55,23 @@ impl SnakeState for MovingState {
self.dt_accumulator -= interval;
}
snake.keep_within_boundaries(field.width as isize, field.height as isize);
None
}
}
impl SnakeState for StoppedState {
fn update(&mut self, _delta: Duration, _inputs: &Inputs, _snake: &mut Snake) -> Option<Box<dyn SnakeState>> {
Some(Box::new(Self))
fn update(&mut self, _delta: Duration, _inputs: &Inputs, _snake: &mut Snake, _other_snakes: &Vec<Snake>, _field: &Field) -> Option<Box<dyn SnakeState>> {
println!("stopped?");
None
}
}
impl SnakeState for DeadState {
fn update(&mut self, _delta: Duration, _inputs: &Inputs, _snake: &mut Snake, _other_snakes: &Vec<Snake>, _field: &Field) -> Option<Box<dyn SnakeState>> {
println!("dead?");
None
}
}
@@ -66,13 +85,14 @@ mod tests {
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();
let field = Field::new(40, 40);
// Should not move
state.update(Duration::from_millis(500), &inputs, &mut snake);
state.update(Duration::from_millis(500), &inputs, &mut snake, &vec![], &field);
assert_eq!(snake.body.position, Position { x: 0, y: 0 });
// Should move
state.update(Duration::from_millis(500), &inputs, &mut snake);
state.update(Duration::from_millis(500), &inputs, &mut snake, &vec![], &field);
assert_eq!(snake.body.position, Position { x: 1, y: 0 });
}
+56
View File
@@ -0,0 +1,56 @@
use std::time::{Duration, Instant};
use std::time;
use std::thread;
pub struct Timer {
timer: Instant,
target_fps: usize,
_target_fps_as_micros: u128,
last_delta_time: Duration,
}
impl Timer {
pub fn new(target_fps: usize) -> Self {
Self {
timer: Instant::now(),
target_fps: target_fps,
_target_fps_as_micros: Timer::get_target_fps_as_micros(target_fps),
last_delta_time: Duration::ZERO
}
}
fn get_target_fps_as_micros(target_fps: usize) -> u128 {
(1000.0 / (target_fps as f64)) as u128 * 1000
}
pub fn set_target_fps(&mut self, target_fps: usize) {
self.target_fps = target_fps;
}
pub fn start(&mut self) {
self.timer = Instant::now();
}
pub fn end(&mut self) {
self.last_delta_time = self.timer.elapsed()
}
pub fn get_elapsed(&mut self) -> Duration {
self.timer.elapsed()
}
pub fn get_delta_time(&self) -> Duration {
// deltaTime should be the elapsed time between the last two frames.
self.last_delta_time
}
pub fn limit_fps(&self) {
// You should be using something like V-Sync instead of this, as there's no warrantee that thread::sleep will always wait the exact desired time.
let elapsed = self.timer.elapsed().as_micros();
if elapsed > self._target_fps_as_micros {
return;
}
let wait = (self._target_fps_as_micros - elapsed).try_into().unwrap();
thread::sleep(time::Duration::from_micros(wait));
}
}
+20
View File
@@ -0,0 +1,20 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
x x
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+20
View File
@@ -0,0 +1,20 @@
xxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx
x x
x x
x xx x
x xx x
x xx x
x xx x
xx
xxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxx
xx
x xx x
x xx x
x xx x
x xx x
x x
x x
x x
x x
xxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx
+20
View File
@@ -0,0 +1,20 @@