From: Geoffrey Allott Date: Thu, 25 Feb 2021 23:49:33 +0000 (+0000) Subject: can play through whole game of knock-out whist through web interface X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=7936ca8d7b41701543ed0436c3c55a6249061039;p=pokerwave.git can play through whole game of knock-out whist through web interface --- diff --git a/site/img/suit-c.svg b/site/img/suit-c.svg new file mode 100644 index 0000000..07704cb --- /dev/null +++ b/site/img/suit-c.svg @@ -0,0 +1,3 @@ + + ♣ + diff --git a/site/img/suit-d.svg b/site/img/suit-d.svg new file mode 100644 index 0000000..ac979d5 --- /dev/null +++ b/site/img/suit-d.svg @@ -0,0 +1,3 @@ + + ♢ + diff --git a/site/img/suit-h.svg b/site/img/suit-h.svg new file mode 100644 index 0000000..b257083 --- /dev/null +++ b/site/img/suit-h.svg @@ -0,0 +1,3 @@ + + ♡ + diff --git a/site/img/suit-s.svg b/site/img/suit-s.svg new file mode 100644 index 0000000..0c2c641 --- /dev/null +++ b/site/img/suit-s.svg @@ -0,0 +1,3 @@ + + ♠ + diff --git a/site/modules/card.js b/site/modules/card.js index 2422de5..cceadbf 100644 --- a/site/modules/card.js +++ b/site/modules/card.js @@ -29,3 +29,7 @@ export function card_href(card) { if (card === null) return "img/card-back-blue.svg"; return "img/card-" + short_rank(card.rank) + short_suit(card.suit) + ".svg"; } + +export function suit_href(suit) { + return "img/suit-" + short_suit(suit) + ".svg"; +} diff --git a/site/modules/socket.js b/site/modules/socket.js index 72ec7fb..b6f098e 100644 --- a/site/modules/socket.js +++ b/site/modules/socket.js @@ -1,6 +1,6 @@ const svgns = "http://www.w3.org/2000/svg"; -import { card_href } from "./card.js"; +import { card_href, suit_href } from "./card.js"; export class Socket { constructor(container, login_all_sockets) { @@ -264,6 +264,36 @@ export class Socket { felt.setAttribute("fill", "green"); this.svg.append(felt); + const suits = document.createElementNS(svgns, "rect"); + suits.setAttribute("x", "210"); + suits.setAttribute("y", "400"); + suits.setAttribute("width", "100"); + suits.setAttribute("height", "30"); + suits.setAttribute("fill", "#00000000"); + this.svg.append(suits); + + this.glyphs = new Map(); + let x = 210; + for (const suit of ["Clubs", "Diamonds", "Hearts", "Spades"]) { + const highlight = document.createElementNS(svgns, "circle"); + highlight.setAttribute("cx", x + 12.5); + highlight.setAttribute("cy", "415"); + highlight.setAttribute("r", "10"); + highlight.classList.add("suit-highlight"); + highlight.onclick = () => this.take_action({action: "ChooseTrumps", suit: suit}); + this.svg.append(highlight); + this.glyphs.set(suit, highlight); + const glyph = document.createElementNS(svgns, "image"); + glyph.setAttribute("x", x); + glyph.setAttribute("y", "400"); + glyph.setAttribute("width", "25"); + glyph.setAttribute("height", "30"); + glyph.setAttribute("href", suit_href(suit)); + glyph.classList.add("suit-glyph"); + this.svg.append(glyph); + x += 25; + } + this.container.append(this.svg); this.redraw_knock_out_whist(); } @@ -353,6 +383,13 @@ export class Socket { return image; } + set_trumps(suit) { + this.game.trump_suit = suit; + for (const [suit, glyph] of this.glyphs) { + glyph.classList.toggle("trumps", suit === this.game.trump_suit); + } + } + add_action(user_action, initialising) { switch (this.game.summary.settings.format) { case "Chatroom": @@ -396,12 +433,27 @@ export class Socket { break; case "NextToDeal": this.dealer = user_action.username; + for (const card of this.game.community) { + this.svg.removeChild(card.image); + } + this.game.community = []; + for (const [username, hand] of this.game.hands) { + for (const card of hand) { + this.svg.removeChild(card.image); + } + } + this.game.hands.clear(); + this.set_trumps(null); break; case "ReceiveCard": + case "RevealCard": const card = { card: user_action.action.card, image: this.card_image(user_action.username, user_action.action.card), }; + if (!this.game.hands.has(user_action.username)) { + this.game.hands.set(user_action.username, []); + } this.game.hands.get(user_action.username).push(card); this.set_card_positions(); break; @@ -426,8 +478,9 @@ export class Socket { }); this.game.hands.set(user_action.username, cards); this.set_card_positions(); + break; case "ChooseTrumps": - this.game.trump_suit = user_action.action.suit; + this.set_trumps(user_action.action.suit); break; case "CommunityCard": const community_card = { @@ -435,7 +488,8 @@ export class Socket { image: this.card_image(null, user_action.action.card), }; this.game.community.push(community_card); - this.game.trump_suit = user_action.action.card.suit; + this.set_trumps(user_action.action.card.suit); + this.set_card_positions(); break; case "WinTrick": for (const card of this.game.trick) { @@ -446,6 +500,10 @@ export class Socket { case "EndDeal": console.log(this); break; + case "WinGame": + console.log(this); + this.set_trumps(null); + break; default: console.error("Unhandled action for knock-out whist", user_action); break; diff --git a/site/style/whist.css b/site/style/whist.css index 7c8d47f..97cae0b 100644 --- a/site/style/whist.css +++ b/site/style/whist.css @@ -11,3 +11,24 @@ svg { .my-card:hover { transform: translateY(-20px); } + +.suit-glyph { + pointer-events: none; +} + +.suit-highlight { + fill: transparent; + transition: fill 0.5s; +} + +.suit-highlight:hover { + fill: #4040ff; +} + +.suit-highlight.trumps { + fill: #2020ff; +} + +.suit-highlight.trumps:hover { + fill: #4040ff; +} diff --git a/src/dealer.rs b/src/dealer.rs index 8331d57..bcd7ed1 100644 --- a/src/dealer.rs +++ b/src/dealer.rs @@ -49,14 +49,19 @@ impl Dealer { let from = self.dealer.game.actions_len(); let actions = self.server.game_state(id, from).await?; for action in actions { + debug!("Taking action: {:?}", action); if let Err(err) = self.dealer.game.take_action(action) { error!("Action from database failed to apply: {}", err); } + debug!("Dealer: Game state: {:#?}", self.dealer.game); } 'take_action: loop { match self.dealer.game.next_dealer_action() { Some(action) => match self.take_action(action).await { - Ok(ActionStatus::Committed) => continue 'take_action, + Ok(ActionStatus::Committed) => { + debug!("Dealer: Game state: {:#?}", self.dealer.game); + continue 'take_action; + } Ok(ActionStatus::Interrupted) => continue 'retrieve_updates, Err(err) => return Err(err), }, diff --git a/src/game/mod.rs b/src/game/mod.rs index ff932a8..f4e7400 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -41,7 +41,7 @@ impl dyn Game { pub fn new(GameSummary{id, settings}: GameSummary) -> Box { match settings { GameSettings::Chatroom(settings) => Box::new(Chatroom::new(id, settings)), - _ => todo!() + GameSettings::KnockOutWhist(settings) => Box::new(KnockOutWhist::new(id, settings)), } } } diff --git a/src/game/whist.rs b/src/game/whist.rs index 3ee5cfe..4f6490c 100644 --- a/src/game/whist.rs +++ b/src/game/whist.rs @@ -82,6 +82,7 @@ impl KnockOutWhist { } fn trick_winner(&self) -> Option { + if self.trick.len() < self.seats.players_len() { return None; } let highest_trump = self.trick.iter() .filter(|(_, card)| Some(card.suit) == self.trumps) .max_by_key(|(_, card)| card.rank); @@ -146,7 +147,7 @@ impl Game for KnockOutWhist { Err(ActionError::OutOfTurn) } else if !self.hand_contains_card(username, card) { Err(ActionError::CardNotPlayable) - } else if self.led.is_some() && Some(card.suit) != self.led && self.hand_contains_suit(username, card.suit) { + } else if matches!(self.led, Some(led) if card.suit != led && self.hand_contains_suit(username, led)) { Err(ActionError::CardNotPlayable) } else { Ok(ValidatedUserAction(UserAction{username, action: Action::PlayCard{card}})) @@ -161,7 +162,7 @@ impl Game for KnockOutWhist { fn take_action(&mut self, ValidatedUserAction(UserAction{username, action}): ValidatedUserAction) -> Result<(), ActionError> { self.actions_len += 1; match (self.state, action) { - (_, Action::AddOn{..}) | (_, Action::RevealCard{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => { + (_, Action::AddOn{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => { Err(ActionError::InvalidActionForGameType) } (KnockOutWhistState::NotStarted, Action::Join{seat, ..}) => { @@ -170,7 +171,7 @@ impl Game for KnockOutWhist { (KnockOutWhistState::NotStarted, Action::Leave) => { self.seats.remove_player(username) } - (KnockOutWhistState::NotStarted, Action::NextToDeal) => { + (_, Action::NextToDeal) => { self.dealer = Some(username); self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect(); self.hands.clear(); @@ -195,7 +196,6 @@ impl Game for KnockOutWhist { (KnockOutWhistState::Dealing, Action::CommunityCard{card}) => { self.trump_card = Some(card); self.trumps = Some(card.suit); - self.state = KnockOutWhistState::Playing; Ok(()) } (KnockOutWhistState::Dealing, Action::EndDeal) => { @@ -234,14 +234,13 @@ impl Game for KnockOutWhist { (KnockOutWhistState::Playing, Action::WinTrick) => { *self.tricks_won.entry(username).or_default() += 1; self.led = None; - self.trumps = None; self.trick.clear(); if self.tricks_won.values().sum::() == self.cards_to_deal { if let Some(&most_tricks_won) = self.tricks_won.values().max() { self.winners = self.tricks_won.iter().filter(|&(_, &tricks)| tricks == most_tricks_won).map(|(&username, _)| username).collect(); self.cards_to_deal -= 1; if self.winners.len() == 1 { - self.call = self.winners.drain().next(); + self.call = self.winners.iter().next().cloned(); } else { self.receiver = self.dealer; while let Some(receiver) = self.receiver { @@ -274,9 +273,7 @@ impl Game for KnockOutWhist { } (KnockOutWhistState::CutForCall, Action::RevealCard{card}) => { self.deck.remove(&card); - if let Some(hand) = self.hands.get_mut(&username) { - hand.insert(card); - } + self.hands.entry(username).or_default().insert(card); if self.hands.values().map(HashSet::len).sum::() == self.winners.len() { if let Some((username, max)) = self.cards_by_player().max_by_key(|(_, card)| card.rank) { if self.cards_by_player().filter(|(_, card)| card.rank == max.rank).count() > 1 { @@ -290,9 +287,8 @@ impl Game for KnockOutWhist { } } while let Some(receiver) = self.receiver { - if !self.winners.contains(&receiver) { - self.receiver = self.seats.player_after(receiver); - } else { + self.receiver = self.seats.player_after(receiver); + if matches!(self.receiver, Some(receiver) if self.winners.contains(&receiver)) { break; } } @@ -319,15 +315,15 @@ impl Game for KnockOutWhist { let card = self.deck.iter().choose(&mut rng).cloned(); Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}})) } else if let Some(username) = self.dealer { - match self.call { - None => { + match (self.call, self.trump_card) { + (None, None) => { if let Some(&card) = self.deck.iter().choose(&mut rng) { Some(ValidatedUserAction(UserAction{username, action: Action::CommunityCard{card}})) } else { None } } - Some(_) => Some(ValidatedUserAction(UserAction{username, action: Action::EndDeal})), + (Some(_), _) | (None, Some(_)) => Some(ValidatedUserAction(UserAction{username, action: Action::EndDeal})), } } else { None @@ -360,8 +356,11 @@ impl Game for KnockOutWhist { } KnockOutWhistState::CutForCall => { if let Some(username) = self.receiver { - let card = self.deck.iter().choose(&mut rng).cloned(); - Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}})) + if let Some(card) = self.deck.iter().choose(&mut rng).cloned() { + Some(ValidatedUserAction(UserAction{username, action: Action::RevealCard{card}})) + } else { + None + } } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})) } else {