Nicolas Winkler 4 rokov pred
rodič
commit
33528bb6bc
11 zmenil súbory, kde vykonal 220 pridanie a 133 odobranie
  1. 1 0
      Cargo.toml
  2. 2 2
      TODO.txt
  3. 18 3
      src/game.rs
  4. 2 2
      src/main.rs
  5. 59 11
      src/server/gamelobby.rs
  6. 10 9
      src/server/messages.rs
  7. 20 7
      src/server/server.rs
  8. 0 57
      src/websocket.rs
  9. 67 31
      static/comm.js
  10. 2 4
      static/index.html
  11. 39 7
      static/style.css

+ 1 - 0
Cargo.toml

@@ -22,6 +22,7 @@ serde_json = "1.0"
 rand = "0.7.3"
 toml = "0.4.2"
 clap = "2.33.3"
+log = "*"
 
 
 r2d2_sqlite = { version = "0.17.0", optional = true }

+ 2 - 2
TODO.txt

@@ -1,5 +1,5 @@
-- one player ready => enough? how?
-- on ready click: bestätigung -> players green when ready
+- one player ready => enough? how? YES
+- on ready click: bestätigung -> players green when ready YES
 - on word submit
 - guessing bug
 - results reveal einzeln

+ 18 - 3
src/game.rs

@@ -32,6 +32,17 @@ pub struct Game {
 }
 
 
+impl Player {
+    pub fn has_submitted_word(&self) -> bool {
+        self.submitted_word.is_some()
+    }
+    
+    pub fn has_submitted_guess(&self) -> bool {
+        self.submitted_guess.is_some()
+    }
+}
+
+
 impl Game {
     pub fn new() -> Self {
         Self {
@@ -42,10 +53,14 @@ impl Game {
         }
     }
 
-    fn get_player(&mut self, nick: &str) -> Option<&mut Player> {
+    pub fn get_player_mut(&mut self, nick: &str) -> Option<&mut Player> {
         self.players.iter_mut().find(|x| x.nick == nick)
     }
 
+    pub fn get_player(&self, nick: &str) -> Option<&Player> {
+        self.players.iter().find(|x| x.nick == nick)
+    }
+
     pub fn check_letters(word: &str, letters: &Vec<char>) -> bool {
         let allowed_chars = [" ", "-", "'"];
         if word.len() == 0 {
@@ -81,7 +96,7 @@ impl Game {
     pub fn submit_creation(&mut self, player_nick: &str, word: String) -> bool {
         match self.state {
             GameState::Creating => {
-                if let Some(player) = self.get_player(player_nick) {
+                if let Some(player) = self.get_player_mut(player_nick) {
                     if let Some(ex) = &player.creating_exercise {
                         if Self::check_letters(&word, &ex.letters) {
                             player.submitted_word = Some(word);
@@ -98,7 +113,7 @@ impl Game {
     pub fn submit_guess(&mut self, player_nick: &str, guess: Vec<(String, String)>) -> bool {
         match self.state {
             GameState::Guessing => {
-                if let Some(player) = self.get_player(player_nick) {
+                if let Some(player) = self.get_player_mut(player_nick) {
                     player.submitted_guess = Some(guess);
                     true
                 }

+ 2 - 2
src/main.rs

@@ -3,7 +3,6 @@ use actix::prelude::*;
 use actix_files as fs;
 use actix_web::{middleware, web, App, HttpServer};
 
-mod websocket;
 mod game;
 mod datasource;
 mod config;
@@ -11,6 +10,7 @@ mod server {
     pub mod server;
     pub mod messages;
     pub mod gamelobby;
+    pub mod protocol;
 }
 use crate::server::server::{Server, ws_initiate};
 
@@ -39,7 +39,7 @@ async fn main() -> std::io::Result<()> {
             None => "8000".to_owned()
         };
 
-    std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
+    //std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
     env_logger::init();
 
     let server = Server {

+ 59 - 11
src/server/gamelobby.rs

@@ -1,18 +1,16 @@
 use std::collections::BTreeMap;
 use std::sync::Arc;
-use std::borrow::Borrow;
 
 use rand::thread_rng;
 use rand::seq::SliceRandom;
 
 use actix::prelude::*;
+use log::info;
 
 use crate::server::server::{GameConnection, Server};
 use crate::datasource;
 use crate::game;
-use crate::websocket;
-use crate::websocket::*;
-
+use crate::server::protocol::*;
 use crate::server::messages::*;
 
 #[derive(PartialEq)]
@@ -40,7 +38,7 @@ pub struct GameLobby {
 
 impl Actor for GameLobby {
     type Context = Context<Self>;
-    fn started(&mut self, ctx: &mut Self::Context) {
+    fn started(&mut self, _ctx: &mut Self::Context) {
         self.lobby_state = LobbyState::Starting;
     }
 }
@@ -57,11 +55,11 @@ impl Handler<JoinRequest> for GameLobby {
                 LobbyJoined {
                     lobby: ctx.address(),
                     game_id: self.game_id.clone(),
-                    nick: jr.nick
+                    nick: jr.nick.clone()
                 }
             );
-
             self.send_game_to_all();
+            info!("Player {} joined lobby {}", jr.nick, self.game_id);
         }
         else {
             self.waiting_players.insert(jr.nick.clone(), jr.p);
@@ -74,6 +72,7 @@ impl Handler<LeaveMsg> for GameLobby {
     fn handle(&mut self, lm: LeaveMsg, _ctx: &mut Self::Context) -> Self::Result {
         self.waiting_players.remove(&lm.0);
         self.connected_players.remove(&lm.0);
+        self.ready_players.retain(|p| p != &lm.0);
         self.game.remove_player(&lm.0);
 
         if self.connected_players.is_empty() {
@@ -88,6 +87,8 @@ impl Handler<ReadyMsg> for GameLobby {
         if !self.ready_players.contains(&rm.0) {
             self.ready_players.push(rm.0);
         }
+        self.send_playerlist_to_all();
+
         if self.ready_players.len() >= self.connected_players.len() {
             self.set_state(LobbyState::Creating);
             self.send_game_to_all();
@@ -95,6 +96,14 @@ impl Handler<ReadyMsg> for GameLobby {
     }
 }
 
+impl Handler<UnreadyMsg> for GameLobby {
+    type Result = ();
+    fn handle(&mut self, urm: UnreadyMsg, _ctx: &mut Self::Context) -> Self::Result {
+        self.ready_players.retain(|p| p != &urm.0);
+        self.send_playerlist_to_all();
+    }
+}
+
 impl Handler<SubmitWordMsg> for GameLobby {
     type Result = ();
     fn handle(&mut self, swm: SubmitWordMsg, _ctx: &mut Self::Context) -> Self::Result {
@@ -104,6 +113,9 @@ impl Handler<SubmitWordMsg> for GameLobby {
             self.game.next_state();
             self.send_game_to_all();
         }
+        else {
+            self.send_playerlist_to_all();
+        }
     }
 }
 
@@ -125,6 +137,9 @@ impl Handler<SubmitGuessMsg> for GameLobby {
             self.game.next_state();
             self.send_game_to_all();
         }
+        else {
+            self.send_playerlist_to_all();
+        }
     }
 }
 
@@ -151,7 +166,6 @@ impl GameLobby {
                     self.game.player_join(nick.clone());
                 }
                 self.connected_players.append(&mut self.waiting_players);
-                self.ready_players.clear();
             },
             LobbyState::Creating => {
                 let s = &mut self.shuffler;
@@ -160,6 +174,7 @@ impl GameLobby {
             },
             _ => {}
         }
+        self.ready_players.clear();
         self.lobby_state = new_state;
     }
 
@@ -182,6 +197,13 @@ impl GameLobby {
         }
     }
 
+    pub fn send_playerlist_to_all(&self) {
+        let player_list = self.create_playerlist();
+        for (_nick, player) in &self.connected_players {
+            player.do_send(PlayerListUpdate{ player_list: player_list.clone() });
+        }
+    }
+
     pub fn create_result_data(&self) -> RoundResultData {
         let (guesses, point_list) = self.game.create_results();
 
@@ -212,14 +234,40 @@ impl GameLobby {
         }
     }
 
+    ///
+    /// determines if a player is ready to progress into the next game state
+    /// 
+    fn player_is_ready(&self, nick: &str) -> bool {
+        match self.lobby_state {
+            LobbyState::Starting => self.ready_players.iter().any(|p| p == nick),
+            LobbyState::Creating => self.game.get_player(nick)
+                                             .map(|p| p.has_submitted_word())
+                                             .unwrap_or(false),
+            LobbyState::Guessing => self.game.get_player(nick)
+                                             .map(|p| p.has_submitted_guess())
+                                             .unwrap_or(false),
+            LobbyState::Revealing => self.ready_players.iter().any(|p| p == nick)
+        }
+    }
+
+
+    fn create_playerlist(&self) -> Vec<PlayerData> {
+        self.game.players.iter()
+                         .map(|p|
+                             PlayerData {
+                                 nick: p.nick.clone(),
+                                 points: self.get_player_points(&p.nick),
+                                 ready: self.player_is_ready(&p.nick),
+                             })
+                         .collect::<Vec<_>>()
+    }
+
     pub fn create_opaque_message(&self, nick: &str) -> GameData {
 
         let player = self.game.players.iter()
                         .find(|p| p.nick == nick);
         GameData {
-            players: self.game.players.iter()
-                        .map(|p| websocket::PlayerData{ nick: p.nick.clone(), points: self.get_player_points(&p.nick) })
-                        .collect::<Vec<_>>(),
+            players: self.create_playerlist(),
             state_data:
                 match self.game.state {
                     game::GameState::Starting => {

+ 10 - 9
src/server/messages.rs

@@ -1,9 +1,7 @@
-use actix::dev::{MessageResponse, ResponseChannel};
 use actix::prelude::*;
 use crate::server::server::GameConnection;
 use crate::server::gamelobby::GameLobby;
-use crate::websocket::*;
-use crate::datasource::DataSource;
+use crate::server::protocol::*;
 
 #[derive(Message)]
 #[rtype(result = "()")]
@@ -27,6 +25,10 @@ pub struct ReadyMsg(pub String);
 
 #[derive(Message)]
 #[rtype(result = "()")]
+pub struct UnreadyMsg(pub String);
+
+#[derive(Message)]
+#[rtype(result = "()")]
 pub struct LeaveMsg(pub String);
 
 #[derive(Message)]
@@ -71,13 +73,12 @@ pub struct LobbyJoined {
 
 #[derive(Message)]
 #[rtype(result = "()")]
-pub struct GameUpdate{
+pub struct GameUpdate {
     pub game_data: GameData
 }
 
-pub enum Answer {
-    LobbyJoined(Addr<GameLobby>),
-    LobbyCreated(Addr<GameLobby>),
-    LobbyAlreadyExists,
-    NoSuchLobby,
+#[derive(Message)]
+#[rtype(result = "()")]
+pub struct PlayerListUpdate {
+    pub player_list: Vec<PlayerData>
 }

+ 20 - 7
src/server/server.rs

@@ -4,8 +4,9 @@ use std::sync::{Arc};
 use actix::prelude::*;
 use actix_web::{web, Error, HttpRequest, HttpResponse};
 use actix_web_actors::ws;
-use crate::websocket::*;
+use log::{debug, error};
 use crate::datasource;
+use crate::server::protocol::*;
 use crate::server::gamelobby::{GameLobby};
 use crate::server::messages::*;
 
@@ -15,9 +16,7 @@ const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
 pub async fn ws_initiate(server: web::Data<Addr<Server>>,
                          r: HttpRequest,
                          stream: web::Payload) -> Result<HttpResponse, Error> {
-    println!("{:?}", r);
     let res = ws::start(GameConnection::new(server.as_ref().clone()), &r, stream)?;
-    println!("{:?}", res);
     Ok(res)
 }
 
@@ -95,7 +94,7 @@ impl Actor for GameConnection {
 
 impl Handler<LobbyJoined> for GameConnection {
     type Result = ();
-    fn handle(&mut self, gu: LobbyJoined, ctx: &mut Self::Context) -> Self::Result {
+    fn handle(&mut self, gu: LobbyJoined, _ctx: &mut Self::Context) -> Self::Result {
         self.game_lobby = Some(gu.lobby);
         self.game_id = Some(gu.game_id);
         self.nick = Some(gu.nick);
@@ -116,6 +115,13 @@ impl Handler<GameUpdate> for GameConnection {
     }
 }
 
+impl Handler<PlayerListUpdate> for GameConnection {
+    type Result = ();
+    fn handle(&mut self, plu: PlayerListUpdate, ctx: &mut Self::Context) -> Self::Result {
+        self.send_message(&UpdateMessage::PlayerList(plu.player_list), ctx);
+    }
+}
+
 impl Handler<ResultMsg> for GameConnection {
     type Result = ();
     fn handle(&mut self, rm: ResultMsg, ctx: &mut Self::Context) -> Self::Result {
@@ -146,7 +152,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for GameConnection {
                 self.heartbeat = Instant::now();
             }
             Ok(ws::Message::Text(text)) => {
-                println!("hmmm: {:?}", text);
+                debug!("received client message: {}", text);
                 self.received_message(&text, ctx);
             },
             Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
@@ -183,7 +189,7 @@ impl GameConnection {
 
     pub fn send_message(&self, m: &UpdateMessage, ctx: &mut <Self as Actor>::Context) {
         let txt = serde_json::to_string(m).unwrap();
-        println!("{:?}", txt);
+        debug!("sending message to client {}", txt);
         ctx.text(txt);
     }
 
@@ -219,6 +225,13 @@ impl GameConnection {
                         }
                     }
                 },
+                ClientMessage::Unready => {
+                    if let Some(lobby) = &self.game_lobby {
+                        if let Some(nick) = &self.nick {
+                            lobby.do_send(UnreadyMsg(nick.clone()));
+                        }
+                    }
+                },
                 ClientMessage::SubmitWord{ word } => {
                     if let Some(lobby) = &self.game_lobby {
                         if let Some(nick) = &self.nick {
@@ -236,7 +249,7 @@ impl GameConnection {
             }
         }
         else {
-            println!("error parsing json");
+            error!("error parsing json: {}", text);
         }
     }
 

+ 0 - 57
src/websocket.rs

@@ -1,57 +0,0 @@
-use serde::{Serialize, Deserialize};
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum ClientMessage {
-    CreateGame{ game_id: String, nick: String },
-    Join{ game_id: String, nick: String },
-    LeaveLobby,
-    Ready,
-    SubmitWord{ word: String },
-    SubmitGuess{ guesses: Vec<(String, String)> },
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub enum GameStateData {
-    Starting,
-    Creating{ question: String, available_chars: Vec<char> },
-    Guessing{ submitted_words: Vec<(String, Vec<char>)>, questions: Vec<String> },
-    Revealing{  },
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub struct PlayerData {
-    pub nick: String,
-    pub points: i64
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub struct GameData {
-    pub players: Vec<PlayerData>,
-    pub state_data: GameStateData
-}
-
-#[derive(Serialize, Clone)]
-#[serde(rename_all = "snake_case")]
-pub struct RoundResultData {
-    pub words: Vec<String>,
-    pub questions: Vec<String>,
-    pub solutions: Vec<(String, String)>,
-    pub guesses: Vec<Vec<Vec<String>>>,
-    pub point_list: Vec<(String, i64)>
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum UpdateMessage {
-    GameNotFound{ game_id: String },
-    GameAlreadyExists{ game_id: String },
-    LeftLobby{ nick: String },
-    GameState(GameData),
-    RoundResult(RoundResultData),
-}
-
-

+ 67 - 31
static/comm.js

@@ -1,5 +1,7 @@
 $(function() {
     var connection = null;
+    var nick = null;
+    var self_player = null;
 
     
     // when creating a word
@@ -44,7 +46,7 @@ $(function() {
 
     $('#join').click(function() {
         var game_id = $('#gameId').val();
-        var nick = $('#nick').val();
+        nick = $('#nick').val();
         var msg = {
             join: {
                 game_id: game_id,
@@ -57,7 +59,7 @@ $(function() {
 
     $('#create').click(function() {
         var game_id = $('#gameId').val();
-        var nick = $('#nick').val();
+        nick = $('#nick').val();
         var msg = {
             create_game: {
                 game_id: game_id,
@@ -73,10 +75,11 @@ $(function() {
         send(msg);
     });
 
-    $('#ready').click(function() {
-        var game_id = $('#gameId').val();
-        var nick = $('#nick').val();
+    $('#ready-button').click(function() {
         var msg = "ready";
+        if (self_player != null && self_player.ready) {
+            msg = "unready";
+        }
         send(msg);
     });
 
@@ -150,10 +153,12 @@ $(function() {
             setView('login');
             $('#lobby-control').hide();
         }
+        else if (obj.player_list != null) {
+            updatePlayerList(obj.player_list);
+            
+        }
         else if (obj.game_state != null) {
-
             updatePlayerList(obj.game_state.players);
-
             var gs = obj.game_state;
             if (gs.state_data === "starting") {
                 setView('starting');
@@ -203,54 +208,85 @@ $(function() {
         var $table = $('<table/>');
         $table.addClass('result-table');
 
-        var wordline = "<tr><th></th>";
+        var $row = $("<tr/>");
+        $row.append($("<th/>"));
         for(var i = 0; i < result.words.length; i++) {
-            wordline += "<th>" + result.words[i] + "</th>";
+            var $wordline = $("<th/>");
+            $wordline.text(result.words[i]);
+            $row.append($wordline);
         }
-        wordline += "</tr>";
-        $table.append(wordline);
+        $table.append($row);
 
         for(var i = 0; i < result.questions.length; i++) {
-            var wordline = "<tr><th>" + result.questions[i] + "</th>";
+            var $row = $("<tr/>");
+            var $header = $("<th/>");
+            $header.text(result.questions[i]);
+            $row.append($header);
             for(var j = 0; j < result.words.length; j++) {
+                var $cell = $("<td/>");
                 if (solution_dict[result.words[j]] == result.questions[i]) {
-                    wordline += "<td class='result-correct'>";
+                    $cell.addClass("result-correct");
                 }
                 else {
-                    wordline += "<td class='result-wrong'>";
+                    $cell.addClass("result-wrong");
                 }
-                wordline += result.guesses[j][i] + "</td>";
+                $cell.text(result.guesses[j][i]);
+                $row.append($cell);
             }
-            wordline += "</tr>";
-            $table.append(wordline);
+            $table.append($row);
         }
 
-        $('#guesses-table').html($table);
+        $('#guesses-table').html("");
+        $('#guesses-table').append($table);
         $('#points-table').html("");
 
-        var header = "<tr>";
-        var points = "<tr>";
+        var $header = $("<tr/>");
+        var $points = $("<tr/>");
         var pl = result.point_list;
         for(var i = 0; i < pl.length; i++) {
-            header += "<td>" + pl[i][0] + "</td>";
-            points += "<td>" + pl[i][1] + "</td>";
+            var $htd = $("<td/>"); $htd.text(pl[i][0]);
+            var $ptd = $("<td/>"); $ptd.text(pl[i][1]);
+            $header.append($htd);
+            $points.append($ptd);
         }
-        header += "</tr>";
-        points += "</tr>";
-        $('#points-table').append(header);
-        $('#points-table').append(points);
+        $('#points-table').append($header);
+        $('#points-table').append($points);
 
         setView('results');
     }
 
+    function updateReadyStates(player) {
+        self_player = player;
+        if (player.ready) {
+            $("#ready-button").addClass("button-unready");
+            $("#ready-button").removeClass("button-ready");
+            $("#submit-word").addClass("button-unready");
+            $("#submit-word").removeClass("button-ready");
+            $("#submit-guess").addClass("button-unready");
+            $("#submit-guess").removeClass("button-ready");
+        } else {
+            $("#ready-button").addClass("button-ready");
+            $("#ready-button").removeClass("button-unready");
+            $("#submit-word").addClass("button-ready");
+            $("#submit-word").removeClass("button-unready");
+            $("#submit-guess").addClass("button-ready");
+            $("#submit-guess").removeClass("button-unready");
+        }
+    }
+
     function updatePlayerList(players) {
-        playerlist = "";
+        $('#player-list').text("");
         for (var i = 0; i < players.length; i++) {
-            playerlist += "<p>" + players[i].nick + " (" + players[i].points + ")";
-            if (i + 1 < players.length)
-                playerlist += "</p>";
+            var playerclass = players[i].ready ? "player-ready" : "player-unready";
+            var $pitem = $('<p/>');
+            $pitem.addClass(playerclass);
+            $pitem.text(players[i].nick + " (" + players[i].points + ")");
+            $('#player-list').append($pitem);
+
+            if (players[i].nick == nick) {
+                updateReadyStates(players[i]);
+            }
         }
-        $('#player-list').html(playerlist);
         $('#lobby-control').show();
     }
 

+ 2 - 4
static/index.html

@@ -17,7 +17,7 @@
             <div id="lobby-control" style="display:none">
                 <div id="lobby-info">
                 </div>
-                
+
                 <h3>Connected Players</h3>
                 <div id="player-list" class="player-list">
                 </div>
@@ -51,9 +51,7 @@
 
         <div id="startingform" style="display:none">
             A new round is ready to be played. Click &quot;Ready&quot; to indicate that you are ready.
-            <form>
-                <input id="ready" type="button" value="Ready" />
-            </form>
+            <p><button id="ready-button"><span class="ready-span">Ready</span><span class="unready-span">Unready</span></button></p>
         </div>
 
         <div id="creating" style="margin: 0px; display:none">

+ 39 - 7
static/style.css

@@ -13,6 +13,10 @@
     --color3-dark: #242438;
     --color3-darker: #161622;
 
+    --color-ready: #0b3a0b;
+    --color-ready-bright: #165f16;
+    --color-unready: #3a0b0b;
+
     --font-color: #d0d0d0;
 }
 
@@ -60,11 +64,18 @@ body {
 .player-list p {
     padding: 6px;
     margin-right: 6px;
-    background-color: var(--color-darker);
     border: none;
     border-radius: 8px;
 }
 
+.player-list .player-ready {
+    background-color: var(--color-ready);
+}
+
+.player-list .player-unready {
+    background-color: var(--color-unready);
+}
+
 @media screen and (max-width: 1260px) {
     .header {
         min-height: unset;
@@ -99,9 +110,9 @@ input[type="button"], input[type="submit"], button {
     box-shadow: 0 4px 10px 0 rgba(0,0,0,0.3);
 
     background-color: var(--color2-darker);
-    -webkit-transition: background-color 0.3s linear;
-    -ms-transition: background-color 0.3s linear;
-    transition: background-color 0.3s linear;
+    -webkit-transition: background-color 0.3s ease;
+    -ms-transition: background-color 0.3s ease;
+    transition: background-color 0.3s ease;
 }
 
 input[type="button"]:hover, input[type="submit"]:hover, button:hover {
@@ -109,6 +120,27 @@ input[type="button"]:hover, input[type="submit"]:hover, button:hover {
     border: none;
 }
 
+.button.button-unready, input[type="submit"].button-unready, button.button-unready {
+    background-color: var(--color-ready);
+}
+
+.button.button-unready:hover, input[type="submit"].button-unready:hover, button.button-unready:hover {
+    background-color: var(--color-ready-bright);
+}
+
+.button-ready .ready-span {
+    display: inline;
+}
+.button-ready .unready-span {
+    display: none;
+}
+.button-unready .ready-span {
+    display: none;
+}
+.button-unready .unready-span {
+    display: inline;
+}
+
 
 input[type="text"], textarea {
     border: 1px solid var(--color3);
@@ -122,9 +154,9 @@ input[type="text"], textarea {
     font-size: 16px;
 
     background-color: var(--color3-darker);
-    -webkit-transition: background-color 0.3s linear;
-    -ms-transition: background-color 0.3s linear;
-    transition: background-color 0.3s linear;
+    -webkit-transition: background-color 0.3s ease;
+    -ms-transition: background-color 0.3s ease;
+    transition: background-color 0.3s ease;
 }
 
 input[type="text"]:focus, textarea:focus {