summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/common/client.js702
-rw-r--r--public/common/grid.css17
-rw-r--r--public/join.js5
-rw-r--r--server.js63
-rw-r--r--views/head.pug13
5 files changed, 542 insertions, 258 deletions
diff --git a/public/common/client.js b/public/common/client.js
index 01b6c05..d9a1f48 100644
--- a/public/common/client.js
+++ b/public/common/client.js
@@ -1,28 +1,25 @@
"use strict";
-/* global io, on_update */
+/* URL: /$title_id/(re)play:$game_id:$role */
-/* URL: /$title_id/play:$game_id:$role */
-if (!/\/[\w-]+\/play:\d+(:[\w-]+)?/.test(window.location.pathname)) {
+if (!/\/[\w-]+\/(re)?play:\d+(:[\w-]+)?/.test(window.location.pathname)) {
document.getElementById("prompt").textContent = "Invalid game ID.";
throw Error("Invalid game ID.");
}
-const param_title_id = window.location.pathname.split("/")[1];
-const param_game_id = decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[1] | 0;
-const param_role = decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[2] || "Observer";
+let params = {
+ mode: window.location.pathname.split("/")[2].split(":")[0],
+ title_id: window.location.pathname.split("/")[1],
+ game_id: decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[1] | 0,
+ role: decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[2] || "Observer",
+}
-let game = null;
-let game_over = false;
-let player = null;
-let socket = null;
+let roles = Array.from(document.querySelectorAll(".role")).map(x=>({id:x.id,role:x.id.replace(/^role_/,"").replace(/_/g," ")}));
-let chat_is_visible = false;
-let chat_text = null;
-let chat_key = null;
-let chat_last_day = null;
-let chat_log = 0;
-let chat_seen = 0;
+let view = null;
+let player = "Observer";
+let socket = null;
+let chat = null;
function scroll_with_middle_mouse(panel_sel, multiplier) {
let panel = document.querySelector(panel_sel);
@@ -35,8 +32,8 @@ function scroll_with_middle_mouse(panel_sel, multiplier) {
down_y = e.clientY;
scroll_x = panel.scrollLeft;
scroll_y = panel.scrollTop;
- window.addEventListener('mousemove', mm);
- window.addEventListener('mouseup', mu);
+ window.addEventListener("mousemove", mm);
+ window.addEventListener("mouseup", mu);
e.preventDefault();
}
}
@@ -49,12 +46,12 @@ function scroll_with_middle_mouse(panel_sel, multiplier) {
}
function mu(e) {
if (e.button === 1) {
- window.removeEventListener('mousemove', mm);
- window.removeEventListener('mouseup', mu);
+ window.removeEventListener("mousemove", mm);
+ window.removeEventListener("mouseup", mu);
e.preventDefault();
}
}
- panel.addEventListener('mousedown', md);
+ panel.addEventListener("mousedown", md);
}
function drag_element_with_mouse(element_sel, grabber_sel) {
@@ -65,8 +62,8 @@ function drag_element_with_mouse(element_sel, grabber_sel) {
if (e.button === 0) {
save_x = e.clientX;
save_y = e.clientY;
- window.addEventListener('mousemove', mm);
- window.addEventListener('mouseup', mu);
+ window.addEventListener("mousemove", mm);
+ window.addEventListener("mouseup", mu);
e.preventDefault();
}
}
@@ -81,12 +78,12 @@ function drag_element_with_mouse(element_sel, grabber_sel) {
}
function mu(e) {
if (e.button === 0) {
- window.removeEventListener('mousemove', mm);
- window.removeEventListener('mouseup', mu);
+ window.removeEventListener("mousemove", mm);
+ window.removeEventListener("mouseup", mu);
e.preventDefault();
}
}
- grabber.addEventListener('mousedown', md);
+ grabber.addEventListener("mousedown", md);
}
/* TITLE BLINKER */
@@ -115,7 +112,18 @@ function stop_blinker() {
window.addEventListener("focus", stop_blinker);
-function load_chat() {
+/* CHAT */
+
+function init_chat() {
+ // only fetch new messages when we reconnect!
+ if (chat !== null) {
+ console.log("RECONNECT CHAT");
+ socket.emit("getchat", chat.log);
+ return;
+ }
+
+ console.log("CONNECT CHAT");
+
let chat_window = document.createElement("div");
chat_window.id = "chat_window";
chat_window.innerHTML = `
@@ -132,15 +140,50 @@ function load_chat() {
chat_button.addEventListener("click", toggle_chat);
document.querySelector("header").insertBefore(chat_button, document.getElementById("prompt"));
- chat_key = "chat/" + param_game_id;
- chat_text = document.getElementById("chat_text");
- chat_last_day = null;
- chat_log = 0;
- chat_seen = window.localStorage.getItem(chat_key) | 0;
+ chat = {
+ is_visible: false,
+ text_element: document.getElementById("chat_text"),
+ key: "chat/" + params.game_id,
+ last_day: null,
+ log: 0
+ }
+
+ chat.seen = window.localStorage.getItem(chat.key) | 0;
+
+ drag_element_with_mouse("#chat_window", "#chat_header");
+
+ document.getElementById("chat_form").addEventListener("submit", e => {
+ let input = document.getElementById("chat_input");
+ e.preventDefault();
+ if (input.value) {
+ socket.emit("chat", input.value);
+ input.value = "";
+ } else {
+ hide_chat();
+ }
+ });
+
+ document.querySelector("body").addEventListener("keydown", e => {
+ if (e.key === "Escape") {
+ if (chat.is_visible) {
+ e.preventDefault();
+ hide_chat();
+ }
+ }
+ if (e.key === "Enter") {
+ let input = document.getElementById("chat_input");
+ if (document.activeElement !== input) {
+ e.preventDefault();
+ show_chat();
+ }
+ }
+ });
+
+ socket.emit("getchat", 0);
}
function save_chat() {
- window.localStorage.setItem(chat_key, chat_log);
+ window.localStorage.setItem(chat.key, chat.log);
}
function update_chat(chat_id, utc_date, user, message) {
@@ -155,38 +198,70 @@ function update_chat(chat_id, utc_date, user, message) {
let line = document.createElement("div");
line.className = "date";
line.textContent = "~ " + date + " ~";
- chat_text.appendChild(line);
+ chat.text_element.appendChild(line);
}
function add_chat_line(time, user, message) {
let line = document.createElement("div");
line.textContent = "[" + time + "] " + user + " \xbb " + message;
- chat_text.appendChild(line);
- chat_text.scrollTop = chat_text.scrollHeight;
+ chat.text_element.appendChild(line);
+ chat.text_element.scrollTop = chat.text_element.scrollHeight;
}
- if (chat_id > chat_log) {
- chat_log = chat_id;
+ if (chat_id > chat.log) {
+ chat.log = chat_id;
let date = new Date(utc_date + "Z");
let day = date.toDateString();
- if (day !== chat_last_day) {
+ if (day !== chat.last_day) {
add_date_line(day);
- chat_last_day = day;
+ chat.last_day = day;
}
add_chat_line(format_time(date), user, message);
}
- if (chat_id > chat_seen) {
+ if (chat_id > chat.seen) {
let button = document.getElementById("chat_button");
start_blinker("NEW MESSAGE");
- if (!chat_is_visible)
+ if (!chat.is_visible)
button.classList.add("new");
else
save_chat();
}
}
-function init_client(roles) {
- game = null;
- player = null;
+function show_chat() {
+ if (!chat.is_visible) {
+ document.getElementById("chat_button").classList.remove("new");
+ document.getElementById("chat_window").classList.add("show");
+ document.getElementById("chat_input").focus();
+ chat.is_visible = true;
+ save_chat();
+ }
+}
+
+function hide_chat() {
+ if (chat.is_visible) {
+ document.getElementById("chat_window").classList.remove("show");
+ document.getElementById("chat_input").blur();
+ chat.is_visible = false;
+ }
+}
+
+function toggle_chat() {
+ if (chat.is_visible)
+ hide_chat();
+ else
+ show_chat();
+}
+
+/* CONNECT TO GAME SERVER */
+
+function init_player_names(players) {
+ for (let i = 0; i < roles.length; ++i) {
+ let sel = "#" + roles[i].id + " .role_user";
+ let p = players.find(p => p.role === roles[i].role);
+ document.querySelector(sel).textContent = p ? p.name : "NONE";
+ }
+}
+function init_play_client() {
const ROLE_SEL = [
".role.one",
".role.two",
@@ -197,162 +272,119 @@ function init_client(roles) {
".role.seven",
];
- const USER_SEL = [
- ".role.one .role_user",
- ".role.two .role_user",
- ".role.three .role_user",
- ".role.four .role_user",
- ".role.five .role_user",
- ".role.six .role_user",
- ".role.seven .role_user",
- ];
-
- load_chat();
-
- console.log("JOINING", param_title_id + "/" + param_game_id + "/" + param_role);
+ console.log("JOINING", params.title_id + "/" + params.game_id + "/" + params.role);
socket = io({
- transports: ['websocket'],
- query: { title: param_title_id, game: param_game_id, role: param_role },
+ transports: ["websocket"],
+ query: { title: params.title_id, game: params.game_id, role: params.role },
});
- socket.on('connect', () => {
+ socket.on("connect", () => {
console.log("CONNECTED");
- document.querySelector("header").classList.remove('disconnected');
- socket.emit('getchat', chat_log); // only send new messages when we reconnect!
+ document.querySelector("header").classList.remove("disconnected");
});
- socket.on('disconnect', () => {
+ socket.on("disconnect", () => {
console.log("DISCONNECTED");
document.getElementById("prompt").textContent = "Disconnected from server!";
- document.querySelector("header").classList.add('disconnected');
+ document.querySelector("header").classList.add("disconnected");
});
- socket.on('roles', (me, players) => {
- console.log("ROLES", me, JSON.stringify(players));
- player = me.replace(/ /g, '_');
- if (player === "Observer")
- document.getElementById("chat_button").style.display = "none";
- document.querySelector("body").classList.add(player);
- for (let i = 0; i < roles.length; ++i) {
- let pr = players.find(p => p.role === roles[i]);
- document.querySelector(USER_SEL[i]).textContent = pr ? pr.name : "NONE";
- }
+ socket.on("roles", (me, players) => {
+ console.log("PLAYERS", me, JSON.stringify(players));
+ player = me;
+ document.querySelector("body").classList.add(player.replace(/ /g, "_"));
+ if (player !== "Observer")
+ init_chat();
+ init_player_names(players);
});
- socket.on('presence', (presence) => {
+ socket.on("presence", (presence) => {
console.log("PRESENCE", JSON.stringify(presence));
for (let i = 0; i < roles.length; ++i) {
- let elt = document.querySelector(ROLE_SEL[i]);
- if (roles[i] in presence)
- elt.classList.add('present');
+ let elt = document.getElementById(roles[i].id);
+ if (roles[i].role in presence)
+ elt.classList.add("present");
else
- elt.classList.remove('present');
+ elt.classList.remove("present");
}
});
- socket.on('state', (new_game, new_game_over) => {
- console.log("STATE", !!new_game.actions, new_game_over);
- game = new_game;
- game_over = new_game_over;
- on_update_bar();
+ socket.on("state", (new_view) => {
+ console.log("STATE");
+ view = new_view;
+ on_update_header();
on_update();
- on_game_over();
on_update_log();
});
- socket.on('save', (msg) => {
+ socket.on("save", (msg) => {
console.log("SAVE");
- window.localStorage[param_title_id + '/save'] = msg;
+ window.localStorage[params.title_id + "/save"] = msg;
});
- socket.on('error', (msg) => {
+ socket.on("error", (msg) => {
console.log("ERROR", msg);
document.getElementById("prompt").textContent = msg;
});
- socket.on('chat', function (item) {
- console.log("CHAT", JSON.stringify(item));
+ socket.on("chat", function (item) {
update_chat(item[0], item[1], item[2], item[3]);
});
-
- document.getElementById("chat_form").addEventListener("submit", e => {
- let input = document.getElementById("chat_input");
- e.preventDefault();
- if (input.value) {
- socket.emit('chat', input.value);
- input.value = '';
- } else {
- hide_chat();
- }
- });
-
- document.querySelector("body").addEventListener("keydown", e => {
- if (player && player !== "Observer") {
- if (e.key === "Escape") {
- if (chat_is_visible) {
- e.preventDefault();
- hide_chat();
- }
- }
- if (e.key === "Enter") {
- let input = document.getElementById("chat_input");
- if (document.activeElement !== input) {
- e.preventDefault();
- show_chat();
- }
- }
- }
- });
-
- drag_element_with_mouse("#chat_window", "#chat_header");
}
+/* HEADER */
+
let is_your_turn = false;
let old_active = null;
-function on_update_bar() {
- document.getElementById("prompt").textContent = game.prompt;
- if (game.actions) {
+function on_update_header() {
+ document.getElementById("prompt").textContent = view.prompt;
+ if (params.mode === "replay")
+ return;
+ if (view.actions) {
document.querySelector("header").classList.add("your_turn");
- if (!is_your_turn || old_active !== game.active)
+ if (!is_your_turn || old_active !== view.active)
start_blinker("YOUR TURN");
is_your_turn = true;
} else {
document.querySelector("header").classList.remove("your_turn");
is_your_turn = false;
}
- old_active = game.active;
+ old_active = view.active;
}
+/* LOG */
+
let create_log_entry = function (text) {
- let p = document.createElement("div");
- p.textContent = text;
- return p;
+ let div = document.createElement("div");
+ div.textContent = text;
+ return div;
}
-let log_scroller = document.getElementById("log");
-
function on_update_log() {
- let parent = document.getElementById("log");
- let to_delete = parent.children.length - game.log_start;
+ let div = document.getElementById("log");
+ let to_delete = div.children.length - view.log_start;
while (to_delete-- > 0)
- parent.removeChild(parent.lastChild);
- for (let entry of game.log)
- parent.appendChild(create_log_entry(entry));
- log_scroller.scrollTop = log_scroller.scrollHeight;
+ div.removeChild(div.lastChild);
+ for (let entry of view.log)
+ div.appendChild(create_log_entry(entry));
+ scroll_log_to_end();
+}
+
+function scroll_log_to_end() {
+ let div = document.getElementById("log");
+ div.scrollTop = div.scrollHeight;
}
try {
- new ResizeObserver(entries => {
- log_scroller.scrollTop = log_scroller.scrollHeight;
- }).observe(log_scroller);
+ new ResizeObserver(scroll_log_to_end).observe(document.getElementById("log"));
} catch (err) {
- window.addEventListener("resize", evt => {
- log_scroller.scrollTop = log_scroller.scrollHeight;
- });
+ window.addEventListener("resize", scroll_log_to_end);
}
+/* MAP ZOOM */
+
function toggle_fullscreen() {
if (document.fullscreen)
document.exitFullscreen();
@@ -360,78 +392,60 @@ function toggle_fullscreen() {
document.documentElement.requestFullscreen();
}
-function show_chat() {
- if (!chat_is_visible) {
- document.getElementById("chat_button").classList.remove("new");
- document.getElementById("chat_window").classList.add("show");
- document.getElementById("chat_input").focus();
- chat_is_visible = true;
- save_chat();
- }
+function toggle_log() {
+ document.querySelector("aside").classList.toggle("hide");
+ zoom_map();
}
-function hide_chat() {
- if (chat_is_visible) {
- document.getElementById("chat_window").classList.remove("show");
- document.getElementById("chat_input").blur();
- chat_is_visible = false;
+function toggle_zoom() {
+ let mapwrap = document.getElementById("mapwrap");
+ if (mapwrap) {
+ mapwrap.classList.toggle("fit");
+ zoom_map();
}
}
-function toggle_chat() {
- if (chat_is_visible)
- hide_chat();
- else
- show_chat();
-}
-
function zoom_map() {
- let grid = document.querySelector("main");
let mapwrap = document.getElementById("mapwrap");
- let map = document.getElementById("map");
- map.style.transform = null;
- mapwrap.style.width = null;
- mapwrap.style.height = null;
- if (mapwrap.classList.contains("fit")) {
- let { width: gw, height: gh } = grid.getBoundingClientRect();
- let { width: ww, height: wh } = mapwrap.getBoundingClientRect();
- let { width: cw, height: ch } = map.getBoundingClientRect();
- let scale = Math.min(ww / cw, gh / ch);
- if (scale < 1) {
- map.style.transform = "scale(" + scale + ")";
- mapwrap.style.width = (cw * scale) + "px";
- mapwrap.style.height = (ch * scale) + "px";
+ if (mapwrap) {
+ let main = document.querySelector("main");
+ let map = document.getElementById("map");
+ map.style.transform = null;
+ mapwrap.style.width = null;
+ mapwrap.style.height = null;
+ if (mapwrap.classList.contains("fit")) {
+ let { width: gw, height: gh } = main.getBoundingClientRect();
+ let { width: ww, height: wh } = mapwrap.getBoundingClientRect();
+ let { width: cw, height: ch } = map.getBoundingClientRect();
+ let scale = Math.min(ww / cw, gh / ch);
+ if (scale < 1) {
+ map.style.transform = "scale(" + scale + ")";
+ mapwrap.style.width = (cw * scale) + "px";
+ mapwrap.style.height = (ch * scale) + "px";
+ }
}
}
}
-function toggle_zoom() {
- document.getElementById("mapwrap").classList.toggle('fit');
- zoom_map();
-}
+zoom_map();
-function init_map_zoom() {
- window.addEventListener('resize', zoom_map);
- zoom_map();
-}
+window.addEventListener("resize", zoom_map);
-function init_shift_zoom() {
- window.addEventListener("keydown", (evt) => {
- if (evt.key === "Shift")
- document.querySelector("body").classList.add("shift");
- });
- window.addEventListener("keyup", (evt) => {
- if (evt.key === "Shift")
- document.querySelector("body").classList.remove("shift");
- });
-}
+window.addEventListener("keydown", (evt) => {
+ if (evt.key === "Shift")
+ document.querySelector("body").classList.add("shift");
+});
-function toggle_log() {
- document.querySelector("aside").classList.toggle("hide");
- zoom_map();
-}
+window.addEventListener("keyup", (evt) => {
+ if (evt.key === "Shift")
+ document.querySelector("body").classList.remove("shift");
+});
+
+/* ACTIONS */
function action_button(action, label) {
+ if (params.mode === "replay")
+ return;
let id = action + "_button";
let button = document.getElementById(id);
if (!button) {
@@ -441,11 +455,11 @@ function action_button(action, label) {
button.addEventListener("click", evt => send_action(action));
document.getElementById("actions").appendChild(button);
}
- if (game.actions && action in game.actions) {
+ if (view.actions && action in view.actions) {
button.classList.remove("hide");
- if (game.actions[action]) {
+ if (view.actions[action]) {
if (label === undefined)
- button.textContent = game.actions[action];
+ button.textContent = view.actions[action];
button.disabled = false;
} else {
button.disabled = true;
@@ -455,66 +469,282 @@ function action_button(action, label) {
}
}
-function confirm_resign() {
- if (window.confirm("Are you sure that you want to resign?"))
- socket.emit('resign');
-}
-
function send_action(verb, noun) {
+ if (params.mode === "replay")
+ return;
// Reset action list here so we don't send more than one action per server prompt!
if (noun !== undefined) {
- if (game.actions && game.actions[verb] && game.actions[verb].includes(noun)) {
- game.actions = null;
+ if (view.actions && view.actions[verb] && view.actions[verb].includes(noun)) {
+ view.actions = null;
console.log("ACTION", verb, JSON.stringify(noun));
- socket.emit('action', verb, noun);
+ socket.emit("action", verb, noun);
return true;
}
} else {
- if (game.actions && game.actions[verb]) {
- game.actions = null;
+ if (view.actions && view.actions[verb]) {
+ view.actions = null;
console.log("ACTION", verb);
- socket.emit('action', verb);
+ socket.emit("action", verb);
return true;
}
}
return false;
}
+function confirm_resign() {
+ if (window.confirm("Are you sure that you want to resign?"))
+ socket.emit("resign");
+}
+
+/* DEBUGGING */
+
function send_save() {
- socket.emit('save');
+ socket.emit("save");
}
function send_restore() {
- socket.emit('restore', window.localStorage[param_title_id + '/save']);
+ socket.emit("restore", window.localStorage[params.title_id + "/save"]);
}
function send_restart(scenario) {
- socket.emit('restart', scenario);
+ socket.emit("restart", scenario);
}
-function on_game_over() {
- if (player) {
- let exit_button = document.getElementById("exit_button");
- if (exit_button) {
- if (game_over || player === "Observer")
- exit_button.classList.remove("hide");
- else
- exit_button.classList.add("hide");
+/* REPLAY */
+
+function adler32(data) {
+ let a = 1, b = 0;
+ for (let i = 0, n = data.length; i < n; ++i) {
+ a = (a + data.charCodeAt(i)) % 65521;
+ b = (b + a) % 65521;
+ }
+ return (b << 16) | a;
+}
+
+async function require(path) {
+ let cache = {};
+
+ if (!path.endsWith(".js"))
+ path = path + ".js";
+ if (path.startsWith("./"))
+ path = path.substring(2);
+
+ console.log("REQUIRE", path);
+
+ let response = await fetch(path);
+ let source = await response.text();
+
+ for (let [_, subpath] of source.matchAll(/require\(['"]([^)]*)['"]\)/g))
+ if (cache[subpath] === undefined)
+ cache[subpath] = await require(subpath);
+
+ let module = { exports: {} };
+ Function("module", "exports", "require", source)
+ (module, module.exports, path => cache[path]);
+ return module.exports;
+}
+
+let replay = null;
+
+async function init_replay_client() {
+ document.getElementById("prompt").textContent = "Loading replay...";
+ document.querySelector("body").classList.add("replay");
+
+ console.log("LOADING RULES");
+ let rules = await require("rules.js");
+
+ console.log("LOADING REPLAY");
+ let response = await fetch("/replay/" + params.game_id);
+ let body = await response.json();
+ replay = body.replay;
+
+ init_player_names(body.players);
+
+ let viewpoint = "Observer";
+ let log_length = 0;
+ let p = 0;
+ let s = {};
+
+ function eval_action(item) {
+ switch (item.action) {
+ case "setup":
+ s = rules.setup(item.arguments[0], item.arguments[1], item.arguments[2]);
+ break;
+ case "resign":
+ s = rules.resign(s, item.role);
+ break;
+ default:
+ s = rules.action(s, item.role, item.action, item.arguments);
+ break;
}
- let rematch_button = document.getElementById("rematch_button");
- if (rematch_button) {
- if (game_over && player !== "Observer")
- rematch_button.classList.remove("hide");
+ }
+
+ let ss;
+ for (p = 0; p < replay.length; ++p) {
+ replay[p].arguments = JSON.parse(replay[p].arguments);
+
+ if (rules.is_checkpoint) {
+ replay[p].is_checkpoint = (p > 0 && rules.is_checkpoint(ss, s));
+ ss = Object.assign({}, s);
+ }
+
+ eval_action(replay[p]);
+
+ replay[p].digest = adler32(JSON.stringify(s));
+ for (let k = p-1; k > 0; --k) {
+ if (replay[k].digest === replay[p].digest && !replay[k].is_undone) {
+ for (let a = k+1; a <= p; ++a)
+ if (!replay[a].is_undone)
+ replay[a].is_undone = true;
+ break;
+ }
+ }
+ }
+
+ replay = replay.filter(x => !x.is_undone);
+
+ function set_hash(n) {
+ history.replaceState(null, "", window.location.pathname + "#" + n);
+ }
+
+ let timer = 0;
+ function play_pause_replay(evt) {
+ if (timer === 0) {
+ evt.target.textContent = "Stop";
+ timer = setInterval(() => {
+ if (p < replay.length)
+ goto_replay(p+1);
+ else
+ play_pause_replay(evt);
+ }, 1000);
+ } else {
+ evt.target.textContent = "Run";
+ clearInterval(timer);
+ timer = 0;
+ }
+ }
+
+ function prev() {
+ for (let i = p - 1; i > 1; --i)
+ if (replay[i].is_checkpoint)
+ return i;
+ return 1;
+ }
+
+ function next() {
+ for (let i = p + 1; i < replay.length; ++i)
+ if (replay[i].is_checkpoint)
+ return i;
+ return replay.length;
+ }
+
+ function on_hash_change() {
+ goto_replay(parseInt(window.location.hash.slice(1)) || 1);
+ }
+
+ function goto_replay(np) {
+ if (np < 1)
+ np = 1;
+ if (np > replay.length)
+ np = replay.length;
+ set_hash(np);
+ if (p > np)
+ p = 0, s = {};
+ while (p < np)
+ eval_action(replay[p++]);
+ update_replay_view();
+ }
+
+ function update_replay_view() {
+ player = viewpoint;
+
+ if (viewpoint === "Active") {
+ player = s.active;
+ if (player === "All" || player === "Both" || !player)
+ player = "Observer";
+ }
+
+ let body = document.querySelector("body");
+ body.classList.remove("Observer");
+ for (let i = 0; i < roles.length; ++i)
+ body.classList.remove(roles[i].role.replace(/ /g, "_"));
+ body.classList.add(player.replace(/ /g, "_"));
+
+ view = rules.view(s, player);
+ view.actions = null;
+
+ if (viewpoint === "Observer")
+ view.game_over = 1;
+ if (s.state === "game_over")
+ view.game_over = 1;
+
+ if (replay.length > 0) {
+ if (document.querySelector("body").classList.contains("shift"))
+ view.prompt = `[${p}/${replay.length}] ${s.active} / ${s.state} / ${replay[p].action} ${replay[p].arguments}`;
else
- rematch_button.classList.add("hide");
+ view.prompt = "[" + p + "/" + replay.length + "] " + view.prompt;
}
+ if (log_length < view.log.length)
+ view.log_start = log_length;
+ else
+ view.log_start = view.log.length;
+ log_length = view.log.length;
+ view.log = view.log.slice(view.log_start);
+
+ on_update_header();
+ on_update();
+ on_update_log();
}
-}
-function send_rematch() {
- window.location = '/rematch/' + param_game_id + '/' + param_role;
-}
+ function replay_button(div, label, fn) {
+ let button = document.createElement("button");
+ button.addEventListener("click", fn);
+ button.className = "replay_button";
+ button.textContent = label;
+ div.appendChild(button);
+ return button;
+ }
-function send_exit() {
- window.location = '/info/' + param_title_id;
+ function set_viewpoint(vp) {
+ viewpoint = vp;
+ update_replay_view();
+ }
+
+ if (replay.length > 0) {
+ console.log("REPLAY READY");
+
+ let div = document.createElement("div");
+ div.className = "replay";
+ replay_button(div, "Active", () => set_viewpoint("Active"));
+ for (let r of roles)
+ replay_button(div, r.role, () => set_viewpoint(r.role));
+ replay_button(div, "Observer", () => set_viewpoint("Observer"));
+ document.querySelector("header").appendChild(div);
+
+ div = document.createElement("div");
+ div.className = "replay";
+ replay_button(div, "<<<", () => goto_replay(1));
+ replay_button(div, "<<", () => goto_replay(prev()));
+ replay_button(div, "<\xa0", () => goto_replay(p-1));
+ replay_button(div, "\xa0>", () => goto_replay(p+1));
+ replay_button(div, ">>", () => goto_replay(next()));
+ replay_button(div, "Run", play_pause_replay).style.width = "65px";
+ document.querySelector("header").appendChild(div);
+
+ if (window.location.hash === "")
+ set_hash(replay.length);
+
+ on_hash_change();
+
+ window.addEventListener("hashchange", on_hash_change);
+ } else {
+ console.log("REPLAY NOT AVAILABLE");
+ s = JSON.parse(body.state);
+ update_replay_view();
+ }
}
+
+if (params.mode === "play")
+ init_play_client();
+if (params.mode === "replay")
+ init_replay_client();
diff --git a/public/common/grid.css b/public/common/grid.css
index 0532648..228c0aa 100644
--- a/public/common/grid.css
+++ b/public/common/grid.css
@@ -25,14 +25,16 @@ body:not(.shift) .debug {
display: none;
}
-body.Observer .resign {
+body.Observer .resign, body.replay .resign {
display: none;
}
button {
- font-size: 1rem;
+ box-sizing: border-box;
+ font-size: 16px;
+ height: 28px;
margin: 0;
- padding: 1px 12px;
+ padding: 1px 12px 1px 12px;
background-color: gainsboro;
}
button:disabled {
@@ -177,6 +179,15 @@ header button {
margin: 0 10px;
}
+header .replay {
+ margin: 0 10px;
+ display: flex;
+}
+
+header button.replay_button {
+ margin: 0;
+}
+
#prompt {
margin: 0 20px;
font-size: 18px;
diff --git a/public/join.js b/public/join.js
index 24e10ab..eef3833 100644
--- a/public/join.js
+++ b/public/join.js
@@ -68,11 +68,14 @@ function start_event_source() {
});
evtsrc.addEventListener("deleted", function (evt) {
console.log("DELETED");
- window.location.href = '/info/' + game.title_id;
+ window.location.href = '/' + game.title_id;
});
evtsrc.onerror = function (err) {
window.message.innerHTML = "Disconnected from server...";
};
+ window.addEventListener('beforeunload', function (evt) {
+ evtsrc.close();
+ });
}
}
diff --git a/server.js b/server.js
index ef65a73..6c4caee 100644
--- a/server.js
+++ b/server.js
@@ -94,7 +94,7 @@ app.set('x-powered-by', false);
app.set('etag', false);
app.set('view engine', 'pug');
app.use(compression());
-app.use(express.static('public', { etag: false, cacheControl: false, setHeaders: set_static_headers }));
+app.use(express.static('public', { redirect: false, etag: false, cacheControl: false, setHeaders: set_static_headers }));
app.use(express.urlencoded({extended:false}));
app.locals.SITE_NAME = SITE_NAME;
@@ -910,6 +910,7 @@ const SQL_UPDATE_GAME_STATE = SQL("INSERT OR REPLACE INTO game_state (game_id,st
const SQL_UPDATE_GAME_RESULT = SQL("UPDATE games SET status=?, result=? WHERE game_id=?");
const SQL_UPDATE_GAME_PRIVATE = SQL("UPDATE games SET is_private=1 WHERE game_id=?");
const SQL_INSERT_REPLAY = SQL("INSERT INTO game_replay (game_id,role,action,arguments) VALUES (?,?,?,?)");
+const SQL_SELECT_REPLAY = SQL("SELECT role,action,arguments FROM game_replay WHERE game_id=?");
const SQL_SELECT_GAME = SQL("SELECT * FROM games WHERE game_id=?");
const SQL_SELECT_GAME_VIEW = SQL("SELECT * FROM game_view WHERE game_id=?");
@@ -917,6 +918,8 @@ const SQL_SELECT_GAME_FULL_VIEW = SQL("SELECT * FROM game_full_view WHERE game_i
const SQL_SELECT_GAME_TITLE = SQL("SELECT title_id FROM games WHERE game_id=?").pluck();
const SQL_SELECT_GAME_RANDOM = SQL("SELECT is_random FROM games WHERE game_id=?").pluck();
+const SQL_SELECT_GAME_HAS_TITLE_AND_STATUS = SQL("SELECT 1 FROM games WHERE game_id=? AND title_id=? AND status=?");
+
const SQL_SELECT_PLAYERS = SQL("SELECT * FROM players NATURAL JOIN user_view WHERE game_id=?");
const SQL_SELECT_PLAYERS_JOIN = SQL("SELECT role, user_id, name FROM players NATURAL JOIN users WHERE game_id=?");
const SQL_SELECT_PLAYER_ROLE = SQL("SELECT role FROM players WHERE game_id=? AND user_id=?").pluck();
@@ -1062,8 +1065,11 @@ app.get('/profile', must_be_logged_in, function (req, res) {
});
app.get('/info/:title_id', function (req, res) {
- LOG(req, "GET /info/" + req.params.title_id);
- let title_id = req.params.title_id;
+ return res.redirect('/' + req.params.title_id);
+});
+
+function get_title_page(req, res, title_id) {
+ LOG(req, "GET /" + title_id);
let title = TITLES[title_id];
if (!title)
return res.status(404).send("Invalid title.");
@@ -1081,7 +1087,10 @@ app.get('/info/:title_id', function (req, res) {
active_games: active_games,
finished_games: finished_games,
});
-});
+}
+
+for (let title_id in TITLES)
+ app.get('/' + title_id, (req, res) => get_title_page(req, res, title_id));
app.get('/create/:title_id', must_be_logged_in, function (req, res) {
LOG(req, "GET /create/" + req.params.title_id);
@@ -1137,7 +1146,7 @@ app.get('/delete/:game_id', must_be_logged_in, function (req, res) {
return res.send("Not authorized to delete that game ID.");
if (info.changes === 1)
update_join_clients_deleted(game_id);
- res.redirect('/info/'+title_id);
+ res.redirect('/'+title_id);
});
function join_rematch(req, res, game_id, role) {
@@ -1344,7 +1353,7 @@ app.get('/play/:game_id/:role', function (req, res) {
let role = req.params.role;
let title = SQL_SELECT_GAME_TITLE.get(game_id);
if (!title)
- return res.redirect('/join/'+game_id);
+ return res.status(404).send("Invalid game ID.");
res.redirect('/'+title+'/play:'+game_id+':'+role);
});
@@ -1354,7 +1363,7 @@ app.get('/play/:game_id', function (req, res) {
let user_id = req.user ? req.user.user_id : 0;
let title = SQL_SELECT_GAME_TITLE.get(game_id);
if (!title)
- return res.redirect('/join/'+game_id);
+ return res.status(404).send("Invalid game ID.");
let role = SQL_SELECT_PLAYER_ROLE.get(game_id, user_id);
if (role)
res.redirect('/'+title+'/play:'+game_id+':'+role);
@@ -1363,6 +1372,7 @@ app.get('/play/:game_id', function (req, res) {
});
app.get('/:title_id/play\::game_id\::role', must_be_logged_in, function (req, res) {
+ LOG(req, "GET /" + req.params.title_id + "/play:" + req.params.game_id + ":" + req.params.role);
let user_id = req.user ? req.user.user_id : 0;
let title_id = req.params.title_id
let game_id = req.params.game_id;
@@ -1373,14 +1383,42 @@ app.get('/:title_id/play\::game_id\::role', must_be_logged_in, function (req, re
});
app.get('/:title_id/play\::game_id', function (req, res) {
+ LOG(req, "GET /" + req.params.title_id + "/play:" + req.params.game_id);
let title_id = req.params.title_id
let game_id = req.params.game_id;
let a_title = SQL_SELECT_GAME_TITLE.get(game_id);
if (a_title !== title_id)
- return res.send("Invalid game ID.");
+ return res.status(404).send("Invalid game ID.");
return res.sendFile(__dirname + '/public/' + title_id + '/play.html');
});
+app.get('/:title_id/replay\::game_id', function (req, res) {
+ LOG(req, "GET /" + req.params.title_id + "/replay:" + req.params.game_id);
+ let title_id = req.params.title_id
+ let game_id = req.params.game_id;
+ let game = SQL_SELECT_GAME.get(game_id);
+ if (!game)
+ return res.status(404).send("Invalid game ID.");
+ if (game.title_id !== title_id)
+ return res.status(404).send("Invalid game ID.");
+ if (game.status < 2)
+ return res.status(404).send("Invalid game ID.");
+ return res.sendFile(__dirname + '/public/' + title_id + '/play.html');
+});
+
+app.get('/replay/:game_id', function (req, res) {
+ let game_id = req.params.game_id;
+ let game = SQL_SELECT_GAME.get(game_id);
+ if (game.status < 2)
+ return res.status(404).send("Invalid game ID.");
+ let players = SQL_SELECT_PLAYERS_JOIN.all(game_id);
+ let replay = SQL_SELECT_REPLAY.all(game_id);
+ if (replay.length > 0)
+ return res.json({players, replay});
+ let state = SQL_SELECT_GAME_STATE.get(game_id);
+ return res.json({players, state, replay});
+});
+
/*
* MAIL NOTIFICATIONS
*/
@@ -1452,7 +1490,6 @@ function mail_your_turn_notification(user, game_id, interval) {
}
}
}
-}
function reset_your_turn_notification(user, game_id) {
SQL_DELETE_NOTIFIED.run(game_id, user.user_id);
@@ -1544,7 +1581,9 @@ function send_state(socket, state) {
view.log_start = view.log.length;
socket.log_length = view.log.length;
view.log = view.log.slice(view.log_start);
- socket.emit('state', view, state.state === 'game_over');
+ if (state.state === 'game_over')
+ view.game_over = 1;
+ socket.emit('state', view);
} catch (err) {
console.log(err);
return socket.emit('error', err.toString());
@@ -1580,7 +1619,7 @@ function on_action(socket, action, arg) {
try {
let state = get_game_state(socket.game_id);
let old_active = state.active;
- socket.rules.action(state, socket.role, action, arg);
+ state = socket.rules.action(state, socket.role, action, arg);
put_game_state(socket.game_id, state, old_active);
put_replay(socket.game_id, socket.role, action, arg);
} catch (err) {
@@ -1594,7 +1633,7 @@ function on_resign(socket) {
try {
let state = get_game_state(socket.game_id);
let old_active = state.active;
- socket.rules.resign(state, socket.role);
+ state = socket.rules.resign(state, socket.role);
put_game_state(socket.game_id, state, old_active);
put_replay(socket.game_id, socket.role, 'resign', null);
} catch (err) {
diff --git a/views/head.pug b/views/head.pug
index 68fa904..df1b762 100644
--- a/views/head.pug
+++ b/views/head.pug
@@ -15,7 +15,7 @@ mixin social(title,description,game)
meta(property="og:description" content=description)
mixin gamecover(title_id)
- a(href="/info/"+title_id)
+ a(href="/"+title_id)
img(src=`/${title_id}/cover.1x.jpg` srcset=`/${title_id}/cover.2x.jpg 2x`)
mixin forumpost(row,show_buttons)
@@ -57,7 +57,7 @@ mixin gametable(status,table,hide_title=0)
tr
td= row.game_id
unless hide_title
- td.w: a(href="/info/"+row.title_id)= row.title_name
+ td.w: a(href="/"+row.title_id)= row.title_name
td.w= row.scenario
td!= row.player_names
td= row.description
@@ -76,15 +76,16 @@ mixin gametable(status,table,hide_title=0)
td.command
if status === 0
a(href="/join/"+row.game_id) Join
- else
- - let cmd = status === 1 ? "Play" : "View"
+ else if status === 1
if row.is_yours
if row.is_shared
- a(href="/join/"+row.game_id)= cmd
+ a(href="/join/"+row.game_id)= "Play"
else
- a(href=`/${row.title_id}/play:${row.game_id}:${row.your_role}`)= cmd
+ a(href=`/${row.title_id}/play:${row.game_id}:${row.your_role}`)= "Play"
else
a(href=`/${row.title_id}/play:${row.game_id}`) View
+ else if status >= 2
+ a(href=`/${row.title_id}/replay:${row.game_id}`) View
else
tr
case status