game list and chat ui are functioning
authorGeoffrey Allott <geoffrey@allott.email>
Sat, 6 Feb 2021 17:06:13 +0000 (17:06 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Sat, 6 Feb 2021 17:06:13 +0000 (17:06 +0000)
site/index.html
site/main.js
site/modules/socket.js
site/style.css
site/style/chatroom.css [new file with mode: 0644]
site/style/game-list.css [new file with mode: 0644]
site/style/login.css [new file with mode: 0644]
src/client.rs
src/game.rs

index 5dee3ab6aa64da1975c239659c69f2c4061733c9..972705166e35dce2e043e6ba3cbe39465aeb894c 100644 (file)
             </div>
         </div>
         <div id="game-tile-background">
-            <!--<div class="game-list">
-                <div class="game-summary">
-                    <div class="game-id">#2</div>
-                    <div class="game-title">Poker chat</div>
-                    <div class="stub"></div>
-                    <div class="game-format">Chatroom</div>
-                    <ul class="game-settings">
-                        <li>max_players: 4</li>
-                    </ul>
-                </div>
-                <div class="game-summary">
-                    <div class="game-id">#1</div>
-                    <div class="game-title">General discussion</div>
-                    <div class="game-format">Chatroom</div>
-                    <ul class="game-settings">
-                        <li>max_players: 8</li>
-                    </ul>
-                </div>
-                <div class="game-summary">
-                    <div class="game-id">#0</div>
-                    <div class="game-title">Test chatroom</div>
-                    <div class="game-format">Chatroom</div>
-                    <ul class="game-settings">
-                        <li>max_players: 2</li>
-                    </ul>
-                </div>
-            </div>-->
         </div>
     </body>
 </html>
index 662230a04052cc309a17e6ccbabf81906adff9be..bfee3697dd6a0d8ddb972436858db7a6227ba5e0 100644 (file)
@@ -3,23 +3,6 @@ import { Socket } from "./modules/socket.js";
 
 var sockets = [];
 
-function hide_login() {
-    document.getElementById("login-background").classList.add("hidden");
-}
-
-function show_login(error) {
-    if (error) {
-        document.getElementById("login-error").classList.remove("hidden");
-        document.getElementById("login-error").innerText = error;
-    } else {
-        document.getElementById("login-error").classList.add("hidden");
-        document.getElementById("login-error").innerText = "";
-    }
-    document.getElementById("login-background").classList.remove("hidden");
-    document.getElementById("login-button").onclick = login;
-    document.getElementById("password-input").onchange = login;
-}
-
 function login() {
     var username = document.getElementById("username-input").value;
     var password = document.getElementById("password-input").value;
@@ -33,6 +16,6 @@ function login() {
 
 window.onload = function() {
     var container = document.getElementById("game-tile-background");
-    sockets.push(new Socket(container));
-    show_login();
+    sockets.push(new Socket(container, login));
+    sockets[0].show_login();
 };
index 342677f6984d71c0658a63fc9575faeb5072b818..7f25db1454a752e552bf3a12af8e6af7a2952ef6 100644 (file)
@@ -1,6 +1,7 @@
 export class Socket {
-    constructor(container) {
+    constructor(container, login_all_sockets) {
         this.container = container;
+        this.login_all_sockets = login_all_sockets;
         let proto = window.location.protocol === "https:" ? "wss:" : "ws:";
         let uri = proto + "//" + window.location.host + "/api";
         this.socket = new WebSocket(uri);
@@ -35,26 +36,52 @@ export class Socket {
                 this.state = "LoginAuthResponseSent";
                 break;
             case "LoginFailure":
-                this.auth = null;
+                delete this.auth;
                 this.show_login(message.reason);
                 this.state = "Connected";
                 break;
             case "LoginSuccess":
                 this.hide_login();
                 this.send({type: "JoinLobby", filter: ""});
-                this.state = "JoinLobbySent";
+                this.state = "LoggedIn";
+                break;
+            case "JoinLobbyFailure":
+                console.error("Joining lobby failed: " + message.reason);
+                this.state = "LoggedIn";
                 break;
             case "JoinLobbySuccess":
-                this.create_game_list();
                 this.games = message.games;
-                this.redraw_games();
+                this.create_game_list();
                 this.state = "InLobby";
                 break;
-            case "AddGame":
-                this.add_game(message.game);
+            case "NewGame":
+                this.new_game(message.game);
+                break;
+            case "JoinGameFailure":
+                console.error("Joining game failed: " + message.reason);
+                this.state = "LoggedIn";
+                break;
+            case "JoinGameSuccess":
+                this.game = message.game;
+                this.create_game_display();
+                this.state = "InGame";
+                break;
+            case "TakeActionSuccess":
+                this.add_action({username: this.auth.username, action: this.action});
+                if (this.action.action === "Leave") {
+                    this.send({type: "JoinLobby", filter: ""});
+                }
+                delete this.action;
+                break;
+            case "TakeActionFailure":
+                console.error("Taking action failed: " + message.reason, this.action);
+                delete this.action;
+                break;
+            case "NewAction":
+                this.add_action(message.action);
                 break;
             case "LogoutSuccess":
-                this.auth = null;
+                delete this.auth;
                 this.show_login();
                 this.state = "Connected";
                 break;
@@ -74,56 +101,204 @@ export class Socket {
             document.getElementById("login-error").innerText = "";
         }
         document.getElementById("login-background").classList.remove("hidden");
-        document.getElementById("login-button").onclick = login;
-        document.getElementById("password-input").onchange = login;
+        document.getElementById("login-button").onclick = this.login_all_sockets;
+        document.getElementById("password-input").onchange = this.login_all_sockets;
     }
 
     game_element(game) {
         console.log("getting game element for ", game);
+
         var game_element = document.createElement("div");
         game_element.classList.add("game-summary");
+
         var id = document.createElement("div");
         id.innerText = "#" + game.id;
         id.classList.add("game-id");
-        game_element.appendChild(id);
+        game_element.append(id);
+
         var title = document.createElement("div");
-        title.innerText = game.settings.format; // TODO
+        title.innerText = game.settings.title;
         title.classList.add("game-title");
-        game_element.appendChild(title);
+        game_element.append(title);
+
         var format = document.createElement("div");
         format.innerText = game.settings.format;
         format.classList.add("game-format");
-        game_element.appendChild(format);
+        game_element.append(format);
+
         var settings = document.createElement("ul");
         settings.classList.add("game-settings");
         for (var setting of Object.keys(game.settings)) {
             if (setting !== "id" && setting !== "title" && setting !== "format") {
                 var li = document.createElement("li");
                 li.innerText = setting + ": " + game.settings[setting];
-                settings.appendChild(li);
+                settings.append(li);
             }
         }
-        game_element.appendChild(settings);
+        game_element.append(settings);
+
+        game_element.onclick = () => this.join_game(game);
+
         return game_element;
     }
 
-    add_game(game) {
+    new_game(game) {
+        var is_at_end = this.game_list_container.scrollTop + this.game_list_container.clientHeight == this.game_list_container.scrollHeight;
         this.games.push(game);
         var game_element = this.game_element(game);
-        this.game_list.prepend(game_element);
+        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 = "";
+        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.container.append(this.game_list);
+        this.game_list_container.append(this.game_list);
+        this.container.append(this.game_list_container);
+        this.redraw_games();
     }
 
     redraw_games() {
+        this.game_list.textContent = "";
         for (var game of this.games) {
             var game_element = this.game_element(game);
-            this.game_list.prepend(game_element);
+            this.game_list.append(game_element);
+        }
+        this.game_list_container.scrollTo({
+            top: this.game_list.clientHeight - this.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);
+        var 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();
+    }
+
+    join_element(username) {
+        var join_element = document.createElement("div");
+        join_element.classList.add("chatroom-join");
+        var username_element = document.createElement("div");
+        username_element.classList.add("chatroom-username");
+        username_element.innerText = username;
+        join_element.append(username_element);
+        var 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) {
+        var message_element = document.createElement("div");
+        message_element.classList.add("chatroom-message");
+        var username_element = document.createElement("div");
+        username_element.classList.add("chatroom-username");
+        username_element.innerText = username;
+        message_element.append(username_element);
+        var 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) {
+        var leave_element = document.createElement("div");
+        leave_element.classList.add("chatroom-leave");
+        var username_element = document.createElement("div");
+        username_element.classList.add("chatroom-username");
+        username_element.innerText = username;
+        leave_element.append(username_element);
+        var text_element = document.createElement("div");
+        text_element.classList.add("chatroom-text");
+        text_element.innerText = "Left";
+        leave_element.append(text_element);
+        return leave_element;
+    }
+
+    add_action(user_action) {
+        switch (this.game.summary.settings.format) {
+            case "Chatroom":
+                var is_at_end = this.chatroom_chat.scrollTop + this.chatroom_chat.clientHeight == this.chatroom_chat.scrollHeight;
+                switch (user_action.action.action) {
+                    case "Join":
+                        this.game.users.add(user_action.username);
+                        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.users.delete(user_action.username);
+                        this.chatroom_chat.append(this.leave_element(user_action.username));
+                        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"
+                    });
+                }
+                break;
+        }
+    }
+
+    redraw_chatroom() {
+        this.chatroom_chat.textContent = "";
+        for (var user_action of this.game.state.actions) {
+            this.add_action(user_action);
+        }
+    }
+
+    create_game_display() {
+        this.container.textContent = "";
+        switch (this.game.summary.settings.format) {
+            case "Chatroom":
+                this.game.users = new Set();
+                this.create_chatroom();
+                if (!this.game.users.has(this.auth.username)) {
+                    this.take_action({action: "Join", seat: 0, chips: 0});
+                }
+                break;
         }
     }
 
index 3d9d22a06063557cd9d62e9c722fb30de5b48e67..a661bdcd966d82e270f6a49f39d674389ade1090 100644 (file)
@@ -1,96 +1,22 @@
+@import url("style/login.css");
+@import url("style/game-list.css");
+@import url("style/chatroom.css");
+
 html, body {
     width: 100%;
     height: 100%;
     margin: 0;
+    overflow: hidden;
 }
 
 .hidden {
     display: none !important;
 }
 
-#login-background {
-    background-color: rgba(0, 0, 100, 0.5);
-    width: 100%;
-    height: 100%;
-    position: fixed;
-    display: grid;
-    grid: 1fr 1fr 1fr / 1fr 1fr 1fr;
-}
-
-#login {
-    grid-column: 2 / 3;
-    grid-row: 2 / 3;
-    background-color: skyblue;
-    border-radius: 2vmax;
-    padding: 4vmax;
-    display: grid;
-    grid: 1fr;
-    font-size: 4vmax;
-}
-
-#login > label {
-    font-family: monospace;
-}
-
-#login > input {
-    font-size: inherit;
-    margin-bottom: 1vmax;
-}
-
-#login > button {
-    font-size: inherit;
-    margin-top: 2vmax;
-}
-
-#login > #login-error {
-    font-size: 2vmax;
-    color: red;
-    margin-bottom: 0;
-}
-
 #game-tile-background {
     width: 100%;
     height: 100%;
     display: grid;
     grid: 1fr / 1fr;
-}
-
-.game-list {
-    width: 100%;
-    display: grid;
-    grid-auto-rows: 30vw;
-}
-
-.game-summary {
-    border: 1px solid grey;
-    border-bottom: none;
-    font-family: sans;
-    font-size: 4vw;
-    display: grid;
-    grid: 'id title blank'
-          'format format format'
-          'settings settings settings'
-          'settings settings settings';
-}
-
-.game-id {
-    font-style: italic;
-    color: grey;
-    grid-area: id;
-}
-
-.game-title {
-    text-align: center;
-    font-weight: bold;
-    grid-area: title;
-}
-
-.game-format {
-    text-align: left;
-    margin-left: 5vw;
-    grid-area: format;
-}
-
-.game-settings {
-    grid-area: settings;
+    overflow: hidden;
 }
diff --git a/site/style/chatroom.css b/site/style/chatroom.css
new file mode 100644 (file)
index 0000000..e5846e5
--- /dev/null
@@ -0,0 +1,65 @@
+.chatroom {
+    width: 100%;
+    height: 100%;
+    display: grid;
+    font-size: 1.5rem;
+    overflow: hidden;
+    position: relative;
+    background-color: #dfefff;
+}
+
+.chatroom-chat {
+    width: 100%;
+    height: 100%;
+    overflow-y: scroll;
+}
+
+.chatroom > input {
+    width: 100%;
+    height: 3rem;
+    font-size: inherit;
+    align-self: end;
+}
+
+.chatroom-join, .chatroom-message, .chatroom-leave {
+    display: flex;
+}
+
+.chatroom-username {
+    justify-self: left;
+    width: 12rem;
+    height: 100%;
+    margin: 0.25rem;
+    background-color: grey;
+    color: white;
+    text-align: center;
+    border-radius: 1rem;
+}
+
+.chatroom-join > .chatroom-username {
+    background-color: limegreen;
+}
+
+.chatroom-join > .chatroom-text, .chatroom-leave > .chatroom-text {
+    font-style: italic;
+    color: grey;
+}
+
+.chatroom-leave > .chatroom-username {
+    background-color: red;
+}
+
+.chatroom-text {
+    justify-self: right;
+    text-align: left;
+    height: 100%;
+    margin: 0.25rem;
+    width: calc(100% - 12rem);
+}
+
+.chatroom-close {
+    display: block;
+    position: absolute;
+    right: 0;
+    top: 0;
+}
diff --git a/site/style/game-list.css b/site/style/game-list.css
new file mode 100644 (file)
index 0000000..6780aaf
--- /dev/null
@@ -0,0 +1,69 @@
+.game-list-container {
+    width: 100%;
+    height: 100%;
+    overflow-x: clip;
+    overflow-y: scroll;
+}
+
+.game-list {
+    width: 100%;
+    display: grid;
+    grid-auto-rows: 30vw;
+}
+
+@keyframes new-game {
+    from {
+        background-color: limegreen;
+        transform: translateY(100%);
+    }
+    50% {
+        background-color: limegreen;
+        transform: translateY(0);
+    }
+    to {
+        background-color: inherit;
+        transform: translateY(0);
+    }
+}
+
+.game-summary {
+    border: 1px solid grey;
+    border-top: none;
+    font-family: sans;
+    font-size: 4vw;
+    display: grid;
+    grid: 'id title format'
+          'settings settings settings'
+          'settings settings settings';
+    animation: new-game 2s;
+    cursor: pointer;
+}
+
+.game-summary:hover {
+    background-color: #dfefff;
+}
+
+.game-id {
+    width: 25vw;
+    font-style: italic;
+    color: grey;
+    grid-area: id;
+}
+
+.game-title {
+    text-align: center;
+    font-weight: bold;
+    grid-area: title;
+    justify-self: center;
+}
+
+.game-format {
+    width: 25vw;
+    grid-area: format;
+    justify-self: end;
+    text-align: right;
+}
+
+.game-settings {
+    grid-area: settings;
+}
diff --git a/site/style/login.css b/site/style/login.css
new file mode 100644 (file)
index 0000000..53d4020
--- /dev/null
@@ -0,0 +1,39 @@
+#login-background {
+    background-color: rgba(0, 0, 100, 0.5);
+    width: 100%;
+    height: 100%;
+    position: fixed;
+    display: grid;
+    grid: 1fr 1fr 1fr / 1fr 1fr 1fr;
+}
+
+#login {
+    grid-column: 2 / 3;
+    grid-row: 2 / 3;
+    background-color: skyblue;
+    border-radius: 2vmax;
+    padding: 4vmax;
+    display: grid;
+    grid: 1fr;
+    font-size: 4vmax;
+}
+
+#login > label {
+    font-family: monospace;
+}
+
+#login > input {
+    font-size: inherit;
+    margin-bottom: 1vmax;
+}
+
+#login > button {
+    font-size: inherit;
+    margin-top: 2vmax;
+}
+
+#login > #login-error {
+    font-size: 2vmax;
+    color: red;
+    margin-bottom: 0;
+}
index d50873ab0fbf8c88ecb7415f84d8853ca3db512b..1f066354ff61414375d440c48b0482e8242e6511 100644 (file)
@@ -92,6 +92,17 @@ impl ConnectionState {
     }
 
     pub async fn apply_message(&mut self, message: ClientMessage) -> ServerMessage {
+        let interests_before = self.interests();
+        let response = self.message_response(message).await;
+        let interests_after = self.interests();
+        if interests_before != interests_after {
+            debug!("Client interests changed from {:?} to {:?}", interests_before, interests_after);
+            self.server.register_interests(interests_after).await;
+        }
+        response
+    }
+
+    async fn message_response(&mut self, message: ClientMessage) -> ServerMessage {
         match (&mut self.client, message) {
             (_, ClientMessage::CreateUser{username, auth, nickname}) => {
                 match self.server.create_user(&username, auth, &nickname).await {
@@ -107,14 +118,13 @@ impl ConnectionState {
             (ClientState::LoginAuthIssued{username, challenge}, ClientMessage::LoginAuthResponse{signature}) => {
                 if self.server.verify(&username, &challenge, &signature).await {
                     self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::Idle};
-                    self.server.register_interests(self.interests()).await;
                     ServerMessage::LoginSuccess
                 } else {
                     self.client = ClientState::Connected;
                     ServerMessage::LoginFailure{reason: "Invalid username or password".to_string()}
                 }
             }
-            (ClientState::LoggedIn{username, state: LoggedInState::Idle}, ClientMessage::JoinLobby{filter}) => {
+            (ClientState::LoggedIn{username, ..}, ClientMessage::JoinLobby{filter}) => {
                 let mut game_list = GameList::new(filter);
                 match self.server.update_game_list(&mut game_list).await {
                     Ok(()) => {
@@ -136,7 +146,6 @@ impl ConnectionState {
                     Ok(game) => {
                         let game_view = game.view_for(&username);
                         self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::InGame{game}};
-                        self.server.register_interests(self.interests()).await;
                         ServerMessage::JoinGameSuccess{game: game_view}
                     }
                     Err(err) => ServerMessage::JoinGameFailure{reason: err.to_string()},
@@ -166,17 +175,14 @@ impl ConnectionState {
             }
             (ClientState::LoggedIn{username, state: LoggedInState::InLobby{..}}, ClientMessage::LeaveLobby) => {
                 self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::Idle};
-                self.server.register_interests(self.interests()).await;
                 ServerMessage::LeaveGameSuccess
             }
             (ClientState::LoggedIn{username, state: LoggedInState::InGame{..}}, ClientMessage::LeaveGame) => {
                 self.client = ClientState::LoggedIn{username: username.clone(), state: LoggedInState::Idle};
-                self.server.register_interests(self.interests()).await;
                 ServerMessage::LeaveGameSuccess
             }
             (ClientState::LoggedIn{..}, ClientMessage::Logout) => {
                 self.client = ClientState::Connected;
-                self.server.register_interests(self.interests()).await;
                 ServerMessage::LogoutSuccess
             }
             (ClientState::LoggedIn{username, ..}, ClientMessage::ChangeAuth{auth}) => {
index 39e87e507d4dd776f85aded3441bfdf2f7a93e0e..27fb131077de3a1c046be3f2e9eb7bceab58ec31 100644 (file)
@@ -7,7 +7,10 @@ use crate::gamestate::GameState;
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(tag = "format")]
 pub enum GameSettings {
-    Chatroom {max_users: u32},
+    Chatroom {
+        title: String,
+        max_users: u32,
+    },
     TexasHoldEm {
         // TODO
     },
@@ -156,7 +159,7 @@ impl Game {
     pub fn verify(&self, UserAction{ref username, ref action}: &UserAction) -> Result<(), ActionError> {
         debug!("Verifying action: UserAction {{ username: {:?}, action: {:?} }}", username, action);
         match self.summary.settings {
-            GameSettings::Chatroom{max_users} => match action {
+            GameSettings::Chatroom{max_users, ..} => match action {
                 Action::Join{seat: 0, chips: 0} if self.state.user_has_joined(username) => Err(ActionError::AlreadyJoined),
                 Action::Join{seat: 0, chips: 0} if self.state.num_players() + 1 > max_users => Err(ActionError::NoSeatAvailable),
                 Action::Join{seat: 0, chips: 0} => Ok(()),