Compare commits

...

3 Commits

Author SHA1 Message Date
b4166d9e63 logic to keep snake within boundaries 2026-03-10 17:09:48 -05:00
f75ab1ac47 snake can now move, eat food and grow 2026-03-10 16:39:40 -05:00
33db0eaaef draw snake 2026-03-10 12:13:21 -05:00
17 changed files with 278 additions and 20 deletions

45
.vscode/launch.json vendored Normal file
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"
]
}
}
]
}

17
Cargo.lock generated
View File

@@ -57,6 +57,7 @@ name = "game_cli_frontend"
version = "0.1.0"
dependencies = [
"game_core",
"termion",
]
[[package]]
@@ -149,6 +150,12 @@ 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"
@@ -259,6 +266,16 @@ dependencies = [
"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"

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
game_core = { path = "../game_core" }
termion = "4.0.6"

View File

@@ -0,0 +1,6 @@
pub fn reset_cursor() {
println!(
"{}",
termion::cursor::Goto(1, 1),
);
}

View File

@@ -1,14 +1,19 @@
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) {
println!("{}", BOUNDARY_TILE.repeat(field.width + 2));
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!("{}", BOUNDARY_TILE);
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 {
@@ -16,9 +21,9 @@ pub fn draw_field(field: &Field) {
Tile::Wall => WALL_TILE,
Tile::Food => FOOD_TILE,
};
print!("{}", char);
print!("{}{}", termion::color::Fg(color), char);
}
println!("{}", BOUNDARY_TILE);
print!("{}{}\r\n", termion::color::Fg(color), BOUNDARY_TILE);
}
println!("{}", BOUNDARY_TILE.repeat(field.width + 2));
println!("{}{}\r\n", termion::color::Fg(color), BOUNDARY_TILE.repeat(field.width + 2));
}

View File

@@ -1 +1,3 @@
pub mod field;
pub mod field;
pub mod snake;
pub mod common;

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

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

View File

@@ -1,6 +1,7 @@
mod drawers;
mod renderers;
mod input_mapping;
use crate::renderers::renderer::main_loop;

View File

@@ -1,11 +1,23 @@
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 std::{thread, time};
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
@@ -14,22 +26,81 @@ fn clear_screen() {
io::stdout().flush().unwrap();
}
pub fn main_loop() {
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();
draw_field(&field);
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();
}
}
}

View File

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

View File

@@ -16,7 +16,9 @@ impl FieldLoader for PlainTextFieldLoader {
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,

View File

@@ -1,6 +1,8 @@
use crate::{field::field::{Field, Tile}, game_handler::states::GameManagerState, 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 {
pub field: Field,
@@ -22,6 +24,16 @@ impl GameData {
}
pub struct GameManager {
game_data: GameData,
game_manager_state: Box<dyn GameManagerState>,
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;
}
}
}

View File

@@ -19,7 +19,7 @@ impl PlayingState {
Self { dt_accumulator: Duration::ZERO }
}
fn spawn_food(&mut self, game_data: &mut GameData) {
pub fn spawn_food(&mut self, game_data: &mut GameData) {
let mut empty_positions = Vec::new();
// Get all empty tiles from the field
@@ -51,10 +51,14 @@ 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()
@@ -68,7 +72,7 @@ impl GameManagerState for PlayingState {
.collect();
snake_handler.update(delta, inputs, &other_snakes, &game_data.field);
}
if should_spawn_food {
if should_spawn_food && !is_food_available {
self.spawn_food(game_data);
}
None

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

View File

@@ -58,7 +58,6 @@ impl Snake {
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;
@@ -96,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;
@@ -127,7 +142,7 @@ impl Snake {
pub struct SnakeHandler {
pub snake: Snake,
state: Box<dyn SnakeState>,
pub state: Box<dyn SnakeState>,
}

View File

@@ -55,18 +55,22 @@ 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, _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
}
}