diff --git a/Cargo.lock b/Cargo.lock index e6d51ec..0ebfd9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,7 +208,7 @@ dependencies = [ "chrono", "hmac 0.8.1", "kv-log-macro", - "rand", + "rand 0.7.3", "serde", "serde_json", "sha2", @@ -473,7 +473,7 @@ dependencies = [ "hkdf", "hmac 0.10.1", "percent-encoding", - "rand", + "rand 0.7.3", "sha2", "time 0.2.23", "version_check", @@ -580,6 +580,7 @@ version = "0.1.0" dependencies = [ "async-std", "html-escape", + "rand 0.8.0", "serde", "serde_json", "tide", @@ -735,6 +736,17 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8025cf36f917e6a52cce185b7c7177689b838b7ec138364e50cc2277a56cf4" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "ghash" version = "0.3.0" @@ -841,7 +853,7 @@ dependencies = [ "futures-lite", "infer", "pin-project-lite 0.1.11", - "rand", + "rand 0.7.3", "serde", "serde_json", "serde_qs", @@ -1119,11 +1131,23 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.15", "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76330fb486679b4ace3670f117bbc9e16204005c4bde9c4bd372f45bed34f12" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.0", + "rand_hc 0.3.0", ] [[package]] @@ -1133,7 +1157,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.0", ] [[package]] @@ -1142,7 +1176,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.15", +] + +[[package]] +name = "rand_core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8b34ba8cfb21243bd8df91854c830ff0d785fff2e82ebd4434c2644cb9ada18" +dependencies = [ + "getrandom 0.2.0", ] [[package]] @@ -1151,7 +1194,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.0", ] [[package]] @@ -1530,7 +1582,7 @@ dependencies = [ "httparse", "input_buffer", "log", - "rand", + "rand 0.7.3", "sha-1", "url", "utf-8", @@ -1607,7 +1659,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" dependencies = [ - "rand", + "rand 0.7.3", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 4240e64..6c0f5e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] async-std = { version = "1.8.0", features = ["attributes"] } html-escape = "0.2.6" +rand = "0.8.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.60" tide = "0.15.0" diff --git a/src/main.rs b/src/main.rs index 568f8c9..c73db39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,8 @@ -extern crate html_escape; - use async_std::{ prelude::*, sync::RwLock }; +use rand::{seq::SliceRandom, thread_rng}; use std::{ collections::HashMap, sync::{Arc} @@ -13,22 +12,7 @@ use tide::{Body, Redirect, Request, Response}; use tide_websockets::{Message, WebSocket, WebSocketConnection}; use uuid::Uuid; -#[derive(Deserialize, Clone)] -struct CreateGameMessage { - name: String, -} - -#[derive(Serialize)] -struct GameStateMessage { - id: String, - state: GameState, -} - -#[derive(Serialize)] -struct GameSetupMessage { - id: String, - setup: GameSetup, -} +type Card = String; #[derive(Deserialize)] #[serde(tag = "type")] @@ -36,7 +20,11 @@ enum ClientMessage { Chat { message: String, }, + CreateGame { + name: String, + }, StartGame, + EndTurn, } #[derive(Serialize)] @@ -49,10 +37,66 @@ enum ServerMessage { PlayerJoined { player: String, }, + GameState { + state: GameState, + players: Vec, + active_player: usize, + }, + GameSetup { + setup: GameSetup, + }, + PlayerHand { + hand: Vec, + }, } -struct Player { +#[derive(Clone, Serialize)] +struct GameSetup { + deck: Vec, +} + + +#[derive(Serialize)] +struct PlayerState { + id: usize, name: String, + draw_pile_count: usize, + hand_count: usize, + discard_pile: Option, +} + + +struct Player { + id: String, + name: String, + draw_pile: Vec, + hand: Vec, + discard_pile: Vec, +} + +impl Player { + pub fn new(id: String, name: String) -> Self { + Self { + id, + name, + draw_pile: vec![], + hand: vec![], + discard_pile: vec![], + } + } + + pub fn draw(&mut self, count: usize) { + for _ in 0..count { + if self.draw_pile.len() == 0 { + self.draw_pile.append(&mut self.discard_pile); + self.draw_pile.shuffle(&mut thread_rng()); + } + + if self.draw_pile.len() > 0 { + self.hand.push(self.draw_pile.pop().unwrap()); + } + } + } } #[derive(Copy, Clone, Serialize)] @@ -61,11 +105,6 @@ enum GameState { InProgress, } -#[derive(Clone, Serialize)] -struct GameSetup { - deck: Vec, -} - impl Default for GameSetup { fn default() -> GameSetup { GameSetup { @@ -79,7 +118,8 @@ struct Game { players: Vec, state: GameState, setup: GameSetup, - connections: HashMap, + connections: HashMap, + active_player: usize, } @@ -90,6 +130,43 @@ impl Game { state: GameState::Setup, setup: GameSetup::default(), connections: HashMap::new(), + active_player: 0, + } + } + + fn start(&mut self) { + match self.state { + GameState::Setup => { + self.state = GameState::InProgress; + + for p in self.players.iter_mut() { + (*p).discard_pile = self.setup.deck.clone(); + p.draw(5); + } + } + _ => {} // 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.hand); + p.draw(5); + } + } + + self.active_player += 1; + self.active_player %= self.players.len(); + } + + fn is_active_player(&self, player_id: &str) -> bool { + match self.players.get(self.active_player) { + None => false, + Some(p) => { + p.id == *player_id + } } } } @@ -99,6 +176,44 @@ struct State { games: Arc>>, } +async fn broadcast(game: &Game, sm: &ServerMessage) { + for (_, (_, con)) in game.connections.iter() { + con.send_json(&sm).await.unwrap(); + } +} + +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(), + active_player: game.active_player, + }; + + broadcast(&game, &sm).await; + + for p in game.players.iter() { + let sm = ServerMessage::PlayerHand { + hand: p.hand.clone(), + }; + + send_msg(&game, &p, &sm).await; + } +} + +async fn send_msg(game: &Game, player: &Player, sm: &ServerMessage) { + for (_, (player_id, con)) in game.connections.iter() { + if *player_id == (*player).id { + con.send_json(&sm).await.unwrap(); + } + } +} + #[async_std::main] async fn main() -> Result<(), std::io::Error> { @@ -112,23 +227,32 @@ async fn main() -> Result<(), std::io::Error> { )); app.at("/").get(|_| async { Ok(Body::from_file("static/index.html").await?) }); + app.at("/static").serve_dir("static/")?; app.at("/game").post(|mut req: Request| async move { - let msg: CreateGameMessage = req.body_form().await?; + let msg: ClientMessage = req.body_form().await?; let session = req.session_mut(); - session.insert("player_name", msg.name.clone()).unwrap(); + match msg { + ClientMessage::CreateGame { name } => { + session.insert("player_name", name.clone())?; + let player_id: String = session.id().clone().into(); - let id = Uuid::new_v4(); - let mut games = req.state().games.write().await; - let player = Player { name: msg.name }; + let id = Uuid::new_v4(); + let mut games = req.state().games.write().await; + let player = Player::new(player_id, name); - games.insert(id, Game::new(player)); + games.insert(id, Game::new(player)); - let url = format!("/game/{}", id.to_simple().to_string()); - let res: tide::Result = Ok(Redirect::new(url).into()); - res + let url = format!("/game/{}", id.to_simple().to_string()); + let res: tide::Result = Ok(Redirect::new(url).into()); + res + } + _ => { + Ok(Response::new(400)) + } + } }); app.at("/game/:id").get(|req: Request| async move { @@ -155,8 +279,20 @@ async fn main() -> Result<(), std::io::Error> { let game = games.get_mut(&game_id).unwrap(); let session = req.session(); + let player_id: String = session.id().clone().into(); + let client_id = Uuid::new_v4(); - game.connections.insert(client_id, 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(()) + }; + + if !game.players.iter().any(|p| p.id == player_id) { + game.players.push(Player::new(player_id.clone(), player_name.clone())); + } + //Ensure the write locks are freed, possibly better to move to a function //with implicit drop? @@ -166,37 +302,28 @@ async fn main() -> Result<(), std::io::Error> { let games = req.state().games.read().await; let game = games.get(&game_id).unwrap(); - let msg = GameStateMessage { - id: "state".into(), - state: game.state, - }; + broadcast_state(&game).await; - stream.send_json(&msg).await?; - - let msg = GameSetupMessage { - id: "setup".into(), + let msg = ServerMessage::GameSetup { setup: game.setup.clone(), }; - stream.send_json(&msg).await?; - let player: String = session.get("player_name").unwrap_or("SOMEONE".into()); let sm = ServerMessage::PlayerJoined { - player: html_escape::encode_text(&player).into(), + player: html_escape::encode_text(&player_name).into(), }; - for (_, con) in game.connections.iter() { - con.send_json(&sm).await?; - } + broadcast(&game, &sm).await; - drop(game); drop(games); + drop(game); + drop(games); while let Some(Ok(Message::Text(input))) = stream.next().await { let msg: ClientMessage = serde_json::from_str(&input)?; match msg { ClientMessage::Chat { message } => { - let sender: String = session.get("player_name").unwrap_or("SOMEONE".into()); + let sender: String = session.get("player_name").unwrap(); let sm = ServerMessage::Chat { sender: html_escape::encode_text(&sender).into(), message: html_escape::encode_text(&message).into(), @@ -205,22 +332,29 @@ async fn main() -> Result<(), std::io::Error> { let games = req.state().games.read().await; let game = games.get(&game_id).unwrap(); - for (_, con) in game.connections.iter() { - con.send_json(&sm).await?; - } + broadcast(&game, &sm).await; } ClientMessage::StartGame => { let mut games = req.state().games.write().await; let game = games.get_mut(&game_id).unwrap(); - match game.state { - GameState::Setup => { - game.state = GameState::InProgress; - } - _ => {} //Ignore, if game not in setup state - } + game.start(); + + broadcast_state(&game).await; } + + ClientMessage::EndTurn => { + let mut games = req.state().games.write().await; + let game = games.get_mut(&game_id).unwrap(); + + if game.is_active_player(&player_id) { + game.end_turn(); + broadcast_state(&game).await; + } + } + + ClientMessage::CreateGame { .. } => {} } } diff --git a/static/game.html b/static/game.html index 2955cd9..7c18164 100644 --- a/static/game.html +++ b/static/game.html @@ -23,18 +23,25 @@ .player-area { display: grid; - grid-template-columns: 100px auto 100px; - border: 1px solid black; + grid-template-columns: 100px auto 90px; + grid-template-rows: auto auto; width: 100%; height: 145px; position: relative; box-sizing: border-box; + grid-gap: 10px; +} + +.player-area .buttons { + grid-column-start: 3; + text-align: center; } .card { position: relative; width: 90px; margin: 1px; + height: 144px; } .player-hand { @@ -45,6 +52,10 @@ top: 0; } +.player-hand img.card:hover { + box-shadow: 0 0 5px blue; +} + .chat-window { border: 1px solid black; width: 300px; @@ -89,15 +100,36 @@ } .setup-screen { +} + +.hidden { display: none; } .discard-pile { width: 90px; border: 1px dotted green; - height: 145px; + height: 144px; box-sizing: border-box; + position: relative; } + +.discard-pile img.card { + margin-top: -1px; + margin-left: -1px; +} + +.discard-pile::after { + color: dimgray; + content: "Discard Pile"; + font-size: 14px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + white-space: nowrap; + z-index: -1; +} .inplay-area { width: 100%; @@ -136,7 +168,6 @@ } .opponent-area { - height: 180px; display: flex; } @@ -148,6 +179,7 @@ border: 1px solid saddlebrown; display: grid; grid-gap: 2px; + padding: 2px; } .opponent-status.active { @@ -206,7 +238,9 @@ view: function(vnode) { return m(".chat-window", m("h4", "Players"), - m("ul", m("li", "Markus (you)")), + m("ul", vnode.attrs.players.map(function(p) { + return m("li", p.name); + })), m("h4", "Messages"), m("ul", {id: "chat"}), m("input", { @@ -248,7 +282,11 @@ function DiscardPile(initialVnode) { return { view: function(vnode) { - return m(".discard-pile", "Discard Pile") + var c; + if (vnode.attrs.card) { + c = m("img", {class: "card", src: "/static/images/cards/" + vnode.attrs.card.toLowerCase() + ".jpg"}); + } + return m(".discard-pile", c) } } } @@ -304,12 +342,21 @@ } function PlayerArea(initialVnode) { + var end_turn_click = function(e) { + var msg = { type: "EndTurn" }; + webSocket.send(JSON.stringify(msg)); + } + return { view: function(vnode) { + var local_player = vnode.attrs.players[my_player_id]; return m(".player-area", - m(DrawPile, {count: vnode.attrs.player_drawpile_count}), + m(DrawPile, {count: local_player.draw_pile_count}), m(PlayerHand, vnode.attrs.player), - m(DiscardPile) + m(DiscardPile, {card: local_player.discard_pile}), + m(".buttons", + m("button", {onclick: end_turn_click}, "Pass turn") + ) ) } } @@ -318,10 +365,10 @@ function OpponentStatus(initialVnode) { return { view: function(vnode) { - var active = vnode.attrs.active ? "active" : ""; + var active = vnode.attrs.id == game_state.active_player ? "active" : ""; return m(".opponent-status", {class: active }, m(".name", vnode.attrs.name), - m(DiscardPile), + m(DiscardPile, {card: vnode.attrs.discard_pile}), m(DrawPile, {count: vnode.attrs.draw_pile_count}), m(OpponentHand, {count: vnode.attrs.hand_count }) ) @@ -333,8 +380,10 @@ return { view: function(vnode) { return m(".opponent-area", - vnode.attrs.opponents.map(function(o) { - return m(OpponentStatus, o) + vnode.attrs.players + .filter((p, i) => i != my_player_id) + .map(function(o) { + return m(OpponentStatus, o) }) ) } @@ -344,7 +393,8 @@ function GameScreen(initialVnode) { return { view: function(vnode) { - return m(".game-screen", + var cls = vnode.attrs.active ? "" : "hidden"; + return m(".game-screen", {class: cls}, m(OpponentArea, vnode.attrs), m(InPlayArea, vnode.attrs.opponent_turn_state), m(SupplyArea, vnode.attrs), @@ -358,16 +408,15 @@ function SetupScreen(initialVnode) { var start_click = function(e) { - console.log("start_click", e); - - let msg = { id: "start_game" }; + let msg = { type: "StartGame" }; initialVnode.attrs.socket.send(JSON.stringify(msg)); } return { view: function(vnode) { - return m(".setup-screen", + var cls = vnode.attrs.active ? "" : "hidden"; + return m(".setup-screen", {class: cls}, m("h3", "Setup"), m("h4", "Basic cards"), m(".basic-cards", @@ -394,6 +443,9 @@ } var game_state; + var setup_state; + var my_player_id = 0; + var webSocket; function App(initialVnode) { if (document.location.protocol == "https:") { @@ -401,9 +453,9 @@ } else { url = document.location.href.replace("http://", "ws://") + "/ws"; } - const webSocket = new WebSocket(url); + webSocket = new WebSocket(url); - var setup_state = { + setup_state = { starting_deck: [], basic_cards: ["Copper", "Silver", "Gold", "Estate", "Duchery", "Province", "Curse"], kingdom_cards: ["Cellar", "Moat", "Village", "Merchant", "Workshop", "Smithy", "Remodel", "Militia", "Market", "Mine"], @@ -414,7 +466,7 @@ setup_state.starting_deck = data.deck; } - game_state = { + game_state = { supply: [ { name: "Copper", count: 46 }, { name: "Silver", count: 38 }, @@ -448,16 +500,19 @@ coin: 0, active: true }, - player_drawpile_count: 10, player: { hand: [] }, - opponents: [ + players: [ + { + name: "YOU", + draw_pile_count: 10, + hand_count: 0 + }, { name: "Alice", draw_pile_count: 10, hand_count: 5, - active: true }, { name: "Bob", @@ -469,13 +524,34 @@ draw_pile_count: 10, hand_count: 3 } - ] + ], + active_player: 0 } var chat_state = { + players: [], socket: webSocket } + var handle_game_state = function(state) { + game_state = { + ...game_state, + ...state, + }; + game_state.opponent_turn_state.active = game_state.active_player != my_player_id; + game_state.player_turn_state.active = game_state.active_player == my_player_id; + chat_state.players = state.players; + + if (state.state == "Setup") { + setup_state.active = true; + game_state.active = false; + } else { + setup_state.active = false; + game_state.active = true; + } + } + + webSocket.onopen = function(event) { console.log("ws open"); //webSocket.send("HALLO"); @@ -496,8 +572,12 @@ newmsg.innerHTML = msg.player + " joined the game."; chatDiv.append(newmsg); //newmsg.scrollIntoView(); - } else if (msg.id == "setup") { + } else if (msg.type == "GameState") { + handle_game_state(msg); + } else if (msg.type == "GameSetup") { handle_setup(msg.setup); + } else if (msg.type == "PlayerHand") { + game_state.player.hand = msg.hand; } else { console.log("event?"); console.log(event.data); diff --git a/static/index.html b/static/index.html index 5d051dd..4397171 100644 --- a/static/index.html +++ b/static/index.html @@ -11,6 +11,7 @@
Your name:
+