feat(snake): implement snake game with pure game logic, renderer, and IPC-based main loop
This commit is contained in:
159
crates/apps/snake/src/game.rs
Normal file
159
crates/apps/snake/src/game.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
pub const GRID_WIDTH: u16 = 20;
|
||||
pub const GRID_HEIGHT: u16 = 15;
|
||||
const MAX_SNAKE_LEN: usize = 128;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Position {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
pub fn opposite(&self) -> Direction {
|
||||
match self {
|
||||
Direction::Up => Direction::Down,
|
||||
Direction::Down => Direction::Up,
|
||||
Direction::Left => Direction::Right,
|
||||
Direction::Right => Direction::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GameState {
|
||||
Running,
|
||||
GameOver,
|
||||
}
|
||||
|
||||
pub struct Snake {
|
||||
body: [Position; MAX_SNAKE_LEN],
|
||||
len: usize,
|
||||
direction: Direction,
|
||||
next_direction: Direction,
|
||||
food: Position,
|
||||
state: GameState,
|
||||
score: u16,
|
||||
rng_state: u32,
|
||||
}
|
||||
|
||||
impl Snake {
|
||||
pub fn new() -> Self {
|
||||
let start = Position { x: GRID_WIDTH / 2, y: GRID_HEIGHT / 2 };
|
||||
let mut body = [Position { x: 0, y: 0 }; MAX_SNAKE_LEN];
|
||||
body[0] = start;
|
||||
body[1] = Position { x: start.x - 1, y: start.y };
|
||||
body[2] = Position { x: start.x - 2, y: start.y };
|
||||
|
||||
let mut game = Self {
|
||||
body,
|
||||
len: 3,
|
||||
direction: Direction::Right,
|
||||
next_direction: Direction::Right,
|
||||
food: Position { x: 5, y: 5 },
|
||||
state: GameState::Running,
|
||||
score: 0,
|
||||
rng_state: 42,
|
||||
};
|
||||
game.place_food();
|
||||
game
|
||||
}
|
||||
|
||||
pub fn set_direction(&mut self, dir: Direction) {
|
||||
if dir != self.direction.opposite() {
|
||||
self.next_direction = dir;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
if self.state != GameState::Running {
|
||||
return;
|
||||
}
|
||||
|
||||
self.direction = self.next_direction;
|
||||
|
||||
let head = self.body[0];
|
||||
let new_head = match self.direction {
|
||||
Direction::Up => {
|
||||
if head.y == 0 { self.state = GameState::GameOver; return; }
|
||||
Position { x: head.x, y: head.y - 1 }
|
||||
}
|
||||
Direction::Down => {
|
||||
if head.y >= GRID_HEIGHT - 1 { self.state = GameState::GameOver; return; }
|
||||
Position { x: head.x, y: head.y + 1 }
|
||||
}
|
||||
Direction::Left => {
|
||||
if head.x == 0 { self.state = GameState::GameOver; return; }
|
||||
Position { x: head.x - 1, y: head.y }
|
||||
}
|
||||
Direction::Right => {
|
||||
if head.x >= GRID_WIDTH - 1 { self.state = GameState::GameOver; return; }
|
||||
Position { x: head.x + 1, y: head.y }
|
||||
}
|
||||
};
|
||||
|
||||
if self.hits_self(new_head) {
|
||||
self.state = GameState::GameOver;
|
||||
return;
|
||||
}
|
||||
|
||||
let ate_food = new_head == self.food;
|
||||
|
||||
if !ate_food {
|
||||
for i in (1..self.len).rev() {
|
||||
self.body[i] = self.body[i - 1];
|
||||
}
|
||||
} else {
|
||||
if self.len < MAX_SNAKE_LEN {
|
||||
for i in (1..self.len + 1).rev() {
|
||||
self.body[i] = self.body[i - 1];
|
||||
}
|
||||
self.len += 1;
|
||||
}
|
||||
self.score += 1;
|
||||
self.place_food();
|
||||
}
|
||||
|
||||
self.body[0] = new_head;
|
||||
}
|
||||
|
||||
fn hits_self(&self, pos: Position) -> bool {
|
||||
for i in 0..self.len {
|
||||
if self.body[i] == pos {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn place_food(&mut self) {
|
||||
loop {
|
||||
self.rng_state ^= self.rng_state << 13;
|
||||
self.rng_state ^= self.rng_state >> 17;
|
||||
self.rng_state ^= self.rng_state << 5;
|
||||
|
||||
let x = (self.rng_state % GRID_WIDTH as u32) as u16;
|
||||
let y = ((self.rng_state / GRID_WIDTH as u32) % GRID_HEIGHT as u32) as u16;
|
||||
let pos = Position { x, y };
|
||||
|
||||
if !self.hits_self(pos) {
|
||||
self.food = pos;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn head(&self) -> Position { self.body[0] }
|
||||
pub fn body(&self) -> &[Position] { &self.body[..self.len] }
|
||||
pub fn food(&self) -> Position { self.food }
|
||||
pub fn state(&self) -> GameState { self.state }
|
||||
pub fn score(&self) -> u16 { self.score }
|
||||
pub fn direction(&self) -> Direction { self.direction }
|
||||
}
|
||||
Reference in New Issue
Block a user