diff --git a/backend/src/game.rs b/backend/src/game.rs index e4d18dd..da6e33c 100644 --- a/backend/src/game.rs +++ b/backend/src/game.rs @@ -1,17 +1,29 @@ -use std::net::SocketAddr; - pub(crate) type Position = (u32, u32); #[derive(Debug, Default, Clone)] -pub(crate) struct Board; +pub(crate) struct Board { + tiles: Vec, + pub(crate) width: u16, + pub(crate) height: u16, +} impl Board { - pub(crate) fn place(&mut self, x: u32, y: u32, addr: SocketAddr) { - todo!() + pub(crate) fn new(width: u16, height: u16) -> Self { + Board { + tiles: vec![Tile::Empty; usize::from(width) * usize::from(height)], + width, + height, + } + } + + pub(crate) fn place(&mut self, x: u16, y: u16, id: u8) { + let x = usize::from(x.min(self.width)); + let y = usize::from(y.min(self.height)); + self.tiles[x + usize::from(self.width) * y] = Tile::Player(id); } pub(crate) fn serialize(&self) -> String { - format!("BOARD") + self.tiles.iter().map(Tile::to_char).collect() } } @@ -19,3 +31,21 @@ pub enum GoError { OutOfBounds, Suicide, } + +#[derive(Debug, Clone, Copy, Default)] +enum Tile { + #[default] + Empty, + Wall, + Player(u8), +} + +impl Tile { + fn to_char(&self) -> char { + match self { + Tile::Empty => '.', + Tile::Wall => '/', + Tile::Player(c) => *c as char, + } + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index f6c2981..e2fea2d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,6 +2,8 @@ mod game; mod network; use std::io::{ErrorKind, Write}; +use std::net::SocketAddr; +use std::time::Duration; use std::{net::TcpListener, str::FromStr}; use network::{Connection, UserAuth}; @@ -14,6 +16,8 @@ struct GameState { users: Vec, user_auth: UserAuth, board: Board, + chars: Vec>, + disconnected: Vec, } impl GameState { @@ -30,10 +34,10 @@ impl GameState { } Ok(Command::Put(pos)) => user.next_stone = Some(pos), Err(Error::WouldBlock) => break, - Err(Error::IO(e)) if e.kind() == ErrorKind::WouldBlock => break, - Err(Error::IO(e)) if e.kind() == ErrorKind::ConnectionAborted => { - // TODO: remove user + Err(Error::ConnectionLost) => { + self.disconnected.push(user.addr); eprintln!("Lost connection to {}", user.addr); + break; } Err(error) => eprintln!("error while reading user input: {error}"), } @@ -41,13 +45,41 @@ impl GameState { } } + pub(crate) fn alloc_char(&mut self, addr: SocketAddr) -> Option { + let pos = self.chars.iter().position(Option::is_none)?; + self.chars[pos] = Some(addr); + Some(pos as u8 + b'A') + } + + fn remove_user(&mut self, addr: SocketAddr) { + eprintln!("Removing user {}", addr); + if let Some(pos) = self.users.iter().position(|u| u.addr == addr) { + self.users.swap_remove(pos); + } + if let Some(value) = self.chars.iter_mut().find(|x| **x == Some(addr)) { + std::mem::take(value); + } + } + + fn remove_disconnected_users(&mut self) { + for addr in std::mem::take(&mut self.disconnected) { + self.remove_user(addr); + } + } + fn broadcast_gamestate(&mut self) { let state = self.board.serialize(); for user in self.users.iter_mut() { - match user.stream.write_all(state.as_bytes()) { - Err(e) if e.kind() == ErrorKind::ConnectionAborted => { - // TODO: remove user - eprintln!("Lost connection to {}", user.addr); + match writeln!( + user.stream, + "BOARD {} {} {} {}", + user.char, self.board.width, self.board.height, state + ) { + Err(e) if e.kind() == ErrorKind::ConnectionAborted => (), + Err(e) if e.kind() == ErrorKind::BrokenPipe => (), + Err(e) if matches!(e.kind(), ErrorKind::ConnectionAborted | ErrorKind::BrokenPipe) => { + dbg!("foo"); + self.disconnected.push(user.addr); } Err(e) => { eprintln!("Error while sending {e}"); @@ -61,13 +93,19 @@ impl GameState { fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:1312")?; listener.set_nonblocking(true)?; - let mut game = GameState::default(); + let mut game = GameState { + board: Board::new(10, 10), + chars: vec![None; 'z' as usize - 'A' as usize], + ..Default::default() + }; loop { if let Err(e) = network::accept_new_connections(&listener, &mut game) { eprintln!("Error while accepting a new connection: {e}"); } game.process_user_input(); + game.remove_disconnected_users(); game.broadcast_gamestate(); + std::thread::sleep(Duration::from_millis(500)); } } diff --git a/backend/src/network.rs b/backend/src/network.rs index ca1f502..7d890d1 100644 --- a/backend/src/network.rs +++ b/backend/src/network.rs @@ -14,14 +14,20 @@ pub enum Error { InvalidArgument, UnknownCommand, InvalidCredentials, + ConnectionLost, WouldBlock, + GameFull, IO(std::io::Error), Utf8(std::str::Utf8Error), } impl From for Error { fn from(value: std::io::Error) -> Self { - Error::IO(value) + match value.kind() { + ErrorKind::WouldBlock => Error::WouldBlock, + ErrorKind::BrokenPipe | ErrorKind::ConnectionAborted => Error::ConnectionLost, + _ => Error::IO(value), + } } } impl From for Error { @@ -36,6 +42,7 @@ pub(crate) struct Connection { pub(crate) addr: SocketAddr, pub(crate) username: Option, color: Color, + pub(crate) char: u8, pub(crate) stream: TcpStream, pub(crate) next_stone: Option, } @@ -46,6 +53,7 @@ impl Display for Error { } } +#[derive(Debug, Clone)] pub(crate) enum Command { Login(String, String), Put(Position), @@ -75,6 +83,9 @@ pub(crate) fn parse_line( ) -> Result { let mut buf = [0; 1024]; let bytes = stream.peek(&mut buf)?; + if bytes == 0 { + return Err(Error::ConnectionLost); + } let pos = buf[0..bytes] .iter() .position(|a| a == &b'\n') @@ -87,7 +98,7 @@ pub(crate) fn parse_line( panic!("{:?}", &buf[..bytes]); } } -pub(crate) fn accept_new_connections(listener: &TcpListener, game: &mut GameState) -> std::io::Result<()> { +pub(crate) fn accept_new_connections(listener: &TcpListener, game: &mut GameState) -> Result<(), Error> { fn random_color() -> Color { std::collections::hash_map::DefaultHasher::new().finish() as Color } @@ -95,10 +106,14 @@ pub(crate) fn accept_new_connections(listener: &TcpListener, game: &mut GameStat match listener.accept() { Ok((stream, addr)) => { stream.set_nonblocking(true)?; + let Some(char) = game.alloc_char(addr) else { + return Err(Error::GameFull); + }; let con = Connection { addr, username: None, color: random_color(), + char, stream, next_stone: None, }; diff --git a/client/src/game.rs b/client/src/game.rs index f89913b..30320d9 100644 --- a/client/src/game.rs +++ b/client/src/game.rs @@ -32,6 +32,7 @@ impl GameState { } } +#[derive(Debug, Default, Clone)] pub struct Map { width: u16, height: u16, @@ -60,8 +61,9 @@ impl Map { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default, Debug)] pub enum Tile { + #[default] Empty, Wall, Player(u8),