add game status flags and filter by completed: false
authorGeoffrey Allott <geoffrey@allott.email>
Mon, 29 Mar 2021 21:15:01 +0000 (22:15 +0100)
committerGeoffrey Allott <geoffrey@allott.email>
Mon, 29 Mar 2021 21:15:01 +0000 (22:15 +0100)
site/modules/mainmenu.js
src/dealer.rs
src/game/filter.rs
src/game/mod.rs
src/server.rs

index 019f666be7bb129f7a07f6fe66500440a2b9bd2e..f65ea23359fc415a8e2f3946bca71279730ae20a 100644 (file)
@@ -16,7 +16,7 @@ export class MainMenu {
         texas_hold_em_text.innerText = "Texas Hold 'em";
         texas_hold_em.append(texas_hold_em_img);
         texas_hold_em.append(texas_hold_em_text);
-        texas_hold_em.onclick = () => this.send({type: "JoinLobby", filter: "format: TexasHoldEm"});
+        texas_hold_em.onclick = () => this.send({type: "JoinLobby", filter: "format: TexasHoldEm and completed: false"});
         menu_container.append(texas_hold_em);
 
         const knock_out_whist = document.createElement("div");
@@ -27,7 +27,7 @@ export class MainMenu {
         knock_out_whist_text.innerText = "Knock-Out Whist";
         knock_out_whist.append(knock_out_whist_img);
         knock_out_whist.append(knock_out_whist_text);
-        knock_out_whist.onclick = () => this.send({type: "JoinLobby", filter: "format: KnockOutWhist"});
+        knock_out_whist.onclick = () => this.send({type: "JoinLobby", filter: "format: KnockOutWhist and completed: false"});
         menu_container.append(knock_out_whist);
 
         const chatroom = document.createElement("div");
index 44a96c620c061ad425ec9575a7236c2c4a5dfcfe..2fe00bfc54a343d31302e61ce69438a66905ab7d 100644 (file)
@@ -125,7 +125,10 @@ impl Dealer {
                         return Ok(Termination::Continue);
                     }
                     DealerAction::WaitForPlayer => return Ok(Termination::Continue),
-                    DealerAction::Leave => return Ok(Termination::Break),
+                    DealerAction::Leave => {
+                        self.server.set_game_completed(id, true).await?;
+                        return Ok(Termination::Break);
+                    }
                 }
             }
         }
index 699f51f030a27dd977b2f9c90b197cd05ff823e2..813a316f73eb7849a666f1464fb43a9dc5749199 100644 (file)
@@ -2,7 +2,7 @@ use nom::{
     branch::alt,
     bytes::complete::{tag, take_until},
     character::complete::{alpha1, multispace0},
-    combinator::map,
+    combinator::{map, value},
     error::ParseError,
     multi::{many0_count, separated_list0, separated_list1},
     sequence::{delimited, preceded, tuple},
@@ -26,6 +26,7 @@ where
 enum Field {
     Format(String),
     Title(String),
+    Completed(bool),
     Parens(Box<Or>),
 }
 
@@ -38,6 +39,7 @@ impl Filter for Field {
                 GameSettings::TexasHoldEm(_) => format.eq_ignore_ascii_case("TexasHoldEm"),
             },
             Field::Title(title) => summary.settings.title().to_lowercase().contains(&title.to_lowercase()),
+            Field::Completed(completed) => summary.status.completed == *completed,
             Field::Parens(filter) => filter.matches(summary),
         }
     }
@@ -45,10 +47,12 @@ impl Filter for Field {
 
 fn parse_field(input: &str) -> IResult<&str, Field> {
     let string = || map(alt((alpha1, delimited(tag("\""), take_until("\""), tag("\"")))), String::from);
+    let boolean = || alt((value(true, tag("true")), value(false, tag("false"))));
     let format = map(preceded(tuple((tag("format"), ws(tag(":")))), string()), Field::Format);
     let title = map(preceded(tuple((tag("title"), ws(tag(":")))), string()), Field::Title);
+    let completed = map(preceded(tuple((tag("completed"), ws(tag(":")))), boolean()), Field::Completed);
     let parens = delimited(tag("("), map(map(ws(parse_or), Box::new), Field::Parens), tag(")"));
-    alt((format, title, parens))(input)
+    alt((format, title, completed, parens))(input)
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -143,11 +147,11 @@ mod tests {
         let filter = parse_filter("format: KnockOutWhist and (title:Game or title:Play)").unwrap();
         let games: Vec<GameSummary> = serde_json::from_str(
             r#"[
-            {"id": 0, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
-            {"id": 1, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
-            {"id": 2, "settings": {"format":"Chatroom","title":"Game 2"}},
-            {"id": 3, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
-            {"id": 4, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
+            {"id": 0, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
+            {"id": 1, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
+            {"id": 2, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"Chatroom","title":"Game 2"}},
+            {"id": 3, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
+            {"id": 4, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
         ]"#,
         )
         .unwrap();
@@ -163,11 +167,11 @@ mod tests {
         let filter = parse_filter("   ").unwrap();
         let games: Vec<GameSummary> = serde_json::from_str(
             r#"[
-            {"id": 0, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
-            {"id": 1, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
-            {"id": 2, "settings": {"format":"Chatroom","title":"Game 2"}},
-            {"id": 3, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
-            {"id": 4, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
+            {"id": 0, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Game 0","max_players":2}},
+            {"id": 1, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"game 1","max_players":2}},
+            {"id": 2, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"Chatroom","title":"Game 2"}},
+            {"id": 3, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"Invalid 3","max_players":2}},
+            {"id": 4, "status":{"created":0,"completed":false,"players":[]}, "settings": {"format":"KnockOutWhist","title":"play 4","max_players":2}}
         ]"#,
         )
         .unwrap();
index 552e0f2c8dba7d1136428da486961eb20355c3c9..cc3e6db638dbe0d2dd6d9316194dd914dc60822a 100644 (file)
@@ -44,7 +44,7 @@ impl Clone for Box<dyn Game> {
 }
 
 impl dyn Game {
-    pub fn new(GameSummary { id, settings }: GameSummary, seed: Seed) -> Box<Self> {
+    pub fn new(GameSummary { id, settings, .. }: GameSummary, seed: Seed) -> Box<Self> {
         match settings {
             GameSettings::Chatroom(settings) => Box::new(Chatroom::new(id, settings)),
             GameSettings::KnockOutWhist(settings) => Box::new(KnockOutWhist::new(id, settings, seed)),
@@ -100,15 +100,35 @@ impl GameList {
     }
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GameStatus {
+    pub created: Timestamp,
+    pub completed: bool,
+    pub players: HashSet<Username>,
+    pub winner: Option<Username>,
+}
+
+impl GameStatus {
+    pub fn new(created: Timestamp) -> Self {
+        Self {
+            created,
+            completed: false,
+            players: HashSet::new(),
+            winner: None,
+        }
+    }
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct GameSummary {
     id: i64,
     settings: GameSettings,
+    status: GameStatus,
 }
 
 impl GameSummary {
-    pub fn new(id: i64, settings: GameSettings) -> Self {
-        Self { id, settings }
+    pub fn new(id: i64, settings: GameSettings, status: GameStatus) -> Self {
+        GameSummary { id, settings, status }
     }
 
     pub fn id(&self) -> i64 {
index 6e567cc3d55f3c024f1dce854c3eb9b85706df0f..9549b5f2b6592ed0596a3c73e5dcaa81e44c757b 100644 (file)
@@ -10,7 +10,7 @@ use scrypt::password_hash::HasherError;
 use serde::{Deserialize, Serialize};
 
 use crate::auth::{Auth, CreateAuth};
-use crate::game::{GameSettings, GameSummary, ValidatedUserAction};
+use crate::game::{GameSettings, GameStatus, GameSummary, ValidatedUserAction};
 use crate::pubsub::{client_interest_channel, ClientInterest, ClientInterestReceiver, ClientInterestSender};
 use crate::rng::Seed;
 use crate::username::Username;
@@ -64,6 +64,10 @@ fn game_settings_key(id: i64) -> String {
     format!("game:{}:settings", id)
 }
 
+fn game_status_key(id: i64) -> String {
+    format!("game:{}:status", id)
+}
+
 fn game_seed_key(id: i64) -> String {
     format!("game:{}:seed", id)
 }
@@ -115,13 +119,24 @@ impl ServerState {
     }
 
     pub async fn create_game(&mut self, settings: GameSettings) -> RedisResult<i64> {
+        let now = self.now().await?;
         let id = self.redis.incr("game:next_id", 1).await?;
         let () = self.redis.set(game_settings_key(id), AsJson(settings)).await?;
+        let GameStatus { created, completed, players, winner } = GameStatus::new(now);
+        let status_key = game_status_key(id);
+        let () = self.redis.hset(&status_key, "created", AsJson(created)).await?;
+        let () = self.redis.hset(&status_key, "completed", AsJson(completed)).await?;
+        let () = self.redis.hset(&status_key, "players", AsJson(players)).await?;
+        let () = self.redis.hset(&status_key, "winner", AsJson(winner)).await?;
         let () = self.redis.set(game_seed_key(id), AsJson(Seed::cha_cha_20_from_entropy())).await?;
         let () = self.redis.rpush("game:list", id).await?;
         Ok(id)
     }
 
+    pub async fn set_game_completed(&mut self, id: i64, completed: bool) -> RedisResult<()> {
+        self.redis.hset(game_status_key(id), "completed", AsJson(completed)).await
+    }
+
     pub async fn game_list(&mut self, from: usize) -> RedisResult<Vec<GameSummary>> {
         debug!("game_list(from: {})", from);
         let games: Vec<i64> = self.redis.lrange("game:list", from as isize, -1).await?;
@@ -142,9 +157,17 @@ impl ServerState {
     }
 
     pub async fn game_summary(&mut self, id: i64) -> RedisResult<GameSummary> {
-        let key = game_settings_key(id);
-        info!("Getting summary from key: {}", key);
-        self.redis.get(key).await.map(AsJson::get).map(|settings| GameSummary::new(id, settings))
+        let settings_key = game_settings_key(id);
+        let status_key = game_status_key(id);
+        info!("Getting settings from key: {}", settings_key);
+        let settings = self.redis.get(settings_key).await.map(AsJson::get)?;
+        info!("Getting status from key: {}", status_key);
+        let created = self.redis.hget(&status_key, "created").await.map(AsJson::get)?;
+        let completed = self.redis.hget(&status_key, "completed").await.map(AsJson::get)?;
+        let players = self.redis.hget(&status_key, "players").await.map(AsJson::get)?;
+        let winner = self.redis.hget(&status_key, "winner").await.map(AsJson::get)?;
+        let status = GameStatus { created, completed, players, winner };
+        Ok(GameSummary::new(id, settings, status))
     }
 
     pub async fn game_seed(&mut self, id: i64) -> RedisResult<Seed> {