summaryrefslogtreecommitdiff
path: root/assets
diff options
context:
space:
mode:
Diffstat (limited to 'assets')
-rw-r--r--assets/VT323-Regular.ttfbin0 -> 149688 bytes
-rw-r--r--assets/client.js267
-rw-r--r--assets/index.html132
-rw-r--r--assets/room.html30
-rw-r--r--assets/style.css86
5 files changed, 515 insertions, 0 deletions
diff --git a/assets/VT323-Regular.ttf b/assets/VT323-Regular.ttf
new file mode 100644
index 0000000..6aec599
--- /dev/null
+++ b/assets/VT323-Regular.ttf
Binary files differ
diff --git a/assets/client.js b/assets/client.js
new file mode 100644
index 0000000..174628f
--- /dev/null
+++ b/assets/client.js
@@ -0,0 +1,267 @@
+window.player = { uid: NaN };
+window.info_elem = document.getElementById("miscinfo");
+window.identform = document.getElementById("identform");
+window.statusline = document.getElementsByClassName("statusline")[0];
+window.bcont_elem = document.getElementById("board-container");
+window.board_elem = document.getElementById("board");
+window.cursor_frame = document.getElementById("cursor-frame");
+window.queued_pos = undefined;
+
+window.room = {
+ name: undefined,
+ bconf: { w: NaN, h: NaN, tile_w: NaN, tile_h: NaN, mine_ratio: undefined },
+ board: {},
+ cbounds: {},
+ socket: undefined,
+ last_packet: undefined,
+ identity: JSON.parse(localStorage.getItem("identity")),
+ cursors: new Map(),
+};
+
+
+if (room.identity == null) {
+ statusline.style.display = "none";
+ identform.style.display = "initial";
+} else {
+ join();
+}
+
+function join() {
+ if (room.identity == null) {
+ room.identity = {};
+ room.identity.name = document.getElementById("name-in").value;
+ room.identity.clr = document.getElementById("clr-in").value;
+ localStorage.setItem("identity", JSON.stringify(room.identity));
+ }
+ identform.style.display = "none";
+ room.socket = connect();
+ statusline.style.display = "flex";
+}
+function clear_ident() {
+ localStorage.removeItem("identity");
+ document.location.reload();
+}
+
+function connect() {
+ let wsproto = (window.location.protocol == "https:")? "wss:": "ws:";
+ let s = new WebSocket(`${wsproto}//${location.hostname}:${location.port}${location.pathname}/ws`);
+ s.onopen = function() {
+ s.send(`register ${room.identity.name} ${room.identity.clr}`);
+ }
+ s.onmessage = function(e) {
+ room.last_packet = e;
+ let d = e.data;
+ if (typeof d == "object") {
+ d.arrayBuffer().then(acceptBoard);
+ info_elem.onclick = undefined;
+ info_elem.innerHTML = `${room.name} (${room.bconf.w}x${room.bconf.h}) >> Running, ${room.bconf.mine_ratio} tiles are mines`;
+ } else if (typeof e.data == "string") {
+ let fields = d.split(" ");
+ switch (fields[0]) {
+ case "pos": {
+ let posdata = JSON.parse(fields[1]);
+ posdata.forEach(pdat => {
+ let oid = Number(pdat[0]);
+ let x = pdat[1][0];
+ let y = pdat[1][1];
+ let curs = room.cursors.get(oid);
+ if (curs != undefined) {
+ movCursor(curs, x, y);
+ } else {
+ console.log("livepos sys incoherent");
+ }
+ });
+ } break;
+ case "players": {
+ let pdata = JSON.parse(fields[1]);
+ console.log(pdata);
+ pdata.forEach(p => {
+ let oid = Number(p[0]);
+ let name = p[1];
+ let clr = p[2];
+ console.log(oid, name, clr);
+ if (!room.cursors.has(oid)) {
+ createCursor(oid, name, clr);
+ }
+ });
+ } break;
+ case "regack": {
+ room.name = fields[1];
+ name = fields[2];
+ player.uid = Number(fields[3]);
+ let dims = fields[4].split("x");
+ room.bconf.w = Number(dims[0]);
+ room.bconf.h = Number(dims[1]);
+ room.bconf.mine_ratio = fields[5];
+ createCursor(player.uid, name, room.identity.clr);
+ } break;
+ case "win": {
+ info_elem.innerHTML = "You win! Click here to play again.";
+ info_elem.onclick = e => { s.send("reset") };
+ } break;
+ case "lose": {
+ let badone = fields[1];
+ info_elem.innerHTML = `You lost, ${badone} was blown up. Click here to retry.`;
+ info_elem.onclick = e => { s.send("reset") };
+ } break;
+ case "logoff": {
+ let oid = Number(fields[1]);
+ room.cursors.get(oid).elem.remove();
+ room.cursors.get(oid).selwin.remove();
+ room.cursors.delete(oid);
+ } break;
+ }
+ }
+ }
+ s.onerror = function(e) { info_elem.innerHTML += `<br>Connection error: ${e}`; }
+ s.onclose = function(e) { info_elem.innerHTML = "Connection closed"; }
+ return s;
+}
+
+function acceptBoard(data) {
+ let dataarr = new Uint8Array(data);
+ let vals = fflate.inflateSync(dataarr);
+ room.board = vals.reduce((s,c) => {
+ let v = String.fromCodePoint(c);
+ if (v == ' ') {
+ s = s + "&nbsp";
+ } else {
+ s = s + v;
+ }
+ return s;
+ }, "");
+ let last = room.board[0];
+ let last_idx = 0;
+ let split_board = [];
+ for (let i = 1; i < room.board.length+1; i++) {
+ let cur = room.board[i];
+ let gamechars = /^[CFO# 1-8]+$/;
+ if ((cur != last && gamechars.test(cur)) || cur == undefined) {
+ let txt = room.board.substr(last_idx, i-last_idx);
+ switch(txt[0]) {
+ case 'O':
+ txt = `<span style="color:red;">${txt}</span>`;
+ break;
+ case 'C':
+ txt = `<span style="color:green;">${txt}</span>`;
+ break;
+ case 'F':
+ txt = `<span style="color:yellow;">${txt}</span>`;
+ break;
+
+ case '1': txt = `<span style="color:#0100FB;">${txt}</span>`; break;
+ case '2': txt = `<span style="color:#027F01;">${txt}</span>`; break;
+ case '3': txt = `<span style="color:#FD0100;">${txt}</span>`; break;
+ case '4': txt = `<span style="color:#01017B;">${txt}</span>`; break;
+ case '5': txt = `<span style="color:#7D0302;">${txt}</span>`; break;
+ case '6': txt = `<span style="color:#00807F;">${txt}</span>`; break;
+
+ default: txt = `<span style="color:white;">${txt}</span>`; break;
+ }
+ split_board.push(txt);
+ last_idx = i;
+ }
+ last = room.board[i];
+ }
+ board_elem.innerHTML = split_board.join("");
+ room.cbounds = getBoardBounds();
+}
+
+function createCursor(id, name, clr) {
+ // shit doesn't line up
+ let cursor = document.createElement("div");
+ cursor.style.position = "absolute";
+ let nametag = document.createElement("p");
+ nametag.innerHTML = name;
+ nametag.classList.add('cursor-name');
+ let selection_window = document.createElement("div");
+ selection_window.style.backgroundColor = clr + "a0";
+ selection_window.style.position = "absolute";
+ selection_window.classList.add('cursor');
+ cursor.appendChild(nametag);
+ cursor.classList.add('cursor');
+ cursor.style.color = clr;
+ document.getElementById('cursor-frame').append(cursor);
+ document.getElementById('cursor-frame').append(selection_window);
+ let c = { name: name, elem: cursor, selwin: selection_window };
+ if (id == window.player.uid) {
+ document.addEventListener('mousemove', e => {
+ let bcoords = pageToBoardPx(e.pageX, e.pageY);
+ movCursor(c, bcoords[0], bcoords[1]);
+ window.queued_pos = bcoords;
+ },
+ false);
+ }
+ room.cursors.set(id, {name: name, elem: cursor, selwin: selection_window});
+ return cursor;
+}
+
+function pageToBoardPx(x,y) {
+ return [Math.floor(x - room.cbounds.ox), Math.floor(y - room.cbounds.oy)];
+}
+
+function movCursor(c, bx, by) {
+ c.elem.style.left = (room.cbounds.ox + bx) + 'px';
+ c.elem.style.top = (room.cbounds.oy + by) + 'px';
+ movSelWin(c.selwin, bx, by);
+}
+function movSelWin(win, bx, by) {
+ let tpos = tilepos(bx,by);
+ console.log(tpos);
+ if (tpos.x > (room.bconf.w - 1) || tpos.x < 0 || tpos.y > (room.bconf.h - 1) || tpos.y < 0) {
+ win.style.display = "none";
+ } else {
+ win.style.display = "";
+ }
+ win.style.left = (tpos.x * room.bconf.tile_w) + 'px';
+ win.style.top = (tpos.y * room.bconf.tile_h) + 'px';
+ win.style.width = room.bconf.tile_w + 'px';
+ win.style.height = room.bconf.tile_h + 'px';
+}
+function getBoardBounds() {
+ let a = bcont_elem.getBoundingClientRect();
+ let b = board_elem.getBoundingClientRect();
+ room.bconf.tile_w = b.width / room.bconf.w;
+ room.bconf.tile_h = 48;
+ return {
+ ox: b.x + window.scrollX,
+ oy: a.y + window.scrollY,
+ w: b.width,
+ h: a.height
+ };
+}
+window.onresize = () => {
+ room.cbounds = getBoardBounds();
+}
+
+bcont_elem.onclick = function(e) {
+ let bcoords = pageToBoardPx(e.pageX, e.pageY);
+ let tpos = tilepos(bcoords[0], bcoords[1]);
+ let cmd = `reveal ${tpos.x} ${tpos.y}`;
+ room.socket.send(cmd);
+}
+bcont_elem.oncontextmenu = function(e) {
+ let bcoords = pageToBoardPx(e.pageX, e.pageY);
+ let tpos = tilepos(bcoords[0], bcoords[1]);
+ let cmd = `flag ${tpos.x} ${tpos.y}`;
+ room.socket.send(cmd);
+ return false;
+}
+// these are board-px coords
+function tilepos(bx,by) {
+ let b = room.cbounds; // we can assume it is already computed by earlier aux calls
+ let tilex = Math.floor(room.bconf.w * bx/b.w);
+ let tiley = Math.floor(room.bconf.h * by/b.h);
+ return { x: tilex, y: tiley };
+}
+
+(function sendPos() {
+ let qp = window.queued_pos;
+ if (qp) {
+ room.socket.send(`pos ${qp[0]} ${qp[1]}`);
+ window.queued_pos = undefined;
+ }
+ setTimeout(function() {
+ sendPos();
+ }, 16);
+})();
diff --git a/assets/index.html b/assets/index.html
new file mode 100644
index 0000000..cce9248
--- /dev/null
+++ b/assets/index.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>websweeper</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <link rel="stylesheet" type="text/css" href="./s.css">
+ </head>
+ <body>
+ <div class="cent">
+ <div id="rlist"></div>
+ <span id="rspace"></span>
+ </div>
+ <form method="post" action="r" class="cent">
+ <fieldset>
+ <legend>-={ Create a new room }=-</legend>
+ <label>room name&nbsp;<input name="rname" type="text" autofocus></label><br>
+ <label>
+ board dimensions
+ <input name="rwidth" type="number" value="30" required>
+ x
+ <input name="rheight" type="number" value="20" required><br>
+ where <input name="rration" type="number" value="1" required>
+ in every <input name="rratiod" type="number" value="8" required>
+ tiles are mines
+ </label><br>
+ <label>public, ie. shown in the lobby <input name="raccess" type="checkbox" checked></label><br>
+ <label>safe first move (if possible) <input name="ralwayssafe1move" type="checkbox" checked></label><br>
+ <label>player limit <input name="rlimit" type="number" value="32"></label><br>
+ <button id="createbtn">create</button>
+ </fieldset>
+ </form>
+ <div class="statusline cent">
+ <p id="ident-name"></p>
+ <a id="ident-clr" href="javascript:clear_ident();">clear identity</a>
+ </div>
+ <script>
+ let rlist = {
+ elem: document.getElementById('rlist'),
+ map: new Map(),
+ };
+ let rspace = {
+ elem: document.getElementById('rspace'),
+ num: NaN,
+ txt: undefined,
+ };
+
+ function fetch_info(callback) {
+ fetch('rlist').then(r => r.json()).then(info => {
+ let rooms = info[0];
+ let pcounts = info[1];
+ Object.keys(rooms).forEach(id => {
+ let room = rlist.map.get(id);
+ if (!room) { room = { init: false }; }
+ let rinfo = JSON.parse(rooms[id]);
+ room.name = rinfo.name;
+ room.pcount = Number(pcounts[id][0]);
+ room.pcapacity = Number(pcounts[id][1]);
+ room.board_conf = rinfo.board_conf;
+ rlist.map.set(id, room);
+ });
+ callback();
+ });
+ fetch("rspace").then(resp => resp.text()).then(roomspace => {
+ rspace.num = Number(roomspace);
+ callback();
+ })
+ }
+
+ function render_info() {
+ rlist.map.forEach((room, id) => {
+ let full = room.pcount == room.pcapacity;
+ if (!room.init) {
+ let entry = (full)? document.createElement('span') : document.createElement('a');
+ room.h1 = document.createElement("h1");
+ room.h1_txt = document.createTextNode("");
+ room.h1.appendChild(room.h1_txt);
+ room.h4 = document.createElement("h4");
+ room.h4.appendChild(document.createTextNode(
+ `${room.board_conf.w} by ${room.board_conf.h} with
+ ${room.board_conf.mine_ratio[0]} in every ${room.board_conf.mine_ratio[1]} tiles mined`
+ ));
+ entry.append(room.h1);
+ entry.append(room.h4);
+ entry.href = 'room/' + id;
+ rlist.elem.append(entry);
+ rlist.elem.append(document.createElement('br'));
+ room.init = true;
+ }
+ let ptxt = `${room.pcount}/${room.pcapacity} players` + ((full)? " (full)" : "");
+ room.h1_txt.textContent = `> ${room.name} — ${ptxt}`;
+
+ });
+ if (!rspace.txt) {
+ rspace.txt = document.createTextNode("");
+ rspace.elem.appendChild(rspace.txt);
+ }
+ if (rspace.num == 0) {
+ rspace.txt.textContent = "all room slots filled, when a room empties it can be replaced by a new one";
+ document.getElementById("createbtn").disabled = "disabled";
+ } else {
+ document.getElementById("createbtn").disabled = "";
+ if (rspace.num == 1) {
+ rspace.txt.textContent = "there is 1 available room slot";
+ } else if (rspace.num > 1) {
+ rspace.txt.textContent = `there are ${rspace.num} available room slots`;
+ }
+ }
+ }
+
+ (function refresh_info() {
+ fetch_info(render_info);
+ setTimeout(function() {
+ refresh_info();
+ }, 2000);
+ })();
+
+ function clear_ident() {
+ localStorage.removeItem("identity");
+ document.location.reload();
+ }
+ let ident = JSON.parse(localStorage.getItem("identity"));
+ let ident_elem = document.getElementById("ident-name");
+ if (ident == null) {
+ ident_elem.innerHTML = "no identity yet";
+ document.getElementById("ident-clr").style.display = "none";
+ } else {
+ ident_elem.innerHTML = `you are <span style="color: ${ident.clr}">${ident.name}</span>`;
+ }
+ </script>
+ </body>
+</html>
diff --git a/assets/room.html b/assets/room.html
new file mode 100644
index 0000000..4dadc6b
--- /dev/null
+++ b/assets/room.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>websweeper</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <link rel="stylesheet" type="text/css" href="../s.css">
+ </head>
+ <body>
+ <div>
+ <div id="board-container">
+ <span id="board"></span>
+ <div id="cursor-frame"></div>
+ </div>
+ <form id="identform" style="display: none" action="javascript:;" onsubmit="join()">
+ <input id="name-in" type="text" value="anon">
+ <input id="clr-in" type="color" value="#33c033"></input>
+ <button>Join</button>
+ </form>
+ <div class="statusline">
+ <p id="miscinfo"></p>
+ <a href="javascript:navigator.clipboard.writeText(window.location.href);alert('copied link to clipboard');">🔗share</p>
+ <a href="javascript:clear_ident();">new identity</p>
+ <a href="..">back to lobby</a>
+ </div>
+ </div>
+ </body>
+ <script src="https://unpkg.com/fflate"></script>
+ <script src="../c.js"></script>
+</html>
diff --git a/assets/style.css b/assets/style.css
new file mode 100644
index 0000000..49e832e
--- /dev/null
+++ b/assets/style.css
@@ -0,0 +1,86 @@
+@font-face {
+ font-family: vt323;
+ src: url("./f.ttf");
+}
+#board-container {
+ font-size: 80px;
+ line-height: 48px;
+ margin: 2vw;
+ position: relative;
+ top: 0px;
+ left 0px;
+}
+#cursor-frame {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+}
+
+body {
+ font-family: vt323, monospace;
+ font-size: 20pt;
+ background-color: black;
+ color: white;
+ margin: 0;
+}
+.unsel {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+.cursor {
+ font-size: 20pt;
+ padding: 0;
+ pointer-events: none;
+ z-index: 1;
+}
+.cursor * {
+ margin: 0 0;
+}
+.cursor-name {
+ background-color: #000000c0;
+ padding: 0 0.1em;
+ line-height: initial;
+}
+
+a {
+ text-decoration: none;
+ color: #8b8be8;
+}
+
+#miscinfo {
+ flex-grow: 1;
+}
+
+.statusline {
+ display: flex;
+ position: sticky;
+ background-color: black;
+ width: 96vw;
+ padding: 0 2vw;
+ bottom: 0px;
+ left: 0px;
+}
+.statusline * {
+ margin: 0.5em 2em 0 0;
+}
+
+/* i was today years old when i learned that css selector declaration order matters */
+/* cent should come after statusline, so it overwrites statusline's props */
+.cent {
+ width: 80vw;
+ margin: 0 auto;
+}
+
+span, h1, h4 {
+ margin: 0 auto;
+}
+
+h4 { margin-left: 1em; }
+
+a :visited {
+ color: inherit;
+}