aboutsummaryrefslogtreecommitdiff
path: root/src/game
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-06-03 21:27:52 +0200
committermetamuffin <metamuffin@disroot.org>2024-06-03 21:27:52 +0200
commit3d107ea4710f3dec0eedd91ed5bc1e52d8f15912 (patch)
tree614a2110d1813c1f0df4170ac9932574c4e22c80 /src/game
downloadgpn-tron-rust-3d107ea4710f3dec0eedd91ed5bc1e52d8f15912.tar
gpn-tron-rust-3d107ea4710f3dec0eedd91ed5bc1e52d8f15912.tar.bz2
gpn-tron-rust-3d107ea4710f3dec0eedd91ed5bc1e52d8f15912.tar.zst
code
Diffstat (limited to 'src/game')
-rw-r--r--src/game/mod.rs71
-rw-r--r--src/game/protocol.rs204
-rw-r--r--src/game/server.rs46
3 files changed, 321 insertions, 0 deletions
diff --git a/src/game/mod.rs b/src/game/mod.rs
new file mode 100644
index 0000000..27faa27
--- /dev/null
+++ b/src/game/mod.rs
@@ -0,0 +1,71 @@
+use glam::IVec2;
+use protocol::Direction;
+use serde::Deserialize;
+use std::{collections::HashMap, net::SocketAddr, ops::ControlFlow};
+
+pub mod protocol;
+pub mod server;
+
+#[derive(Deserialize)]
+pub struct Config {
+ bind: SocketAddr,
+}
+
+pub struct Game {
+ pub heads: HashMap<u32, (Direction, IVec2, String)>,
+ pub map: HashMap<IVec2, u32>,
+ pub size: IVec2,
+ pub dead: Vec<u32>,
+}
+impl Game {
+ pub fn new(players: Vec<String>) -> Self {
+ let mut map = HashMap::new();
+ let mut heads = HashMap::new();
+ let plen = players.len();
+ for (p, name) in players.into_iter().enumerate() {
+ let pos = IVec2::ONE * p as i32 * 2;
+ map.insert(pos, p as u32);
+ heads.insert(p as u32, (Direction::Up, pos, name));
+ }
+ Self {
+ size: IVec2::ONE * plen as i32 * 2,
+ heads,
+ map,
+ dead: Vec::new(),
+ }
+ }
+ pub fn tick(&mut self) -> ControlFlow<Option<u32>, ()> {
+ for (_player, (dir, head, _)) in &mut self.heads {
+ *head = (*head + dir.vector()).rem_euclid(self.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) {
+ self.dead.push(*player);
+ }
+ }
+ for (_, hp) in h {
+ if hp.len() > 1 {
+ self.dead.extend(hp)
+ }
+ }
+
+ for (player, (_, head, _)) in &mut self.heads {
+ self.map.insert(*head, *player);
+ }
+
+ for d in &self.dead {
+ self.map.retain(|_, p| *d != *p);
+ self.heads.remove(&d);
+ }
+
+ if self.heads.len() <= 1 {
+ ControlFlow::Break(self.heads.keys().next().cloned())
+ } else {
+ ControlFlow::Continue(())
+ }
+ }
+}
diff --git a/src/game/protocol.rs b/src/game/protocol.rs
new file mode 100644
index 0000000..c180282
--- /dev/null
+++ b/src/game/protocol.rs
@@ -0,0 +1,204 @@
+use anyhow::{anyhow, bail, Result};
+use glam::IVec2;
+use log::debug;
+use serde::Serialize;
+use std::{
+ fmt::Display,
+ io::{BufRead, BufReader, BufWriter, Lines, Write},
+ net::TcpStream,
+ str::FromStr,
+};
+
+pub struct Client {
+ rx: Lines<BufReader<TcpStream>>,
+ tx: BufWriter<TcpStream>,
+}
+impl Client {
+ pub fn new(host: &str) -> Result<Self> {
+ let sock = TcpStream::connect(host)?;
+ let rx = BufReader::new(sock.try_clone()?).lines();
+ let tx = BufWriter::new(sock);
+ Ok(Self { rx, tx })
+ }
+ pub fn read(&mut self) -> Result<Packet> {
+ let line = self.rx.next().ok_or(anyhow!("eof"))??;
+ let packet = Packet::parse(&line)?;
+ debug!("<- {packet:?}");
+ Ok(packet)
+ }
+ pub fn write(&mut self, packet: Packet) -> Result<()> {
+ debug!("-> {packet:?}");
+ self.tx.write_all(packet.dump().as_bytes())?;
+ self.tx.flush()?;
+ Ok(())
+ }
+}
+
+impl Packet {
+ pub fn dump(&self) -> String {
+ match self {
+ Packet::Join { username, password } => {
+ format!("join|{username}|{password}")
+ }
+ Packet::Move(dir) => {
+ format!("move|{dir}")
+ }
+ Packet::Chat(message) => {
+ format!("chat|{message}")
+ }
+ Packet::Motd(motd) => format!("motd|{motd}"),
+ Packet::Error(message) => format!("erro|{message}"),
+ Packet::Game {
+ my_id,
+ width,
+ height,
+ } => format!("game|{width}|{height}|{my_id}"),
+ Packet::Pos { id, x, y } => format!("pos|{id}|{x}|{y}"),
+ Packet::Player { id, name } => format!("player|{id}|{name}"),
+ Packet::Tick => format!("tick"),
+ Packet::Die(players) => format!(
+ "die|{}",
+ players
+ .into_iter()
+ .map(|x| format!("{x}"))
+ .collect::<Vec<_>>()
+ .join("|")
+ ),
+ Packet::Message { id, message } => format!("message|{id}|{message}"),
+ Packet::Win(x, y) => format!("win|{x}|{y}"),
+ Packet::Lose(x, y) => format!("lose|{x}|{y}"),
+ }
+ }
+ pub fn parse(line: &str) -> Result<Self> {
+ let mut toks = line.split("|");
+ Ok(match toks.next().ok_or(anyhow!("packet type missing"))? {
+ "motd" => Packet::Motd(toks.next().ok_or(anyhow!("motd missing"))?.to_string()),
+ "error" => Packet::Error(
+ toks.next()
+ .ok_or(anyhow!("error message missing"))?
+ .to_string(),
+ ),
+ "pos" => Packet::Pos {
+ id: toks.next().ok_or(anyhow!("id missing"))?.parse()?,
+ x: toks.next().ok_or(anyhow!("x missing"))?.parse()?,
+ y: toks.next().ok_or(anyhow!("y missing"))?.parse()?,
+ },
+ "game" => Packet::Game {
+ width: toks.next().ok_or(anyhow!("width missing"))?.parse()?,
+ height: toks.next().ok_or(anyhow!("height missing"))?.parse()?,
+ my_id: toks.next().ok_or(anyhow!("my id missing"))?.parse()?,
+ },
+ "player" => Packet::Player {
+ id: toks.next().ok_or(anyhow!("id missing"))?.parse()?,
+ name: toks.next().ok_or(anyhow!("name missing"))?.to_string(),
+ },
+ "tick" => Packet::Tick,
+ "die" => Packet::Die(toks.map(|e| e.parse()).try_collect()?),
+ "message" => Packet::Message {
+ id: toks.next().ok_or(anyhow!("id missing"))?.parse()?,
+ message: toks.next().ok_or(anyhow!("message missing"))?.to_string(),
+ },
+ "win" => Packet::Win(
+ toks.next().ok_or(anyhow!("something missing"))?.parse()?,
+ toks.next().ok_or(anyhow!("something missing"))?.parse()?,
+ ),
+ "lose" => Packet::Lose(
+ toks.next().ok_or(anyhow!("something missing"))?.parse()?,
+ toks.next().ok_or(anyhow!("something missing"))?.parse()?,
+ ),
+ "move" => Packet::Move(Direction::from_str(
+ toks.next().ok_or(anyhow!("direction missing"))?,
+ )?),
+ "join" => Packet::Join {
+ username: toks.next().ok_or(anyhow!("username missing"))?.to_string(),
+ password: toks.next().ok_or(anyhow!("password missing"))?.to_string(),
+ },
+ x => bail!("unknown command: {x:?}"),
+ })
+ }
+}
+
+#[derive(Debug, Serialize, Clone)]
+#[serde(rename_all = "snake_case")]
+pub enum Packet {
+ Motd(String),
+ Error(String),
+ Game {
+ my_id: u32,
+ width: usize,
+ height: usize,
+ },
+ Pos {
+ id: u32,
+ x: i32,
+ y: i32,
+ },
+ Player {
+ id: u32,
+ name: String,
+ },
+ Tick,
+ Die(Vec<u32>),
+ Message {
+ id: u32,
+ message: String,
+ },
+ Win(usize, usize),
+ Lose(usize, usize),
+ Join {
+ username: String,
+ password: String,
+ },
+ Move(Direction),
+ Chat(String),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+pub enum Direction {
+ Up,
+ Down,
+ Left,
+ Right,
+}
+
+impl Display for Direction {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match self {
+ Direction::Up => "up",
+ Direction::Down => "down",
+ Direction::Left => "left",
+ Direction::Right => "right",
+ })
+ }
+}
+impl FromStr for Direction {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(match s {
+ "up" => Direction::Up,
+ "down" => Direction::Down,
+ "left" => Direction::Left,
+ "right" => Direction::Right,
+ x => bail!("unknown direction: {x:?}"),
+ })
+ }
+}
+impl Direction {
+ pub const ALL: [Direction; 4] = [Self::Up, Self::Down, Self::Left, Self::Right];
+ pub fn vector(self) -> IVec2 {
+ match self {
+ Direction::Up => (0, -1).into(),
+ Direction::Down => (0, 1).into(),
+ Direction::Left => (-1, 0).into(),
+ Direction::Right => (1, 0).into(),
+ }
+ }
+ pub fn rotate_ccw(self) -> Self {
+ match self {
+ Direction::Up => Direction::Right,
+ Direction::Down => Direction::Left,
+ Direction::Left => Direction::Up,
+ Direction::Right => Direction::Down,
+ }
+ }
+}
diff --git a/src/game/server.rs b/src/game/server.rs
new file mode 100644
index 0000000..1775bd1
--- /dev/null
+++ b/src/game/server.rs
@@ -0,0 +1,46 @@
+use super::{
+ protocol::{Direction, Packet},
+ Config,
+};
+use crate::State;
+use anyhow::{bail, Result};
+use glam::IVec2;
+use log::{error, info};
+use std::sync::Arc;
+use tokio::{
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
+ net::{TcpListener, TcpStream},
+ spawn,
+};
+
+pub async fn game_server(config: Config, state: Arc<State>) -> Result<()> {
+ let listener = TcpListener::bind(config.bind).await?;
+ info!("listening on {}", listener.local_addr()?);
+ while let Ok((sock, addr)) = listener.accept().await {
+ spawn(async move {
+ info!("connected {addr}");
+ if let Err(e) = handle_client(sock).await {
+ error!("client error: {e}")
+ }
+ info!("disconnected {addr}");
+ });
+ }
+ bail!("accept failure")
+}
+
+async fn handle_client(sock: TcpStream) -> Result<()> {
+ let (rx, tx) = sock.into_split();
+ let rx = BufReader::new(rx);
+ let mut tx = BufWriter::new(tx);
+
+ let mut lines = rx.lines();
+
+ while let Some(line) = lines.next_line().await? {
+ let packet = Packet::parse(&line)?;
+
+ tx.write_all(packet.dump().as_bytes()).await?;
+ tx.flush().await?;
+ }
+
+ Ok(())
+}