summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2022-06-08 21:53:40 +0200
committerTor Andersson <tor@ccxvii.net>2022-06-09 17:42:23 +0200
commitff4bc953f16002befbd6cf8dd120a96cfeca26fe (patch)
tree0aef045b9338a3bb1ac522fefb557bf8f54d746b
parent68df9a26cda411be54403883baa752c38e15e797 (diff)
downloadserver-ff4bc953f16002befbd6cf8dd120a96cfeca26fe.tar.gz
Allow users to leave and join active games.
Add "Need replacement" list of games.
-rw-r--r--public/join.js211
-rw-r--r--schema.sql22
-rw-r--r--server.js91
-rw-r--r--views/games_active.pug4
-rw-r--r--views/games_public.pug4
-rw-r--r--views/info.pug10
-rw-r--r--views/join.pug2
7 files changed, 189 insertions, 155 deletions
diff --git a/public/join.js b/public/join.js
index eef3833..2703f97 100644
--- a/public/join.js
+++ b/public/join.js
@@ -1,157 +1,192 @@
-"use strict";
+"use strict"
-let start_status = game.status;
-let evtsrc = null;
-let timer = 0;
+let start_status = game.status
+let evtsrc = null
+let timer = 0
-function confirm_delete(status) {
- let warning = "Are you sure you want to DELETE this game?";
+function confirm_delete() {
+ let warning = `Are you sure you want to DELETE this game?`
if (window.confirm(warning))
- window.location.href = "/delete/" + game.game_id;
+ window.location.href = "/delete/" + game.game_id
}
-let blink_title = document.title;
-let blink_timer = 0;
+function post(url) {
+ fetch(url, { method: "POST" })
+ .then(r => r.text())
+ .then(t => window.error.textContent = (t === "SUCCESS") ? "" : t)
+ .catch(e => window.error.textContent = e)
+ start_event_source()
+}
+
+function start() {
+ post(`/start/${game.game_id}`)
+}
+
+function join(role) {
+ post(`/join/${game.game_id}/${role}`)
+}
+
+function part(role) {
+ let warning = "Are you sure you want to LEAVE this game?"
+ if (game.status === 0 || window.confirm(warning))
+ post(`/part/${game.game_id}/${role}`)
+}
+
+function kick(role) {
+ let player = players.find(p => p.role === role)
+ let warning = `Are you sure you want to KICK player ${player.name} (${role}) from this game?`
+ if (game.status === 0 || window.confirm(warning))
+ post(`/part/${game.game_id}/${role}`)
+}
+
+let blink_title = document.title
+let blink_timer = 0
function start_blinker(message) {
- let tick = false;
+ let tick = false
if (blink_timer)
- stop_blinker();
+ stop_blinker()
if (!document.hasFocus()) {
- document.title = message;
+ document.title = message
blink_timer = setInterval(function () {
- document.title = tick ? message : blink_title;
- tick = !tick;
- }, 1000);
+ document.title = tick ? message : blink_title
+ tick = !tick
+ }, 1000)
}
}
function stop_blinker() {
- document.title = blink_title;
- clearInterval(blink_timer);
- blink_timer = 0;
+ document.title = blink_title
+ clearInterval(blink_timer)
+ blink_timer = 0
}
-window.addEventListener("focus", stop_blinker);
-
-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;
-}
+window.addEventListener("focus", stop_blinker)
function start_event_source() {
if (!evtsrc || evtsrc.readyState === 2) {
- console.log("STARTING EVENT SOURCE");
- evtsrc = new EventSource("/join-events/" + game.game_id);
+ 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();
- });
+ 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();
- });
+ 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);
+ console.log("GAME:", evt.data)
+ game = JSON.parse(evt.data)
if (game.status > 1) {
- clearInterval(timer);
- evtsrc.close();
+ clearInterval(timer)
+ evtsrc.close()
}
- update();
- });
+ update()
+ })
evtsrc.addEventListener("deleted", function (evt) {
- console.log("DELETED");
- window.location.href = '/' + game.title_id;
- });
+ console.log("DELETED")
+ window.location.href = '/' + game.title_id
+ })
evtsrc.onerror = function (err) {
- window.message.innerHTML = "Disconnected from server...";
- };
+ window.message.innerHTML = "Disconnected from server..."
+ }
window.addEventListener('beforeunload', function (evt) {
- evtsrc.close();
- });
+ evtsrc.close()
+ })
}
}
function is_active(player, role) {
- return (game.active === role || game.active === "Both" || game.active === "All");
+ return (game.active === role || game.active === "Both" || game.active === "All")
}
function is_solo() {
- return players.every(p => p.user_id === players[0].user_id);
+ return players.every(p => p.user_id === players[0].user_id)
}
function update() {
for (let i = 0; i < roles.length; ++i) {
- let role = roles[i];
- let role_id = "role_" + role.replace(/ /g, "_");
+ let role = roles[i]
+ let role_id = "role_" + role.replace(/ /g, "_")
if (game.is_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);
+ 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 (is_active(player, role))
- element.classList.add("is_active");
+ switch (game.status) {
+ case 2:
+ if (player.user_id === user_id)
+ element.innerHTML = `<a href="/${game.title_id}/play:${game.game_id}:${role}">${player.name}</a>`
else
- element.classList.remove("is_active");
+ element.innerHTML = player.name
+ break
+ case 1:
+ element.classList.toggle("is_active", is_active(player, role))
if (player.user_id === user_id)
- element.innerHTML = `<a href="/${game.title_id}/play:${game.game_id}:${role}">Play</a>`;
+ element.innerHTML = `<a class="red" href="javascript:part('${role}')">\u274c</a> <a href="/${game.title_id}/play:${game.game_id}:${role}">${player.name}</a>`
+ else if (game.owner_id === user_id)
+ element.innerHTML = `<a class="red" href="javascript:kick('${role}')">\u274c</a> ${player.name}`
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}`;
+ element.innerHTML = player.name
+ break
+ case 0:
+ if (player.user_id === user_id)
+ element.innerHTML = `<a class="red" href="javascript:part('${role}')">\u274c</a> ${player.name}`
+ else if (game.owner_id === user_id)
+ element.innerHTML = `<a class="red" href="javascript:kick('${role}')">\u274c</a> ${player.name}`
else
- element.innerHTML = player.name;
+ element.innerHTML = player.name
+ break
}
} else {
- if (game.status === 0)
- element.innerHTML = `<a class="join" href="javascript:send('/join/${game.game_id}/${role}')">Join</a>`;
- else
- element.innerHTML = "<i>Empty</i>";
+ switch (game.status) {
+ case 2:
+ element.innerHTML = `<i>Empty</i>`
+ break
+ case 1:
+ case 0:
+ element.innerHTML = `<a class="join" href="javascript:join('${role}')">Join</a>`
+ break
+ }
}
}
- let message = window.message;
+ let message = window.message
if (game.status === 0) {
if (ready && (game.owner_id === user_id))
- message.innerHTML = "Ready to start...";
+ message.innerHTML = "Ready to start..."
else if (ready)
- message.innerHTML = `Waiting for ${game.owner_name} to start the game...`;
+ message.innerHTML = `Waiting for ${game.owner_name} to start the game...`
else
- message.innerHTML = "Waiting for players to join...";
+ message.innerHTML = "Waiting for players to join..."
} else {
- message.innerHTML = `<a href="/${game.title_id}/play:${game.game_id}">Observe</a>`;
+ message.innerHTML = `<a href="/${game.title_id}/play:${game.game_id}">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 || is_solo()) ? "" : "hide";
+ window.start_button.disabled = !ready
+ window.start_button.classList = (game.status === 0) ? "" : "hide"
+ window.delete_button.classList = (game.status === 0 || is_solo()) ? "" : "hide"
if (game.status === 0 && ready)
- start_blinker("READY TO START");
+ start_blinker("READY TO START")
else
- stop_blinker();
+ stop_blinker()
} else {
if (start_status === 0 && game.status === 1)
- start_blinker("STARTED");
+ start_blinker("STARTED")
else
- stop_blinker();
+ stop_blinker()
}
}
window.onload = function () {
- update();
+ update()
if (game.status < 2) {
- start_event_source();
- timer = setInterval(start_event_source, 15000);
+ start_event_source()
+ timer = setInterval(start_event_source, 15000)
}
}
diff --git a/schema.sql b/schema.sql
index 14912d0..a5ec561 100644
--- a/schema.sql
+++ b/schema.sql
@@ -387,28 +387,6 @@ create view your_turn as
and active in ('All', 'Both', role)
;
--- Triggers --
-
-drop trigger if exists no_part_on_active_game;
-create trigger no_part_on_active_game before delete on players
-begin
- select
- raise(abort, 'Cannot remove players from started games.')
- where
- (select status from games where games.game_id = old.game_id) > 0
- ;
-end;
-
-drop trigger if exists no_join_on_active_game;
-create trigger no_join_on_active_game before insert on players
-begin
- select
- raise(abort, 'Cannot add players to started games.')
- where
- (select status from games where games.game_id = new.game_id) > 0
- ;
-end;
-
-- Manual key management if pragma foreign_keys = off
drop trigger if exists trigger_delete_on_games;
create trigger trigger_delete_on_games after delete on games
diff --git a/server.js b/server.js
index 7d40234..1210e62 100644
--- a/server.js
+++ b/server.js
@@ -867,6 +867,26 @@ let RULES = {};
let HTML_ABOUT = {};
let HTML_CREATE = {};
+function is_open_game(game) {
+ return game.status === 0 && !game.is_ready;
+}
+
+function is_ready_game(game) {
+ return game.status === 0 && game.is_ready;
+}
+
+function is_replacement_game(game) {
+ return game.status === 1 && !game.is_ready;
+}
+
+function is_active_game(game) {
+ return game.status === 1 && game.is_ready;
+}
+
+function is_finished_game(game) {
+ return game.status === 2;
+}
+
function load_rules() {
const SQL_SELECT_TITLES = SQL("SELECT * FROM titles");
for (let title of SQL_SELECT_TITLES.all()) {
@@ -944,16 +964,16 @@ const SQL_INSERT_REMATCH = SQL(`
)
`);
-const QUERY_LIST_GAMES = SQL(`
+const QUERY_LIST_PUBLIC_GAMES = SQL(`
SELECT * FROM game_view
- WHERE is_private=0 AND status=?
+ WHERE is_private=0 AND status < 2
AND EXISTS ( SELECT 1 FROM players WHERE players.game_id = game_view.game_id )
ORDER BY mtime DESC
`);
const QUERY_LIST_GAMES_OF_TITLE = SQL(`
SELECT * FROM game_view
- WHERE is_private=0 AND title_id=? AND status=?
+ WHERE is_private=0 AND title_id=? AND status>=? AND status<=?
AND EXISTS ( SELECT 1 FROM players WHERE players.game_id = game_view.game_id )
ORDER BY mtime DESC
LIMIT ?
@@ -1039,12 +1059,8 @@ function annotate_game(game, user_id) {
function annotate_games(games, user_id) {
for (let i = 0; i < games.length; ++i) {
let game = games[i];
- if (game.status === 0) {
- let players = SQL_SELECT_PLAYERS_JOIN.all(game.game_id);
- game.is_ready = RULES[game.title_id].ready(game.scenario, JSON.parse(game.options), players);
- } else {
- game.is_ready = false;
- }
+ let players = SQL_SELECT_PLAYERS_JOIN.all(game.game_id);
+ game.is_ready = RULES[game.title_id].ready(game.scenario, JSON.parse(game.options), players);
annotate_game(game, user_id);
}
}
@@ -1063,23 +1079,19 @@ app.get('/games', function (req, res) {
});
app.get('/games/active', must_be_logged_in, function (req, res) {
- req.user.notify = SQL_SELECT_USER_NOTIFY.get(req.user.user_id);
let games = QUERY_LIST_ACTIVE_GAMES_OF_USER.all({user_id: req.user.user_id});
annotate_games(games, req.user.user_id);
- let open_games = games.filter(game => game.status === 0);
- let active_games = games.filter(game => game.status === 1);
- let finished_games = games.filter(game => game.status === 2);
res.render('games_active.pug', {
user: req.user,
- open_games: open_games.filter(g => !g.is_ready),
- ready_games: open_games.filter(g => g.is_ready),
- active_games: active_games,
- finished_games: finished_games,
+ open_games: games.filter(is_open_game),
+ replacement_games: games.filter(is_replacement_game),
+ ready_games: games.filter(is_ready_game),
+ active_games: games.filter(is_active_game),
+ finished_games: games.filter(is_finished_game),
});
});
app.get('/games/finished', must_be_logged_in, function (req, res) {
- req.user.notify = SQL_SELECT_USER_NOTIFY.get(req.user.user_id);
let games = QUERY_LIST_FINISHED_GAMES_OF_USER.all({user_id: req.user.user_id});
annotate_games(games, req.user.user_id);
res.render('games_finished.pug', {
@@ -1089,20 +1101,18 @@ app.get('/games/finished', must_be_logged_in, function (req, res) {
});
app.get('/games/public', function (req, res) {
- let open_games = QUERY_LIST_GAMES.all(0);
- let active_games = QUERY_LIST_GAMES.all(1);
- if (req.user) {
- annotate_games(open_games, req.user.user_id);
- annotate_games(active_games, req.user.user_id);
- } else {
- annotate_games(open_games, 0);
- annotate_games(active_games, 0);
- }
+ let games = QUERY_LIST_PUBLIC_GAMES.all()
+ if (req.user)
+ annotate_games(games, req.user.user_id);
+ else
+ annotate_games(games, 0);
res.render('games_public.pug', {
user: req.user,
- open_games: open_games.filter(g => !g.is_ready),
- ready_games: open_games.filter(g => g.is_ready),
- active_games: active_games,
+ open_games: games.filter(is_open_game),
+ replacement_games: games.filter(is_replacement_game),
+ ready_games: games.filter(is_ready_game),
+ active_games: games.filter(is_active_game),
+ finished_games: games.filter(is_finished_game),
});
});
@@ -1114,19 +1124,18 @@ function get_title_page(req, res, title_id) {
let title = TITLES[title_id];
if (!title)
return res.status(404).send("Invalid title.");
- let open_games = QUERY_LIST_GAMES_OF_TITLE.all(title_id, 0, 1000);
- let active_games = QUERY_LIST_GAMES_OF_TITLE.all(title_id, 1, 1000);
- let finished_games = QUERY_LIST_GAMES_OF_TITLE.all(title_id, 2, 50);
- annotate_games(open_games, req.user ? req.user.user_id : 0);
+ let active_games = QUERY_LIST_GAMES_OF_TITLE.all(title_id, 0, 1, 1000);
+ let finished_games = QUERY_LIST_GAMES_OF_TITLE.all(title_id, 2, 2, 50);
annotate_games(active_games, req.user ? req.user.user_id : 0);
annotate_games(finished_games, req.user ? req.user.user_id : 0);
res.render('info.pug', {
user: req.user,
title: title,
about_html: HTML_ABOUT[title_id],
- open_games: open_games.filter(g => !g.is_ready),
- ready_games: open_games.filter(g => g.is_ready),
- active_games: active_games,
+ open_games: active_games.filter(is_open_game),
+ replacement_games: active_games.filter(is_replacement_game),
+ ready_games: active_games.filter(is_ready_game),
+ active_games: active_games.filter(is_active_game),
finished_games: finished_games,
});
}
@@ -1323,12 +1332,12 @@ app.get('/join-events/:game_id', must_be_logged_in, function (req, res) {
res.flush();
});
-app.get('/join/:game_id/:role', must_be_logged_in, function (req, res) {
+app.post('/join/:game_id/:role', must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0;
let role = req.params.role;
let game = SQL_SELECT_GAME.get(game_id);
let roles = get_game_roles(game.title_id, game.scenario, game.options);
- if (game.is_random) {
+ if (game.is_random && game.status === 0) {
let m = role.match(/^Random (\d+)$/);
if (!m || Number(m[1]) < 1 || Number(m[1]) > roles.length)
return res.status(404).send("Invalid role.");
@@ -1345,7 +1354,7 @@ app.get('/join/:game_id/:role', must_be_logged_in, function (req, res) {
}
});
-app.get('/part/:game_id/:role', must_be_logged_in, function (req, res) {
+app.post('/part/:game_id/:role', must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0;
let role = req.params.role;
SQL_DELETE_PLAYER_ROLE.run(game_id, role);
@@ -1391,7 +1400,7 @@ function start_game(game_id, game) {
mail_your_turn_notification_to_offline_users(game_id, null, state.active);
}
-app.get('/start/:game_id', must_be_logged_in, function (req, res) {
+app.post('/start/:game_id', must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0;
let game = SQL_SELECT_GAME.get(game_id);
if (game.owner_id !== req.user.user_id)
diff --git a/views/games_active.pug b/views/games_active.pug
index 342c673..49d20d8 100644
--- a/views/games_active.pug
+++ b/views/games_active.pug
@@ -19,6 +19,10 @@ html
h2 Open
+gametable(0,open_games)
+ if replacement_games.length > 0
+ h2 Need replacement
+ +gametable(0, replacement_games)
+
if active_games.length > 0
h2 Active
+gametable(1,active_games)
diff --git a/views/games_public.pug b/views/games_public.pug
index 267c8f7..4af36bd 100644
--- a/views/games_public.pug
+++ b/views/games_public.pug
@@ -14,6 +14,10 @@ html
h2 Open
+gametable(0, open_games)
+ if replacement_games.length > 0
+ h2 Need replacement
+ +gametable(0, replacement_games)
+
if ready_games.length > 0
h2 Ready to start
+gametable(0, ready_games)
diff --git a/views/info.pug b/views/info.pug
index 47367ab..785ee91 100644
--- a/views/info.pug
+++ b/views/info.pug
@@ -19,9 +19,13 @@ html
p Read more about the game on #[a(href="https://boardgamegeek.com/boardgame/"+title.bgg) boardgamegeek.com].
- h2 Open games
+ h2 Open
+gametable(0,open_games,1)
+ if replacement_games.length > 0
+ h2 Need replacement
+ +gametable(0, replacement_games)
+
p
a(href="/create/"+title.title_id) Create a new game
@@ -30,9 +34,9 @@ html
+gametable(0,ready_games,1)
if active_games.length > 0
- h2 Active games
+ h2 Active
+gametable(1,active_games,1)
if finished_games.length > 0
- h2 Finished games
+ h2 Finished
+gametable(2,finished_games,1)
diff --git a/views/join.pug b/views/join.pug
index 8232645..e195bf9 100644
--- a/views/join.pug
+++ b/views/join.pug
@@ -58,4 +58,4 @@ html
p
button.hide#delete_button(onclick="confirm_delete()") Delete
- button.hide#start_button(onclick=`javascript:send('/start/${game.game_id}')` disabled) Start
+ button.hide#start_button(onclick="start()" disabled) Start