summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--comp.c2
-rw-r--r--comp.h2
-rw-r--r--web/dartboat_wasm.c186
-rw-r--r--web/static/dartboat.js260
-rw-r--r--web/static/index.html52
-rw-r--r--web/static/style.css305
6 files changed, 805 insertions, 2 deletions
diff --git a/comp.c b/comp.c
index 4932e37..e2449e8 100644
--- a/comp.c
+++ b/comp.c
@@ -7,7 +7,7 @@
#include <stdbool.h>
#include <stdlib.h>
-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 <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;
+ }
+}