diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bot/mod.rs | 1 | ||||
-rw-r--r-- | src/game/map.rs | 41 | ||||
-rw-r--r-- | src/game/mod.rs | 28 | ||||
-rw-r--r-- | src/game/server.rs | 70 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/spectate/index.html | 6 | ||||
-rw-r--r-- | src/spectate/main.ts | 93 | ||||
-rw-r--r-- | src/spectate/server.rs | 28 |
8 files changed, 235 insertions, 33 deletions
diff --git a/src/bot/mod.rs b/src/bot/mod.rs new file mode 100644 index 0000000..71c659f --- /dev/null +++ b/src/bot/mod.rs @@ -0,0 +1 @@ +pub async fn spawn_bots() {} diff --git a/src/game/map.rs b/src/game/map.rs new file mode 100644 index 0000000..420e256 --- /dev/null +++ b/src/game/map.rs @@ -0,0 +1,41 @@ +use glam::IVec2; +use std::ops::{Index, IndexMut}; + +pub struct Map { + pub cells: Vec<Option<u32>>, + pub size: IVec2, +} +impl Map { + pub fn new(width: usize, height: usize) -> Self { + Self { + cells: vec![None; width * height], + size: IVec2::new(width as i32, height as i32), + } + } + pub fn index(&self, v: IVec2) -> usize { + let i = v.x.rem_euclid(self.size.x as i32) + + v.y.rem_euclid(self.size.y as i32) * self.size.x as i32; + i as usize + } + pub fn clear_player(&mut self, p: u32) { + self.cells.iter_mut().for_each(|c| { + if let Some(cm) = c { + if *cm == p { + *c = None; + } + } + }); + } +} +impl Index<IVec2> for Map { + type Output = Option<u32>; + fn index(&self, index: IVec2) -> &Self::Output { + &self.cells[self.index(index)] + } +} +impl IndexMut<IVec2> for Map { + fn index_mut(&mut self, index: IVec2) -> &mut Self::Output { + let i = self.index(index); + &mut self.cells[i] + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 27faa27..f59b5d5 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,34 +1,34 @@ use glam::IVec2; +use map::Map; use protocol::Direction; use serde::Deserialize; use std::{collections::HashMap, net::SocketAddr, ops::ControlFlow}; +pub mod map; pub mod protocol; pub mod server; -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct Config { bind: SocketAddr, + tickrate: f32, } pub struct Game { pub heads: HashMap<u32, (Direction, IVec2, String)>, - pub map: HashMap<IVec2, u32>, - pub size: IVec2, + pub map: Map, pub dead: Vec<u32>, } impl Game { - pub fn new(players: Vec<String>) -> Self { - let mut map = HashMap::new(); + pub fn new(players: Vec<(u32, String)>) -> Self { + let mut map = Map::new(players.len() * 2, players.len() * 2); let mut heads = HashMap::new(); - let plen = players.len(); - for (p, name) in players.into_iter().enumerate() { + for (p, name) in players { let pos = IVec2::ONE * p as i32 * 2; - map.insert(pos, p as u32); - heads.insert(p as u32, (Direction::Up, pos, name)); + map[pos] = Some(p); + heads.insert(p, (Direction::Up, pos, name)); } Self { - size: IVec2::ONE * plen as i32 * 2, heads, map, dead: Vec::new(), @@ -36,14 +36,14 @@ impl Game { } pub fn tick(&mut self) -> ControlFlow<Option<u32>, ()> { for (_player, (dir, head, _)) in &mut self.heads { - *head = (*head + dir.vector()).rem_euclid(self.size) + *head = (*head + dir.vector()).rem_euclid(self.map.size) } self.dead.clear(); let mut h = HashMap::<IVec2, Vec<u32>>::new(); for (player, (_, head, _)) in &self.heads { h.entry(*head).or_default().push(*player); - if self.map.contains_key(head) { + if self.map[*head].is_some() { self.dead.push(*player); } } @@ -54,11 +54,11 @@ impl Game { } for (player, (_, head, _)) in &mut self.heads { - self.map.insert(*head, *player); + self.map[*head] = Some(*player); } for d in &self.dead { - self.map.retain(|_, p| *d != *p); + self.map.clear_player(*d); self.heads.remove(&d); } diff --git a/src/game/server.rs b/src/game/server.rs index a4472a8..f1d10d2 100644 --- a/src/game/server.rs +++ b/src/game/server.rs @@ -1,15 +1,18 @@ -use super::{protocol::Packet, Config}; +use super::{protocol::Packet, Config, Game}; use crate::State; use anyhow::{anyhow, bail, Result}; use log::{debug, error, info}; -use std::sync::Arc; +use std::{ops::ControlFlow, sync::Arc, time::Duration}; use tokio::{ io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}, net::{TcpListener, TcpStream}, spawn, + time::sleep, }; pub async fn game_server(config: Config, state: Arc<State>) -> Result<()> { + spawn(game_loop(config.clone(), state.clone())); + let listener = TcpListener::bind(config.bind).await?; info!("listening on {}", listener.local_addr()?); while let Ok((sock, addr)) = listener.accept().await { @@ -25,10 +28,39 @@ pub async fn game_server(config: Config, state: Arc<State>) -> Result<()> { bail!("accept failure") } +async fn game_loop(config: Config, state: Arc<State>) { + loop { + sleep(Duration::from_secs_f32(1. / config.tickrate)).await; + + let mut g = state.game.write().await; + let res = g.tick(); + match res { + ControlFlow::Continue(()) => { + let _ = state.tick.send(false); + } + ControlFlow::Break(winner) => { + info!("winner: {winner:?}"); + let p = state.players.read().await; + *g = Game::new(p.clone().into_iter().collect()); + let _ = state.tick.send(true); + } + } + drop(g); + } +} + +struct ClientState { + pid: Option<u32>, + alive: bool, +} + async fn handle_client(sock: TcpStream, state: Arc<State>) -> Result<()> { - let mut pid = None; - let res = handle_client_inner(sock, &state, &mut pid).await; - if let Some(pid) = pid { + let mut cstate = ClientState { + pid: None, + alive: false, + }; + let res = handle_client_inner(sock, &state, &mut cstate).await; + if let Some(pid) = cstate.pid { state.players.write().await.remove(&pid); } res @@ -37,7 +69,7 @@ async fn handle_client(sock: TcpStream, state: Arc<State>) -> Result<()> { async fn handle_client_inner( sock: TcpStream, state: &Arc<State>, - pid: &mut Option<u32>, + pid: &mut ClientState, ) -> anyhow::Result<()> { let (rx, tx) = sock.into_split(); let rx = BufReader::new(rx); @@ -79,19 +111,25 @@ impl<T: AsyncWrite + Unpin> SendPacketExt for T { async fn handle_tick( mut tx: impl AsyncWrite + Unpin, - pid: &mut Option<u32>, + cstate: &mut ClientState, state: &Arc<State>, new_game: bool, ) -> anyhow::Result<()> { - let Some(pid) = pid else { return Ok(()) }; + let Some(pid) = cstate.pid else { return Ok(()) }; let mut events = Vec::new(); + if new_game { + if cstate.alive { + tx.send_packet(Packet::Win(0, 0)).await?; + } + cstate.alive = true; + } { let g = state.game.read().await; if new_game { events.push(Packet::Game { my_id: 0, - width: g.size.x as usize, - height: g.size.y as usize, + width: g.map.size.x as usize, + height: g.map.size.y as usize, }); for (player, (_, _, name)) in &g.heads { events.push(Packet::Player { @@ -110,7 +148,7 @@ async fn handle_tick( if !g.dead.is_empty() { events.push(Packet::Die(g.dead.clone())); } - if g.dead.contains(pid) { + if g.dead.contains(&pid) { events.push(Packet::Lose(0, 0)); // TODO implement stats } events.push(Packet::Tick); @@ -123,7 +161,7 @@ async fn handle_tick( async fn handle_packet( mut tx: impl AsyncWrite + Unpin, - pid: &mut Option<u32>, + cstate: &mut ClientState, state: &Arc<State>, line: String, ) -> anyhow::Result<()> { @@ -141,7 +179,7 @@ async fn handle_packet( username, password: _, } => { - if pid.is_some() { + if cstate.pid.is_some() { tx.send_packet(Packet::Error("already joined".to_string())) .await? } else { @@ -151,13 +189,13 @@ async fn handle_packet( id += 1; } g.insert(id, username); - *pid = Some(id); + cstate.pid = Some(id); } } Packet::Move(dir) => { - if let Some(pid) = pid { + if let Some(pid) = cstate.pid { let mut g = state.game.write().await; - if let Some((head_dir, _, _)) = g.heads.get_mut(pid) { + if let Some((head_dir, _, _)) = g.heads.get_mut(&pid) { *head_dir = dir } else { drop(g); @@ -7,6 +7,7 @@ use tokio::sync::{broadcast, RwLock}; pub mod config; pub mod game; pub mod spectate; +pub mod bot; pub struct State { pub tick: broadcast::Sender<bool>, // true for new game diff --git a/src/spectate/index.html b/src/spectate/index.html index 235a894..94ef698 100644 --- a/src/spectate/index.html +++ b/src/spectate/index.html @@ -4,8 +4,12 @@ <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GPN Tron</title> + <script src="main.js"></script> </head> <body> - + <noscript> + This is the GPN-Tron spectator application. You need JavaScript to + run it. + </noscript> </body> </html> diff --git a/src/spectate/main.ts b/src/spectate/main.ts new file mode 100644 index 0000000..4d508b6 --- /dev/null +++ b/src/spectate/main.ts @@ -0,0 +1,93 @@ +/// <reference lib="dom" /> + +const ws = new WebSocket("/events") + +type Packet = "tick" + | { pos: { id: number, x: number, y: number } } + | { game: { width: number, height: number } } + | { player: { id: number, name: string } } + +class Snake { + parts: { x: number, y: number, dx: number, dy: number }[] = [] + constructor(public name: string) { } + add_part(x: number, y: number) { + if (!this.parts.length) return this.parts.push({ x, y, dx: 0, dy: 0 }) + const last = this.parts[this.parts.length - 1] + let dx = x - last.x, dy = y - last.y; + if (x > last.x + 1 || x < last.x - 1) dx *= -1, dx /= Math.abs(dx) + if (y > last.y + 1 || y < last.y - 1) dy *= -1, dy /= Math.abs(dy) + this.parts.push({ x, y, dx, dy }) + console.log(this.parts); + + } +} +let size = 0 +let snakes = new Map<number, Snake>() + +let canvas: HTMLCanvasElement +let ctx: CanvasRenderingContext2D + +document.addEventListener("DOMContentLoaded", () => { + canvas = document.createElement("canvas") + ctx = canvas.getContext("2d")! + document.body.append(canvas) + canvas.width = 1000; + canvas.height = 1000; + redraw() +}) + +function redraw() { + ctx.fillStyle = "black" + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save() + const scale = canvas.width / size + ctx.scale(scale, scale) + for (let x = 0; x < size; x++) { + for (let y = 0; y < size; y++) { + ctx.strokeStyle = "grey" + ctx.lineWidth = 0.02 + ctx.strokeRect(x, y, 1, 1) + } + } + ctx.translate(0.5, 0.5) + for (const [xo, yo] of [[0, 0], [-size, 0], [size, 0], [0, -size], [0, size]]) { + ctx.save() + ctx.translate(xo, yo) + for (const snake of snakes.values()) { + ctx.beginPath(); + for (let i = 0; i < snake.parts.length; i++) { + const p = snake.parts[i]; + ctx.moveTo(p.x - p.dx, p.y - p.dy) + ctx.lineTo(p.x, p.y) + } + ctx.lineCap = "round" + ctx.lineJoin = "round" + ctx.lineWidth = 0.6; + ctx.strokeStyle = "red" + ctx.stroke() + } + ctx.restore() + } + ctx.restore() + + requestAnimationFrame(redraw) +} + +ws.onerror = console.error +ws.onmessage = message => { + const p = JSON.parse(message.data) as Packet + console.log(p); + if (p == "tick") { + + } else if ("game" in p) { + snakes.clear() + size = p.game.width + } else if ("player" in p) { + snakes.set(p.player.id, new Snake(p.player.name)) + } else if ("pos" in p) { + snakes.get(p.pos.id)?.add_part(p.pos.x, p.pos.y) + } + +} + diff --git a/src/spectate/server.rs b/src/spectate/server.rs index 9dbfebe..e3e9f80 100644 --- a/src/spectate/server.rs +++ b/src/spectate/server.rs @@ -6,6 +6,7 @@ use anyhow::Result; use axum::extract; use axum::extract::connect_info::ConnectInfo; use axum::extract::ws::Message; +use axum::http::HeaderMap; use axum::response::Html; use axum::{ extract::ws::{WebSocket, WebSocketUpgrade}, @@ -13,8 +14,10 @@ use axum::{ routing::get, Router, }; +use headers::ContentType; use log::{info, warn}; use std::net::SocketAddr; +use std::str::FromStr; use std::sync::Arc; use tokio::spawn; use tokio::sync::{broadcast, RwLock}; @@ -32,6 +35,7 @@ pub async fn spectate_server(config: Config, state: Arc<State>) -> Result<()> { spawn(broadcaster(sstate.clone(), state)); let app = Router::new() .route("/", get(index)) + .route("/main.js", get(javascript)) .route("/events", get(ws_handler)) .with_state(sstate); let listener = tokio::net::TcpListener::bind(config.bind).await.unwrap(); @@ -47,6 +51,26 @@ pub async fn spectate_server(config: Config, state: Arc<State>) -> Result<()> { async fn index() -> Html<&'static str> { Html(include_str!("index.html")) } +#[cfg(debug_assertions)] +async fn javascript() -> (HeaderMap, String) { + use headers::HeaderMapExt; + use tokio::fs::read_to_string; + let mut hm = HeaderMap::new(); + hm.typed_insert(ContentType::from_str("application/javascript").unwrap()); + ( + hm, + read_to_string(concat!(env!("OUT_DIR"), "/main.js")) + .await + .unwrap(), + ) +} +#[cfg(not(debug_assertions))] +async fn javascript() -> (HeaderMap, &'static str) { + use headers::HeaderMapExt; + let mut hm = HeaderMap::new(); + hm.typed_insert(ContentType::from_str("application/javascript").unwrap()); + (hm, include_str!(concat!(env!("OUT_DIR"), "/main.js"))) +} async fn broadcaster(sstate: Arc<SpectateState>, state: Arc<State>) { let mut ticks = state.tick.subscribe(); @@ -59,8 +83,8 @@ async fn broadcaster(sstate: Arc<SpectateState>, state: Arc<State>) { sstate.past_events.write().await.clear(); events.push(Packet::Game { my_id: 0, - width: g.size.x as usize, - height: g.size.y as usize, + width: g.map.size.x as usize, + height: g.map.size.y as usize, }); for (player, (_, _, name)) in &g.heads { events.push(Packet::Player { |