use clap::Parser;
use futures::{Stream, lock::Mutex, StreamExt};
use lighthouse_client::{Lighthouse, Result, TokioWebSocket, LIGHTHOUSE_URL, protocol::{Authentication, Color, Frame, ServerMessage, LIGHTHOUSE_RECT, LIGHTHOUSE_SIZE}};
use lighthouse_protocol::{Delta, InputEvent, KeyEvent, Pos};
use tracing::{info, debug};
use tokio::{task, time};
use std::{collections::{VecDeque, HashSet}, sync::Arc, time::Duration};
const UPDATE_INTERVAL: Duration = Duration::from_millis(200);
const FRUIT_COLOR: Color = Color::RED;
const SNAKE_COLOR: Color = Color::GREEN;
const SNAKE_INITIAL_LENGTH: usize = 3;
#[derive(Debug, PartialEq, Eq, Clone)]
struct Snake {
fields: VecDeque<Pos<i32>>,
dir: Delta<i32>,
}
impl Snake {
fn from_initial_length(length: usize) -> Self {
let mut pos: Pos<i32> = LIGHTHOUSE_RECT.sample_random().unwrap();
let dir = Delta::<i32>::random_cardinal();
let mut fields = VecDeque::new();
for _ in 0..length {
fields.push_back(pos);
pos = LIGHTHOUSE_RECT.wrap(pos - dir);
}
Self { fields, dir }
}
fn head(&self) -> Pos<i32> { *self.fields.front().unwrap() }
fn back(&self) -> Pos<i32> { *self.fields.back().unwrap() }
fn grow(&mut self) {
self.fields.push_back(LIGHTHOUSE_RECT.wrap(self.back() - self.dir));
}
fn step(&mut self) {
let head = self.head();
self.fields.pop_back();
self.fields.push_front(LIGHTHOUSE_RECT.wrap(head + self.dir));
}
fn intersects_itself(&self) -> bool {
self.fields.iter().collect::<HashSet<_>>().len() < self.fields.len()
}
fn rotate_head(&mut self, dir: Delta<i32>) {
self.dir = dir;
}
fn render_to(&self, frame: &mut Frame) {
for field in &self.fields {
frame[*field] = SNAKE_COLOR;
}
}
fn len(&self) -> usize {
self.fields.len()
}
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);
}
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct State {
snake: Snake,
fruit: Pos<i32>,
}
impl State {
fn new() -> Self {
let snake = Snake::from_initial_length(SNAKE_INITIAL_LENGTH);
let fruit = snake.random_fruit_pos().unwrap();
Self { snake, fruit }
}
fn reset(&mut self) {
*self = Self::new();
}
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;
} else {
info!("You win!");
self.reset();
}
} else if self.snake.intersects_itself() {
info!("Game over!");
self.reset();
}
}
fn render(&self) -> Frame {
let mut frame = Frame::empty();
frame[self.fruit] = FRUIT_COLOR;
self.snake.render_to(&mut frame);
frame
}
}
async fn run_updater(lh: Lighthouse<TokioWebSocket>, shared_state: Arc<Mutex<State>>) -> Result<()> {
loop {
let frame = {
let mut state = shared_state.lock().await;
state.step();
state.render()
};
lh.put_model(frame).await?;
debug!("Sent frame");
time::sleep(UPDATE_INTERVAL).await;
}
}
async fn run_controller(mut stream: impl Stream<Item = Result<ServerMessage<InputEvent>>> + Unpin, shared_state: Arc<Mutex<State>>) -> Result<()> {
while let Some(msg) = stream.next().await {
match msg?.payload {
InputEvent::Key(KeyEvent { code, down, .. }) if down => {
let opt_dir = match code.as_str() {
"ArrowLeft" => Some(Delta::<i32>::LEFT),
"ArrowUp" => Some(Delta::<i32>::UP),
"ArrowRight" => Some(Delta::<i32>::RIGHT),
"ArrowDown" => Some(Delta::<i32>::DOWN),
_ => None,
};
if let Some(dir) = opt_dir {
debug!("Rotating snake head to {:?}", dir);
let mut state = shared_state.lock().await;
state.snake.rotate_head(dir);
}
}
_ => {},
}
}
Ok(())
}
#[derive(Parser)]
struct Args {
#[arg(short, long, env = "LIGHTHOUSE_USER")]
username: String,
#[arg(short, long, env = "LIGHTHOUSE_TOKEN")]
token: String,
#[arg(long, env = "LIGHTHOUSE_URL", default_value = LIGHTHOUSE_URL)]
url: String,
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
tracing_subscriber::fmt().init();
_ = dotenvy::dotenv();
let args = Args::parse();
let auth = Authentication::new(&args.username, &args.token);
let state = Arc::new(Mutex::new(State::new()));
let lh = Lighthouse::connect_with_tokio_to(&args.url, auth).await?;
info!("Connected to the Lighthouse server");
let stream = lh.stream_input().await?;
let updater_handle = task::spawn(run_updater(lh, state.clone()));
let controller_handle = task::spawn(run_controller(stream, state));
updater_handle.await.unwrap()?;
controller_handle.await.unwrap()?;
Ok(())
}