refactor site code into modules for each game type
authorGeoffrey Allott <geoffrey@allott.email>
Sat, 27 Feb 2021 12:43:00 +0000 (12:43 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Sat, 27 Feb 2021 12:43:00 +0000 (12:43 +0000)
12 files changed:
site/modules/chatroom.js [new file with mode: 0644]
site/modules/gamelist.js [new file with mode: 0644]
site/modules/socket.js
site/modules/whist.js [new file with mode: 0644]
site/style/whist.css
src/api.rs
src/client.rs
src/game/action.rs
src/game/whist.rs
src/main.rs
src/seats.rs
src/server.rs

diff --git a/site/modules/chatroom.js b/site/modules/chatroom.js
new file mode 100644 (file)
index 0000000..9a80c6c
--- /dev/null
@@ -0,0 +1,107 @@
+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;
+    }
+}
diff --git a/site/modules/gamelist.js b/site/modules/gamelist.js
new file mode 100644 (file)
index 0000000..5207190
--- /dev/null
@@ -0,0 +1,89 @@
+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;
+    }
+}
index b6f098ea16d7c87e8f9bd7398b6ca97c042f1d9b..27e2ba9d8c79ff450ffc4c485242784428041f46 100644 (file)
@@ -1,6 +1,8 @@
 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) {
@@ -58,12 +60,11 @@ export class Socket {
                 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);
@@ -71,23 +72,19 @@ export class Socket {
                 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;
@@ -114,436 +111,17 @@ export class Socket {
         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;
         }
     }
diff --git a/site/modules/whist.js b/site/modules/whist.js
new file mode 100644 (file)
index 0000000..d0b63de
--- /dev/null
@@ -0,0 +1,309 @@
+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;
+        }
+    }
+}
index 97cae0b5e54751857a15939ceae32638ff8ac17a..43b41cd22f3035453be418b0f52efe84c6d1d8a5 100644 (file)
@@ -32,3 +32,11 @@ svg {
 .suit-highlight.trumps:hover {
     fill: #4040ff;
 }
+
+.active-indicator {
+    fill: #000060;
+}
+
+.active-indicator.active {
+    fill: #2020ff;
+}
index e90176cad67f32d10df1a4cdef7746d1c412dd87..27b566d261f17987aa2def1953b768ecdaf3c39a 100644 (file)
@@ -50,8 +50,8 @@ pub enum ServerMessage {
     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 },
index 6507d6910ddff69053c9a05a2f3c32265d35d850..c7b0f1c507558741207e6d4e605fd5f76a83876a 100644 (file)
@@ -179,27 +179,27 @@ impl ConnectionState {
                 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()},
                     }
                 }
             }
index 4c8b3472a731c20b5762796001abc2096863c32f..ed5af55a86e02d11499f7213ac969336819ed083 100644 (file)
@@ -37,6 +37,7 @@ pub enum Action {
     Fold,
     Bet { chips: u64 },
     WinTrick,
+    WinCall,
     WinHand { chips: u64 },
     WinGame,
     Message { message: String },
@@ -61,7 +62,10 @@ pub enum ActionError {
     SeatNotAvailable,
     NoSeatAvailable,
     OutOfTurn,
+    Dealing,
+    MustChooseTrumps,
     CardNotPlayable,
+    CardNotAvailable,
     InvalidActionForGameType,
     GameHasEnded,
 }
@@ -69,15 +73,18 @@ pub enum ActionError {
 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"),
         }
     }
 }
index 4f6490c2db52974c2ab9fe44d7844718c26e4f9a..f4d4ecd7798518b7b968877a31efed96f2a2997b 100644 (file)
@@ -16,6 +16,7 @@ enum KnockOutWhistState {
     ChoosingTrumps,
     Playing,
     CutForCall,
+    RoundCompleted,
     Completed,
 }
 
@@ -128,12 +129,14 @@ impl Game for KnockOutWhist {
                     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}}))
@@ -141,19 +144,20 @@ impl Game for KnockOutWhist {
                     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),
         }
@@ -242,14 +246,7 @@ impl Game for KnockOutWhist {
                         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;
@@ -294,6 +291,11 @@ impl Game for KnockOutWhist {
                 }
                 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),
         }
@@ -344,8 +346,8 @@ impl Game for KnockOutWhist {
                             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() {
@@ -361,7 +363,14 @@ impl Game for KnockOutWhist {
                     } 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
index 599a756941d94879610ef77fc7ed7aadd1849cd6..a417cb9a3288aeb057a3e3d1d43c7e33df1a471b 100644 (file)
@@ -116,9 +116,9 @@ pub async fn handle_client_interest(mut connection: PubSub, mut new_clients: Rec
             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);
                             }
@@ -163,10 +163,12 @@ async fn handle_new_games(server: Server) -> Result<(), std::io::Error> {
         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));
                     }
                 }
index 420744fa69718ebc1895c30930b1be5a331f9e4c..f7a0b8c9dea5f0146756591ddf23eb9d7ef6413a 100644 (file)
@@ -43,6 +43,25 @@ impl Seats {
         }
     }
 
+    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 {
index 59e6ba4fba423140294a27393e30a3561317f090..36e9ed72ad0be1e78dfcb2a42998c42645457b29 100644 (file)
@@ -155,7 +155,7 @@ impl ServerState {
     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>> {