From ff4bc953f16002befbd6cf8dd120a96cfeca26fe Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Wed, 8 Jun 2022 21:53:40 +0200 Subject: Allow users to leave and join active games. Add "Need replacement" list of games. --- public/join.js | 211 ++++++++++++++++++++++++++++--------------------- schema.sql | 22 ------ server.js | 91 +++++++++++---------- views/games_active.pug | 4 + views/games_public.pug | 4 + views/info.pug | 10 ++- views/join.pug | 2 +- 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 = `${player.name}` 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 = `Play`; + element.innerHTML = `\u274c ${player.name}` + else if (game.owner_id === user_id) + element.innerHTML = `\u274c ${player.name}` else - element.innerHTML = player.name; - } else { - if ((player.user_id === user_id) || (game.owner_id === user_id)) - element.innerHTML = `\u274c ${player.name}`; + element.innerHTML = player.name + break + case 0: + if (player.user_id === user_id) + element.innerHTML = `\u274c ${player.name}` + else if (game.owner_id === user_id) + element.innerHTML = `\u274c ${player.name}` else - element.innerHTML = player.name; + element.innerHTML = player.name + break } } else { - if (game.status === 0) - element.innerHTML = `Join`; - else - element.innerHTML = "Empty"; + switch (game.status) { + case 2: + element.innerHTML = `Empty` + break + case 1: + case 0: + element.innerHTML = `Join` + 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 = `Observe`; + message.innerHTML = `Observe` } 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 -- cgit v1.2.3