feat(snake): implement snake game with pure game logic, renderer, and IPC-based main loop

This commit is contained in:
2026-10-18 04:00:09 +02:00
parent a7913ddeef
commit 0389c85108
8 changed files with 458 additions and 2 deletions

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