summaryrefslogtreecommitdiff
path: root/web/static
diff options
context:
space:
mode:
authorDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2022-04-20 11:00:24 -0700
committerDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2022-04-20 11:00:24 -0700
commit05dd0652a768ac05740d1465d30e1792b874414f (patch)
tree4a631c8abd87fd06e969188fbc91519a3a154630 /web/static
parent36b74c04154cdecba1f999f5b358874502755cd0 (diff)
downloaddartboat-05dd0652a768ac05740d1465d30e1792b874414f.tar.gz
dartboat-05dd0652a768ac05740d1465d30e1792b874414f.tar.xz
add web interface using Wasm
Diffstat (limited to 'web/static')
-rw-r--r--web/static/dartboat.js260
-rw-r--r--web/static/index.html52
-rw-r--r--web/static/style.css305
3 files changed, 617 insertions, 0 deletions
diff --git a/web/static/dartboat.js b/web/static/dartboat.js
new file mode 100644
index 0000000..b261f28
--- /dev/null
+++ b/web/static/dartboat.js
@@ -0,0 +1,260 @@
+let match_active = false;
+let prompt_disable = false;
+let match_state, user_rem, boat_rem;
+let oi_timeout;
+let delay_ms = 1000;
+
+const POINT_CLASSES = [180, 140, 100, 60, 40, 20, 1, 0];
+
+function stcall(f, ret_type, arg_types, args) {
+ return Module.ccall(f, ret_type,
+ arg_types ? ['number'].concat(arg_types) : ['number'],
+ args ? [match_state].concat(args) : [match_state]);
+}
+
+function initMatch() {
+ stcall('free_match');
+ updateDelay(delay_ms);
+ match_state = stcall('start_match', 'number');
+ match_active = true;
+ document.getElementById('user-rem').className = 'active';
+ document.getElementById('match').textContent = '';
+ promptSuggStr("");
+ stcall('draw_match');
+}
+
+function oi() {
+ document.getElementById('oi').textContent = 'oi!';
+
+ oi_timeout = setTimeout(function() { oi_timeout = null; clearOi() }, 3000);
+}
+
+function clearOi() {
+ document.getElementById('oi').textContent = '';
+
+ if (oi_timeout) {
+ clearTimeout(oi_timeout);
+ oi_timeout = null;
+ }
+}
+
+function updateUserRem(rem) {
+ user_rem = document.getElementById('user-rem').textContent = rem;
+}
+
+function updateBoatRem(rem) {
+ boat_rem = document.getElementById('boat-rem').textContent = rem;
+}
+
+function promptMsg(p) {
+ document.getElementById('prompt-msg').textContent = UTF8ToString(p);
+}
+
+function promptMsgStr(str) {
+ document.getElementById('prompt-msg').textContent = str;
+}
+
+function promptSugg(p) {
+ document.getElementById('prompt-sugg').textContent = UTF8ToString(p);
+}
+
+function promptSuggStr(str) {
+ document.getElementById('prompt-sugg').textContent = str;
+}
+
+function promptUpdateRem() {
+ let elem = document.getElementById('user-rem');
+ let pts = document.getElementById('prompt').textContent;
+ if (pts)
+ elem.textContent = `${user_rem} » ${user_rem - pts}`;
+ else
+ elem.textContent = user_rem;
+}
+
+function boatTempRem(pts, str) {
+ let elem = document.getElementById('boat-rem');
+ elem.textContent = `${boat_rem - pts} « ${boat_rem}`;
+
+ document.getElementById('prompt').textContent = pts;
+ promptSuggStr(str);
+}
+
+function promptAppend(val) {
+ if (!match_active || prompt_disable) return;
+ clearOi();
+
+ let elem = document.getElementById('prompt');
+ if (elem.textContent.length < 3) {
+ elem.textContent += val;
+ promptUpdateRem();
+ }
+}
+
+function promptClear() {
+ if (!match_active || prompt_disable) return;
+ clearOi();
+
+ document.getElementById('prompt').textContent = '';
+ promptUpdateRem();
+}
+
+function promptBackspace() {
+ if (!match_active || prompt_disable) return;
+ clearOi();
+
+ let elem = document.getElementById('prompt');
+ elem.textContent = elem.textContent.slice(0, -1);
+ promptUpdateRem();
+}
+
+function setBoatActive() {
+ document.getElementById('user-rem').className = '';
+ document.getElementById('boat-rem').className = 'active';
+ promptMsgStr("Bot is throwing…");
+ prompt_disable = true;
+}
+
+function setUserActive() {
+ document.getElementById('user-rem').className = 'active';
+ document.getElementById('boat-rem').className = '';
+ prompt_disable = false;
+}
+
+function promptSubmit() {
+ clearOi();
+ if (prompt_disable) return;
+ if (!match_active) {
+ initMatch();
+ return;
+ }
+
+ let elem = document.getElementById('prompt');
+ let p_user = elem.textContent;
+ if (!p_user) return;
+ promptClear();
+
+ if (!stcall('user_visit', 'number', ['number'], [p_user])) {
+ oi();
+ return;
+ }
+
+ elem = document.getElementById('match');
+ elem.textContent = '';
+ stcall('draw_match');
+
+ if (!stcall('is_match_over', 'number')) {
+ setBoatActive();
+ stcall('boat_visit', 'number');
+ }
+}
+
+function boatVisitRes(rem, n, p1, p2, p3, ptr1, ptr2, ptr3) {
+ // cannot convert in timeout func because strings are freed in c func
+ let s1 = UTF8ToString(ptr1);
+ let s2 = UTF8ToString(ptr2);
+ let s3 = UTF8ToString(ptr3);
+
+ if (delay_ms == 0) {
+ updateBoatRem(rem);
+ document.getElementById('match').textContent = '';
+ stcall('draw_match');
+ setUserActive();
+ } else {
+ // backup to restore after bot is done
+ let user_sugg = document.getElementById('prompt-sugg').textContent;
+ promptSuggStr('');
+
+ setTimeout(function() { boatTempRem(p1, s1); }, delay_ms);
+
+ if (n > 1) {
+ setTimeout(function() { boatTempRem(p1 + p2, `${s1}-${s2}`); }, delay_ms * 2);
+ }
+
+ if (n > 2) {
+ setTimeout(function() { boatTempRem(p1 + p2 + p3, `${s1}-${s2}-${s3}`); }, delay_ms * 3);
+ }
+
+ setTimeout(function() {
+ updateBoatRem(rem);
+ document.getElementById('prompt').textContent = '';
+ document.getElementById('match').textContent = '';
+ promptSuggStr(user_sugg);
+ stcall('draw_match');
+ setUserActive();
+ }, delay_ms * (n + 1));
+ }
+}
+
+function matchOver() {
+ match_active = false;
+ promptSuggStr("Press OK to play again.");
+ document.getElementById('user-rem').className = '';
+}
+
+function drawVisitNames(n1, n2) {
+ let elem = document.getElementById('match');
+ for (let [k, v] of Object.entries({ 'user-name': n1, 'boat-name': n2 })) {
+ let div = document.createElement('div');
+ div.className = k;
+ div.textContent = UTF8ToString(v);
+ elem.append(div);
+ }
+}
+
+function drawVisit(visit_no, u_pts, u_rem, b_pts, b_rem, b_darts) {
+ let elem = document.getElementById('match');
+
+ for (let [i, v] of [visit_no, u_pts, u_rem, b_rem, b_pts, b_darts].entries()) {
+ let div = document.createElement('div');
+ let vv = div.textContent = UTF8ToString(v);
+ if (i == 0) {
+ div.className = 'visit-no';
+ } else if (i == 1 || i == 4) {
+ div.className = `p${POINT_CLASSES.find(x => x <= vv)}`;
+ } else if (i == 5) {
+ div.className = 'darts';
+ }
+ elem.append(div);
+ }
+
+ elem.scrollTop = elem.scrollHeight;
+}
+
+function stdevChanged(val) {
+ Module.ccall('change_stdev', null, ['number', 'number'], [val, val]);
+}
+
+function updateStdev(val) {
+ document.getElementById('stdev').value = val;
+}
+
+function updateDelay(val) {
+ document.getElementById('delay').value = val;
+}
+
+function delayChanged(val) {
+ delay_ms = val;
+}
+
+function processKey(data) {
+ if (data.altKey || data.ctrlKey || data.metaKey || data.target.type == 'text')
+ return;
+ let key = data.key;
+
+ if (isFinite(key))
+ promptAppend(key);
+ else if (key == 'Enter')
+ promptSubmit();
+ else if (key == 'Backspace')
+ promptBackspace();
+}
+
+function modal(id) {
+ document.getElementById(id).style.display = 'block';
+}
+
+window.onclick = function(e) {
+ if (e.target.classList.contains('modal')) e.target.style.display = 'none';
+}
+
+document.addEventListener('keydown', processKey);
diff --git a/web/static/index.html b/web/static/index.html
new file mode 100644
index 0000000..861adc5
--- /dev/null
+++ b/web/static/index.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>dartboat™</title>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <meta name="viewport" content="width=device-width, user-scalable=no" />
+ <link rel="stylesheet" type="text/css" href="style.css" />
+ <script src="dartboat.js"></script>
+ <script src="dartboat_wasm.js"></script>
+ </head>
+ <body>
+ <div id="main">
+ <div id="keypad">
+ <div id="user-rem"></div>
+ <div id="oi"></div>
+ <div id="boat-rem"></div>
+ <div id="prompt-container">
+ <div id="prompt-msg"></div>
+ <div id="prompt"></div>
+ <div id="prompt-sugg"></div>
+ </div>
+ <div onclick="promptAppend(1)" class="key num">1</div>
+ <div onclick="promptAppend(2)" class="key num">2</div>
+ <div onclick="promptAppend(3)" class="key num">3</div>
+ <div onclick="promptAppend(4)" class="key num">4</div>
+ <div onclick="promptAppend(5)" class="key num">5</div>
+ <div onclick="promptAppend(6)" class="key num">6</div>
+ <div onclick="promptAppend(7)" class="key num">7</div>
+ <div onclick="promptAppend(8)" class="key num">8</div>
+ <div onclick="promptAppend(9)" class="key num">9</div>
+ <div onclick="promptClear()" class="key">CLEAR</div>
+ <div onclick="promptAppend(0)" class="key num">0</div>
+ <div onclick="promptSubmit()" class="key">OK</div>
+ </div>
+ <div id="settings-bar">
+ <div>dartboat™</div>
+ <div class="input first"><span>delay</span><input id="delay" onchange="delayChanged(this.value)" maxlength="4" value="1000"></div>
+ <div class="input"><span>stdev</span><input id="stdev" onchange="stdevChanged(this.value)" maxlength="4" value=""></div>
+ <div class="help-button" onclick="modal('help-modal')">?</div>
+ </div>
+ <div id="match"></div>
+ </div>
+ <div id="help-modal" class="modal">
+ <div class="modal-content">
+ <p><strong>dartboat</strong> works using an internal representation of a specification dartboard. Upon selecting a target, the dart is thrown following a normal distribution with configurable inaccuracy. The resultant coordinates are then used to calculate the segment in which the dart landed. The idea is that this provides a more realistic opponent than picking points at random.</p>
+ <p>The <em>stdev</em> setting controls the standard deviation of the bot's throws in millimetres. A value of 24 translates to a three-dart average of roughly 35. A value of 13 would be a 65 average, and a value of 8 a 95 average.</p>
+ <p>The <em>delay</em> setting controls how many milliseconds it takes the bot to throw each dart.</p>
+ <p>dartboat is <a href="https://retarded.software/dartbot.git/" target="_blank">free and open-source software</a>. It is written primarily in C. The web target is compiled to WebAssembly, along with some JavaScript for the interactive elements. This is a work in progress—a lot of features are missing and a lot of things will change.</p>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/web/static/style.css b/web/static/style.css
new file mode 100644
index 0000000..f22e29c
--- /dev/null
+++ b/web/static/style.css
@@ -0,0 +1,305 @@
+@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,700;1,400&display=swap');
+
+body {
+ color: #ddd;
+ background-color: #1a1a1a;
+ font-family: 'Lato', sans-serif;
+ user-select: none;
+
+ margin: 0;
+ padding: 0;
+}
+
+div#main {
+ height: calc(100vh - 2*2px);
+ width: calc(100vw - 2*2px);
+ margin: 2px;
+
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ grid-template-rows: min-content 1fr;
+ grid-template-areas: "keypad settings-bar" "keypad match";
+}
+
+div#keypad {
+ grid-area: keypad;
+
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: min-content 2fr;
+ grid-auto-rows: 1fr;
+}
+
+div#oi {
+ font-size: 4vh;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+div#user-rem, div#boat-rem {
+ background-color: #333;
+ font-size: 3vh;
+
+ margin: 2px;
+ padding: 0.25em;
+
+ display: flex;
+ align-items: center;
+ justify-content: left;
+}
+
+div#boat-rem {
+ grid-column: 3;
+
+ justify-content: right;
+}
+
+div#user-rem.active, div#boat-rem.active {
+ background-color: #240;
+}
+
+div#prompt-container {
+ grid-column: 1 / span 3;
+
+ background-color: #333;
+
+ margin: 2px;
+
+ display: grid;
+ grid-template-rows: 1fr 3fr 1fr;
+ grid-template-columns: 1fr;
+}
+
+div#prompt-msg, div#prompt, div#prompt-sugg {
+ font-size: 3vh;
+
+ display: grid;
+ align-items: center;
+ justify-content: center;
+}
+
+div#prompt {
+ font-size: 10vh;
+}
+
+div#prompt-sugg {
+ color: #aaa;
+}
+
+div.key {
+ color: #aaa;
+ background-color: #282828;
+ font-size: 3vh;
+
+ margin: 2px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@media (hover: hover) and (pointer: fine) {
+ div.key:hover {
+ background-color: #240;
+ }
+}
+
+div.key:active {
+ color: #fff;
+ background-color: #360;
+}
+
+div.key.num {
+ font-size: 5vh;
+ font-weight: 700;
+}
+
+div#settings-bar {
+ grid-area: settings-bar;
+
+ background-color: #333;
+ font-size: 3vh;
+
+ margin: 2px;
+ padding: 0.25em;
+
+ display: flex;
+ align-items: center;
+ justify-content: left;
+ gap: 0.25em;
+}
+
+div#settings-bar div.input.first {
+ margin-left: auto;
+}
+
+div#settings-bar div.input {
+ background-color: #282828;
+
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+}
+
+div#settings-bar span {
+ padding: 0 0.25em;
+}
+
+div#settings-bar input {
+ color: #ddd;
+ background-color: #304;
+ font-family: inherit;
+ font-size: 3vh;
+ text-align: right;
+
+ width: 4ch;
+ padding: 0 0.25em;
+ border: 0;
+}
+
+div#settings-bar div.help-button {
+ background-color: #304;
+ font-weight: bold;
+
+ padding: 0 0.5em;
+}
+
+@media (hover: hover) and (pointer: fine) {
+ div#settings-bar input:hover {
+ background-color: #608;
+ }
+
+ div#settings-bar div.help-button:hover {
+ background-color: #608;
+ }
+}
+
+div#settings-bar input#stdev:focus {
+ color: #fff;
+ background-color: #80a;
+}
+
+div#settings-bar div.help-button:active {
+ color: #fff;
+ background-color: #80a;
+}
+
+div#settings-bar input#stdev:focus {
+ outline: #5a0 solid 3px;
+}
+
+div#match {
+ grid-area: match;
+
+ background-color: #333;
+ font-size: 3vh;
+
+ overflow-y: scroll;
+ margin: 2px;
+ padding: 0.25em;
+
+ display: grid;
+ grid-template-columns: repeat(6, max-content);
+ grid-auto-rows: min-content;
+ grid-row-gap: 0.2em;
+ grid-column-gap: 1em;
+}
+
+div#match div {
+ font-family: monospace;
+ white-space: pre;
+
+ min-width: 3ch;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+div#match .user-name { color: #aaa; background-color: #444; grid-column: 2 / span 2; justify-content: center; }
+div#match .boat-name { color: #aaa; background-color: #444; grid-column: 4 / span 2; justify-content: center; }
+div#match .visit-no { color: #888; grid-column: 1; }
+div#match .p0 { color: #f00; font-weight: bold; }
+div#match .p1 { color: #f00; }
+div#match .p20 { color: #f60; }
+div#match .p40 { color: #fa0; }
+div#match .p60 { color: #dd0; }
+div#match .p100 { color: #2c2; }
+div#match .p140 { color: #0f0; }
+div#match .p180 { color: #0f0; font-weight: bold; }
+div#match .darts { color: #888; justify-content: left; }
+
+.modal {
+ background-color: rgba(0, 0, 0, 0.6);
+
+ width: 100%;
+ height: 100%;
+
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1;
+}
+
+.modal-content {
+ background-color: #333;
+ font-size: 1.4em;
+ font-family: 'Source Serif Pro', serif;
+
+ width: calc(min(40em, 90vw) - 4em);
+ max-height: calc(calc(100vh - 10vw) - 4em);
+ margin: 5vw auto;
+ padding: 2em;
+ outline: #5a0 solid 3px;
+ overflow: scroll;
+}
+
+@media (max-width: 400px) {
+ .modal-content {
+ font-size: 1em;
+ }
+}
+
+.modal-content p {
+ margin: 0;
+}
+
+.modal-content p + p {
+ margin-top: 1em;
+}
+
+a {
+ color: #5a0;
+}
+
+a:hover {
+ color: #7d0;
+}
+
+@media (max-aspect-ratio: 1/1) {
+ div#main {
+ grid-template-columns: 1fr;
+ grid-template-rows: min-content 1fr 3fr;
+ grid-template-areas: "settings-bar" "match" "keypad";
+ }
+}
+
+@media (max-aspect-ratio: 3/5) {
+ div#settings-bar div.input {
+ display: flex;
+ flex-direction: column;
+ }
+
+ div#settings-bar span {
+ padding: 0 0.25em;
+ }
+
+ div#settings-bar .help-button {
+ font-size: 6vh;
+ }
+}