From 05dd0652a768ac05740d1465d30e1792b874414f Mon Sep 17 00:00:00 2001 From: David Vazgenovich Shakaryan Date: Wed, 20 Apr 2022 11:00:24 -0700 Subject: add web interface using Wasm --- comp.c | 2 +- comp.h | 2 +- web/dartboat_wasm.c | 186 ++++++++++++++++++++++++++++++ web/static/dartboat.js | 260 +++++++++++++++++++++++++++++++++++++++++ web/static/index.html | 52 +++++++++ web/static/style.css | 305 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 805 insertions(+), 2 deletions(-) create mode 100644 web/dartboat_wasm.c create mode 100644 web/static/dartboat.js create mode 100644 web/static/index.html create mode 100644 web/static/style.css diff --git a/comp.c b/comp.c index 4932e37..e2449e8 100644 --- a/comp.c +++ b/comp.c @@ -7,7 +7,7 @@ #include #include -int horizontal_stdev = 24, vertical_stdev = 24; +double horizontal_stdev = 24, vertical_stdev = 24; double drand() { diff --git a/comp.h b/comp.h index 460291e..4cab400 100644 --- a/comp.h +++ b/comp.h @@ -3,7 +3,7 @@ #include "match.h" -extern int horizontal_stdev, vertical_stdev; +extern double horizontal_stdev, vertical_stdev; void comp_visit(struct leg *l); 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 +#include +#include + +#include + +// 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 @@ + + + + dartboat™ + + + + + + + +
+
+
+
+
+
+
+
+
+
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
CLEAR
+
0
+
OK
+
+
+
dartboat™
+
delay
+
stdev
+
?
+
+
+
+ + + 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; + } +} -- cgit v1.2.3-70-g09d2