diff --git a/Cargo.lock b/Cargo.lock index 91226c5..080ad4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,9 +661,9 @@ checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "lighthouse-client" -version = "5.1.0" +version = "5.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8f80b963cfaa9286a91cb6bdb09b4eeabafc8fec028b2789d8565a37ef94d3" +checksum = "9f4df0db0e23ed992335a922308be153de310fec1d602f7a82ef1d7154a1f064" dependencies = [ "async-tungstenite", "futures", @@ -680,9 +680,9 @@ dependencies = [ [[package]] name = "lighthouse-protocol" -version = "5.1.0" +version = "5.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e7b5425a480372a5268b94f3548bc1340cca16efe3d2250ef425366f2cf3f" +checksum = "8524ca1fe8a772e8795c95c304127cc805e9cb3736a8fdb130010b06b1af02a2" dependencies = [ "rand 0.8.5", "rmpv", diff --git a/Cargo.toml b/Cargo.toml index 451c65e..0b31dca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/constants.rs b/src/constants.rs index ebd0f0b..27960c4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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; diff --git a/src/controller.rs b/src/controller.rs index 0002d23..e014a42 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -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>> + Unpin, shared_state: Arc>) -> Result<()> { + let mut mapped_players: HashMap = 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()); } } diff --git a/src/model/snake.rs b/src/model/snake.rs index 8d14bbd..bb0535b 100644 --- a/src/model/snake.rs +++ b/src/model/snake.rs @@ -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>, dir: Delta, + 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 = LIGHTHOUSE_RECT.sample_random().unwrap(); let dir = Delta::random_cardinal(); @@ -21,7 +26,7 @@ impl Snake { pos = LIGHTHOUSE_RECT.wrap(pos - dir); } - Self { fields, dir } + Self { fields, dir, color } } pub fn head(&self) -> Pos { *self.fields.front().unwrap() } @@ -39,7 +44,17 @@ impl Snake { } pub fn intersects_itself(&self) -> bool { - self.fields.iter().collect::>().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) -> bool { + self.fields.contains(&pos) } pub fn rotate_head(&mut self, dir: Delta) { @@ -48,7 +63,7 @@ impl Snake { pub fn render_to(&self, frame: &mut Frame) { for field in &self.fields { - frame[*field] = SNAKE_COLOR; + frame[*field] = self.color; } } @@ -56,17 +71,15 @@ impl Snake { self.fields.len() } - pub fn random_fruit_pos(&self) -> Option> { - let fields = self.fields.iter().collect::>(); - 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> { + &self.fields + } + + pub fn field_set(&self) -> HashSet> { + self.fields.iter().cloned().collect::>() + } + + pub fn color(&self) -> Color { + self.color } } diff --git a/src/model/state.rs b/src/model/state.rs index d412301..f3e7275 100644 --- a/src/model/state.rs +++ b/src/model/state.rs @@ -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, + snakes: Vec, + fruit: Option>, } 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> { + let occupied = self.snakes.iter().flat_map(|s| s.fields()).collect::>(); + 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::>>(); + 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] + } }