Improve instructions, export score, split code
dkl9

dkl9 commited on 2025-209 17:28:24
Showing 4 changed files, with 363 additions and 295 deletions.

... ...
@@ -4,9 +4,9 @@
4 4
         <title>Functle</title>
5 5
         <style>
6 6
             body { font-family: sans-serif; }
7
-            #plot { border: 1px solid grey; flex-shrink: 0; height: 400px; }
7
+            #plot { border: 1px solid grey; flex-shrink: 0; width: min(90vw, 25em); height: min(90vw, 25em); flex-shrink: 0; }
8 8
             #grid { border-collapse: separate; border-spacing: 3px 12px; margin-top: 10px; }
9
-            #grid td { min-width: 60px; height: 60px; border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; transition: transform 0.3s ease; }
9
+            #grid td { min-width: min(13vw, 4rem); height: min(13vw, 4rem); border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; transition: transform 0.3s ease; }
10 10
             .grey { background: #3a3a3c; color: white; }
11 11
             .yellow { background: #b59f3b; color: white; }
12 12
             .green { background: #538e4e; color: white; }
... ...
@@ -26,6 +26,29 @@
26 26
     </head>
27 27
     <body>
28 28
         <div class="container">
29
+            <section id="instructions">
30
+                <h1>How to play</h1>
31
+                <p>Look at the green plot <var>y</var> = <var>f</var>(<var>x</var>) and guess the formula for <var>f</var>(<var>x</var>) in <a href="https://en.wikipedia.org/wiki/Reverse_Polish_notation#Explanation" target="_blank">postfix</a>.
32
+                Use one character per number or operator:</p>
33
+                <ul>
34
+                    <li><var>x</var>, 1, or 2 (values)</li>
35
+                    <li>q for "squared" (unary)</li>
36
+                    <li>c for "cubed" (unary)</li>
37
+                    <li>s for sine</li>
38
+                    <li>e for exp (<var>e</var> to the ...)</li>
39
+                    <li>l for natural log</li>
40
+                    <li>+, -, *, / for basic operations</li>
41
+                </ul>
42
+                <p>For example, write 1 / (sin² <var>x</var>) as "1 x s q /", or ln(2) <var>e</var><sup><var>x</var></sup> as "2 l x e *".</p>
43
+                <p>Your answer will be coloured as feedback.
44
+                <span class="green">Green: correct, right spot.</span>
45
+                <span class="yellow">Yellow: correct, wrong spot.</span>
46
+                <span class="grey">Grey: not in answer</span></p>
47
+                <p>Remember that 1 and 2 can be used to build other, more complicated constants.
48
+                Remember how syntax narrows down your options: operators tend to be near the end, and inputs near the start.
49
+                Axis ticks show <var>x</var> = ±1 and <var>y</var> = ±1.</p>
50
+                <p><button id="gotit" type="button">Got it</a></p>
51
+            </section>
29 52
             <canvas id="plot" width="400" height="400"></canvas>
30 53
             <div class="side">
31 54
                 <div>
... ...
@@ -33,299 +56,12 @@
33 56
                 </div>
34 57
                 <table id="grid"></table>
35 58
             </div>
36
-            <div class="side">
37
-                <p>Guess the function based on its plot on the left.
38
-                Tick marks on axes indicate <var>x</var> = ±1 and <var>y</var> = ±1.
39
-                Functions here are made of values <var>x</var>, 1, and 2, modified by functions ^2 (square), ^3 (cube), sin, exp, and ln, combined by operators +, -, *, /.</p>
40
-                <p>Enter your answer in postfix (reverse Polish) notation.
41
-                For example, express 1 - <var>x</var> / (sin <var>x</var>) as "1 x x sin / -".</p>
42
-                <p>The correct answer will be five tokens.
43
-                Numerically equivalent rearranged answers will be accepted.</p>
44
-                <p>Enhanced from a prototype by ChatGPT o4-mini.</p>
45
-            </div>
59
+            <!-- <div class="side">
60
+                TODO: comments and questions ...
61
+            </div> -->
46 62
         </div>
47
-        <script>
48
-            const allowed = {
49
-                terminals: ["x", "2", "1"],
50
-                unary: ["^2", "^3", "sin", "exp", "ln"],
51
-                binary: ["+", "-", "*", "/"]
52
-            };
53
-            const allTokens = [...allowed.terminals, ...allowed.unary, ...allowed.binary];
54
-            const tl = 5;
55
-            const keyToToken = { x:"x", 1:"1", 2:"2", q:"^2", c:"^3", s:"sin", e:"exp", l:"ln", "+":"+", "-":"-", "*":"*", "/":"/" };
56
-            const target = generateExpr();
57
-            const maxGuesses = 6;
58
-            const width = 400, height = 400;
59
-            const ctx = document.getElementById("plot").getContext("2d");
60
-            const bound = 10;
61
-            const xs = [];
62
-            const targetYs = [];
63
-            for (let i = 0; i <= width; i++) {
64
-                const x = bound * (2 * i / width - 1);
65
-                xs.push(x);
66
-                const yv = evalPostfix(target, x);
67
-                targetYs.push(yv);
68
-            }
69
-            const clampLow = 1.5;
70
-            const clampHigh = 20;
71
-            let yMin = Math.min(...targetYs);
72
-            let yMax = Math.max(...targetYs);
73
-            if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh;
74
-            else if (yMin > -clampLow) yMin = -clampLow;
75
-            if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh;
76
-            else if (yMax < clampLow) yMax = clampLow;
77
-            const guessYsList = [];
78
-            const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches;
79
-
80
-            function shakeRow(row) {
81
-                row.classList.remove("shake");
82
-                void row.offsetWidth;
83
-                row.classList.add("shake");
84
-            }
85
-
86
-            const grid = document.getElementById("grid");
87
-            for (let i = 0; i < 6; i++) {
88
-                const tr = document.createElement("tr");
89
-                for (let j = 0; j < tl; j++) {
90
-                    const td = document.createElement("td");
91
-                    tr.appendChild(td);
92
-                }
93
-                grid.appendChild(tr);
94
-            }
95
-
96
-            let currentRow = 0;
97
-            let currentInput = [];
98
-            redrawAll();
99
-
100
-            document.addEventListener("keydown", e => {
101
-                const row = grid.rows[currentRow];
102
-                if (e.key === "Backspace") {
103
-                    if (currentInput.length > 0) {
104
-                        currentInput.pop();
105
-                        row.cells[currentInput.length].textContent = "";
106
-                    } else {
107
-                        shakeRow(row);
108
-                    }
109
-                } else if (e.key === "Enter") {
110
-                    submitGuess();
111
-                } else if (keyToToken.hasOwnProperty(e.key)) {
112
-                    if (currentInput.length < tl) {
113
-                        const token = keyToToken[e.key];
114
-                        currentInput.push(token);
115
-                        const c = row.cells[currentInput.length - 1];
116
-                        c.style.fontSize = `${3.5 - 0.5 * token.length}em`;
117
-                        c.textContent = token;
118
-                    } else {
119
-                        shakeRow(row);
120
-                    }
121
-                }
122
-            });
123
-
124
-            function submitGuess() {
125
-                const row = grid.rows[currentRow];
126
-                document.getElementById("message").textContent = "";
127
-                if (currentInput.length !== tl) {
128
-                    document.getElementById("message").textContent = `Use exactly ${tl} tokens.`;
129
-                    shakeRow(row);
130
-                    return;
131
-                }
132
-                for (const t of currentInput) {
133
-                    if (!allTokens.includes(t)) {
134
-                        document.getElementById("message").textContent = "Invalid token: " + t;
135
-                        shakeRow(row);
136
-                        return;
137
-                    }
138
-                }
139
-                let ys;
140
-                try {
141
-                    ys = xs.map(x => evalPostfix(currentInput, x));
142
-                } catch (e) {
143
-                    document.getElementById("message").textContent = "Syntax error.";
144
-                    shakeRow(row);
145
-                    return;
146
-                }
147
-                guessYsList.push(ys);
148
-                redrawAll();
149
-                const close = closeEnough(ys, targetYs);
150
-                animateFlip(currentInput, close);
151
-                if (close) {
152
-                    setTimeout(() => fillRow(currentInput, "green"), 300 * tl);
153
-                    return;
154
-                }
155
-                currentRow++;
156
-                currentInput = [];
157
-                if (currentRow >= 6) return;
158
-            }
159
-
160
-            function animateFlip(tokens, close) {
161
-                const row = grid.rows[currentRow];
162
-                const colors = Array(tl);
163
-                if (close) {
164
-                    colors.fill("green");
165
-                } else {
166
-                    const freq = {};
167
-                    for (const t of target) freq[t] = (freq[t] || 0) + 1;
168
-                    colors.fill("grey");
169
-                    for (let i = 0; i < tl; i++) {
170
-                        if (tokens[i] === target[i]) {
171
-                            colors[i] = "green";
172
-                            freq[tokens[i]]--;
173
-                        }
174
-                    }
175
-                    for (let i = 0; i < tl; i++) {
176
-                        if (colors[i] === "grey" && freq[tokens[i]] > 0) {
177
-                            colors[i] = "yellow";
178
-                            freq[tokens[i]]--;
179
-                        }
180
-                    }
181
-                }
182
-                tokens.forEach((token, i) => {
183
-                    const cell = row.cells[i];
184
-                    setTimeout(() => {
185
-                        cell.style.transform = "rotateX(90deg)";
186
-                        setTimeout(() => {
187
-                            cell.classList.add(colors[i]);
188
-                            cell.style.transform = "rotateX(0deg)";
189
-                        }, 150);
190
-                    }, 150 * i);
191
-                });
192
-            }
193
-
194
-            function fillRow(tokens, cls) {
195
-                const row = grid.rows[currentRow];
196
-                for (let i = 0; i < tl; i++) {
197
-                    const cell = row.cells[i];
198
-                    cell.textContent = tokens[i];
199
-                    cell.classList.add(cls);
200
-                }
201
-            }
202
-
203
-            function evalPostfix(tokens, x) {
204
-                const s = [];
205
-                for (const t of tokens) {
206
-                    if (t === "x") s.push(x);
207
-                    else if (t === "1") s.push(1);
208
-                    else if (t === "2") s.push(2);
209
-                    else if (allowed.unary.includes(t)) {
210
-                        if (s.length < 1) throw 0;
211
-                        const v = s.pop();
212
-                        if (t === "^2") s.push(v ** 2);
213
-                        if (t === "^3") s.push(v ** 3);
214
-                        if (t === "sin") s.push(Math.sin(v));
215
-                        if (t === "exp") s.push(Math.exp(v));
216
-                        if (t === "ln") s.push(Math.log(v));
217
-                    } else if (allowed.binary.includes(t)) {
218
-                        if (s.length < 2) throw 0;
219
-                        const b = s.pop(), a = s.pop();
220
-                        if (t === "+") s.push(a + b);
221
-                        if (t === "-") s.push(a - b);
222
-                        if (t === "*") s.push(a * b);
223
-                        if (t === "/") s.push(a / b);
224
-                    } else throw 0;
225
-                }
226
-                if (s.length !== 1) throw 0;
227
-                return s[0];
228
-            }
229
-
230
-            function rand(l) {
231
-                return l[Math.floor(l.length * Math.random())];
232
-            }
233
-            
234
-            function generateExpr(len = tl, force = true) {
235
-                if (len == 1) {
236
-                    return force ? ["x"] : [rand(allowed.terminals)];
237
-                } else if (len == 2 || Math.random >= 0.2) {
238
-                    const e = generateExpr(len - 1, force);
239
-                    e.push(rand(evalPostfix(e, 0.6) == 1 ? ["sin", "exp"] : allowed.unary));
240
-                    return e;
241
-                } else {
242
-                    const k = Math.floor(1 + (len - 2) * Math.random());
243
-                    const f = force && Math.random() > 0.5;
244
-                    const a = generateExpr(k, f);
245
-                    const b = generateExpr(len - 1 - k, force && !f);
246
-                    const one = evalPostfix(a, 0.6) == 1 || evalPostfix(b, 0.6) == 1;
247
-                    const e = a.concat(b)
248
-                    e.push(rand(one ? ["+", "-"] : allowed.binary));
249
-                    return e;
250
-                }
251
-            }
252
-
253
-            function drawAxes() {
254
-                ctx.clearRect(0, 0, width, height);
255
-                ctx.beginPath();
256
-                const y0 = mapY(0);
257
-                ctx.moveTo(0, y0);
258
-                ctx.lineTo(width, y0);
259
-                const x0 = mapX(0);
260
-                ctx.moveTo(x0, 0);
261
-                ctx.lineTo(x0, height);
262
-                [1, -1].forEach(val => {
263
-                    const px = mapX(val);
264
-                    ctx.moveTo(px, y0 - 5);
265
-                    ctx.lineTo(px, y0 + 5);
266
-                });
267
-                [1, -1].forEach(val => {
268
-                    const py = mapY(val);
269
-                    ctx.moveTo(x0 - 5, py);
270
-                    ctx.lineTo(x0 + 5, py);
271
-                });
272
-                ctx.lineWidth = 1;
273
-                ctx.strokeStyle = darkTheme ? "white" : "black";
274
-                ctx.stroke();
275
-            }
276
-
277
-            function drawCurve(ys, color, width = 4) {
278
-                ctx.beginPath();
279
-                for (let i = 0; i < xs.length; i++) {
280
-                    const px = mapX(xs[i]), py = mapY(ys[i]);
281
-                    if (i === 0) ctx.moveTo(px, py);
282
-                    else ctx.lineTo(px, py);
283
-                }
284
-                ctx.lineWidth = width;
285
-                ctx.strokeStyle = color;
286
-                ctx.stroke();
287
-            }
288
-
289
-            function redrawAll() {
290
-                drawAxes();
291
-                drawCurve(targetYs, darkTheme ? "lime" : "green");
292
-                guessYsList.forEach((ys, idx) => drawCurve(ys, "red", (idx < guessYsList.length - 1) ? 1 : 4));
293
-            }
294
-
295
-            function markRow(tokens) {
296
-                const freq = {};
297
-                for (const t of target) freq[t] = (freq[t] || 0) + 1;
298
-                const colors = Array(tl).fill("grey");
299
-                for (let i = 0; i < tokens.length; i++) {
300
-                    if (tokens[i] === target[i]) {
301
-                        colors[i] = "green";
302
-                        freq[tokens[i]]--;
303
-                    }
304
-                }
305
-                for (let i = 0; i < tokens.length; i++) {
306
-                    if (colors[i] === "grey" && freq[tokens[i]] > 0) {
307
-                        colors[i] = "yellow";
308
-                        freq[tokens[i]]--;
309
-                    }
310
-                }
311
-                const row = grid.rows[currentRow];
312
-                for (let i = 0; i < tl; i++) {
313
-                    const cell = row.cells[i];
314
-                    cell.textContent = tokens[i];
315
-                    cell.classList.add(colors[i]);
316
-                }
317
-            }
318
-
319
-            function mapX(x) { return (x + bound) / (2 * bound) * width; }
320
-            function mapY(y) { return height - (y - yMin) / (yMax - yMin) * height; }
321
-
322
-            function closeEnough(a, b) {
323
-                for (let i = 0; i < a.length; i++) {
324
-                    if (isNaN(a[i]) && !isNaN(b[i])) return false;
325
-                    if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false;
326
-                }
327
-                return true;
328
-            }
329
-        </script>
63
+        <script src="guess.js"></script>
64
+        <script src="plot.js"></script>
65
+        <script src="input.js"></script>
330 66
     </body>
331 67
 </html>
... ...
@@ -0,0 +1,218 @@
1
+const allowed = {
2
+    terminals: ["x", "2", "1"],
3
+    unary: ["^2", "^3", "sin", "exp", "ln"],
4
+    binary: ["+", "-", "*", "/"]
5
+};
6
+const allTokens = [...allowed.terminals, ...allowed.unary, ...allowed.binary];
7
+const tl = 5;
8
+let target;
9
+let puzzleNum;
10
+if (true) {
11
+    target = generateExpr();
12
+    puzzleNum = 0;
13
+} else {
14
+    (async () => {
15
+        const r = await fetch("https://dkl9.net/cgi-bin/fps_curr", { redirect: "follow" });
16
+        puzzleNum = parseInt(r.url.match(/n=(\d+)/)[1]);
17
+        target = await r.text().split(" ");
18
+    })();
19
+}
20
+const container = document.getElementsByClassName("container")[0];
21
+const boxes = {grey: "⬜️", yellow: "🟨", green: "🟩"};
22
+const congrats = ["Incredible", "Amazing", "Great", "Nice", "You got it", "Whew"];
23
+const gotitButton = document.getElementById("gotit");
24
+
25
+if (localStorage.getItem("gotit") === "true") hideInstructions(gotitButton);
26
+
27
+if (gotitButton) {
28
+    gotitButton.addEventListener("click", e => {
29
+        if (localStorage.getItem("gotit") !== "true") {
30
+            hideInstructions(e.target);
31
+            localStorage.setItem("gotit", "true");
32
+        }
33
+    });
34
+}
35
+
36
+function submitGuess() {
37
+    const row = grid.rows[currentRow];
38
+    document.getElementById("message").textContent = "";
39
+    if (currentInput.length !== tl) {
40
+        document.getElementById("message").textContent = `Use exactly ${tl} tokens.`;
41
+        shakeRow(row);
42
+        return;
43
+    }
44
+    for (const t of currentInput) {
45
+        if (!allTokens.includes(t)) {
46
+            document.getElementById("message").textContent = "Invalid token: " + t;
47
+            shakeRow(row);
48
+            return;
49
+        }
50
+    }
51
+    let ys;
52
+    try {
53
+        ys = xs.map(x => evalPostfix(currentInput, x));
54
+    } catch (e) {
55
+        document.getElementById("message").textContent = "Syntax error.";
56
+        shakeRow(row);
57
+        return;
58
+    }
59
+    guessYsList.push(ys);
60
+    redrawAll();
61
+    const close = closeEnough(ys, targetYs);
62
+    animateFlip(currentInput, close);
63
+    if (close) {
64
+        setTimeout(() => {
65
+            fillRow(currentInput, "green");
66
+            winMessage();
67
+        }, 300 * tl);
68
+        return;
69
+    }
70
+    currentRow++;
71
+    if (currentRow >= maxGuesses) loseMessage();
72
+    currentInput = [];
73
+}
74
+
75
+function animateFlip(tokens, close) {
76
+    const row = grid.rows[currentRow];
77
+    const colors = Array(tl);
78
+    if (close) {
79
+        colors.fill("green");
80
+    } else {
81
+        const freq = {};
82
+        for (const t of target) freq[t] = (freq[t] || 0) + 1;
83
+        colors.fill("grey");
84
+        for (let i = 0; i < tl; i++) {
85
+            if (tokens[i] === target[i]) {
86
+                colors[i] = "green";
87
+                freq[tokens[i]]--;
88
+            }
89
+        }
90
+        for (let i = 0; i < tl; i++) {
91
+            if (colors[i] === "grey" && freq[tokens[i]] > 0) {
92
+                colors[i] = "yellow";
93
+                freq[tokens[i]]--;
94
+            }
95
+        }
96
+    }
97
+    tokens.forEach((token, i) => {
98
+        const cell = row.cells[i];
99
+        setTimeout(() => {
100
+            cell.style.transform = "rotateX(90deg)";
101
+            setTimeout(() => {
102
+                cell.classList.add(colors[i]);
103
+                cell.style.transform = "rotateX(0deg)";
104
+            }, 150);
105
+        }, 150 * i);
106
+    });
107
+}
108
+
109
+function fillRow(tokens, cls) {
110
+    const row = grid.rows[currentRow];
111
+    for (let i = 0; i < tl; i++) {
112
+        const cell = row.cells[i];
113
+        cell.textContent = tokens[i];
114
+        cell.classList.add(cls);
115
+    }
116
+}
117
+
118
+function evalPostfix(tokens, x) {
119
+    const s = [];
120
+    for (const t of tokens) {
121
+        if (t === "x") s.push(x);
122
+        else if (t === "1") s.push(1);
123
+        else if (t === "2") s.push(2);
124
+        else if (allowed.unary.includes(t)) {
125
+            if (s.length < 1) throw 0;
126
+            const v = s.pop();
127
+            if (t === "^2") s.push(v ** 2);
128
+            if (t === "^3") s.push(v ** 3);
129
+            if (t === "sin") s.push(Math.sin(v));
130
+            if (t === "exp") s.push(Math.exp(v));
131
+            if (t === "ln") s.push(Math.log(v));
132
+        } else if (allowed.binary.includes(t)) {
133
+            if (s.length < 2) throw 0;
134
+            const b = s.pop(), a = s.pop();
135
+            if (t === "+") s.push(a + b);
136
+            if (t === "-") s.push(a - b);
137
+            if (t === "*") s.push(a * b);
138
+            if (t === "/") s.push(a / b);
139
+        } else throw 0;
140
+    }
141
+    if (s.length !== 1) throw 0;
142
+    return s[0];
143
+}
144
+
145
+function rand(l) {
146
+    return l[Math.floor(l.length * Math.random())];
147
+}
148
+
149
+function generateExpr(len = tl, force = true) {
150
+    if (len == 1) {
151
+        return force ? ["x"] : [rand(allowed.terminals)];
152
+    } else if (len == 2 || Math.random() >= 0.5) {
153
+        const e = generateExpr(len - 1, force);
154
+        e.push(rand(evalPostfix(e, 0.6) == 1 ? ["sin", "exp"] : allowed.unary));
155
+        return e;
156
+    } else {
157
+        const k = Math.floor(1 + (len - 2) * Math.random());
158
+        const f = force && Math.random() > 0.5;
159
+        const a = generateExpr(k, f);
160
+        const b = generateExpr(len - 1 - k, force && !f);
161
+        const one = evalPostfix(a, 0.6) == 1 || evalPostfix(b, 0.6) == 1;
162
+        const e = a.concat(b)
163
+        e.push(rand(one ? ["+", "-"] : allowed.binary));
164
+        return e;
165
+    }
166
+}
167
+
168
+function closeEnough(a, b) {
169
+    for (let i = 0; i < a.length; i++) {
170
+        if (isNaN(a[i]) && !isNaN(b[i])) return false;
171
+        if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false;
172
+    }
173
+    return true;
174
+}
175
+
176
+function hideInstructions(button) {
177
+    const inst = document.getElementById("instructions");
178
+    inst.remove();
179
+    button.remove();
180
+    const heading = inst.firstElementChild;
181
+    heading.remove();
182
+    const summary = document.createElement("summary");
183
+    summary.textContent = heading.textContent;
184
+    const newInst = document.createElement("details");
185
+    newInst.append(summary);
186
+    for (const c of inst.childNodes) newInst.append(c);
187
+    container.append(newInst);
188
+}
189
+
190
+function winMessage() {
191
+    let boast = `Functle ${puzzleNum} ${currentRow + 1}/${maxGuesses}\n`;
192
+    for (i = 0; i <= currentRow; i++) {
193
+        r = grid.rows[i];
194
+        for (c of r.cells) {
195
+            const l = c.classList;
196
+            let p = "?";
197
+            for (o in boxes) if (l.contains(o)) p = boxes[o];
198
+            boast = boast + p;
199
+        }
200
+        boast = boast + "\n";
201
+    }
202
+    boast = boast + "https://DKL9.net/functle/";
203
+    navigator.clipboard.writeText(boast);
204
+    const w = document.createElement("section");
205
+    w.append(`${congrats[currentRow]}! Share your score. It's on your clipboard. `);
206
+    const b = document.createElement("button");
207
+    b.textContent = "Copy again";
208
+    b.type = "button";
209
+    b.addEventListener("click", () => navigator.clipboard.writeText(boast));
210
+    w.append(b);
211
+    container.prepend(w);
212
+}
213
+
214
+function loseMessage() {
215
+    const d = document.createElement("p");
216
+    d.textContent = `Better luck next time. The function was ${target.join(" ")}`;
217
+    container.prepend(d);
218
+}
... ...
@@ -0,0 +1,46 @@
1
+const keyToToken = { x:"x", 1:"1", 2:"2", q:"^2", c:"^3", s:"sin", e:"exp", l:"ln", "+":"+", "-":"-", "*":"*", "/":"/" };
2
+const maxGuesses = 6;
3
+
4
+function shakeRow(row) {
5
+    row.classList.remove("shake");
6
+    void row.offsetWidth;
7
+    row.classList.add("shake");
8
+}
9
+
10
+const grid = document.getElementById("grid");
11
+for (let i = 0; i < maxGuesses; i++) {
12
+    const tr = document.createElement("tr");
13
+    for (let j = 0; j < tl; j++) {
14
+        const td = document.createElement("td");
15
+        tr.appendChild(td);
16
+    }
17
+    grid.appendChild(tr);
18
+}
19
+
20
+let currentRow = 0;
21
+let currentInput = [];
22
+
23
+document.addEventListener("keyup", e => {
24
+    const row = grid.rows[currentRow];
25
+    if (e.key === "Backspace") {
26
+        if (currentInput.length > 0) {
27
+            currentInput.pop();
28
+            row.cells[currentInput.length].textContent = "";
29
+        } else {
30
+            shakeRow(row);
31
+        }
32
+    } else if (e.key === "Enter") {
33
+        submitGuess();
34
+    } else if (keyToToken.hasOwnProperty(e.key)) {
35
+        if (currentRow >= maxGuesses) {
36
+        } else if (currentInput.length < tl) {
37
+            const token = keyToToken[e.key];
38
+            currentInput.push(token);
39
+            const c = row.cells[currentInput.length - 1];
40
+            c.style.fontSize = `${3.5 - 0.5 * token.length}em`;
41
+            c.textContent = token;
42
+        } else {
43
+            shakeRow(row);
44
+        }
45
+    }
46
+});
... ...
@@ -0,0 +1,68 @@
1
+const width = 400, height = 400;
2
+const ctx = document.getElementById("plot").getContext("2d");
3
+const bound = 10;
4
+const xs = [];
5
+const targetYs = [];
6
+for (let i = 0; i <= width; i++) {
7
+    const x = bound * (2 * i / width - 1);
8
+    xs.push(x);
9
+    const yv = evalPostfix(target, x);
10
+    targetYs.push(yv);
11
+}
12
+const clampLow = 1.5;
13
+const clampHigh = 20;
14
+let yMin = Math.min(...targetYs);
15
+let yMax = Math.max(...targetYs);
16
+if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh;
17
+else if (yMin > -clampLow) yMin = -clampLow;
18
+if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh;
19
+else if (yMax < clampLow) yMax = clampLow;
20
+const guessYsList = [];
21
+const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches;
22
+
23
+redrawAll();
24
+
25
+function drawAxes() {
26
+    ctx.clearRect(0, 0, width, height);
27
+    ctx.beginPath();
28
+    const y0 = mapY(0);
29
+    ctx.moveTo(0, y0);
30
+    ctx.lineTo(width, y0);
31
+    const x0 = mapX(0);
32
+    ctx.moveTo(x0, 0);
33
+    ctx.lineTo(x0, height);
34
+    [1, -1].forEach(val => {
35
+        const px = mapX(val);
36
+        ctx.moveTo(px, y0 - 5);
37
+        ctx.lineTo(px, y0 + 5);
38
+    });
39
+    [1, -1].forEach(val => {
40
+        const py = mapY(val);
41
+        ctx.moveTo(x0 - 5, py);
42
+        ctx.lineTo(x0 + 5, py);
43
+    });
44
+    ctx.lineWidth = 1;
45
+    ctx.strokeStyle = darkTheme ? "white" : "black";
46
+    ctx.stroke();
47
+}
48
+
49
+function drawCurve(ys, color, width = 4) {
50
+    ctx.beginPath();
51
+    for (let i = 0; i < xs.length; i++) {
52
+        const px = mapX(xs[i]), py = mapY(ys[i]);
53
+        if (i === 0) ctx.moveTo(px, py);
54
+        else ctx.lineTo(px, py);
55
+    }
56
+    ctx.lineWidth = width;
57
+    ctx.strokeStyle = color;
58
+    ctx.stroke();
59
+}
60
+
61
+function redrawAll() {
62
+    drawAxes();
63
+    drawCurve(targetYs, darkTheme ? "lime" : "green");
64
+    guessYsList.forEach((ys, idx) => drawCurve(ys, "red", (idx < guessYsList.length - 1) ? 1 : 4));
65
+}
66
+
67
+function mapX(x) { return (x + bound) / (2 * bound) * width; }
68
+function mapY(y) { return height - (y - yMin) / (yMax - yMin) * height; }
0 69