diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/dartboat_wasm.c | 186 | ||||
-rw-r--r-- | web/static/dartboat.js | 260 | ||||
-rw-r--r-- | web/static/index.html | 52 | ||||
-rw-r--r-- | web/static/style.css | 305 |
4 files changed, 803 insertions, 0 deletions
diff --git a/web/dartboat_wasm.c b/web/dartboat_wasm.c new file mode 100644 index 0000000..f43afb0 --- /dev/null +++ b/web/dartboat_wasm.c @@ -0,0 +1,186 @@ +#include "checkouts.h" +#include "comp.h" + +#include <stdlib.h> +#include <stdio.h> +#include <time.h> + +#include <emscripten/emscripten.h> + +// TODO refactor *everything* + +struct match_state { + struct leg *l1, *l2; + int complete; +}; + +void suggested(int rem, char *buf) { + buf[0] = 0; + + if (rem <= 170) { + char *target = CHECKOUTS[2][rem-1]; + if (target) { + int trem = rem - segment_points(segment_from_name(target)); + int len = sprintf(buf, "%s", target); + if (trem) { + target = CHECKOUTS[1][trem-1]; + len += sprintf(buf + len, "-%s", target); + trem = trem - segment_points(segment_from_name(target)); + + if (trem) { + target = CHECKOUTS[0][trem-1]; + len += sprintf(buf + len, "-%s", target); + } + } + } + } +} + +EMSCRIPTEN_KEEPALIVE bool user_visit(struct match_state *state, int points) { + if (state->l1->rem <= 0 || state->l2->rem <= 0) + return false; + + if (points > state->l1->rem || points > 180 || points == 179 || + points == 178 || points == 176 || points == 175 || points == 173 || + points == 172 || points == 169 || points == 166 || points == 163) + return false; + + struct leg *l = state->l1; + struct visit *v = l->visits + l->n_visits++; + v->points = points; + l->rem -= points; + v->rem = l->rem; + EM_ASM({updateUserRem($0)}, l->rem); + + char sug[100]; + suggested(v->rem, sug); + EM_ASM({promptSugg($0)}, sug); + + return true; +} + +EMSCRIPTEN_KEEPALIVE int is_match_over(struct match_state *state) { + if (state->l1->rem <= 0 || state->l2->rem <= 0) + return 1; + return 0; +} + +EMSCRIPTEN_KEEPALIVE void boat_visit(struct match_state *state) { + if (state->l1->rem <= 0 || state->l2->rem <= 0) + return; + + struct leg *l = state->l2; + comp_visit(l); + + struct visit *v = l->visits + l->n_visits - 1; + + char *s1 = segment_name(v->darts[0]), + *s2 = v->n_darts > 1 ? segment_name(v->darts[1]) : NULL, + *s3 = v->n_darts > 2 ? segment_name(v->darts[2]) : NULL; + + EM_ASM({boatVisitRes($0, $1, $2, $3, $4, $5, $6, $7)}, + l->rem, v->n_darts, segment_points(v->darts[0]), + v->n_darts > 1 ? segment_points(v->darts[1]) : 0, + v->n_darts > 2 ? segment_points(v->darts[2]) : 0, + s1, s2, s3); + + free(s1); + free(s2); + free(s3); +} + +EMSCRIPTEN_KEEPALIVE void draw_match(struct match_state *state) { + struct leg *l1 = state->l1; + struct leg *l2 = state->l2; + + char visit_no[10], u_pts[10], u_rem[10], b_pts[10], b_rem[10], b_darts[100]; + visit_no[0] = u_pts[0] = u_rem[0] = b_pts[0] = b_rem[0] = b_darts[0] = 0; + + EM_ASM({drawVisitNames($0, $1)}, + l1->name, l2->name); + EM_ASM({drawVisit($0, $1, $2, $3, $4, $5)}, + "0", "", "501", "", "501", ""); + + int n_visits = l1->n_visits > l2->n_visits ? l1->n_visits : l2->n_visits; + for (int i = 0; i < n_visits; ++i) { + sprintf(visit_no, "%d", i + 1); + + struct visit *v = l1->visits + i; + sprintf(u_pts, "%d", v->points); + sprintf(u_rem, "%d", v->rem); + + if (i < l2->n_visits) { + v = l2->visits + i; + sprintf(b_pts, "%d", v->points); + sprintf(b_rem, "%d", v->rem); + + for (int j = 0; j < v->n_darts; ++j) { + char *n = segment_name(v->darts[j]); + sprintf(b_darts + (j ? (j * 5 - 1) : 0), j == 0 ? "%4s" : " %4s", n); + free(n); + } + } + + EM_ASM({drawVisit($0, $1, $2, $3, $4, $5)}, + visit_no, u_pts, u_rem, b_pts, b_rem, b_darts); + + visit_no[0] = 0; + u_pts[0] = 0; + u_rem[0] = 0; + b_pts[0] = 0; + b_rem[0] = 0; + b_darts[0] = 0; + } + + if (l1->rem <= 0) { + EM_ASM({promptMsg($0)}, "You win! :)"); + EM_ASM(matchOver()); + } else if (l2->rem <= 0) { + EM_ASM({promptSugg($0)}, ""); + EM_ASM({promptMsg($0)}, "Bot wins. :("); + EM_ASM(matchOver()); + } else { + EM_ASM({promptMsg($0)}, "Enter points:"); + } + +} + +void init_boat() { + static int ran; + + if (ran) return; + ran = 1; + + srand(time(NULL)); + init_board(); + + EM_ASM({updateStdev($0)}, horizontal_stdev); +} + +EMSCRIPTEN_KEEPALIVE struct match_state *start_match() { + init_boat(); + struct match_state *state = calloc(1, sizeof(struct match_state)); + state->l1 = leg_init(501, "User"); + state->l2 = leg_init(501, "Bot"); + + EM_ASM({updateUserRem($0)}, state->l1->rem); + EM_ASM({updateBoatRem($0)}, state->l2->rem); + + return state; +} + +EMSCRIPTEN_KEEPALIVE void free_match(struct match_state *state) { + leg_free(state->l1); + leg_free(state->l2); + free(state); +} + +EMSCRIPTEN_KEEPALIVE void change_stdev(float hstdev, float vstdev) { + printf("%f %f\n", hstdev, vstdev); + horizontal_stdev = hstdev; + vertical_stdev = vstdev; +} + +int main() { + EM_ASM(initMatch()); +} 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; + } +} |