Hey,
I have spent 99% of my dev life using dynamic typed OO languages, and this is my first real attempt at a typed language.
I decided to implement the snake game as a pretty easy first project.
I have spent hours deliberating back and forth, pretty frustrated at some design decisions that I'm not happy with. I would love some insight into how to make it better.
Specifically the areas I don't like...
- the fact that an
Apple::new
needs to know about aSnake
as well as aBoard
so it can generate a random point which is within the bounds of the game, and not on a snake. This method feels very wrong, but I've tried maybe a dozen different ways to do this and they all feel bad. This is my main question on how to improve this... - Same is true for
Snake::new
... it has the same problem needing to know aboutBoard
. I cant decide if aSnake
should know that stuff, or it should just be told a specific point and theGame
handles that. IfGame
handles that then making aGame::generate_apple()
also feels wrong... - I thought about making
Snake
beSnake(Vec<Point>)
instead of a struct, however then down below I have to callself.0[0]
which feels wrong. If I make a method calledhead()
then i also need a mutable reference to head....... so does that mean i should makehead()
andmut_head()
or do it a different way?
use rand_derive2::RandGen;
use rand::Rng;
#[derive(Debug, PartialEq)]
struct Point {
x: u32,
y: u32,
}
#[derive(Debug, RandGen)]
pub enum Direction {
N,
E,
S,
W,
}
#[derive(Debug)]
struct Apple(Point);
impl Apple {
pub fn new(board: &Board, snake: &Snake) -> Self {
loop {
let point = board.random_point();
if !snake.tail.contains(&point) && point != snake.head {
return Self(point);
}
}
}
}
enum SnakeMove {
AteApple,
AteSelf,
Move,
}
#[derive(Debug)]
struct Snake {
head: Point,
tail: Vec<Point>,
}
impl Snake {
pub fn new(board: &Board) -> Self {
Self {
head: board.random_point(),
tail: Vec::new(),
}
}
// (0, 1)
// [(0, 2), (0, 3), (0, 4), (0, 5)]
fn move_snake(&mut self, board: &Board, apple: &Apple, direction: &Direction) -> SnakeMove {
// move existing head into tail and replace with new head
let new_head = board.next_point(&self.head, direction);
let old_head = std::mem::replace(&mut self.head, new_head);
self.tail.insert(0, old_head);
let ate_apple = self.head == apple.0;
if !ate_apple {
self.tail.pop();
}
if self.tail.contains(&self.head) {
SnakeMove::AteSelf
} else if ate_apple {
SnakeMove::AteApple
} else {
SnakeMove::Move
}
}
}
#[derive(Debug)]
struct Board {
width: u32,
height: u32,
}
impl Board {
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
fn random_point(&self) -> Point {
Point {
x: rand::thread_rng().gen_range(1..=self.width),
y: rand::thread_rng().gen_range(1..=self.height),
}
}
// 0 0 0 4
// 0 0 3 0
// 0 2 0 0
// 1 0 0 0
fn next_point(&self, point: &Point, direction: &Direction) -> Point {
match direction {
Direction::N => match point.y {
y if y == self.height => Point { x: point.x, y: 1 },
_ => Point {
x: point.x,
y: point.y + 1,
},
},
Direction::E => match point.x {
x if x == self.width => Point { x: 1, y: point.y },
_ => Point {
x: point.x + 1,
y: point.y,
},
},
Direction::S => match point.y {
1 => Point {
x: point.x,
y: self.height,
},
_ => Point {
x: point.x,
y: point.y - 1,
},
},
Direction::W => match point.x {
1 => Point {
x: self.width,
y: point.y,
},
_ => Point {
x: point.x - 1,
y: point.y,
},
},
}
}
}
#[derive(PartialEq)]
pub enum GameStatus {
Running,
GameOver,
}
#[derive(Debug)]
pub struct Game {
board: Board,
snake: Snake,
apple: Apple,
direction: Direction,
}
impl Game {
pub fn new(width: u32, height: u32) -> Self {
let board = Board::new(width, height);
let snake = Snake::new(&board);
let apple = Apple::new(&board, &snake);
let direction = Direction::generate_random();
Self { board, snake, apple, direction }
}
pub fn change_direction(&mut self, direction: Direction) {
self.direction = direction;
}
pub fn update(&mut self) -> GameStatus {
let snake_move = self.snake.move_snake(&self.board, &self.apple, &self.direction);
match snake_move {
SnakeMove::AteSelf => GameStatus::GameOver,
SnakeMove::AteApple => {
self.apple = Apple::new(&self.board, &self.snake);
GameStatus::Running
}
SnakeMove::Move => GameStatus::Running,
}
}
}
Here's how it's currently used. I haven't done any rendering logic yet:
#![allow(dead_code)]
use std::time::Duration;
use crate::engine::game::Game;
pub mod engine;
fn main() {
let mut game = Game::new(100, 300);
loop {
println!("{:?}", game);
let game_status = game.update();
if game_status == engine::game::GameStatus::GameOver {
break;
}
// randomly change direction
if rand::random() {
game.change_direction(engine::game::Direction::generate_random());
}
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
}
}