summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2021-06-25 17:41:05 +0200
committerTor Andersson <tor@ccxvii.net>2021-06-25 23:53:13 +0200
commitad4a4ea757cce61cde102be2aea2d07fe55aaa2c (patch)
tree2d1878b5e9b2a1a887a73bdcd793a6a1025786d3
parent3009c951931002c9021d76a1c221e3013f0cd046 (diff)
downloadserver-ad4a4ea757cce61cde102be2aea2d07fe55aaa2c.tar.gz
New and improved join page with server sent events.
-rw-r--r--public/join.js109
-rw-r--r--public/style.css1
-rw-r--r--server.js121
-rw-r--r--views/create.ejs2
-rw-r--r--views/join.ejs93
5 files changed, 231 insertions, 95 deletions
diff --git a/public/join.js b/public/join.js
new file mode 100644
index 0000000..96ca321
--- /dev/null
+++ b/public/join.js
@@ -0,0 +1,109 @@
+"use strict";
+
+let evtsrc = null;
+
+function confirm_delete(status) {
+ let warning = "Are you sure you want to DELETE this game?";
+ if (window.confirm(warning))
+ window.open("/delete/" + game.game_id);
+}
+
+function send(url) {
+ fetch(url)
+ .then(r => r.text())
+ .then(t => window.error.textContent = (t === "SUCCESS") ? "" : t)
+ .catch(e => window.error.textContent = e );
+ start_event_source();
+ return void 0;
+}
+
+function start_event_source() {
+ if (!evtsrc || evtsrc.readyState === 2) {
+ console.log("STARTING EVENT SOURCE");
+ evtsrc = new EventSource("/join-events/" + game.game_id);
+ evtsrc.addEventListener("players", function (evt) {
+ console.log("PLAYERS:", evt.data);
+ players = JSON.parse(evt.data);
+ update();
+ });
+ evtsrc.addEventListener("ready", function (evt) {
+ console.log("READY:", evt.data);
+ ready = JSON.parse(evt.data);
+ update();
+ });
+ evtsrc.addEventListener("game", function (evt) {
+ console.log("GAME:", evt.data);
+ game = JSON.parse(evt.data);
+ if (game.status > 1)
+ evtsrc.close();
+ update();
+ });
+ evtsrc.onerror = function (err) {
+ window.message.innerHTML = "Disconnected from server...";
+ };
+ }
+}
+
+function update() {
+ window.game_status.textContent = ["Open","Active","Finished","Abandoned"][game.status];
+ window.game_result.textContent = game.result || "\u2014";
+
+ for (let i = 0; i < roles.length; ++i) {
+ let role = roles[i];
+ let role_id = "role_" + role.replace(/ /g, "_");
+ if (game.random && game.status === 0)
+ role = "Random " + (i+1);
+ document.getElementById(role_id + "_name").textContent = role;
+ let player = players.find(p => p.role === role);
+ let element = document.getElementById(role_id);
+ if (player) {
+ if (game.status > 0) {
+ if (game.active === role || game.active === "Both" || game.active === "All")
+ element.className = "your_turn";
+ else
+ element.className = "";
+ if (player.user_id === user_id)
+ element.innerHTML = `<a href="/play/${game.game_id}/${role}">Play</a>`;
+ else
+ element.innerHTML = player.name;
+ } else {
+ if ((player.user_id === user_id) || (game.owner_id === user_id))
+ element.innerHTML = `<a class="red" href="javascript:send('/part/${game.game_id}/${role}')">\u274c</a> ${player.name}`;
+ else
+ element.innerHTML = player.name;
+ }
+ } else {
+ if (game.status === 0)
+ //element.innerHTML = `<a class="join" href="javascript:send('/join/${game.game_id}/${role}')">Join</a>`;
+ element.innerHTML = `<a class="join" onclick="send('/join/${game.game_id}/${role}')" href="javascript:void 0">Join</a>`;
+ else
+ element.innerHTML = "<i>Empty</i>";
+ }
+ }
+
+ let message = window.message;
+ if (game.status === 0) {
+ if (ready && (game.owner_id === user_id))
+ message.innerHTML = "Ready to start...";
+ else if (ready)
+ message.innerHTML = "Waiting for game to start...";
+ else
+ message.innerHTML = "Waiting for players to join...";
+ } else {
+ message.innerHTML = `<a class="play" href="/play/${game.game_id}/Observer">Observe</a>`;
+ }
+
+ if (game.owner_id === user_id) {
+ window.start_button.disabled = !ready;
+ window.start_button.classList = (game.status === 0) ? "" : "hide";
+ window.delete_button.classList = (game.status === 0 || solo) ? "" : "hide";
+ }
+}
+
+window.onload = function () {
+ update();
+ if (game.status < 2) {
+ start_event_source();
+ setInterval(start_event_source, 15000);
+ }
+}
diff --git a/public/style.css b/public/style.css
index 157661b..0503574 100644
--- a/public/style.css
+++ b/public/style.css
@@ -44,6 +44,7 @@ button, select {
margin: 5px 10px 5px 0;
padding: 1px 10px;
background-color: gainsboro;
+ vertical-align: top;
}
button:disabled {
color: gray;
diff --git a/server.js b/server.js
index fbad44b..5540833 100644
--- a/server.js
+++ b/server.js
@@ -632,7 +632,7 @@ const QUERY_ROLE_FROM_GAME_AND_USER = db.prepare("SELECT role FROM players WHERE
const QUERY_IS_SOLO = db.prepare("SELECT COUNT(DISTINCT user_id) = 1 FROM players WHERE game_id = ?").pluck();
const QUERY_JOIN_GAME = db.prepare("INSERT INTO players (user_id, game_id, role) VALUES (?,?,?)");
-const QUERY_PART_GAME = db.prepare("DELETE FROM players WHERE game_id = ? AND user_id = ? AND role = ?");
+const QUERY_PART_GAME = db.prepare("DELETE FROM players WHERE game_id = ? AND role = ?");
const QUERY_START_GAME = db.prepare("UPDATE games SET status = 1, state = ?, active = ? WHERE game_id = ?");
const QUERY_CREATE_GAME = db.prepare(`
INSERT INTO games
@@ -829,6 +829,39 @@ app.get('/rematch/:old_game_id', must_be_logged_in, function (req, res) {
}
});
+let join_clients = {};
+
+function update_join_clients_players(game_id) {
+ let list = join_clients[game_id];
+ if (list) {
+ console.log("UPDATE JOIN PLAYERS", game_id, list.length)
+ let players = QUERY_PLAYERS.all(game_id);
+ let ready = RULES[list.title_id].ready(list.scenario, players);
+ for (let res of list) {
+ console.log("PUSH JOIN PLAYERS", game_id);
+ res.write("retry: 10000\n");
+ res.write("event: players\n");
+ res.write("data: " + JSON.stringify(players) + "\n\n");
+ res.write("event: ready\n");
+ res.write("data: " + ready + "\n\n");
+ }
+ }
+}
+
+function update_join_clients_game(game_id) {
+ let list = join_clients[game_id];
+ if (list && list.length > 0) {
+ console.log("UPDATE JOIN GAME", game_id, list.length)
+ let game = QUERY_GAME.get(game_id);
+ for (let res of list) {
+ console.log("PUSH JOIN GAME", game_id);
+ res.write('retry: 10000\n');
+ res.write('event: game\n');
+ res.write('data: ' + JSON.stringify(game) + '\n\n');
+ }
+ }
+}
+
app.get('/join/:game_id', must_be_logged_in, function (req, res) {
LOG(req, "GET /join/" + req.params.game_id);
let game_id = req.params.game_id | 0;
@@ -838,10 +871,8 @@ app.get('/join/:game_id', must_be_logged_in, function (req, res) {
return res.redirect('/');
}
let roles = QUERY_ROLES.all(game.title_id);
- if (game.random && game.status == 0)
- for (let i = 0; i < roles.length; ++i)
- roles[i] = "Random " + (i+1);
let players = QUERY_PLAYERS.all(game_id);
+ let ready = (game.status == 0) && RULES[game.title_id].ready(game.scenario, players);
res.set("Cache-Control", "no-store");
res.render('join.ejs', {
user: req.user,
@@ -849,34 +880,70 @@ app.get('/join/:game_id', must_be_logged_in, function (req, res) {
roles: roles,
players: players,
solo: players.every(p => p.user_id == req.user.user_id),
+ ready: players.length == roles.length,
message: req.flash('message')
});
});
+app.get('/join-events/:game_id', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /join-events/" + req.params.game_id);
+ let game_id = req.params.game_id | 0;
+ let players = QUERY_PLAYERS.all(game_id);
+ let game = QUERY_GAME.get(game_id);
+
+ res.setHeader("Cache-Control", "no-store");
+ res.setHeader("Content-Type", "text/event-stream");
+ res.setHeader("Connection", "keep-alive");
+
+ if (!game)
+ return res.end();
+ if (!(game_id in join_clients)) {
+ join_clients[game_id] = [];
+ join_clients[game_id].title_id = game.title_id;
+ join_clients[game_id].scenario = game.scenario;
+ }
+ join_clients[game_id].push(res);
+
+ res.on('close', err => {
+ console.log("CLOSE JOIN EVENTS", err);
+ let list = join_clients[game_id];
+ let i = list.indexOf(res);
+ if (i >= 0)
+ list.splice(i, 1);
+ });
+
+ res.write("retry: 15000\n\n");
+ res.write("event: game\n");
+ res.write("data: " + JSON.stringify(game) + "\n\n");
+ res.write("event: players\n");
+ res.write("data: " + JSON.stringify(players) + "\n\n");
+});
+
app.get('/join/:game_id/:role', must_be_logged_in, function (req, res) {
LOG(req, "GET /join/" + req.params.game_id + "/" + req.params.role);
let game_id = req.params.game_id | 0;
let role = req.params.role;
try {
QUERY_JOIN_GAME.run(req.user.user_id, game_id, role);
- return res.redirect('/join/'+game_id);
+ update_join_clients_players(game_id);
+ res.send("SUCCESS");
} catch (err) {
- req.flash('message', err.toString());
- return res.redirect('/join/'+game_id);
+ console.log(err);
+ res.send(err.toString());
}
});
-app.get('/part/:game_id/:part_id/:role', must_be_logged_in, function (req, res) {
- LOG(req, "GET /part/" + req.params.game_id + "/" + req.params.part_id + "/" + req.params.role);
+app.get('/part/:game_id/:role', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /part/" + req.params.game_id + "/" + req.params.role);
let game_id = req.params.game_id | 0;
- let part_id = req.params.part_id | 0;
let role = req.params.role;
try {
- QUERY_PART_GAME.run(game_id, part_id, role);
- return res.redirect('/join/'+game_id);
+ QUERY_PART_GAME.run(game_id, role);
+ update_join_clients_players(game_id);
+ res.send("SUCCESS");
} catch (err) {
- req.flash('message', err.toString());
- return res.redirect('/join/'+game_id);
+ console.log(err);
+ res.send(err.toString());
}
});
@@ -900,26 +967,27 @@ app.get('/start/:game_id', must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0;
try {
let game = QUERY_GAME_OWNER.get(game_id, req.user.user_id);
- if (!game) {
- req.flash('message', "Only the game owner can start the game!");
- return res.redirect('/join/'+game_id);
- }
- if (game.status != 0) {
- req.flash('message', "The game is already started!");
- return res.redirect('/join/'+game_id);
- }
+ if (!game)
+ return res.send("Only the game owner can start the game!");
+ if (game.status != 0)
+ return res.send("The game is already started!");
let players = QUERY_PLAYERS.all(game_id);
- if (game.random)
+ if (!RULES[game.title_id].ready(game.scenario, players))
+ return res.send("Invalid player configuration!");
+ if (game.random) {
assign_random_roles(game, players);
+ update_join_clients_players(game_id);
+ }
let state = RULES[game.title_id].setup(game.scenario, players);
QUERY_START_GAME.run(JSON.stringify(state), state.active, game_id);
let is_solo = players.every(p => p.user_id == players[0].user_id);
if (is_solo)
QUERY_UPDATE_GAME_SET_PRIVATE.run(game_id);
- return res.redirect('/join/'+game_id);
+ update_join_clients_game(game_id);
+ res.send("SUCCESS");
} catch (err) {
- req.flash('message', err.toString());
- return res.redirect('/join/'+game_id);
+ console.log(err);
+ res.send(err.toString());
}
});
@@ -1131,6 +1199,7 @@ function put_game_state(game_id, state, old_active) {
QUERY_UPDATE_GAME_STATE.run(JSON.stringify(state), state.active, status, result, game_id);
for (let other of clients[game_id])
send_state(other, state);
+ update_join_clients_game(game_id);
mail_your_turn_notification_to_offline_users(game_id, old_active, state.active);
}
diff --git a/views/create.ejs b/views/create.ejs
index 5db8fe6..b995639 100644
--- a/views/create.ejs
+++ b/views/create.ejs
@@ -1,5 +1,5 @@
<%- include('header', { title: title.title_name }) %>
-<style>form{display:block;margin-left:200px;}</style>
+<style>form{display:block;margin-left:170px;}</style>
<a href="/info/<%= title.title_id %>"><img class="logo" src="/<%= title.title_id %>/cover.jpg"></a>
<form action="/create/<%= title.title_id %>" method="post">
<p>
diff --git a/views/join.ejs b/views/join.ejs
index 2da5f03..46c1e56 100644
--- a/views/join.ejs
+++ b/views/join.ejs
@@ -1,14 +1,23 @@
-<%- include('header', { title: game.title_name, refresh: game.status == 0 ? 15 : 0 }) %>
+<%- include('header', { title: game.title_name }) -%>
+<style>
+th, td { min-width: 10em; font-size: 16px; }
+a.red { text-decoration: none; color: brown; font-size: 15px; }
+.hide { display: none; }
+</style>
<script>
-function confirm_delete(status) {
- let warning = "Are you sure you want to DELETE this game?";
- if (window.confirm(warning))
- window.open("/delete/<%= game.game_id %>");
-}
+let game = <%- JSON.stringify(game) %>;
+let roles = <%- JSON.stringify(roles) %>;
+let players = <%- JSON.stringify(players) %>;
+let user_id = <%- user.user_id %>;
+let solo = <%- solo %>;
+let ready = <%- ready %>;
</script>
+<script src="/join.js"></script>
+<p id="error" class="error"></p>
<a href="/info/<%= game.title_id %>"><img class="logo" src="/<%= game.title_id %>/cover.jpg"></a>
+<div class="info">
<p>
Owner: <%= game.owner_name %>
<br>
@@ -17,74 +26,22 @@ Private: <%= game.private ? "yes" : "no" %>
Scenario: <%= game.scenario %>
<br>
Description: <%= game.description || "No description." %>
+<br>
+Status: <span id="game_status"></span>
+<br>
+Result: <span id="game_result"></span>
<br clear=left>
-<p>
<table>
<tr>
-<%
- roles.forEach((role) => {
- %><th><%= role %><%
- });
-%>
+<% roles.forEach((role) => { %><th id="role_<%= role.replace(/ /g, '_') %>_name"><%= role %></th><% }); %>
+<tr>
+<% roles.forEach((role) => { %><td id="role_<%= role.replace(/ /g, '_') %>">-</td><% }); %>
<tr>
-<%
- roles.forEach((role) => {
- if (game.active == role || game.active == "Both" || game.active == "All") {
- %><td style="min-width:9em" class="your_turn"><%
- } else {
- %><td style="min-width:9em"><%
- }
- let p = players.find(p => p.role == role);
- if (game.status == 0) {
- if (p) {
- if ((p.user_id == user.user_id) || (game.owner_id == user.user_id)) {
- %><a style="color:red;text-decoration:none" href="/part/<%= game.game_id %>/<%= p.user_id %>/<%= p.role %>">&#x274c;</a> <%
- %><%= p.name %><%
- } else {
- %><%= p.name %><%
- }
- } else {
- %><a href="/join/<%= game.game_id %>/<%= role %>">Join</a><%
- }
- } else {
- if (p) {
- if (p.user_id == user.user_id) {
- %><a href="/play/<%= game.game_id %>/<%= p.role %>">Play</a><%
- } else {
- %><%= p.name %><%
- }
- } else {
- %><i>Empty</i><%
- }
- }
- });
- if (game.status > 0 && !players.some(p => p.user_id == user.user_id)) {
- %>
- <tr><td colspan="<%= roles.length %>"><a href="/play/<%= game.game_id %>/Observer">View</a>
- <%
- }
-%>
+<td id="message" colspan="<%= roles.length %>">-</td>
</table>
<p>
-<%
- if (game.status == 0) {
- if (players.length == roles.length) {
- if (game.owner_id == user.user_id) {
- %><form action="/start/<%= game.game_id %>"><button type="submit">Start!</button></form><%
- } else {
- %>Waiting for <%= game.owner_name %> to start the game.<%
- }
- } else {
- %>Waiting for players to join the game.<%
- }
- }
- if (game.status == 2 && players.some(p => p.user_id == user.user_id)) {
- %><form action="/rematch/<%= game.game_id %>"><button type="submit">Rematch</button></form><%
- }
- if (game.owner_id == user.user_id && (game.status == 0 || solo)) {
- %><p><br><button onclick="confirm_delete()">Delete</button><%
- }
-%>
+<button class="hide" id="delete_button" onclick="confirm_delete()">Delete</button>
+<button class="hide" id="start_button" onclick="javascript:send('/start/<%= game.game_id %>')" disabled>Start!</button>