share more code between cribbage, whist and poker
authorGeoffrey Allott <geoffrey@allott.email>
Wed, 14 Jun 2023 10:28:31 +0000 (11:28 +0100)
committerGeoffrey Allott <geoffrey@allott.email>
Wed, 14 Jun 2023 10:28:31 +0000 (11:28 +0100)
site/modules/betcontrols.js [new file with mode: 0644]
site/modules/cribbage.js
site/modules/game.js
site/modules/gamechat.js
site/modules/poker.js
site/modules/whist.js

diff --git a/site/modules/betcontrols.js b/site/modules/betcontrols.js
new file mode 100644 (file)
index 0000000..9231772
--- /dev/null
@@ -0,0 +1,89 @@
+import { create_svg_element } from "./svg.js";
+
+export class BetControls {
+    constructor(game) {
+        this.game = game
+        this.mouse_clicked = false;
+        document.addEventListener("mousedown", () => this.mouse_clicked = true);
+        document.addEventListener("mouseup", () => this.mouse_clicked = false);
+
+        this.slider_min = 120;
+        this.slider_max = 430;
+
+        this.background = create_svg_element(this.game.svg, "rect", ["controls"], [["x", "50"], ["y", "500"], ["width", "400"], ["height", "96"], ["rx", "15"]])
+        this.bet_size_area = create_svg_element(this.game.svg, "rect", ["bet-size-area"], [["x", "57"], ["y", "507"], ["width", "58"], ["height", "22"]])
+        this.bet_size_area.onclick = () => {
+            const chips = prompt("Chips to bet", this.bet_size_text.textContent);
+            if (chips !== null && !isNaN(Number(chips))) {
+                const bet = Math.max(this.game.min_bet(), Math.min(this.game.all_in_bet(), Number(chips)));
+                this.bet_size_text.textContent = Number(chips);
+                const p = (bet - this.game.min_bet()) / (this.game.all_in_bet() - this.game.min_bet());
+                const x = Math.max(this.slider_min, Math.min(this.slider_max, Math.sqrt(p) * (this.slider_max - this.slider_min) + this.slider_min));
+                this.bet_slider_thumb.setAttribute("x", x);
+            }
+        };
+
+        this.bet_size = create_svg_element(this.game.svg, "text", ["bet-size"], [["x", "86"], ["y", "522"]])
+        this.bet_size_text = document.createTextNode("150");
+        this.bet_size.append(this.bet_size_text);
+
+        const point = this.game.svg.createSVGPoint();
+        const move_slider = e => {
+            point.x = e.clientX;
+            point.y = e.clientY;
+            const coords = point.matrixTransform(this.game.svg.getScreenCTM().inverse());
+            const x = Math.max(this.slider_min, Math.min(this.slider_max, coords.x - 5));
+            const p = Math.pow((x - this.slider_min) / (this.slider_max - this.slider_min), 2);
+            const bet = Math.round(this.game.min_bet() * (1 - p) + this.game.all_in_bet() * p);
+            this.bet_size_text.textContent = bet;
+            this.bet_slider_thumb.setAttribute("x", x);
+        };
+
+        this.bet_slider = create_svg_element(this.game.svg, "g", ["bet-slider"], []);
+        this.bet_slider_area = create_svg_element(this.bet_slider, "rect", ["bet-slider-area"], [["x", this.slider_min], ["y", "507"], ["width", this.slider_max - this.slider_min], ["height", "22"]]);
+        this.bet_slider_track = create_svg_element(this.bet_slider, "rect", ["bet-slider-track"], [["x", this.slider_min], ["y", "515"], ["width", this.slider_max - this.slider_min], ["height", "6"]]);
+        this.bet_slider_thumb = create_svg_element(this.bet_slider, "rect", ["bet-slider-thumb"], [["x", this.slider_min], ["y", "509"], ["width", "10"], ["height", "18"]]);
+        this.bet_slider_area.onmousedown = this.bet_slider_track.onmousedown = this.bet_slider_thumb.onmousedown = move_slider;
+        this.bet_slider_area.onmousemove = this.bet_slider_track.onmousemove = this.bet_slider_thumb.onmousemove = e => { if (this.mouse_clicked) move_slider(e); };
+        this.bet_slider_area.ontouchmove = this.bet_slider_track.ontouchmove = this.bet_slider_thumb.ontouchmove = e => move_slider(e.touches.item(0));
+
+        this.fold_control = create_svg_element(this.game.svg, "rect", ["fold-control"], [["x", "60"], ["y", "536"], ["width", "120"], ["height", "50"], ["rx", "10"]])
+        this.fold_control.onclick = () => this.game.send({type: "TakeAction", action: {action: "Fold"}});
+
+        this.fold_control_label = create_svg_element(this.game.svg, "text", ["fold-control-label"], [["x", "120"], ["y", "566"]])
+        this.fold_control_text = document.createTextNode("Fold");
+        this.fold_control_label.append(this.fold_control_text);
+
+        this.call_control = create_svg_element(this.game.svg, "rect", ["call-control"], [["x", "190"], ["y", "536"], ["width", "120"], ["height", "50"], ["rx", "10"]])
+        this.call_control.onclick = () => this.game.send({type: "TakeAction", action: {action: "Bet", chips: this.game.chips_to_call()}});
+
+        this.call_control_label = create_svg_element(this.game.svg, "text", ["call-control-label"], [["x", "250"], ["y", "566"]]);
+        this.call_control_text = document.createTextNode("Call");
+        this.call_control_label.append(this.call_control_text);
+
+        this.bet_control = create_svg_element(this.game.svg, "rect", ["bet-control"], [["x", "320"], ["y", "536"], ["width", "120"], ["height", "50"], ["rx", "10"]])
+        this.bet_control.onclick = () => {
+            const chips = +this.bet_size_text.textContent;
+            if (chips !== null && !isNaN(Number(chips))) {
+                game.send({type: "TakeAction", action: {action: "Bet", chips: Number(chips)}});
+            }
+        };
+
+        this.bet_control_label = create_svg_element(this.game.svg, "text", ["bet-control-label"], [["x", "380"], ["y", "566"]]);
+        this.bet_control_text = document.createTextNode("Bet");
+        this.bet_control_label.append(this.bet_control_text);
+    }
+
+    redraw() {
+        this.fold_control.classList.toggle("active", this.game.active === this.game.username && this.game.chips_to_call() > 0);
+        this.call_control.classList.toggle("active", this.game.active === this.game.username);
+        this.call_control_text.textContent = this.game.active === this.game.username && this.game.chips_to_call() == 0 ? "Check" : "Call";
+        const can_bet = this.game.active === this.game.username && this.game.chips_to_call() < this.game.all_in_bet();
+        this.bet_control.classList.toggle("active", can_bet);
+        this.bet_size_area.classList.toggle("active", can_bet);
+        this.bet_slider.classList.toggle("active", can_bet);
+        this.bet_size_text.textContent = this.game.min_bet();
+        this.bet_slider_thumb.setAttribute("x", this.slider_min);
+    }
+};
+
index cfc631634dfbfab00f9df40645d404078c970e3b..8cf308731ae1439f2343cfff33c38bc882531946 100644 (file)
@@ -21,9 +21,6 @@ export class Cribbage extends GameWithChat {
         this.dealer = null;
         this.dealing = false;
 
-        const table = create_svg_element(this.svg, "ellipse", [], [["cx", "250"], ["cy", "253"], ["rx", "250"], ["ry", "110"], ["fill", "#604010"]]);
-        const felt = create_svg_element(this.svg, "ellipse", [], [["cx", "250"], ["cy", "250"], ["rx", "240"], ["ry", "100"], ["fill", "green"]]);
-
         this.pass_control = create_svg_element(this.svg, "rect", ["pass-control", "active"], [["x", "400"], ["y", "400"], ["width", "120"], ["height", "50"], ["rx", "10"]]);
         this.pass_control.onclick = () => {
             this.send({type: "TakeAction", action: {action: "Pass"}});
@@ -173,16 +170,6 @@ export class Cribbage extends GameWithChat {
         }
     }
 
-    next_active_player(username) {
-        console.log('Finding player after', username);
-        let active = username;
-        do {
-            active = this.player_after(active);
-            console.log('.. got ', active);
-        } while (active !== username && (this.passed.has(active) || this.hands.has(active) && this.hands.get(active).length === 0));
-        return active;
-    }
-
     clear_pegging() {
         this.count = 0;
         this.passed.clear();
@@ -229,7 +216,7 @@ export class Cribbage extends GameWithChat {
                 this.hands.clear();
                 this.played.clear();
                 this.passed.clear();
-                this.active = this.player_after(user_action.username);
+                this.active = this.player_after(user_action.username, _ => true);
                 this.redraw_players();
                 break;
             case "ReceiveCard":
@@ -265,9 +252,6 @@ export class Cribbage extends GameWithChat {
                 break;
             case "PlayCard":
                 this.count += value_ace_low(user_action.action.card.rank);
-                if (this.count === 31) {
-                    this.clear_pegging();
-                }
                 const played = this.remove_card(user_action.username, user_action.action.card);
                 if (played !== undefined) {
                     this.svg.removeChild(played.image);
@@ -279,7 +263,10 @@ export class Cribbage extends GameWithChat {
                         image:this.card_image(user_action.username, user_action.action.card),
                     });
                 }
-                this.active = this.next_active_player(user_action.username);
+                if (this.count === 31) {
+                    this.clear_pegging();
+                }
+                this.active = this.player_after(user_action.username, username => !this.passed.has(username) || this.hands.has(username) && this.hands.get(username).length > 0);
                 this.redraw_cards();
                 this.redraw_players();
                 break;
@@ -288,7 +275,7 @@ export class Cribbage extends GameWithChat {
                 if (this.passed.size === this.hands.size) {
                     this.clear_pegging();
                 }
-                this.active = this.next_active_player(user_action.username);
+                this.active = this.player_after(user_action.username, username => !this.passed.has(username) || this.hands.has(username) && this.hands.get(username).length > 0);
                 this.redraw_cards();
                 this.redraw_players();
                 break;
@@ -314,7 +301,7 @@ export class Cribbage extends GameWithChat {
                     image: this.card_image(null, user_action.action.card),
                 };
                 this.community.push(turnup);
-                this.active = this.player_after(this.dealer);
+                this.active = this.player_after(this.dealer, _ => true);
                 this.redraw_cards();
                 this.redraw_players();
                 break;
index 31a0eb077c4d144bf0f9d99f0a02d924e8d6da15..33564d0b42a717cd95887733e9b1232e1cae52d9 100644 (file)
@@ -13,7 +13,8 @@ export class Game {
         }
 
         if (!this.seats.has(this.username)) {
-            this.send({type: "TakeAction", action: {action: "Join", seat: 0, chips: 0}});
+            const chips = this.summary.settings.starting_stack || 0;
+            this.send({type: "TakeAction", action: {action: "Join", seat: 0, chips: chips}});
         }
     }
 
@@ -34,17 +35,17 @@ export class Game {
         }
     }
 
-    player_after(username) {
+    player_after(username, is_playing) {
         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)) {
+            if (is_playing(username) && 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) {
+            if (is_playing(username) && player_after_seat === null || seat < player_after_seat) {
                 player_after = username;
                 player_after_seat = seat;
             }
index f37c764cc7d9c633e78c931df132da53af01b9ca..7758751cf0df810b5217463d024f2f36f1d06bce 100644 (file)
@@ -7,8 +7,8 @@ export class GameWithChat extends Game {
     constructor(container, summary, actions, username, send, close) {
         super(container, summary, actions, username, send, close);
 
-        this.svg = create_svg_element(container, "svg", [], [["viewBox", "0 0 500 500"]]);
-        this.background = create_svg_element(this.svg, "rect", [], [["width", "500"], ["height", "500"], ["fill", "#404040"]]);
+        this.svg = create_svg_element(container, "svg", [], [["viewBox", "0 0 500 600"]]);
+        this.background = create_svg_element(this.svg, "rect", [], [["width", "500"], ["height", "600"], ["fill", "#404040"]]);
 
         this.error_text_timeout = null;
         this.error = create_svg_element(this.svg, "text", ["game-error"], [["x", "20"], ["y", "450"]]);
@@ -31,6 +31,9 @@ export class GameWithChat extends Game {
         this.chat_control.onclick = () => this.chatroom_container.classList.toggle("hidden");
         this.chat_image = create_svg_element(this.svg, "image", ["chat-image"], [["x", "425"], ["y", "10"], ["width", "25"], ["height", "25"], ["href", "img/chat.svg"]]);
 
+        const table = create_svg_element(this.svg, "ellipse", [], [["cx", "250"], ["cy", "253"], ["rx", "250"], ["ry", "110"], ["fill", "#604010"]]);
+        const felt = create_svg_element(this.svg, "ellipse", [], [["cx", "250"], ["cy", "250"], ["rx", "240"], ["ry", "100"], ["fill", "green"]]);
+
         this.chatroom_container = document.createElement("div");
         this.chatroom_container.classList.add("embedded-chatroom");
         const send_chat_messages_only = message => {
index a645a7a04ee8db13fadfc9f3196e51f3432a0c0c..7a661490d2cf6cfb4bdba3f0b3eb8eeda493d005 100644 (file)
-import { svgns, create_svg_element } from "./svg.js";
+import { create_svg_element } from "./svg.js";
 import { card_href, suit_href } from "./card.js";
-import { Chatroom } from "./chatroom.js";
+import { GameWithChat } from "./gamechat.js";
+import { BetControls } from "./betcontrols.js";
 import { CongratulateWinner } from "./winner.js";
 
-export class BetControls {
-    constructor(game) {
-        this.game = game
-        this.mouse_clicked = false;
-        document.addEventListener("mousedown", () => this.mouse_clicked = true);
-        document.addEventListener("mouseup", () => this.mouse_clicked = false);
-
-        this.slider_min = 120;
-        this.slider_max = 430;
-
-        this.background = create_svg_element(this.game.svg, "rect", ["controls"], [["x", "50"], ["y", "500"], ["width", "400"], ["height", "96"], ["rx", "15"]])
-        this.bet_size_area = create_svg_element(this.game.svg, "rect", ["bet-size-area"], [["x", "57"], ["y", "507"], ["width", "58"], ["height", "22"]])
-        this.bet_size_area.onclick = () => {
-            const chips = prompt("Chips to bet", this.bet_size_text.textContent);
-            if (chips !== null && !isNaN(Number(chips))) {
-                const bet = Math.max(this.game.min_bet(), Math.min(this.game.all_in_bet(), Number(chips)));
-                this.bet_size_text.textContent = Number(chips);
-                const p = (bet - this.game.min_bet()) / (this.game.all_in_bet() - this.game.min_bet());
-                const x = Math.max(this.slider_min, Math.min(this.slider_max, Math.sqrt(p) * (this.slider_max - this.slider_min) + this.slider_min));
-                this.bet_slider_thumb.setAttribute("x", x);
-            }
-        };
-
-        this.bet_size = create_svg_element(this.game.svg, "text", ["bet-size"], [["x", "86"], ["y", "522"]])
-        this.bet_size_text = document.createTextNode("150");
-        this.bet_size.append(this.bet_size_text);
-
-        const point = this.game.svg.createSVGPoint();
-        const move_slider = e => {
-            point.x = e.clientX;
-            point.y = e.clientY;
-            const coords = point.matrixTransform(this.game.svg.getScreenCTM().inverse());
-            const x = Math.max(this.slider_min, Math.min(this.slider_max, coords.x - 5));
-            const p = Math.pow((x - this.slider_min) / (this.slider_max - this.slider_min), 2);
-            const bet = Math.round(this.game.min_bet() * (1 - p) + this.game.all_in_bet() * p);
-            this.bet_size_text.textContent = bet;
-            this.bet_slider_thumb.setAttribute("x", x);
-        };
-
-        this.bet_slider = create_svg_element(this.game.svg, "g", ["bet-slider"], []);
-        this.bet_slider_area = create_svg_element(this.bet_slider, "rect", ["bet-slider-area"], [["x", String(this.slider_min)], ["y", "507"], ["width", String(this.slider_max - this.slider_min)], ["height", "22"]])
-        this.bet_slider_track = create_svg_element(this.bet_slider, "rect", ["bet-slider-track"], [["x", String(this.slider_min)], ["y", "515"], ["width", String(this.slider_max - this.slider_min)], ["height", "6"]])
-        this.bet_slider_thumb = create_svg_element(this.bet_slider, "rect", ["bet-slider-thumb"], [["x", String(this.slider_min)], ["y", "509"], ["width", "10"], ["height", "18"]])
-        this.bet_slider_area.onmousedown = this.bet_slider_track.onmousedown = this.bet_slider_thumb.onmousedown = move_slider;
-        this.bet_slider_area.onmousemove = this.bet_slider_track.onmousemove = this.bet_slider_thumb.onmousemove = e => { if (this.mouse_clicked) move_slider(e); };
-        this.bet_slider_area.ontouchmove = this.bet_slider_track.ontouchmove = this.bet_slider_thumb.ontouchmove = e => move_slider(e.touches.item(0));
-
-        this.fold_control = create_svg_element(this.game.svg, "rect", ["fold-control"], [["x", "60"], ["y", "536"], ["width", "120"], ["height", "50"], ["rx", "10"]])
-        this.fold_control.onclick = () => this.game.send({type: "TakeAction", action: {action: "Fold"}});
-
-        this.fold_control_label = create_svg_element(this.game.svg, "text", ["fold-control-label"], [["x", "120"], ["y", "566"]])
-        this.fold_control_text = document.createTextNode("Fold");
-        this.fold_control_label.append(this.fold_control_text);
-
-        this.call_control = create_svg_element(this.game.svg, "rect", ["call-control"], [["x", "190"], ["y", "536"], ["width", "120"], ["height", "50"], ["rx", "10"]])
-        this.call_control.onclick = () => this.game.send({type: "TakeAction", action: {action: "Bet", chips: this.game.chips_to_call()}});
-
-        this.call_control_label = create_svg_element(this.game.svg, "text", ["call-control-label"], [["x", "250"], ["y", "566"]]);
-        this.call_control_text = document.createTextNode("Call");
-        this.call_control_label.append(this.call_control_text);
-
-        this.bet_control = create_svg_element(this.game.svg, "rect", ["bet-control"], [["x", "320"], ["y", "536"], ["width", "120"], ["height", "50"], ["rx", "10"]])
-        this.bet_control.onclick = () => {
-            const chips = +this.bet_size_text.textContent;
-            if (chips !== null && !isNaN(Number(chips))) {
-                game.send({type: "TakeAction", action: {action: "Bet", chips: Number(chips)}});
-            }
-        };
-
-        this.bet_control_label = create_svg_element(this.game.svg, "text", ["bet-control-label"], [["x", "380"], ["y", "566"]]);
-        this.bet_control_text = document.createTextNode("Bet");
-        this.bet_control_label.append(this.bet_control_text);
-    }
-
-    redraw() {
-        this.fold_control.classList.toggle("active", this.game.active === this.game.username && this.game.chips_to_call() > 0);
-        this.call_control.classList.toggle("active", this.game.active === this.game.username);
-        this.call_control_text.textContent = this.game.active === this.game.username && this.game.chips_to_call() == 0 ? "Check" : "Call";
-        const can_bet = this.game.active === this.game.username && this.game.chips_to_call() < this.game.all_in_bet();
-        this.bet_control.classList.toggle("active", can_bet);
-        this.bet_size_area.classList.toggle("active", can_bet);
-        this.bet_slider.classList.toggle("active", can_bet);
-        this.bet_size_text.textContent = this.game.min_bet();
-        this.bet_slider_thumb.setAttribute("x", this.slider_min);
-    }
-};
-
-export class TexasHoldEm {
+export class TexasHoldEm extends GameWithChat {
     constructor(container, summary, actions, username, send, close) {
-        this.container = container;
-        this.summary = summary;
-        this.actions = actions;
-        this.username = username;
-        this.send = send;
-        this.seats = new Map();
+        super(container, summary, actions, username, send, close);
+        this.svg.classList.add("texas-hold-em");
+
         this.stacks = new Map();
         this.hands = new Map();
         this.bets = new Map();
@@ -107,30 +18,6 @@ export class TexasHoldEm {
         this.user_icons = new Map();
         this.small_blind = this.summary.settings.small_blind;
         this.big_blind = this.small_blind * 2;
-        this.error_text_timeout = null;
-
-        this.svg = create_svg_element(this.container, "svg", ["texas-hold-em"], [["viewBox", "0 0 500 600"]])
-
-        const background = create_svg_element(this.svg, "rect", [], [["width", "500"], ["height", "600"], ["fill", ["#404040"]]]);
-
-        this.error = create_svg_element(this.svg, "text", ["game-error"], [["x", "20"], ["y", "450"]]);
-
-        this.error_lines = [];
-        this.error_text = [];
-        const num_error_lines = 2;
-        for (let i = 0; i < num_error_lines; i++) {
-            const line = document.createElementNS(svgns, "tspan");
-            const text = document.createTextNode("");
-            line.setAttribute("x", "20");
-            line.setAttribute("dy", "20");
-            line.append(text);
-            this.error.append(line);
-            this.error_lines.push(line);
-            this.error_text.push(text);
-        }
-
-        const table = create_svg_element(this.svg, "ellipse", [], [["cx", "250"], ["cy", "253"], ["rx", "250"], ["ry", "110"], ["fill", "#604010"]]);
-        const felt = create_svg_element(this.svg, "ellipse", [], [["cx", "250"], ["cy", "250"], ["rx", "240"], ["ry", "100"], ["fill", "green"]]);
 
         const pot_size = create_svg_element(this.svg, "text", ["pot-size"], [["x", "400"], ["y", "250"]]);
         this.pot_size_text = document.createTextNode("");
@@ -146,72 +33,7 @@ export class TexasHoldEm {
 
         this.bet_controls = new BetControls(this)
 
-        this.close_control = create_svg_element(this.svg, "rect", ["close-control"], [["x", "460"], ["y", "5"], ["width", "35"], ["height", "35"], ["rx", "2"]])
-        this.close_control.onclick = close;
-        this.close_image = create_svg_element(this.svg, "image", ["close-image"], [["x", "465"], ["y", "10"], ["width", "25"], ["height", "25"], ["href", "img/close.svg"]]);
-
-        this.chat_control = create_svg_element(this.svg, "rect", ["chat-control"], [["x", "420"], ["y", "5"], ["width", "35"], ["height", "35"], ["rx", "2"]])
-        this.chat_control.onclick = () => this.chatroom_container.classList.toggle("hidden");
-        this.chat_image = create_svg_element(this.svg, "image", ["chat-image"], [["x", "425"], ["y", "10"], ["width", "25"], ["height", "25"], ["href", "img/chat.svg"]]);
-
-        this.chatroom_container = document.createElement("div");
-        this.chatroom_container.classList.add("embedded-chatroom");
-        const send_chat_messages_only = message => {
-            if (message.type === "TakeAction" && message.action.action === "Message") {
-                send(message);
-            }
-        };
-        this.chatroom = new Chatroom(this.chatroom_container, summary, [], username, send_chat_messages_only, null);
-        this.container.append(this.chatroom_container);
-
-        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: this.summary.settings.starting_stack}});
-        }
-    }
-
-    set_text(style, text) {
-        this.error.setAttribute("class", style);
-        const lines = text.split("\n", this.error_text.length);
-        for (const text of this.error_text) {
-            text.textContent = "";
-        }
-        for (let i = 0; i < lines.length; i++) {
-            this.error_text[i + this.error_text.length - lines.length].textContent = lines[i];
-        }
-        if (this.error_text_timeout !== null) clearTimeout(this.error_text_timeout);
-        this.error_text_timeout = setTimeout(() => {
-            for (const text of this.error_text) {
-                text.textContent = "";
-            }
-            this.error_text_timeout = null;
-        }, 5000);
-    }
-
-    set_info_text(text) {
-        this.set_text("game-info", text);
-    }
-
-    set_error_text(text) {
-        this.set_text("game-error", text);
-    }
-
-    player_angle(seat) {
-        const rel_seat = this.my_seat > seat ? this.num_seats + seat - this.my_seat : seat - this.my_seat;
-        if (this.num_seats % 4 === 0) {
-            if (rel_seat < this.num_seats / 4) {
-                return rel_seat * 2 * Math.PI / (this.num_seats + 2);
-            } else if (rel_seat >= this.num_seats / 4 && rel_seat <= 3 * this.num_seats / 4) {
-                return (rel_seat + 1) * 2 * Math.PI / (this.num_seats + 2);
-            } else {
-                return (rel_seat + 2) * 2 * Math.PI / (this.num_seats + 2);
-            }
-        } else {
-            return rel_seat * 2 * Math.PI / this.num_seats;
-        }
+        this.take_initial_actions(actions);
     }
 
     redraw_players() {
@@ -255,37 +77,23 @@ export class TexasHoldEm {
         }
         for (const [username, seat] of this.seats) {
             if (!this.user_icons.has(username)) {
-                const user = document.createElementNS(svgns, "text");
-                const text = document.createTextNode(username);
-                user.append(text);
                 const angle = this.player_angle(seat);
                 const x = 240 - 180 * Math.sin(angle);
                 const y = 240 + 120 * Math.cos(angle) + 60 * (Math.sign(Math.cos(angle)) || 1);
-                user.setAttribute("x", x);
-                user.setAttribute("y", y);
-                this.svg.append(user);
+                const user = create_svg_element(this.svg, "text", [], [["x", x], ["y", y]]);
+                const text = document.createTextNode(username);
+                user.append(text);
 
-                const stack = document.createElementNS(svgns, "text");
+                const stack = create_svg_element(this.svg, "text", [], [["x", x], ["y", y + 20]]);
                 const stack_text = document.createTextNode(this.stacks.get(username));
                 stack.append(stack_text);
-                stack.setAttribute("x", x);
-                stack.setAttribute("y", y + 20);
-                this.svg.append(stack);
 
-                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");
+                const active = create_svg_element(this.svg, "circle", ["active-indicator"], [["cx", x - 10], ["cy", y - 5], ["r", 5]]);
                 active.classList.toggle("active", this.active === username);
-                this.svg.append(active);
 
-                const bet = document.createElementNS(svgns, "text");
+                const bet = create_svg_element(this.svg, "text", [], [["x", 240 - 120 * Math.sin(angle)], ["y", 250 + 70 * Math.cos(angle)]]);
                 const bet_text = document.createTextNode(this.bets.get(username) || "");
                 bet.append(bet_text);
-                bet.setAttribute("x", 240 - 120 * Math.sin(angle));
-                bet.setAttribute("y", 250 + 70 * Math.cos(angle));
-                this.svg.append(bet);
 
                 this.user_icons.set(username, [user, stack, active, bet]);
             }
@@ -315,30 +123,7 @@ export class TexasHoldEm {
     }
 
     card_image(username, card) {
-        const image = document.createElementNS(svgns, card === null ? "image" : "use");
-        image.setAttribute("width", "45");
-        image.setAttribute("height", "70");
-        image.setAttribute("href", card_href(card));
-        this.svg.append(image);
-        return image;
-    }
-
-    player_after(username, is_playing) {
-        const in_seat = this.seats.get(username);
-        let [player_after, player_after_seat] = [null, null];
-        for (const [username, seat] of this.seats) {
-            if (is_playing(username) && 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 (is_playing(username) && (player_after_seat === null || seat < player_after_seat)) {
-                player_after = username;
-                player_after_seat = seat;
-            }
-        }
-        return player_after;
+        return create_svg_element(this.svg, card === null ? "image" : "use", [], [["width", "45"], ["height", "70"], ["href", card_href(card)]]);
     }
 
     chips_to_call() {
@@ -388,25 +173,19 @@ export class TexasHoldEm {
     }
 
     take_action(user_action) {
-        this.chatroom.take_action(user_action);
+        super.take_action(user_action);
         switch (user_action.action.action) {
             case "Message":
                 break;
             case "Join":
-                this.seats.set(user_action.username, user_action.action.seat);
                 this.stacks.set(user_action.username, user_action.action.chips);
                 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.stacks.delete(user_action.username);
                 this.hands.delete(user_action.username);
                 this.bets.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":
@@ -418,8 +197,8 @@ export class TexasHoldEm {
                 }
                 this.hands.delete(user_action.username);
                 this.bets.delete(user_action.username);
-                const small_blind = this.player_after(this.dealer, () => true);
-                const big_blind = this.player_after(small_blind, () => true);
+                const small_blind = this.player_after(this.dealer, _ => true);
+                const big_blind = this.player_after(small_blind, _ => true);
                 if (this.stacks.length <= 1) {
                     this.seats.delete(user_action.username);
                 } else if (user_action.username === this.dealer) {
@@ -530,7 +309,7 @@ export class TexasHoldEm {
                 this.redraw_players();
                 break;
             case "EndDeal":
-                this.active = this.player_after(user_action.username, username => true);
+                this.active = this.player_after(user_action.username, _ => true);
                 this.redraw_players();
                 break;
             case "WinGame":
index f7b450386b591cd6a7ff293d0b5ad42a85cc3c8d..8969fd9144b1da95028a4df57f9d16707d74d635 100644 (file)
 import { svgns, create_svg_element } from "./svg.js";
 import { card_href, suit_href } from "./card.js";
-import { Chatroom } from "./chatroom.js";
+import { GameWithChat } from "./gamechat.js";
 import { CongratulateWinner } from "./winner.js";
 
-export class KnockOutWhist {
+export class KnockOutWhist extends GameWithChat {
     constructor(container, summary, actions, username, send, close) {
-        this.container = container;
-        this.summary = summary;
-        this.username = username;
-        this.send = send;
-        this.seats = new Map();
+        super(container, summary, actions, username, send, close);
+        this.svg.classList.add("knock-out-whist");
+
         this.hands = new Map();
         this.tricks_won = new Map();
         this.community = [];
         this.trick = [];
         this.user_icons = new Map();
-        this.error_text_timeout = null;
-
-        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);
 
-        this.error = document.createElementNS(svgns, "text");
-        this.error.setAttribute("x", "20");
-        this.error.setAttribute("y", "450");
-        this.error.classList.add("game-error");
-        this.error_lines = [];
-        this.error_text = [];
-        const num_error_lines = 2;
-        for (let i = 0; i < num_error_lines; i++) {
-            const line = document.createElementNS(svgns, "tspan");
-            const text = document.createTextNode("");
-            line.setAttribute("x", "20");
-            line.setAttribute("dy", "20");
-            line.append(text);
-            this.error.append(line);
-            this.error_lines.push(line);
-            this.error_text.push(text);
-        }
-        this.svg.append(this.error);
-
-        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);
+        const suits = create_svg_element(this.svg, "rect", [], [["x", "340"], ["y", "235"], ["width", "100"], ["height", "30"], ["rx", "10"], ["fill", "darkgreen"]]);
 
         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");
+            const highlight = create_svg_element(this.svg, "circle", ["suit-highlight"], [["cx", x + 12.5], ["cy", "250"], ["r", "10"]]);
             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, "use");
-            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);
+            const glyph = create_svg_element(this.svg, "use", ["suit-glyph"], [["x", x], ["y", "235"], ["width", "25"], ["height", "30"], ["href", suit_href(suit)]]);
             x += 25;
         }
 
-        this.close_control = document.createElementNS(svgns, "rect");
-        this.close_control.setAttribute("x", "460");
-        this.close_control.setAttribute("y", "5");
-        this.close_control.setAttribute("width", "35");
-        this.close_control.setAttribute("height", "35");
-        this.close_control.setAttribute("rx", "2");
-        this.close_control.onclick = close;
-        this.close_control.classList.add("close-control");
-        this.svg.append(this.close_control);
-        this.close_image = document.createElementNS(svgns, "image");
-        this.close_image.setAttribute("x", "465");
-        this.close_image.setAttribute("y", "10");
-        this.close_image.setAttribute("width", "25");
-        this.close_image.setAttribute("height", "25");
-        this.close_image.setAttribute("href", "img/close.svg");
-        this.close_image.classList.add("close-image");
-        this.svg.append(this.close_image);
-
-        this.chat_control = document.createElementNS(svgns, "rect");
-        this.chat_control.setAttribute("x", "420");
-        this.chat_control.setAttribute("y", "5");
-        this.chat_control.setAttribute("width", "35");
-        this.chat_control.setAttribute("height", "35");
-        this.chat_control.setAttribute("rx", "2");
-        this.chat_control.onclick = () => this.chatroom_container.classList.toggle("hidden");
-        this.chat_control.classList.add("chat-control");
-        this.svg.append(this.chat_control);
-        this.chat_image = document.createElementNS(svgns, "image");
-        this.chat_image.setAttribute("x", "425");
-        this.chat_image.setAttribute("y", "10");
-        this.chat_image.setAttribute("width", "25");
-        this.chat_image.setAttribute("height", "25");
-        this.chat_image.setAttribute("href", "img/chat.svg");
-        this.chat_image.classList.add("chat-image");
-        this.svg.append(this.chat_image);
-
-        this.container.append(this.svg);
-
-        this.chatroom_container = document.createElement("div");
-        this.chatroom_container.classList.add("embedded-chatroom");
-        const send_chat_messages_only = message => {
-            if (message.type === "TakeAction" && message.action.action === "Message") {
-                send(message);
-            }
-        };
-        this.chatroom = new Chatroom(this.chatroom_container, summary, [], username, send_chat_messages_only, null);
-        this.container.append(this.chatroom_container);
-
-        for (const user_action of actions) {
-            this.take_action(user_action);
-        }
-
-        if (!this.seats.has(this.username)) {
-            this.send({type: "TakeAction", action: {action: "Join", seat: 0, chips: 0}});
-        }
-    }
-
-    set_text(style, text) {
-        this.error.setAttribute("class", style);
-        const lines = text.split("\n", this.error_text.length);
-        for (const text of this.error_text) {
-            text.textContent = "";
-        }
-        for (let i = 0; i < lines.length; i++) {
-            this.error_text[i + this.error_text.length - lines.length].textContent = lines[i];
-        }
-        if (this.error_text_timeout !== null) clearTimeout(this.error_text_timeout);
-        this.error_text_timeout = setTimeout(() => {
-            for (const text of this.error_text) {
-                text.textContent = "";
-            }
-            this.error_text_timeout = null;
-        }, 5000);
-    }
-
-    set_info_text(text) {
-        this.set_text("game-info", text);
-    }
-
-    set_error_text(text) {
-        this.set_text("game-error", text);
-    }
-
-    player_angle(seat) {
-        const rel_seat = this.my_seat > seat ? this.num_seats + seat - this.my_seat : seat - this.my_seat;
-        if (this.num_seats % 4 === 0) {
-            if (rel_seat < this.num_seats / 4) {
-                return rel_seat * 2 * Math.PI / (this.num_seats + 2);
-            } else if (rel_seat >= this.num_seats / 4 && rel_seat <= 3 * this.num_seats / 4) {
-                return (rel_seat + 1) * 2 * Math.PI / (this.num_seats + 2);
-            } else {
-                return (rel_seat + 2) * 2 * Math.PI / (this.num_seats + 2);
-            }
-        } else {
-            return rel_seat * 2 * Math.PI / this.num_seats;
-        }
+        this.take_initial_actions(actions);
     }
 
     redraw_players() {
@@ -283,52 +121,24 @@ export class KnockOutWhist {
     }
 
     card_image(username, card) {
-        const image = document.createElementNS(svgns, card === null ? "image" : "use");
-        image.setAttribute("width", "45");
-        image.setAttribute("height", "70");
-        image.setAttribute("href", card_href(card));
+        const image = create_svg_element(this.svg, card === null ? "image" : "use", [], [["width", "45"], ["height", "70"], ["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) {
-        this.chatroom.take_action(user_action);
+        super.take_action(user_action);
         switch (user_action.action.action) {
             case "Message":
                 break;
             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":
@@ -350,7 +160,7 @@ export class KnockOutWhist {
                 this.hands.clear();
                 this.tricks_won.clear();
                 this.trumps = null;
-                this.active = this.player_after(user_action.username);
+                this.active = this.player_after(user_action.username, _ => true);
                 this.redraw_trumps();
                 this.redraw_players();
                 break;
@@ -387,7 +197,7 @@ export class KnockOutWhist {
                     }
                 });
                 this.hands.set(user_action.username, cards);
-                this.active = this.player_after(user_action.username);
+                this.active = this.player_after(user_action.username, _ => true);
                 this.redraw_cards();
                 this.redraw_players();
                 break;
@@ -423,7 +233,7 @@ export class KnockOutWhist {
                 this.redraw_players();
                 break;
             case "EndDeal":
-                this.active = this.player_after(user_action.username);
+                this.active = this.player_after(user_action.username, _ => true);
                 this.redraw_players();
                 break;
             case "WinGame":