--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <text x="17.5" y="75" style="font: bold 75px serif;">♣</text>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <text x="17.5" y="75" style="font: bold 75px serif;" fill="red">♢</text>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <text x="17.5" y="75" style="font: bold 75px serif;" fill="red">♡</text>
+</svg>
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <text x="17.5" y="75" style="font: bold 75px serif;">♠</text>
+</svg>
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";
+}
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) {
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();
}
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":
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;
});
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 = {
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) {
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;
.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;
+}
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),
},
pub fn new(GameSummary{id, settings}: GameSummary) -> Box<Self> {
match settings {
GameSettings::Chatroom(settings) => Box::new(Chatroom::new(id, settings)),
- _ => todo!()
+ GameSettings::KnockOutWhist(settings) => Box::new(KnockOutWhist::new(id, settings)),
}
}
}
}
fn trick_winner(&self) -> Option<Username> {
+ 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);
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}}))
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, ..}) => {
(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();
(KnockOutWhistState::Dealing, Action::CommunityCard{card}) => {
self.trump_card = Some(card);
self.trumps = Some(card.suit);
- self.state = KnockOutWhistState::Playing;
Ok(())
}
(KnockOutWhistState::Dealing, Action::EndDeal) => {
(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::<u32>() == 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 {
}
(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::<usize>() == 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 {
}
}
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;
}
}
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
}
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 {