From c68dc16bef476203a4424c4b857f16eeb8f0e119 Mon Sep 17 00:00:00 2001
From: stale <redkugelblitzin@gmail.com>
Date: Fri, 1 Jul 2022 15:15:05 -0300
Subject: initial commit, but with scarequotes

---
 assets/VT323-Regular.ttf | Bin 0 -> 149688 bytes
 assets/client.js         | 267 +++++++++++++++++++++++++++++++++++++++++++++++
 assets/index.html        | 132 +++++++++++++++++++++++
 assets/room.html         |  30 ++++++
 assets/style.css         |  86 +++++++++++++++
 5 files changed, 515 insertions(+)
 create mode 100644 assets/VT323-Regular.ttf
 create mode 100644 assets/client.js
 create mode 100644 assets/index.html
 create mode 100644 assets/room.html
 create mode 100644 assets/style.css

(limited to 'assets')

diff --git a/assets/VT323-Regular.ttf b/assets/VT323-Regular.ttf
new file mode 100644
index 0000000..6aec599
Binary files /dev/null and b/assets/VT323-Regular.ttf 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;
+}
-- 
cgit v1.2.3