Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ anyhow = "1.0.81"
clap = { version = "4.5.3", features = ["derive", "env"] }
dotenvy = "0.15.7"
futures = "0.3.30"
lighthouse-client = "5.1.0"
lighthouse-client = "5.1.5"
rand = "0.9.0"
tokio = { version = "1.36.0", features = ["rt", "macros", "time"] }
tracing = "0.1.40"
Expand Down
2 changes: 1 addition & 1 deletion src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ use lighthouse_client::protocol::Color;

pub const UPDATE_INTERVAL: Duration = Duration::from_millis(200);
pub const FRUIT_COLOR: Color = Color::RED;
pub const SNAKE_COLOR: Color = Color::GREEN;
pub const SNAKE_COLORS: [Color; 7] = [Color::GREEN, Color::YELLOW, Color::CYAN, Color::MAGENTA, Color::BLUE, Color::WHITE, Color::GRAY];
pub const SNAKE_INITIAL_LENGTH: usize = 3;
24 changes: 20 additions & 4 deletions src/controller.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};

use anyhow::Result;
use futures::{lock::Mutex, prelude::*, Stream};
use lighthouse_client::protocol::{InputEvent, ServerMessage};
use tracing::debug;
use lighthouse_client::protocol::{EventSource, InputEvent, ServerMessage};
use tracing::{debug, info};

use crate::model::State;

pub async fn run(mut stream: impl Stream<Item = lighthouse_client::Result<ServerMessage<InputEvent>>> + Unpin, shared_state: Arc<Mutex<State>>) -> Result<()> {
let mut mapped_players: HashMap<EventSource, usize> = HashMap::new();

while let Some(msg) = stream.next().await {
let input_event = msg?.payload;
let source = input_event.source();

// Map the player if needed to a snake
if !mapped_players.contains_key(&source) {
let i = mapped_players.len();
info!("Mapped new player {} to snake {}", &source, i + 1);
mapped_players.insert(source.clone(), i);

let mut state = shared_state.lock().await;
state.ensure_snakes(mapped_players.len());
}

let i = mapped_players[&source];

// Update the snake's direction
if let Some(dir) = input_event.direction() {
debug!("Rotating snake head to {:?}", dir);
let mut state = shared_state.lock().await;
state.snake.rotate_head(dir.into());
state.snake_mut(i).rotate_head(dir.into());
}
}

Expand Down
49 changes: 31 additions & 18 deletions src/model/snake.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
use std::collections::{HashSet, VecDeque};

use lighthouse_client::protocol::{Delta, Frame, Pos, LIGHTHOUSE_RECT, LIGHTHOUSE_SIZE};
use lighthouse_client::protocol::{Color, Delta, Frame, Pos, LIGHTHOUSE_RECT};

use crate::constants::SNAKE_COLOR;
use crate::constants::SNAKE_INITIAL_LENGTH;

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Snake {
fields: VecDeque<Pos<i32>>,
dir: Delta<i32>,
color: Color,
}

impl Snake {
pub fn from_initial_length(length: usize) -> Self {
pub fn new(color: Color) -> Self {
Self::with_length(SNAKE_INITIAL_LENGTH, color)
}

pub fn with_length(length: usize, color: Color) -> Self {
let mut pos: Pos<i32> = LIGHTHOUSE_RECT.sample_random().unwrap();
let dir = Delta::random_cardinal();

Expand All @@ -21,7 +26,7 @@ impl Snake {
pos = LIGHTHOUSE_RECT.wrap(pos - dir);
}

Self { fields, dir }
Self { fields, dir, color }
}

pub fn head(&self) -> Pos<i32> { *self.fields.front().unwrap() }
Expand All @@ -39,7 +44,17 @@ impl Snake {
}

pub fn intersects_itself(&self) -> bool {
self.fields.iter().collect::<HashSet<_>>().len() < self.fields.len()
self.field_set().len() < self.fields.len()
}

pub fn intersects(&self, other: &Snake) -> bool {
let own_fields = self.field_set();
let other_fields = other.field_set();
!own_fields.is_disjoint(&other_fields)
}

pub fn contains(&self, pos: Pos<i32>) -> bool {
self.fields.contains(&pos)
}

pub fn rotate_head(&mut self, dir: Delta<i32>) {
Expand All @@ -48,25 +63,23 @@ impl Snake {

pub fn render_to(&self, frame: &mut Frame) {
for field in &self.fields {
frame[*field] = SNAKE_COLOR;
frame[*field] = self.color;
}
}

pub fn len(&self) -> usize {
self.fields.len()
}

pub fn random_fruit_pos(&self) -> Option<Pos<i32>> {
let fields = self.fields.iter().collect::<HashSet<_>>();
if fields.len() >= LIGHTHOUSE_SIZE {
None
} else {
loop {
let pos = LIGHTHOUSE_RECT.sample_random().unwrap();
if !fields.contains(&pos) {
break Some(pos);
}
}
}
pub fn fields(&self) -> &VecDeque<Pos<i32>> {
&self.fields
}

pub fn field_set(&self) -> HashSet<Pos<i32>> {
self.fields.iter().cloned().collect::<HashSet<_>>()
}

pub fn color(&self) -> Color {
self.color
}
}
135 changes: 113 additions & 22 deletions src/model/state.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,143 @@
use lighthouse_client::protocol::{Frame, Pos};
use std::collections::HashSet;

use lighthouse_client::protocol::{Frame, Pos, LIGHTHOUSE_COLS, LIGHTHOUSE_ROWS};
use rand::seq::IndexedRandom;
use tracing::info;

use crate::constants::{FRUIT_COLOR, SNAKE_INITIAL_LENGTH};
use crate::constants::{FRUIT_COLOR, SNAKE_COLORS};

use super::Snake;

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct State {
pub snake: Snake,
pub fruit: Pos<i32>,
snakes: Vec<Snake>,
fruit: Option<Pos<i32>>,
}

impl State {
pub fn empty() -> Self {
Self {
snakes: Vec::new(),
fruit: None,
}
}

pub fn new() -> Self {
let snake = Snake::from_initial_length(SNAKE_INITIAL_LENGTH);
let fruit = snake.random_fruit_pos().unwrap();
Self { snake, fruit }
let mut state = Self::empty();
state.ensure_snakes(1);
state.fruit = state.random_fruit_pos();
state
}

pub fn reset(&mut self) {
*self = Self::new();
}

pub fn respawn(&mut self, i: usize) {
// TODO: Be smarter about this, i.e. avoid intersecting another snake or the fruit
self.snakes[i] = Snake::new(self.snakes[i].color());
}

fn random_fruit_pos(&self) -> Option<Pos<i32>> {
let occupied = self.snakes.iter().flat_map(|s| s.fields()).collect::<HashSet<_>>();
let free = (0..LIGHTHOUSE_ROWS)
.flat_map(|y| (0..LIGHTHOUSE_COLS).map(move |x| Pos::new(x as i32, y as i32)))
.filter(|pos| !occupied.contains(pos))
.collect::<Vec<Pos<i32>>>();
return free.choose(&mut rand::rng()).cloned();
}

pub fn step(&mut self) {
self.snake.step();

if self.snake.head() == self.fruit {
self.snake.grow();
let length = self.snake.len();
info! { %length, "Snake grew" };
if let Some(fruit) = self.snake.random_fruit_pos() {
self.fruit = fruit;
self.step_snakes();
self.check_self_collisions();
self.check_collisions();
self.check_fruits();
}

fn step_snakes(&mut self) {
for snake in &mut self.snakes {
snake.step();
}
}

fn check_self_collisions(&mut self) {
if let Some(i) = 'outer: {
for (i, snake) in self.snakes.iter_mut().enumerate() {
if snake.intersects_itself() {
break 'outer Some(i);
}
}
None
} {
info!("Snake {} died!", i + 1);
self.respawn(i);
}
}

fn check_collisions(&mut self) {
if let Some(loser) = 'outer: {
for i in 0..self.snakes.len() {
for j in (i + 1)..self.snakes.len() {
let snake1 = &self.snakes[i];
let snake2 = &self.snakes[j];

if let Some(loser) = if snake1.head() == snake2.head() {
// Decide randomly which snake dies
Some(if rand::random() { i } else { j })
} else if snake1.contains(snake2.head()) {
Some(j) // Snake 2 dies
} else if snake2.contains(snake1.head()) {
Some(i) // Snake 1 dies
} else {
None
} {
break 'outer Some(loser);
};
}
}
None
} {
info!("Snake {} was killed!", loser + 1);
self.respawn(loser);
}
}

fn check_fruits(&mut self) {
if let Some((i, snake)) = self.snakes.iter_mut().enumerate().find(|(_, s)| Some(s.head()) == self.fruit) {
snake.grow();
let length = snake.len();
info! { %length, "Snake {} grew", i + 1 };
if let Some(fruit) = self.random_fruit_pos() {
self.fruit = Some(fruit);
} else {
info!("You win!");
info!("Snake {} wins!", i + 1);
self.reset();
}
} else if self.snake.intersects_itself() {
info!("Game over!");
self.reset();
}
}

pub fn render(&self) -> Frame {
let mut frame = Frame::empty();

frame[self.fruit] = FRUIT_COLOR;
self.snake.render_to(&mut frame);
if let Some(fruit) = self.fruit {
frame[fruit] = FRUIT_COLOR;
}

for snake in &self.snakes {
snake.render_to(&mut frame);
}

frame
}

pub fn ensure_snakes(&mut self, count: usize) {
while self.snakes.len() < count {
// TODO: Be smarter about this, i.e. avoid intersecting another snake or the fruit
self.snakes.push(Snake::new(SNAKE_COLORS[self.snakes.len() % SNAKE_COLORS.len()]));
}
}

pub fn snake_mut(&mut self, i: usize) -> &mut Snake {
&mut self.snakes[i]
}
}