From: Geoffrey Allott Date: Tue, 23 Feb 2021 22:41:39 +0000 (+0000) Subject: initial knock-out whist implementation (wip) X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=cb183e383090b4d89cd46e2c1ac16a3acdf96df3;p=pokerwave.git initial knock-out whist implementation (wip) --- diff --git a/site/modules/socket.js b/site/modules/socket.js index 2a50510..72ec7fb 100644 --- a/site/modules/socket.js +++ b/site/modules/socket.js @@ -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 index 0000000..3d67b95 --- /dev/null +++ b/src/game/action.rs @@ -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 }, + 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"), + } + } +} diff --git a/src/game/chatroom.rs b/src/game/chatroom.rs index 8bb476c..d218a7f 100644 --- a/src/game/chatroom.rs +++ b/src/game/chatroom.rs @@ -13,7 +13,7 @@ enum ChatroomAction { Leave, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatroomSettings { title: String, } diff --git a/src/game/mod.rs b/src/game/mod.rs index eb40b11..ff932a8 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -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; } -pub trait CloneBox { +trait CloneBox { fn clone_box(&self) -> Box; } @@ -51,22 +38,19 @@ impl Clone for Box { } impl dyn Game { - pub fn new(summary: GameSummary) -> Box { - todo!() + pub fn new(GameSummary{id, settings}: GameSummary) -> Box { + 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 }, - 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, diff --git a/src/game/whist.rs b/src/game/whist.rs index a4ad244..c04ec53 100644 --- a/src/game/whist.rs +++ b/src/game/whist.rs @@ -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, + hands: HashMap>, + tricks_won: HashMap, + winners: HashSet, + trump_card: Option, + cards_to_deal: u32, +} + +impl Game for KnockOutWhist { + fn id(&self) -> u32 { + self.id } - Dealing { - dealer: Username, - call: Username, - deck: HashSet, - hands: HashMap>, - trump_card: Option, - seats: Seats, - cards_to_deal: u32, + + fn players(&self) -> HashSet { + self.seats.player_set() } - ChoosingTrumps { - dealer: Username, - call: Username, - hands: HashMap>, - seats: Seats, + + fn actions_len(&self) -> usize { + self.actions_len } - Playing { - turn: Username, - trumps: Suit, - trick: HashMap, - hands: HashMap>, - seats: Seats, - tricks_won: HashMap, + + fn validate_action(&self, action: UserAction) -> Result { + todo!() } - CutForCall { - winners: HashSet, - cards: HashMap, - seats: Seats, + + fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError> { + todo!() } - Completed { - winner: Username, + + fn next_dealer_action(&self) -> Option { + todo!() } } diff --git a/src/seats.rs b/src/seats.rs index bb861b3..e654cf9 100644 --- a/src/seats.rs +++ b/src/seats.rs @@ -1,8 +1,10 @@ use std::collections::{BTreeMap, HashSet}; +use crate::username::Username; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Seats { - players: BTreeMap, + players: BTreeMap, } 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 { - 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 { + 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 { - for (&seat, player) in &self.players { + pub fn player_after(&self, username: Username) -> Option { + 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 { + 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)); diff --git a/src/username.rs b/src/username.rs index 4313ad5..cdf9d9c 100644 --- a/src/username.rs +++ b/src/username.rs @@ -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;