initial knock-out whist implementation (wip)
authorGeoffrey Allott <geoffrey@allott.email>
Tue, 23 Feb 2021 22:41:39 +0000 (22:41 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Tue, 23 Feb 2021 22:41:39 +0000 (22:41 +0000)
site/modules/socket.js
src/game/action.rs [new file with mode: 0644]
src/game/chatroom.rs
src/game/mod.rs
src/game/whist.rs
src/seats.rs
src/username.rs

index 2a5051020933c1d67ac32a2e3b1122bc52a16f5c..72ec7fb59a08089c59328a9578c23715ef8c0c1d 100644 (file)
@@ -70,7 +70,7 @@ export class Socket {
                 this.state = "LoggedIn";
                 break;
             case "JoinGameSuccess":
-                this.game = message.game;
+                this.game = {summary: message.summary, actions: message.actions};
                 this.create_game_display();
                 this.state = "InGame";
                 break;
@@ -367,7 +367,6 @@ export class Socket {
                         break;
                     case "Leave":
                         this.game.seats.delete(user_action.username);
-                        this.game.hands.delete(user_action.username);
                         this.chatroom_chat.append(this.leave_element(user_action.username));
                         if (!initialising && user_action.username === this.auth.username) {
                             this.send({type: "JoinLobby", filter: ""});
@@ -457,13 +456,13 @@ export class Socket {
 
     redraw_chatroom() {
         this.chatroom_chat.textContent = "";
-        for (const user_action of this.game.state.actions) {
+        for (const user_action of this.game.actions) {
             this.add_action(user_action, true);
         }
     }
 
     redraw_knock_out_whist() {
-        for (const user_action of this.game.state.actions) {
+        for (const user_action of this.game.actions) {
             this.add_action(user_action, true);
         }
     }
diff --git a/src/game/action.rs b/src/game/action.rs
new file mode 100644 (file)
index 0000000..3d67b95
--- /dev/null
@@ -0,0 +1,81 @@
+use std::fmt::{Debug, Display, Formatter};
+
+use crate::card::{Card, Suit};
+use crate::username::Username;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct UserAction {
+    pub username: Username,
+    pub action: Action,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct ValidatedUserAction(pub UserAction);
+
+impl ValidatedUserAction {
+    pub fn view_for(&self, username: Username) -> UserAction {
+        UserAction {
+            username: self.0.username.clone(),
+            action: if username == self.0.username { self.0.action.clone() } else { self.0.action.anonymise() },
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "action")]
+pub enum Action {
+    Join { seat: u32, chips: u64 },
+    AddOn { chips: u64 },
+    NextToDeal,
+    CommunityCard { card: Card },
+    ReceiveCard { card: Option<Card> },
+    EndDeal,
+    RevealCard { card: Card },
+    PlayCard { card: Card },
+    ChooseTrumps { suit: Suit },
+    Fold,
+    Bet { chips: u64 },
+    WinTrick,
+    WinHand { chips: u64 },
+    WinGame,
+    Message { message: String },
+    Leave,
+    KnockedOut,
+}
+
+impl Action {
+    pub fn anonymise(&self) -> Self {
+        match self {
+            Action::ReceiveCard{..} => Action::ReceiveCard{card: None},
+            action => action.clone(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub enum ActionError {
+    NotAuthorised,
+    AlreadyJoined,
+    GameHasStarted,
+    SeatNotAvailable,
+    NoSeatAvailable,
+    OutOfTurn,
+    CardNotPlayable,
+    InvalidActionForGameType,
+}
+
+impl Display for ActionError {
+    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
+        match self {
+            ActionError::NotAuthorised => f.write_str("NotAuthorised"),
+            ActionError::AlreadyJoined => f.write_str("AlreadyJoined"),
+            ActionError::GameHasStarted => f.write_str("GameHasStarted"),
+            ActionError::SeatNotAvailable => f.write_str("SeatNotAvailable"),
+            ActionError::NoSeatAvailable => f.write_str("NoSeatAvailable"),
+            ActionError::OutOfTurn => f.write_str("OutOfTurn"),
+            ActionError::CardNotPlayable => f.write_str("CardNotPlayable"),
+            ActionError::InvalidActionForGameType => f.write_str("InvalidActionForGameType"),
+        }
+    }
+}
index 8bb476ccd85c4d1b96c081e623d884e0a52142b5..d218a7f57f338cc78fe46acbb953ddbfd686021f 100644 (file)
@@ -13,7 +13,7 @@ enum ChatroomAction {
     Leave,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct ChatroomSettings {
     title: String,
 }
index eb40b11b14adf9c0c2715d16b3fa2f2bbb31a9f8..ff932a8b443e7f4b5ec36b344334b0c6879e1f32 100644 (file)
@@ -1,29 +1,16 @@
-pub mod chatroom;
+mod action;
+mod chatroom;
+mod whist;
 
 use std::collections::HashSet;
 use std::fmt::{Debug, Display, Formatter};
 
-use crate::card::{Card, Suit};
 use crate::username::Username;
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(transparent)]
-pub struct ValidatedUserAction(UserAction);
+use self::chatroom::{Chatroom, ChatroomSettings};
+use self::whist::{KnockOutWhist, KnockOutWhistSettings};
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct UserAction {
-    pub username: Username,
-    pub action: Action,
-}
-
-impl ValidatedUserAction {
-    pub fn view_for(&self, username: Username) -> UserAction {
-        UserAction {
-            username: self.0.username.clone(),
-            action: if username == self.0.username { self.0.action.clone() } else { self.0.action.anonymise() },
-        }
-    }
-}
+pub use self::action::{Action, ActionError, UserAction, ValidatedUserAction};
 
 pub trait Game : Debug + CloneBox + Send + Sync {
     fn id(&self) -> u32;
@@ -34,7 +21,7 @@ pub trait Game : Debug + CloneBox + Send + Sync {
     fn next_dealer_action(&self) -> Option<ValidatedUserAction>;
 }
 
-pub trait CloneBox {
+trait CloneBox {
     fn clone_box(&self) -> Box<dyn Game>;
 }
 
@@ -51,22 +38,19 @@ impl Clone for Box<dyn Game> {
 }
 
 impl dyn Game {
-    pub fn new(summary: GameSummary) -> Box<Self> {
-        todo!()
+    pub fn new(GameSummary{id, settings}: GameSummary) -> Box<Self> {
+        match settings {
+            GameSettings::Chatroom(settings) => Box::new(Chatroom::new(id, settings)),
+            _ => todo!()
+        }
     }
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(tag = "format")]
 pub enum GameSettings {
-    Chatroom {
-        title: String,
-        max_players: u32,
-    },
-    KnockOutWhist {
-        title: String,
-        max_players: u32,
-    },
+    Chatroom(ChatroomSettings),
+    KnockOutWhist(KnockOutWhistSettings),
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -93,64 +77,6 @@ impl GameList {
     }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "action")]
-pub enum Action {
-    Join { seat: u32, chips: u64 },
-    AddOn { chips: u64 },
-    NextToDeal,
-    CommunityCard { card: Card },
-    ReceiveCard { card: Option<Card> },
-    EndDeal,
-    RevealCard { card: Card },
-    PlayCard { card: Card },
-    ChooseTrumps { suit: Suit },
-    Fold,
-    Bet { chips: u64 },
-    WinTrick,
-    WinHand { chips: u64 },
-    WinGame,
-    Message { message: String },
-    Leave,
-    KnockedOut,
-}
-
-impl Action {
-    pub fn anonymise(&self) -> Self {
-        match self {
-            Action::ReceiveCard{..} => Action::ReceiveCard{card: None},
-            action => action.clone(),
-        }
-    }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub enum ActionError {
-    NotAuthorised,
-    AlreadyJoined,
-    GameHasStarted,
-    SeatNotAvailable,
-    NoSeatAvailable,
-    OutOfTurn,
-    CardNotPlayable,
-    InvalidActionForGameType,
-}
-
-impl Display for ActionError {
-    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
-        match self {
-            ActionError::NotAuthorised => f.write_str("NotAuthorised"),
-            ActionError::AlreadyJoined => f.write_str("AlreadyJoined"),
-            ActionError::GameHasStarted => f.write_str("GameHasStarted"),
-            ActionError::SeatNotAvailable => f.write_str("SeatNotAvailable"),
-            ActionError::NoSeatAvailable => f.write_str("NoSeatAvailable"),
-            ActionError::OutOfTurn => f.write_str("OutOfTurn"),
-            ActionError::CardNotPlayable => f.write_str("CardNotPlayable"),
-            ActionError::InvalidActionForGameType => f.write_str("InvalidActionForGameType"),
-        }
-    }
-}
-
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct GameSummary {
     id: u32,
index a4ad244d23feae1d02fd1a0b73fbcb6d1e083bd1..c04ec530ac5c5647c303924afda2c41ad8a2aafa 100644 (file)
@@ -1,36 +1,65 @@
-pub enum KnockOutWhist {
-    NotYetStarted {
-        seats: Seats,
+use std::collections::{HashMap, HashSet};
+
+use crate::card::Card;
+use crate::seats::Seats;
+use crate::username::Username;
+
+use super::{Action, ActionError, Game, UserAction, ValidatedUserAction};
+
+#[derive(Copy, Clone, Debug)]
+enum KnockOutWhistState {
+    NotYetStarted,
+    Dealing,
+    ChoosingTrumps,
+    Playing,
+    CutForCall,
+    Completed,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct KnockOutWhistSettings {
+    title: String,
+}
+
+#[derive(Clone, Debug)]
+pub struct KnockOutWhist {
+    id: u32,
+    actions_len: usize,
+    settings: KnockOutWhistSettings,
+    state: KnockOutWhistState,
+    seats: Seats,
+    dealer: Username,
+    call: Username,
+    deck: HashSet<Card>,
+    hands: HashMap<Username, HashSet<Card>>,
+    tricks_won: HashMap<Username, u32>,
+    winners: HashSet<Username>,
+    trump_card: Option<Card>,
+    cards_to_deal: u32,
+}
+
+impl Game for KnockOutWhist {
+    fn id(&self) -> u32 {
+        self.id
     }
-    Dealing {
-        dealer: Username,
-        call: Username,
-        deck: HashSet<Card>,
-        hands: HashMap<Username, HashSet<Card>>,
-        trump_card: Option<Card>,
-        seats: Seats,
-        cards_to_deal: u32,
+
+    fn players(&self) -> HashSet<Username> {
+        self.seats.player_set()
     }
-    ChoosingTrumps {
-        dealer: Username,
-        call: Username,
-        hands: HashMap<Username, HashSet<Card>>,
-        seats: Seats,
+
+    fn actions_len(&self) -> usize {
+        self.actions_len
     }
-    Playing {
-        turn: Username,
-        trumps: Suit,
-        trick: HashMap<Username, Card>,
-        hands: HashMap<Username, HashSet<Card>>,
-        seats: Seats,
-        tricks_won: HashMap<Username, u32>,
+
+    fn validate_action(&self, action: UserAction) -> Result<ValidatedUserAction, ActionError> {
+        todo!()
     }
-    CutForCall {
-        winners: HashSet<Username>,
-        cards: HashMap<Username, Card>,
-        seats: Seats,
+
+    fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError> {
+        todo!()
     }
-    Completed {
-        winner: Username,
+
+    fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
+        todo!()
     }
 }
index bb861b39f9fd5d5d12d5a39bcef5917f153d5c9c..e654cf91612faf065defcf603e001e8729aeb040 100644 (file)
@@ -1,8 +1,10 @@
 use std::collections::{BTreeMap, HashSet};
 
+use crate::username::Username;
+
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Seats {
-    players: BTreeMap<u32, String>,
+    players: BTreeMap<u32, Username>,
 }
 
 impl Seats {
@@ -12,28 +14,28 @@ impl Seats {
         }
     }
 
-    pub fn add_player(&mut self, seat: u32, username: &str) {
-        self.players.insert(seat, username.to_owned());
+    pub fn add_player(&mut self, seat: u32, username: Username) {
+        self.players.insert(seat, username);
     }
 
-    pub fn remove_player(&mut self, username: &str) {
-        if let Some(seat) = self.players.iter().find(|(_, player)| &**player == username).map(|(&seat, _)| seat) {
+    pub fn remove_player(&mut self, username: Username) {
+        if let Some(seat) = self.players.iter().find(|(_, &player)| player == username).map(|(&seat, _)| seat) {
             self.players.remove(&seat);
         }
     }
 
-    fn player_after_seat(&self, seat: u32) -> Option<String> {
-        if let Some((_, name)) = self.players.range(seat+1..).next() {
-            Some(name.to_owned())
-        } else if let Some((_, name)) = self.players.range(..seat).next() {
-            Some(name.to_owned())
+    fn player_after_seat(&self, seat: u32) -> Option<Username> {
+        if let Some((_, &name)) = self.players.range(seat+1..).next() {
+            Some(name)
+        } else if let Some((_, &name)) = self.players.range(..seat).next() {
+            Some(name)
         } else {
             None
         }
     }
 
-    pub fn player_after(&self, username: &str) -> Option<String> {
-        for (&seat, player) in &self.players {
+    pub fn player_after(&self, username: Username) -> Option<Username> {
+        for (&seat, &player) in &self.players {
             if player == username {
                 return self.player_after_seat(seat);
             }
@@ -41,8 +43,8 @@ impl Seats {
         None
     }
 
-    pub fn contains_player(&self, username: &str) -> bool {
-        for (_, player) in &self.players {
+    pub fn contains_player(&self, username: Username) -> bool {
+        for (_, &player) in &self.players {
             if player == username {
                 return true;
             }
@@ -50,8 +52,8 @@ impl Seats {
         false
     }
 
-    pub fn player_set(&self) -> HashSet<&str> {
-        self.players.iter().map(|(_, player)| &**player).collect()
+    pub fn player_set(&self) -> HashSet<Username> {
+        self.players.iter().map(|(_, player)| *player).collect()
     }
 
     pub fn seat_is_available(&self, seat: u32) -> bool {
@@ -70,10 +72,10 @@ mod tests {
     #[test]
     fn player_after_absent_player_is_none() {
         let mut seats = Seats::new();
-        seats.add_player(1, "Player 1");
-        seats.add_player(2, "Player 2");
-        seats.add_player(3, "Player 3");
-        assert_eq!(None, seats.player_after("Player 4"));
+        seats.add_player(1, "Player_1".parse().unwrap());
+        seats.add_player(2, "Player_2".parse().unwrap());
+        seats.add_player(3, "Player_3".parse().unwrap());
+        assert_eq!(None, seats.player_after("Player_4".parse().unwrap()));
         assert!(seats.seat_is_available(0));
         assert!(!seats.seat_is_available(1));
         assert!(!seats.seat_is_available(2));
@@ -84,8 +86,8 @@ mod tests {
     #[test]
     fn player_after_single_player_is_none() {
         let mut seats = Seats::new();
-        seats.add_player(1, "Player 1");
-        assert_eq!(None, seats.player_after("Player 1"));
+        seats.add_player(1, "Player_1".parse().unwrap());
+        assert_eq!(None, seats.player_after("Player_1".parse().unwrap()));
         assert!(seats.seat_is_available(0));
         assert!(!seats.seat_is_available(1));
         assert!(seats.seat_is_available(2));
@@ -93,42 +95,53 @@ mod tests {
 
     #[test]
     fn player_after_simple() {
+        let player_1 = "Player_1".parse().unwrap();
+        let player_2 = "Player_2".parse().unwrap();
+        let player_3 = "Player_3".parse().unwrap();
         let mut seats = Seats::new();
-        seats.add_player(1, "Player 1");
-        seats.add_player(2, "Player 2");
-        seats.add_player(3, "Player 3");
-        assert_eq!("Player 2", seats.player_after("Player 1").unwrap());
-        assert_eq!("Player 3", seats.player_after("Player 2").unwrap());
+        seats.add_player(1, player_1);
+        seats.add_player(2, player_2);
+        seats.add_player(3, player_3);
+        assert_eq!(player_2, seats.player_after(player_1).unwrap());
+        assert_eq!(player_3, seats.player_after(player_2).unwrap());
     }
 
     #[test]
     fn player_after_wrap_around() {
+        let player_1 = "Player_1".parse().unwrap();
+        let player_5 = "Player_5".parse().unwrap();
+        let player_8 = "Player_8".parse().unwrap();
         let mut seats = Seats::new();
-        seats.add_player(1, "Player 1");
-        seats.add_player(5, "Player 5");
-        seats.add_player(8, "Player 8");
-        assert_eq!("Player 1", seats.player_after("Player 8").unwrap());
+        seats.add_player(1, player_1);
+        seats.add_player(5, player_5);
+        seats.add_player(8, player_8);
+        assert_eq!(player_1, seats.player_after(player_8).unwrap());
     }
 
     #[test]
     fn player_after_two_player() {
+        let player_1 = "Player_1".parse().unwrap();
+        let player_4 = "Player_4".parse().unwrap();
         let mut seats = Seats::new();
-        seats.add_player(1, "Player 1");
-        seats.add_player(4, "Player 4");
-        assert_eq!("Player 4", seats.player_after("Player 1").unwrap());
-        assert_eq!("Player 1", seats.player_after("Player 4").unwrap());
+        seats.add_player(1, player_1);
+        seats.add_player(4, player_4);
+        assert_eq!(player_4, seats.player_after(player_1).unwrap());
+        assert_eq!(player_1, seats.player_after(player_4).unwrap());
     }
 
     #[test]
     fn remove_player() {
+        let player_1 = "Player_1".parse().unwrap();
+        let player_2 = "Player_2".parse().unwrap();
+        let player_3 = "Player_3".parse().unwrap();
         let mut seats = Seats::new();
-        seats.add_player(1, "Player 1");
-        seats.add_player(2, "Player 2");
-        seats.add_player(3, "Player 3");
-        seats.remove_player("Player 2");
-        assert_eq!("Player 3", seats.player_after("Player 1").unwrap());
-        assert_eq!(None, seats.player_after("Player 2"));
-        assert_eq!("Player 1", seats.player_after("Player 3").unwrap());
+        seats.add_player(1, player_1);
+        seats.add_player(2, player_2);
+        seats.add_player(3, player_3);
+        seats.remove_player(player_2);
+        assert_eq!(player_3, seats.player_after(player_1).unwrap());
+        assert_eq!(None, seats.player_after(player_2));
+        assert_eq!(player_1, seats.player_after(player_3).unwrap());
         assert!(seats.seat_is_available(0));
         assert!(!seats.seat_is_available(1));
         assert!(seats.seat_is_available(2));
index 4313ad5a4c0911ea1cce631ebcbde974ca7f825b..cdf9d9cc25cd92cd3661d4e1006513a0c23ae69c 100644 (file)
@@ -1,9 +1,9 @@
-use std::fmt::{self, Display, Formatter};
+use std::fmt::{self, Debug, Display, Formatter};
 use std::str::{self, FromStr, Utf8Error};
 
 use serde::{Serialize, Deserialize, Serializer, Deserializer, ser::Error, de::{self, Visitor}};
 
-#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[repr(align(8))]
 pub struct Username {
     username: [u8; Self::MAX_LENGTH],
@@ -37,6 +37,15 @@ impl Display for Username {
     }
 }
 
+impl Debug for Username {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self.as_str() {
+            Ok(str) => Debug::fmt(str, f),
+            Err(_) => Debug::fmt(String::from_utf8_lossy(&self.username).trim_end_matches('\0'), f),
+        }
+    }
+}
+
 impl FromStr for Username {
     type Err = &'static str;