diff --git a/src/main.rs b/src/main.rs index c73db39..98681d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,7 @@ -use async_std::{ - prelude::*, - sync::RwLock -}; +use async_std::{prelude::*, sync::RwLock}; use rand::{seq::SliceRandom, thread_rng}; -use std::{ - collections::HashMap, - sync::{Arc} -}; use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, sync::Arc}; use tide::{Body, Redirect, Request, Response}; use tide_websockets::{Message, WebSocket, WebSocketConnection}; use uuid::Uuid; @@ -17,14 +11,16 @@ type Card = String; #[derive(Deserialize)] #[serde(tag = "type")] enum ClientMessage { - Chat { - message: String, - }, - CreateGame { - name: String, - }, + Chat { message: String }, + CreateGame { name: String }, + JoinGame { name: String }, StartGame, EndTurn, + PlayCard { name: String, index: usize }, + GainCard { name: String, index: usize }, + DrawCard, + Discard { index: usize }, + TrashHand { index: usize }, } #[derive(Serialize)] @@ -41,6 +37,9 @@ enum ServerMessage { state: GameState, players: Vec, active_player: usize, + supply: Vec, + turn_state: TurnState, + trash: Vec, }, GameSetup { setup: GameSetup, @@ -48,6 +47,12 @@ enum ServerMessage { PlayerHand { hand: Vec, }, + PlayerId { + id: usize, + }, + Notification { + text: String, + }, } #[derive(Clone, Serialize)] @@ -55,7 +60,6 @@ struct GameSetup { deck: Vec, } - #[derive(Serialize)] struct PlayerState { id: usize, @@ -63,8 +67,21 @@ struct PlayerState { draw_pile_count: usize, hand_count: usize, discard_pile: Option, + played_cards: Vec, } +#[derive(Serialize)] +struct PileState { + name: String, + count: usize, +} + +#[derive(Serialize)] +struct TurnState { + actions: u32, + buys: u32, + coin: u32, +} struct Player { id: String, @@ -72,6 +89,7 @@ struct Player { draw_pile: Vec, hand: Vec, discard_pile: Vec, + played_cards: Vec, } impl Player { @@ -82,6 +100,7 @@ impl Player { draw_pile: vec![], hand: vec![], discard_pile: vec![], + played_cards: vec![], } } @@ -97,9 +116,17 @@ impl Player { } } } + + pub fn play_card(&mut self, index: usize) { + self.played_cards.push(self.hand.remove(index)); + } + + pub fn discard(&mut self, index: usize) { + self.discard_pile.push(self.hand.remove(index)); + } } -#[derive(Copy, Clone, Serialize)] +#[derive(Copy, Clone, PartialEq, Serialize)] enum GameState { Setup, InProgress, @@ -108,8 +135,18 @@ enum GameState { impl Default for GameSetup { fn default() -> GameSetup { GameSetup { - deck: vec!["Copper".into(), "Copper".into(), "Copper".into(), "Copper".into(), "Copper".into(), - "Copper".into(), "Copper".into(), "Estate".into(), "Estate".into(), "Estate".into()] + deck: vec![ + "Copper".into(), + "Copper".into(), + "Copper".into(), + "Copper".into(), + "Copper".into(), + "Copper".into(), + "Copper".into(), + "Estate".into(), + "Estate".into(), + "Estate".into(), + ], } } } @@ -120,9 +157,10 @@ struct Game { setup: GameSetup, connections: HashMap, active_player: usize, + supply: Vec<(String, usize)>, + trash: Vec, } - impl Game { fn new(player: Player) -> Self { Self { @@ -131,6 +169,8 @@ impl Game { setup: GameSetup::default(), connections: HashMap::new(), active_player: 0, + supply: vec![], + trash: vec![], } } @@ -143,15 +183,37 @@ impl Game { (*p).discard_pile = self.setup.deck.clone(); p.draw(5); } + + self.supply = vec![ + ("Copper".into(), 46), + ("Silver".into(), 38), + ("Gold".into(), 30), + ("Estate".into(), 8), + ("Duchery".into(), 8), + ("Province".into(), 8), + ("Curse".into(), 10), + ("Cellar".into(), 10), + ("Moat".into(), 10), + ("Village".into(), 10), + ("Merchant".into(), 10), + ("Workshop".into(), 10), + ("Smithy".into(), 10), + ("Remodel".into(), 10), + ("Militia".into(), 10), + ("Market".into(), 10), + ("Mine".into(), 10), + ]; } + _ => {} // Ignore if game is not in setup state - } + } } fn end_turn(&mut self) { match self.players.get_mut(self.active_player) { None => {} Some(p) => { + p.discard_pile.append(&mut p.played_cards); p.discard_pile.append(&mut p.hand); p.draw(5); } @@ -164,11 +226,14 @@ impl Game { fn is_active_player(&self, player_id: &str) -> bool { match self.players.get(self.active_player) { None => false, - Some(p) => { - p.id == *player_id - } + Some(p) => p.id == *player_id, } } + + pub fn trash_hand(&mut self, player_number: usize, index: usize) { + self.trash + .push(self.players[player_number].hand.remove(index)); + } } #[derive(Clone, Default)] @@ -176,6 +241,11 @@ struct State { games: Arc>>, } +async fn notify_players(game: &Game, text: String) { + let sm = ServerMessage::Notification { text }; + broadcast(game, &sm).await; +} + async fn broadcast(game: &Game, sm: &ServerMessage) { for (_, (_, con)) in game.connections.iter() { con.send_json(&sm).await.unwrap(); @@ -185,14 +255,34 @@ async fn broadcast(game: &Game, sm: &ServerMessage) { async fn broadcast_state(game: &Game) { let sm = ServerMessage::GameState { state: game.state, - players: game.players.iter().enumerate().map(|(i, p)| PlayerState { - id: i, - name: html_escape::encode_text(&p.name).into(), - draw_pile_count: p.draw_pile.len(), - hand_count: p.hand.len(), - discard_pile: p.discard_pile.last().map(|c| c.clone()), - }).collect(), + players: game + .players + .iter() + .enumerate() + .map(|(i, p)| PlayerState { + id: i, + name: html_escape::encode_text(&p.name).into(), + draw_pile_count: p.draw_pile.len(), + hand_count: p.hand.len(), + discard_pile: p.discard_pile.last().map(|c| c.clone()), + played_cards: p.played_cards.clone(), + }) + .collect(), active_player: game.active_player, + supply: game + .supply + .iter() + .map(|(name, count)| PileState { + name: name.into(), + count: count.clone(), + }) + .collect(), + turn_state: TurnState { + actions: 1, + buys: 1, + coin: 0, + }, + trash: game.trash.clone(), }; broadcast(&game, &sm).await; @@ -214,7 +304,6 @@ async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) { } } - #[async_std::main] async fn main() -> Result<(), std::io::Error> { tide::log::with_level(tide::log::LevelFilter::Debug); @@ -222,11 +311,12 @@ async fn main() -> Result<(), std::io::Error> { let mut app = tide::with_state(State::default()); app.with(tide::sessions::SessionMiddleware::new( - tide::sessions::MemoryStore::new(), - "1234567890abcdef1234567890abcdef".as_bytes(), - )); + tide::sessions::MemoryStore::new(), + "1234567890abcdef1234567890abcdef".as_bytes(), + )); - app.at("/").get(|_| async { Ok(Body::from_file("static/index.html").await?) }); + app.at("/") + .get(|_| async { Ok(Body::from_file("static/index.html").await?) }); app.at("/static").serve_dir("static/")?; @@ -249,9 +339,7 @@ async fn main() -> Result<(), std::io::Error> { let res: tide::Result = Ok(Redirect::new(url).into()); res } - _ => { - Ok(Response::new(400)) - } + _ => Ok(Response::new(400)), } }); @@ -259,20 +347,47 @@ async fn main() -> Result<(), std::io::Error> { let games = req.state().games.read().await; let id = req.param("id")?; let game_id = Uuid::parse_str(&id).unwrap(); + let session = req.session(); match games.get(&game_id) { - None => Ok(Response::new(404)), - Some(_) => { - let mut res = Response::new(200); - res.set_body(Body::from_file("static/game.html").await?); - Ok(res) + None => return Ok(Response::new(404)), + Some(_) => (), + }; + + match session.get::("player_name") { + None => { + let url = format!("/game/{}/join", id); + return Ok(Redirect::new(url).into()); } - } + Some(_) => (), + }; + + let mut res = Response::new(200); + res.set_body(Body::from_file("static/game.html").await?); + Ok(res) }); + app.at("/game/:id/join") + .get(|_| async { Ok(Body::from_file("static/join.html").await?) }) + .post(|mut req: Request| async move { + let msg: ClientMessage = req.body_form().await?; + let id: String = { req.param("id").unwrap().into() }; + let session = req.session_mut(); - app.at("/game/:id/ws") - .get(WebSocket::new(|req: Request, mut stream| async move { + match msg { + ClientMessage::JoinGame { name } => { + session.insert("player_name", name.clone())?; + + let url = format!("/game/{}", id); + let res: tide::Result = Ok(Redirect::new(url).into()); + res + } + _ => Ok(Response::new(500)), + } + }); + + app.at("/game/:id/ws").get(WebSocket::new( + |req: Request, mut stream| async move { let id = req.param("id")?; let game_id = Uuid::parse_str(&id).unwrap(); let mut games = req.state().games.write().await; @@ -282,17 +397,27 @@ async fn main() -> Result<(), std::io::Error> { let player_id: String = session.id().clone().into(); let client_id = Uuid::new_v4(); - game.connections.insert(client_id, (player_id.clone(), stream.clone())); + game.connections + .insert(client_id, (player_id.clone(), stream.clone())); let player_name: String = match session.get("player_name") { Some(p) => p, - None => return Ok(()) + None => return Ok(()), }; - if !game.players.iter().any(|p| p.id == player_id) { - game.players.push(Player::new(player_id.clone(), player_name.clone())); + if game.state == GameState::Setup && !game.players.iter().any(|p| p.id == player_id) { + game.players + .push(Player::new(player_id.clone(), player_name.clone())); } - + + let player_number = game + .players + .iter() + .enumerate() + .filter(|(_, p)| p.id == player_id) + .fold(0, |_, (i, _)| i); + let msg = ServerMessage::PlayerId { id: player_number }; + stream.send_json(&msg).await?; //Ensure the write locks are freed, possibly better to move to a function //with implicit drop? @@ -352,9 +477,93 @@ async fn main() -> Result<(), std::io::Error> { game.end_turn(); broadcast_state(&game).await; } - } + } ClientMessage::CreateGame { .. } => {} + + ClientMessage::JoinGame { .. } => {} + + ClientMessage::PlayCard { name: _, index } => { + let mut games = req.state().games.write().await; + let game = games.get_mut(&game_id).unwrap(); + + let card_name = game.players[player_number].hand[index].clone(); + game.players[player_number].play_card(index); + + notify_players( + &game, + format!("{} spielt {}", game.players[player_number].name, card_name), + ) + .await; + broadcast_state(&game).await; + } + + ClientMessage::GainCard { name: _, index } => { + let mut games = req.state().games.write().await; + let game = games.get_mut(&game_id).unwrap(); + + game.supply.get_mut(index).unwrap().1 = + game.supply.get(index).unwrap().1 - 1; + let card_name = game.supply[index].0.clone(); + + game.players[player_number] + .discard_pile + .push(card_name.clone()); + + notify_players( + &game, + format!("{} nimmt {}", game.players[player_number].name, card_name), + ) + .await; + broadcast_state(&game).await; + } + + ClientMessage::DrawCard => { + let mut games = req.state().games.write().await; + let game = games.get_mut(&game_id).unwrap(); + + game.players[player_number].draw(1); + + notify_players( + &game, + format!("{} zieht eine Karte", game.players[player_number].name), + ) + .await; + broadcast_state(&game).await; + } + + ClientMessage::Discard { index } => { + let mut games = req.state().games.write().await; + let game = games.get_mut(&game_id).unwrap(); + + let card_name = game.players[player_number].hand[index].clone(); + game.players[player_number].discard(index); + + notify_players( + &game, + format!("{} legt {} ab", game.players[player_number].name, card_name), + ) + .await; + broadcast_state(&game).await; + } + + ClientMessage::TrashHand { index } => { + let mut games = req.state().games.write().await; + let game = games.get_mut(&game_id).unwrap(); + + let card_name = game.players[player_number].hand[index].clone(); + game.trash_hand(player_number, index); + + notify_players( + &game, + format!( + "{} entsorgt {}", + game.players[player_number].name, card_name + ), + ) + .await; + broadcast_state(&game).await; + } } } @@ -364,9 +573,9 @@ async fn main() -> Result<(), std::io::Error> { game.connections.remove(&client_id); Ok(()) - })); + }, + )); app.listen("0.0.0.0:5000").await?; Ok(()) } - diff --git a/static/game.html b/static/game.html index 7c18164..ce88a2d 100644 --- a/static/game.html +++ b/static/game.html @@ -6,6 +6,12 @@ DnD @@ -225,6 +316,27 @@