--- /dev/null
+export class Chatroom {
+ constructor(container, summary, actions, username, send) {
+ this.container = container;
+ this.summary = summary;
+ this.actions = actions;
+ this.username = username;
+ this.send = send;
+ this.seats = new Map();
+
+ this.chatroom = document.createElement("div");
+ this.chatroom.classList.add("chatroom");
+ this.chat = document.createElement("div");
+ this.chat.classList.add("chatroom-chat");
+ this.chatroom.append(this.chat);
+ this.chatroom_input = document.createElement("input");
+ this.chatroom_input.onchange = () => {
+ this.send({type: "TakeAction", action: {action: "Message", message: this.chatroom_input.value}});
+ this.chatroom_input.value = "";
+ };
+ this.chatroom.append(this.chatroom_input);
+ const close = document.createElement("button");
+ close.classList.add("chatroom-close");
+ close.innerText = "✗";
+ close.onclick = () => this.send({type: "TakeAction", action: {action: "Leave"}});
+ this.chatroom.append(close);
+ this.container.append(this.chatroom);
+
+ for (const user_action of this.actions) {
+ this.take_action(user_action, true);
+ }
+
+ if (!this.seats.has(this.username)) {
+ this.send({type: "TakeAction", action: {action: "Join", seat: 0, chips: 0}});
+ }
+ }
+
+ take_action(user_action, initialising) {
+ const is_at_end = this.chat.scrollTop + this.chat.clientHeight == this.chat.scrollHeight;
+ switch (user_action.action.action) {
+ case "Join":
+ this.seats.set(user_action.username, user_action.action.seat);
+ this.chat.append(this.join_element(user_action.username));
+ break;
+ case "Message":
+ this.chat.append(this.message_element(user_action.username, user_action.action.message));
+ break;
+ case "Leave":
+ this.seats.delete(user_action.username);
+ this.chat.append(this.leave_element(user_action.username));
+ if (!initialising && user_action.username === this.username) {
+ this.send({type: "JoinLobby", filter: ""});
+ }
+ break;
+ default:
+ console.error("Unknown action for chatroom", user_action);
+ break;
+ }
+ if (is_at_end) {
+ this.chat.scrollTo({
+ top: this.chat.scrollHeight - this.chat.clientHeight,
+ behavior: "smooth"
+ });
+ }
+ }
+
+ join_element(username) {
+ const join_element = document.createElement("div");
+ join_element.classList.add("chatroom-join");
+ const username_element = document.createElement("div");
+ username_element.classList.add("chatroom-username");
+ username_element.innerText = username;
+ join_element.append(username_element);
+ const text_element = document.createElement("div");
+ text_element.classList.add("chatroom-text");
+ text_element.innerText = "Joined";
+ join_element.append(text_element);
+ return join_element;
+ }
+
+ message_element(username, message) {
+ const message_element = document.createElement("div");
+ message_element.classList.add("chatroom-message");
+ const username_element = document.createElement("div");
+ username_element.classList.add("chatroom-username");
+ username_element.innerText = username;
+ message_element.append(username_element);
+ const text_element = document.createElement("div");
+ text_element.classList.add("chatroom-text");
+ text_element.innerText = message;
+ message_element.append(text_element);
+ return message_element;
+ }
+
+ leave_element(username) {
+ const leave_element = document.createElement("div");
+ leave_element.classList.add("chatroom-leave");
+ const username_element = document.createElement("div");
+ username_element.classList.add("chatroom-username");
+ username_element.innerText = username;
+ leave_element.append(username_element);
+ const text_element = document.createElement("div");
+ text_element.classList.add("chatroom-text");
+ text_element.innerText = "Left";
+ leave_element.append(text_element);
+ return leave_element;
+ }
+}
--- /dev/null
+export class GameList {
+ constructor(container, games, send) {
+ this.container = container;
+ this.games = games;
+ this.send = send;
+
+ const game_list_outside = document.createElement("div");
+ game_list_outside.classList.add("game-list-outside");
+ this.game_list_container = document.createElement("div");
+ this.game_list_container.classList.add("game-list-container");
+ this.game_list = document.createElement("div");
+ this.game_list.classList.add("game-list");
+ this.game_list_container.append(this.game_list);
+ game_list_outside.append(this.game_list_container);
+ this.game_list_filter_input = document.createElement("input");
+ game_list_outside.append(this.game_list_filter_input);
+ const game_list_options = document.createElement("button");
+ const game_list_option_menu = document.createElement("div");
+ game_list_option_menu.classList.add("game-list-option-menu");
+ game_list_option_menu.classList.add("hidden");
+ const game_list_option_logout = document.createElement("div");
+ game_list_option_logout.innerText = "Logout";
+ game_list_option_logout.onclick = () => this.send({type: "Logout"});
+ game_list_option_menu.append(game_list_option_logout);
+ game_list_options.append(game_list_option_menu);
+ game_list_options.onclick = () => game_list_option_menu.classList.toggle("hidden");
+ game_list_outside.append(game_list_options);
+ this.container.append(game_list_outside);
+
+ for (const game of this.games) {
+ const game_element = this.game_element(game);
+ this.game_list.append(game_element);
+ }
+ this.game_list_container.scrollTo({
+ top: this.game_list_container.scrollHeight - this.game_list_container.clientHeight,
+ behavior: "auto"
+ });
+ }
+
+ new_game(game) {
+ const is_at_end = this.game_list_container.scrollTop + this.game_list_container.clientHeight == this.game_list_container.scrollHeight;
+ this.games.push(game);
+ const game_element = this.game_element(game);
+ this.game_list.append(game_element);
+ if (is_at_end) {
+ this.game_list_container.scrollTo({
+ top: this.game_list_container.scrollHeight - this.game_list_container.clientHeight,
+ behavior: "smooth"
+ });
+ }
+ }
+
+ game_element(game) {
+ console.log("getting game element for ", game);
+
+ const game_element = document.createElement("div");
+ game_element.classList.add("game-summary");
+
+ const id = document.createElement("div");
+ id.innerText = "#" + game.id;
+ id.classList.add("game-id");
+ game_element.append(id);
+
+ const title = document.createElement("div");
+ title.innerText = game.settings.title;
+ title.classList.add("game-title");
+ game_element.append(title);
+
+ const format = document.createElement("div");
+ format.innerText = game.settings.format;
+ format.classList.add("game-format");
+ game_element.append(format);
+
+ const settings = document.createElement("ul");
+ settings.classList.add("game-settings");
+ for (const setting of Object.keys(game.settings)) {
+ if (setting !== "id" && setting !== "title" && setting !== "format") {
+ const li = document.createElement("li");
+ li.innerText = setting + ": " + game.settings[setting];
+ settings.append(li);
+ }
+ }
+ game_element.append(settings);
+
+ game_element.onclick = () => this.send({type: "JoinGame", id: game.id});
+
+ return game_element;
+ }
+}
const svgns = "http://www.w3.org/2000/svg";
-import { card_href, suit_href } from "./card.js";
+import { GameList } from "./gamelist.js";
+import { Chatroom } from "./chatroom.js";
+import { KnockOutWhist } from "./whist.js";
export class Socket {
constructor(container, login_all_sockets) {
this.state = "LoggedIn";
break;
case "JoinLobbySuccess":
- this.games = message.games;
- this.create_game_list();
+ this.game = new GameList(this.container, message.games, message => this.send(message))
this.state = "InLobby";
break;
case "NewGame":
- this.new_game(message.game);
+ this.game.new_game(message.game);
break;
case "JoinGameFailure":
console.error("Joining game failed: " + message.reason);
break;
case "JoinGameSuccess":
this.game = {summary: message.summary, actions: message.actions};
- this.create_game_display();
+ this.create_game_display(message.summary, message.actions);
this.state = "InGame";
break;
- case "TakeActionSuccess":
- this.add_action({username: this.auth.username, action: this.action});
- delete this.action;
- break;
case "TakeActionFailure":
- if (message.reason === "SeatNotAvailable") {
- this.take_action({action: "Join", seat: this.action.seat + 1, chips: this.action.chips});
+ if (message.reason === "Seat not available") {
+ this.send({type: "TakeAction", action: {action: "Join", seat: message.action.action.seat + 1, chips: message.action.action.chips}});
} else {
- console.error("Taking action failed: " + message.reason, this.action);
- delete this.action;
+ console.error("Taking action failed: " + message.reason, message.action);
}
break;
+ case "TakeActionSuccess":
case "NewAction":
- this.add_action(message.action);
+ this.game.take_action(message.action);
break;
case "LogoutSuccess":
delete this.auth;
document.getElementById("password-input").onchange = this.login_all_sockets;
}
- game_element(game) {
- console.log("getting game element for ", game);
-
- const game_element = document.createElement("div");
- game_element.classList.add("game-summary");
-
- const id = document.createElement("div");
- id.innerText = "#" + game.id;
- id.classList.add("game-id");
- game_element.append(id);
-
- const title = document.createElement("div");
- title.innerText = game.settings.title;
- title.classList.add("game-title");
- game_element.append(title);
-
- const format = document.createElement("div");
- format.innerText = game.settings.format;
- format.classList.add("game-format");
- game_element.append(format);
-
- const settings = document.createElement("ul");
- settings.classList.add("game-settings");
- for (const setting of Object.keys(game.settings)) {
- if (setting !== "id" && setting !== "title" && setting !== "format") {
- const li = document.createElement("li");
- li.innerText = setting + ": " + game.settings[setting];
- settings.append(li);
- }
- }
- game_element.append(settings);
-
- game_element.onclick = () => this.join_game(game);
-
- return game_element;
- }
-
- new_game(game) {
- const is_at_end = this.game_list_container.scrollTop + this.game_list_container.clientHeight == this.game_list_container.scrollHeight;
- this.games.push(game);
- const game_element = this.game_element(game);
- this.game_list.append(game_element);
- if (is_at_end) {
- this.game_list_container.scrollTo({
- top: this.game_list_container.scrollHeight - this.game_list_container.clientHeight,
- behavior: "smooth"
- });
- }
- }
-
- join_game(game) {
- this.send({type: "JoinGame", id: game.id});
- this.state = "JoinGameSent";
- }
-
- create_game_list() {
- this.container.textContent = "";
- const game_list_outside = document.createElement("div");
- game_list_outside.classList.add("game-list-outside");
- this.game_list_container = document.createElement("div");
- this.game_list_container.classList.add("game-list-container");
- this.game_list = document.createElement("div");
- this.game_list.classList.add("game-list");
- this.game_list_container.append(this.game_list);
- game_list_outside.append(this.game_list_container);
- this.game_list_filter_input = document.createElement("input");
- game_list_outside.append(this.game_list_filter_input);
- const game_list_options = document.createElement("button");
- const game_list_option_menu = document.createElement("div");
- game_list_option_menu.classList.add("game-list-option-menu");
- game_list_option_menu.classList.add("hidden");
- const game_list_option_logout = document.createElement("div");
- game_list_option_logout.innerText = "Logout";
- game_list_option_logout.onclick = () => this.logout();
- game_list_option_menu.append(game_list_option_logout);
- game_list_options.append(game_list_option_menu);
- game_list_options.onclick = () => game_list_option_menu.classList.toggle("hidden");
- game_list_outside.append(game_list_options);
- this.container.append(game_list_outside);
- this.redraw_games();
- }
-
- redraw_games() {
- this.game_list.textContent = "";
- for (const game of this.games) {
- const game_element = this.game_element(game);
- this.game_list.append(game_element);
- }
- this.game_list_container.scrollTo({
- top: this.game_list_container.scrollHeight - this.game_list_container.clientHeight,
- behavior: "auto"
- });
- }
-
- take_action(action) {
- this.action = action;
- this.send({type: "TakeAction", action: action});
- }
-
- create_chatroom() {
- this.container.textContent = "";
- this.chatroom = document.createElement("div");
- this.chatroom.classList.add("chatroom");
- this.chatroom_chat = document.createElement("div");
- this.chatroom_chat.classList.add("chatroom-chat");
- this.chatroom.append(this.chatroom_chat);
- this.chatroom_input = document.createElement("input");
- this.chatroom_input.onchange = () => {
- this.take_action({action: "Message", message: this.chatroom_input.value});
- this.chatroom_input.value = "";
- };
- this.chatroom.append(this.chatroom_input);
- const chatroom_close = document.createElement("button");
- chatroom_close.classList.add("chatroom-close");
- chatroom_close.innerText = "✗";
- chatroom_close.onclick = () => this.take_action({action: "Leave"});
- this.chatroom.append(chatroom_close);
- this.container.append(this.chatroom);
- this.redraw_chatroom();
- }
-
- create_knock_out_whist() {
+ create_game_display(summary, actions) {
this.container.textContent = "";
-
- this.svg = document.createElementNS(svgns, "svg");
- this.svg.classList.add("knock-out-whist");
- this.svg.setAttribute("viewBox", "0 0 500 500");
-
- const background = document.createElementNS(svgns, "rect");
- background.setAttribute("width", "500");
- background.setAttribute("height", "500");
- background.setAttribute("fill", "#404040");
- this.svg.append(background);
-
- const table = document.createElementNS(svgns, "ellipse");
- table.setAttribute("cx", "250");
- table.setAttribute("cy", "253");
- table.setAttribute("rx", "250");
- table.setAttribute("ry", "110");
- table.setAttribute("fill", "#604010");
- this.svg.append(table);
-
- const felt = document.createElementNS(svgns, "ellipse");
- felt.setAttribute("cx", "250");
- felt.setAttribute("cy", "250");
- felt.setAttribute("rx", "240");
- felt.setAttribute("ry", "100");
- 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();
- }
-
- join_element(username) {
- const join_element = document.createElement("div");
- join_element.classList.add("chatroom-join");
- const username_element = document.createElement("div");
- username_element.classList.add("chatroom-username");
- username_element.innerText = username;
- join_element.append(username_element);
- const text_element = document.createElement("div");
- text_element.classList.add("chatroom-text");
- text_element.innerText = "Joined";
- join_element.append(text_element);
- return join_element;
- }
-
- message_element(username, message) {
- const message_element = document.createElement("div");
- message_element.classList.add("chatroom-message");
- const username_element = document.createElement("div");
- username_element.classList.add("chatroom-username");
- username_element.innerText = username;
- message_element.append(username_element);
- const text_element = document.createElement("div");
- text_element.classList.add("chatroom-text");
- text_element.innerText = message;
- message_element.append(text_element);
- return message_element;
- }
-
- leave_element(username) {
- const leave_element = document.createElement("div");
- leave_element.classList.add("chatroom-leave");
- const username_element = document.createElement("div");
- username_element.classList.add("chatroom-username");
- username_element.innerText = username;
- leave_element.append(username_element);
- const text_element = document.createElement("div");
- text_element.classList.add("chatroom-text");
- text_element.innerText = "Left";
- leave_element.append(text_element);
- return leave_element;
- }
-
- set_card_positions() {
- const seats = Math.max(...this.game.seats.values()) + 1;
- const my_seat = this.game.seats.get(this.auth.username);
- for (const [username, cards] of this.game.hands) {
- const seat = this.game.seats.get(username);
- const angle = ((seat - my_seat) % seats) * 2 * Math.PI / seats;
- const offset = cards.length * 10;
- let x = 227.5 + offset + 240 * Math.sin(angle);
- const y = 210 + 120 * Math.cos(angle);
- for (const {card, image} of cards) {
- image.classList.toggle("my-card", username === this.auth.username);
- image.setAttribute("x", x);
- image.setAttribute("y", y);
- x -= 20;
- }
- }
- for (const {card, image} of this.game.trick) {
- const x = 227.5;
- const y = 210;
- image.classList.remove("my-card");
- image.setAttribute("x", x);
- image.setAttribute("y", y);
- }
- for (const {card, image} of this.game.community) {
- const x = 120.0;
- const y = 210;
- image.setAttribute("x", x);
- image.setAttribute("y", y);
- }
- }
-
- card_image(username, card) {
- const image = document.createElementNS(svgns, "image");
- image.setAttribute("width", "45");
- image.setAttribute("height", "70");
- image.setAttribute("href", card_href(card));
- if (username === this.auth.username) {
- image.onclick = () => this.take_action({action: "PlayCard", card: card});
- }
- this.svg.append(image);
- 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":
- const is_at_end = this.chatroom_chat.scrollTop + this.chatroom_chat.clientHeight == this.chatroom_chat.scrollHeight;
- switch (user_action.action.action) {
- case "Join":
- this.game.seats.set(user_action.username, user_action.action.seat);
- this.chatroom_chat.append(this.join_element(user_action.username));
- break;
- case "Message":
- this.chatroom_chat.append(this.message_element(user_action.username, user_action.action.message));
- break;
- case "Leave":
- this.game.seats.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: ""});
- }
- break;
- default:
- console.error("Unknown action for chatroom", user_action);
- break;
- }
- if (is_at_end) {
- this.chatroom_chat.scrollTo({
- top: this.chatroom_chat.scrollHeight - this.chatroom_chat.clientHeight,
- behavior: "smooth"
- });
- }
+ this.game = new Chatroom(this.container, summary, actions, this.auth.username, message => this.send(message));
break;
case "KnockOutWhist":
- switch (user_action.action.action) {
- case "Join":
- this.game.seats.set(user_action.username, user_action.action.seat);
- this.game.hands.set(user_action.username, []);
- break;
- case "Leave":
- case "KnockedOut":
- this.game.seats.delete(user_action.username);
- this.game.hands.delete(user_action.username);
- 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;
- case "PlayCard":
- let removed = false;
- const cards = this.game.hands.get(user_action.username).filter((card) => {
- if (card.card === null) {
- if (!removed) {
- this.svg.removeChild(card.image);
- this.game.trick.push({card: user_action.action.card, image: this.card_image(user_action.username, user_action.action.card)});
- removed = true;
- return false;
- } else {
- return true;
- }
- } else if (card.card.suit === user_action.action.card.suit && card.card.rank === user_action.action.card.rank) {
- this.game.trick.push(card);
- return false;
- } else {
- return true;
- }
- });
- this.game.hands.set(user_action.username, cards);
- this.set_card_positions();
- break;
- case "ChooseTrumps":
- this.set_trumps(user_action.action.suit);
- break;
- case "CommunityCard":
- const community_card = {
- card: user_action.action.card,
- image: this.card_image(null, user_action.action.card),
- };
- this.game.community.push(community_card);
- this.set_trumps(user_action.action.card.suit);
- this.set_card_positions();
- break;
- case "WinTrick":
- for (const card of this.game.trick) {
- this.svg.removeChild(card.image);
- }
- this.game.trick = [];
- break;
- 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;
- }
- break;
- }
- }
-
- redraw_chatroom() {
- this.chatroom_chat.textContent = "";
- 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.actions) {
- this.add_action(user_action, true);
- }
- }
-
- create_game_display() {
- this.container.textContent = "";
- switch (this.game.summary.settings.format) {
- case "Chatroom":
- this.game.seats = new Map();
- this.create_chatroom();
- if (!this.game.seats.has(this.auth.username)) {
- this.take_action({action: "Join", seat: 0, chips: 0});
- }
+ this.game = new KnockOutWhist(this.container, summary, actions, this.auth.username, message => this.send(message));
break;
- case "KnockOutWhist":
- this.game.seats = new Map();
- this.game.hands = new Map();
- this.game.community = [];
- this.game.trick = [];
- this.create_knock_out_whist();
- if (!this.game.seats.has(this.auth.username)) {
- this.take_action({action: "Join", seat: 0, chips: 0});
- }
+ default:
+ console.error("Unknown format: ", this.game.summary.settings.format);
break;
}
}
--- /dev/null
+const svgns = "http://www.w3.org/2000/svg";
+
+import { card_href, suit_href } from "./card.js";
+
+export class KnockOutWhist {
+ constructor(container, summary, actions, username, send) {
+ this.container = container;
+ this.summary = summary;
+ this.actions = actions;
+ this.username = username;
+ this.send = send;
+ this.seats = new Map();
+ this.hands = new Map();
+ this.community = [];
+ this.trick = [];
+ this.user_icons = new Map();
+
+ this.svg = document.createElementNS(svgns, "svg");
+ this.svg.classList.add("knock-out-whist");
+ this.svg.setAttribute("viewBox", "0 0 500 500");
+
+ const background = document.createElementNS(svgns, "rect");
+ background.setAttribute("width", "500");
+ background.setAttribute("height", "500");
+ background.setAttribute("fill", "#404040");
+ this.svg.append(background);
+
+ const table = document.createElementNS(svgns, "ellipse");
+ table.setAttribute("cx", "250");
+ table.setAttribute("cy", "253");
+ table.setAttribute("rx", "250");
+ table.setAttribute("ry", "110");
+ table.setAttribute("fill", "#604010");
+ this.svg.append(table);
+
+ const felt = document.createElementNS(svgns, "ellipse");
+ felt.setAttribute("cx", "250");
+ felt.setAttribute("cy", "250");
+ felt.setAttribute("rx", "240");
+ felt.setAttribute("ry", "100");
+ felt.setAttribute("fill", "green");
+ this.svg.append(felt);
+
+ const suits = document.createElementNS(svgns, "rect");
+ suits.setAttribute("x", "340");
+ suits.setAttribute("y", "235");
+ suits.setAttribute("width", "100");
+ suits.setAttribute("height", "30");
+ suits.setAttribute("rx", "10");
+ suits.setAttribute("fill", "darkgreen");
+ this.svg.append(suits);
+
+ this.glyphs = new Map();
+ let x = 340;
+ for (const suit of ["Clubs", "Diamonds", "Hearts", "Spades"]) {
+ const highlight = document.createElementNS(svgns, "circle");
+ highlight.setAttribute("cx", x + 12.5);
+ highlight.setAttribute("cy", "250");
+ highlight.setAttribute("r", "10");
+ highlight.classList.add("suit-highlight");
+ highlight.onclick = () => this.send({type: "TakeAction", 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", "235");
+ 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);
+ for (const user_action of this.actions) {
+ this.take_action(user_action);
+ }
+
+ if (!this.seats.has(this.username)) {
+ this.send({type: "TakeAction", action: {action: "Join", seat: 0, chips: 0}});
+ }
+ }
+
+ redraw_players() {
+ const active_player = this.call || this.active;
+ for (const [username, [icon, active]] of this.user_icons) {
+ if (!this.seats.has(username)) {
+ this.svg.removeChild(icon);
+ this.svg.removeChild(active);
+ this.user_icons.delete(username);
+ } else {
+ const seat = this.seats.get(username);
+ const angle = ((seat - this.my_seat) % this.num_seats) * 2 * Math.PI / this.num_seats;
+ const x = 240 - 180 * Math.sin(angle);
+ const y = 250 + 120 * Math.cos(angle) + 50 * Math.sign(Math.cos(angle));
+ icon.setAttribute("x", x);
+ icon.setAttribute("y", y);
+ active.classList.toggle("active", active_player === username);
+ active.setAttribute("cx", x - 10);
+ active.setAttribute("cy", y - 5);
+ }
+ }
+ for (const [username, seat] of this.seats) {
+ if (!this.user_icons.has(username)) {
+ const icon = document.createElementNS(svgns, "text");
+ const text = document.createTextNode(username);
+ icon.append(text);
+ const angle = ((seat - this.my_seat) % this.num_seats) * 2 * Math.PI / this.num_seats;
+ const x = 240 - 180 * Math.sin(angle);
+ const y = 250 + 120 * Math.cos(angle) + 50 * (Math.sign(Math.cos(angle)) || 1);
+ icon.setAttribute("x", x);
+ icon.setAttribute("y", y);
+ this.svg.append(icon);
+ const active = document.createElementNS(svgns, "circle");
+ active.setAttribute("cx", x - 10);
+ active.setAttribute("cy", y - 5);
+ active.setAttribute("r", 5);
+ active.classList.add("active-indicator");
+ active.classList.toggle("active", active_player === username);
+ this.svg.append(active);
+ this.user_icons.set(username, [icon, active]);
+ }
+ }
+ }
+
+ redraw_cards() {
+ for (const [username, cards] of this.hands) {
+ const seat = this.seats.get(username);
+ const angle = ((seat - this.my_seat) % this.num_seats) * 2 * Math.PI / this.num_seats;
+ const offset = cards.length * 10;
+ let x = 227.5 + offset - 180 * Math.sin(angle);
+ const y = 210 + 120 * Math.cos(angle);
+ for (const {card, image} of cards) {
+ image.classList.toggle("my-card", username === this.username);
+ image.setAttribute("x", x);
+ image.setAttribute("y", y);
+ x -= 20;
+ }
+ }
+ for (const {card, image} of this.trick) {
+ const x = 227.5;
+ const y = 210;
+ image.classList.remove("my-card");
+ image.setAttribute("x", x);
+ image.setAttribute("y", y);
+ }
+ for (const {card, image} of this.community) {
+ const x = 120.0;
+ const y = 210;
+ image.setAttribute("x", x);
+ image.setAttribute("y", y);
+ }
+ }
+
+ redraw_trumps() {
+ for (const [suit, glyph] of this.glyphs) {
+ glyph.classList.toggle("trumps", suit === this.trumps);
+ }
+ }
+
+ card_image(username, card) {
+ const image = document.createElementNS(svgns, "image");
+ image.setAttribute("width", "45");
+ image.setAttribute("height", "70");
+ image.setAttribute("href", card_href(card));
+ if (username === this.username) {
+ image.onclick = () => this.send({type: "TakeAction", action: {action: "PlayCard", card: card}});
+ }
+ this.svg.append(image);
+ return image;
+ }
+
+ player_after(username) {
+ const in_seat = this.seats.get(username);
+ let [player_after, player_after_seat] = [username, null];
+ for (const [username, seat] of this.seats) {
+ if (seat > in_seat && (player_after_seat === null || seat < player_after_seat)) {
+ player_after = username;
+ player_after_seat = seat;
+ }
+ }
+ if (player_after_seat === null) for (const [username, seat] of this.seats) {
+ if (player_after_seat === null || seat < player_after_seat) {
+ player_after = username;
+ player_after_seat = seat;
+ }
+ }
+ return player_after;
+ }
+
+ take_action(user_action) {
+ switch (user_action.action.action) {
+ case "Join":
+ this.seats.set(user_action.username, user_action.action.seat);
+ this.hands.set(user_action.username, []);
+ this.num_seats = Math.max(...this.seats.values()) + 1;
+ this.my_seat = this.seats.get(this.username) || 0;
+ this.redraw_players();
+ break;
+ case "Leave":
+ this.seats.delete(user_action.username);
+ this.hands.delete(user_action.username);
+ this.num_seats = Math.max(...this.seats.values()) + 1;
+ this.my_seat = this.seats.get(this.username) || 0;
+ this.redraw_players();
+ break;
+ case "KnockedOut":
+ this.seats.delete(user_action.username);
+ this.hands.delete(user_action.username);
+ this.redraw_players();
+ break;
+ case "NextToDeal":
+ this.dealer = user_action.username;
+ for (const card of this.community) {
+ this.svg.removeChild(card.image);
+ }
+ this.community = [];
+ for (const [username, hand] of this.hands) {
+ for (const card of hand) {
+ this.svg.removeChild(card.image);
+ }
+ }
+ this.hands.clear();
+ this.trumps = null;
+ this.active = this.player_after(user_action.username);
+ this.redraw_trumps();
+ this.redraw_players();
+ 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.hands.has(user_action.username)) {
+ this.hands.set(user_action.username, []);
+ }
+ this.hands.get(user_action.username).push(card);
+ this.redraw_cards();
+ break;
+ case "PlayCard":
+ let removed = false;
+ const cards = this.hands.get(user_action.username).filter((card) => {
+ if (card.card === null) {
+ if (!removed) {
+ this.svg.removeChild(card.image);
+ this.trick.push({card: user_action.action.card, image: this.card_image(user_action.username, user_action.action.card)});
+ removed = true;
+ return false;
+ } else {
+ return true;
+ }
+ } else if (card.card.suit === user_action.action.card.suit && card.card.rank === user_action.action.card.rank) {
+ this.trick.push(card);
+ return false;
+ } else {
+ return true;
+ }
+ });
+ this.hands.set(user_action.username, cards);
+ this.active = this.player_after(user_action.username);
+ console.log("active: ", this.active);
+ this.redraw_cards();
+ this.redraw_players();
+ break;
+ case "ChooseTrumps":
+ this.trumps = user_action.action.suit;
+ this.call = null;
+ this.redraw_trumps();
+ this.redraw_players();
+ break;
+ case "CommunityCard":
+ const community_card = {
+ card: user_action.action.card,
+ image: this.card_image(null, user_action.action.card),
+ };
+ this.community.push(community_card);
+ this.trumps = user_action.action.card.suit;
+ this.redraw_trumps();
+ this.redraw_cards();
+ break;
+ case "WinTrick":
+ for (const card of this.trick) {
+ this.svg.removeChild(card.image);
+ }
+ this.trick = [];
+ this.active = user_action.username;
+ this.redraw_players();
+ break;
+ case "WinCall":
+ this.call = user_action.username;
+ this.redraw_players();
+ break;
+ case "EndDeal":
+ this.active = this.player_after(user_action.username);
+ this.redraw_players();
+ break;
+ case "WinGame":
+ console.log(this);
+ this.trumps = null;
+ this.redraw_trumps();
+ break;
+ default:
+ console.error("Unhandled action for knock-out whist", user_action);
+ break;
+ }
+ }
+}
.suit-highlight.trumps:hover {
fill: #4040ff;
}
+
+.active-indicator {
+ fill: #000060;
+}
+
+.active-indicator.active {
+ fill: #2020ff;
+}
JoinGameSuccess { summary: GameSummary, actions: Vec<UserAction> },
JoinGameFailure { reason: String },
NewAction { action: UserAction },
- TakeActionSuccess,
- TakeActionFailure { reason: String },
+ TakeActionSuccess { action: UserAction },
+ TakeActionFailure { action: UserAction, reason: String },
NewMessage { username: Username, message: String },
LeaveGameSuccess,
LeaveGameFailure { reason: String },
loop {
let len = game.actions_len();
match game.validate_action(action.clone()) {
- Ok(action) => match self.server.take_action(id, len, &action).await {
- Ok(ActionStatus::Committed) => match game.take_action(action) {
- Ok(()) => return ServerMessage::TakeActionSuccess,
- Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
+ Ok(validated) => match self.server.take_action(id, len, &validated).await {
+ Ok(ActionStatus::Committed) => match game.take_action(validated.clone()) {
+ Ok(()) => return ServerMessage::TakeActionSuccess{action},
+ Err(err) => return ServerMessage::TakeActionFailure{action, reason: err.to_string()},
}
Ok(ActionStatus::Interrupted) => {
debug!("Action {:?} was interrupted - updating game state", action);
match self.server.game_state(id, len).await {
- Ok(actions) => for action in actions {
- if let Err(err) = game.take_action(action) {
- return ServerMessage::TakeActionFailure{reason: err.to_string()};
+ Ok(actions) => for new_action in actions {
+ if let Err(err) = game.take_action(new_action) {
+ return ServerMessage::TakeActionFailure{action, reason: err.to_string()};
}
}
Err(err) => {
- return ServerMessage::TakeActionFailure{reason: err.to_string()};
+ return ServerMessage::TakeActionFailure{action, reason: err.to_string()};
}
}
}
- Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
+ Err(err) => return ServerMessage::TakeActionFailure{action, reason: err.to_string()},
}
- Err(err) => return ServerMessage::TakeActionFailure{reason: err.to_string()},
+ Err(err) => return ServerMessage::TakeActionFailure{action, reason: err.to_string()},
}
}
}
Fold,
Bet { chips: u64 },
WinTrick,
+ WinCall,
WinHand { chips: u64 },
WinGame,
Message { message: String },
SeatNotAvailable,
NoSeatAvailable,
OutOfTurn,
+ Dealing,
+ MustChooseTrumps,
CardNotPlayable,
+ CardNotAvailable,
InvalidActionForGameType,
GameHasEnded,
}
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"),
- ActionError::GameHasEnded => f.write_str("GameHasEnded"),
+ ActionError::NotAuthorised => f.write_str("Not authorised"),
+ ActionError::AlreadyJoined => f.write_str("User has already joined"),
+ ActionError::GameHasStarted => f.write_str("Game has already started"),
+ ActionError::SeatNotAvailable => f.write_str("Seat not available"),
+ ActionError::NoSeatAvailable => f.write_str("No seat available"),
+ ActionError::OutOfTurn => f.write_str("Out of turn"),
+ ActionError::Dealing => f.write_str("Please wait for deal to complete"),
+ ActionError::MustChooseTrumps => f.write_str("Trumps must be chosen first"),
+ ActionError::CardNotPlayable => f.write_str("Card is not legally playable"),
+ ActionError::CardNotAvailable => f.write_str("Card does not exist in player's hand"),
+ ActionError::InvalidActionForGameType => f.write_str("Invalid action for game type"),
+ ActionError::GameHasEnded => f.write_str("Game has ended"),
}
}
}
ChoosingTrumps,
Playing,
CutForCall,
+ RoundCompleted,
Completed,
}
Ok(ValidatedUserAction(UserAction{username, action: Action::Join{seat, chips: 0}}))
}
}
+ (KnockOutWhistState::Completed, Action::Join{..}) => Err(ActionError::GameHasEnded),
+ (_, Action::Join{..}) => Err(ActionError::GameHasStarted),
(_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised),
(KnockOutWhistState::NotStarted, Action::Leave) => {
Ok(ValidatedUserAction(UserAction{username, action: Action::Leave}))
}
(_, Action::Leave) => Err(ActionError::GameHasStarted),
- (KnockOutWhistState::Dealing, _) => Err(ActionError::OutOfTurn),
+ (KnockOutWhistState::Dealing, _) => Err(ActionError::Dealing),
(KnockOutWhistState::ChoosingTrumps, Action::ChooseTrumps{suit}) => {
if Some(username) == self.call {
Ok(ValidatedUserAction(UserAction{username, action: Action::ChooseTrumps{suit}}))
Err(ActionError::OutOfTurn)
}
}
- (KnockOutWhistState::ChoosingTrumps, _) => Err(ActionError::OutOfTurn),
+ (KnockOutWhistState::ChoosingTrumps, _) => Err(ActionError::MustChooseTrumps),
(KnockOutWhistState::Playing, Action::PlayCard{card}) => {
if Some(username) != self.active {
Err(ActionError::OutOfTurn)
} else if !self.hand_contains_card(username, card) {
- Err(ActionError::CardNotPlayable)
+ Err(ActionError::CardNotAvailable)
} 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}}))
}
}
- (KnockOutWhistState::CutForCall, _) => Err(ActionError::OutOfTurn),
+ (KnockOutWhistState::CutForCall, _) => Err(ActionError::Dealing),
+ (KnockOutWhistState::RoundCompleted, _) => Err(ActionError::Dealing),
(KnockOutWhistState::Completed, _) => Err(ActionError::GameHasEnded),
(_, _) => Err(ActionError::InvalidActionForGameType),
}
if self.winners.len() == 1 {
self.call = self.winners.iter().next().cloned();
} else {
- self.receiver = self.dealer;
- while let Some(receiver) = self.receiver {
- if !self.winners.contains(&receiver) {
- self.receiver = self.seats.player_after(receiver);
- } else {
- break;
- }
- }
+ self.receiver = self.dealer.and_then(|dealer| self.seats.player_after_in(dealer, &self.winners));
self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect();
self.hands.clear();
self.trump_card = None;
}
Ok(())
}
+ (KnockOutWhistState::CutForCall, Action::WinCall) | (KnockOutWhistState::Playing, Action::WinCall) => {
+ self.call = Some(username);
+ self.state = KnockOutWhistState::RoundCompleted;
+ Ok(())
+ }
(KnockOutWhistState::Completed, _) => Err(ActionError::GameHasEnded),
(_, _) => Err(ActionError::InvalidActionForGameType),
}
return Some(ValidatedUserAction(UserAction{username, action: Action::WinGame}));
}
}
- if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
- return Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal}));
+ if let Some(username) = self.call {
+ return Some(ValidatedUserAction(UserAction{username, action: Action::WinCall}));
}
None
} else if let Some(username) = self.trick_winner() {
} else {
None
}
- } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
+ } else if let Some(username) = self.call {
+ Some(ValidatedUserAction(UserAction{username, action: Action::WinCall}))
+ } else {
+ None
+ }
+ }
+ KnockOutWhistState::RoundCompleted => {
+ if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal}))
} else {
None
Action::SendInterest{interest} => {
debug!("handle_client_interest: Action::SendInterest {{ interest: {:?} }}", interest);
if let Ok(interest) = TryFrom::try_from(interest) {
- for Client{sender, interests} in &mut clients {
+ for (index, Client{sender, interests}) in clients.iter_mut().enumerate() {
if interests.contains(&interest) {
- debug!("handle_client_interest: Sending {:?} to {:?}", interest, sender);
+ debug!("handle_client_interest: Sending {:?} to clients[{}]", interest, index);
if let Err(err) = sender.interest.send(interest.clone()).await {
error!("handle_client_interest: Send failed: {}", err);
}
match server_state.game_list(games_len).await {
Ok(games) => {
for game in games {
+ info!("Starting new game {:?}", game);
let id = game.id();
game_list.push(game);
let (server_state, update_stream) = server.new_state().await;
if let Ok(dealer) = Dealer::new(server_state, id).await {
+ info!("Spawning new dealer for game {}", id);
spawn(dealer.start(update_stream));
}
}
}
}
+ pub fn player_after_in(&self, username: Username, set: &HashSet<Username>) -> Option<Username> {
+ for (&seat, &player) in &self.players {
+ if player == username {
+ for (_, &name) in self.players.range(seat+1..) {
+ if set.contains(&name) {
+ return Some(name);
+ }
+ }
+ for (_, &name) in self.players.range(..seat) {
+ if set.contains(&name) {
+ return Some(name);
+ }
+ }
+ return None;
+ }
+ }
+ None
+ }
+
pub fn player_after(&self, username: Username) -> Option<Username> {
for (&seat, &player) in &self.players {
if player == username {
pub async fn game_list(&mut self, from: usize) -> RedisResult<Vec<GameSummary>> {
debug!("game_list(from: {})", from);
let games: Vec<AsJson<GameSettings>> = self.redis.lrange("games", from as isize, -1).await?;
- Ok(games.into_iter().map(AsJson::get).enumerate().map(|(id, settings)| GameSummary::new(id as u32, settings)).collect())
+ Ok(games.into_iter().map(AsJson::get).enumerate().map(|(id, settings)| GameSummary::new((from + id) as u32, settings)).collect())
}
pub async fn game_state(&mut self, id: u32, from: usize) -> RedisResult<Vec<ValidatedUserAction>> {