Responsively adjust layout, dark theme, better generator
dkl9

dkl9 commited on 2025-209 17:19:08
Showing 1 changed files, with 67 additions and 39 deletions.

... ...
@@ -1,17 +1,20 @@
1 1
 <!DOCTYPE html>
2 2
 <html>
3 3
     <head>
4
+        <title>Functle</title>
4 5
         <style>
5 6
             body { font-family: sans-serif; }
6
-            #plot { border: 1px solid #000; }
7
-            #grid { border-collapse: separate; border-spacing: 5px 10px; margin-top: 10px; }
8
-            #grid td { width: 40px; height: 40px; border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; }
9
-            .grey { background: #787c7e; color: #fff; }
10
-            .yellow { background: #c9b458; color: #fff; }
11
-            .green { background: #6aaa64; color: #fff; }
7
+            #plot { border: 1px solid grey; flex-shrink: 0; height: 400px; }
8
+            #grid { border-collapse: separate; border-spacing: 3px 12px; margin-top: 10px; }
9
+            #grid td { min-width: 40px; height: 40px; border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; }
10
+            .grey { background: #3a3a3c; color: white; }
11
+            .yellow { background: #b59f3b; color: white; }
12
+            .green { background: #538e4e; color: white; }
12 13
             #message { color: red; margin-top: 5px; }
13
-            .container { display: flex; }
14 14
             .side { margin-left: 20px; }
15
+            #guessInput { width: 6em; }
16
+            @media (min-aspect-ratio: 4/3) { .container { display: flex; } }
17
+            @media (prefers-color-scheme: dark) { body { color: white; background-color: black; } }
15 18
         </style>
16 19
     </head>
17 20
     <body>
... ...
@@ -19,21 +22,31 @@
19 22
             <canvas id="plot" width="400" height="400"></canvas>
20 23
             <div class="side">
21 24
                 <div>
22
-                    <input id="guessInput" placeholder="token1 token2 ... token6" style="width: 300px;">
23
-                    <button id="submitBtn">Submit</button>
25
+                    <input id="guessInput" placeholder="token1 token2 ... token5">
26
+                    <button id="submitBtn">Guess</button>
24 27
                     <div id="message"></div>
25 28
                 </div>
26 29
                 <table id="grid"></table>
27 30
             </div>
31
+            <div class="side">
32
+                <p>Guess the function based on its plot on the left.
33
+                Tick marks on axes indicate <var>x</var> = ±1 and <var>y</var> = ±1.
34
+                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>
35
+                <p>Enter your answer in postfix (reverse Polish) notation.
36
+                For example, express 1 - <var>x</var> / (sin <var>x</var>) as "1 x x sin / -".</p>
37
+                <p>The correct answer will be six tokens.
38
+                Numerically equivalent rearranged answers will be accepted.</p>
39
+                <p>Enhanced from a prototype by ChatGPT o4-mini.</p>
40
+            </div>
28 41
         </div>
29 42
         <script>
30 43
             const allowed = {
31
-                terminals: ["x", "1", "2"],
44
+                terminals: ["x", "2", "1"],
32 45
                 unary: ["^2", "^3", "sin", "exp", "ln"],
33 46
                 binary: ["+", "-", "*", "/"]
34 47
             };
35 48
             const allTokens = [...allowed.terminals, ...allowed.unary, ...allowed.binary];
36
-            const tl = 6;
49
+            const tl = 5;
37 50
             
38 51
             function evalPostfix(tokens, x) {
39 52
                 const s = [];
... ...
@@ -62,21 +75,30 @@
62 75
                 return s[0];
63 76
             }
64 77
 
65
-            function generateTarget() {
66
-                while (true) {
67
-                    const cand = [];
68
-                    for (let i = 0; i < tl; i++) {
69
-                        cand.push(allTokens[Math.floor(Math.random() * allTokens.length)]);
78
+            function rand(l) {
79
+                return l[Math.floor(l.length * Math.random())];
70 80
             }
71
-                    try {
72
-                        // test on a sample of x values to ensure valid
73
-                        for (let xTest of [0, 1, -1]) evalPostfix(cand, xTest);
74
-                        return cand;
75
-                    } catch (e) { continue; }
81
+            
82
+            function generateExpr(len = tl, force = true) {
83
+                if (len == 1) {
84
+                    return force ? ["x"] : [rand(allowed.terminals)];
85
+                } else if (len == 2 || Math.random >= 0.2) {
86
+                    const e = generateExpr(len - 1, force);
87
+                    e.push(rand(evalPostfix(e, 0.6) == 1 ? ["sin", "exp"] : allowed.unary));
88
+                    return e;
89
+                } else {
90
+                    const k = Math.floor(1 + (len - 2) * Math.random());
91
+                    const f = force && Math.random() > 0.5;
92
+                    const a = generateExpr(k, f);
93
+                    const b = generateExpr(len - 1 - k, force && !f);
94
+                    const one = evalPostfix(a, 0.6) == 1 || evalPostfix(b, 0.6) == 1;
95
+                    const e = a.concat(b)
96
+                    e.push(rand(one ? ["+", "-"] : allowed.binary));
97
+                    return e;
76 98
                 }
77 99
             }
78 100
             
79
-            const target = generateTarget();
101
+            const target = generateExpr();
80 102
             const maxGuesses = 6;
81 103
             let currentRow = 0;
82 104
             const width = 400, height = 400;
... ...
@@ -87,10 +109,19 @@
87 109
             for (let i = 0; i <= width; i++) {
88 110
                 const x = bound * (2 * i / width - 1);
89 111
                 xs.push(x);
90
-                targetYs.push(evalPostfix(target, x));
112
+                const yv = evalPostfix(target, x);
113
+                targetYs.push(yv);
91 114
             }
92
-            const yMin = Math.min(...targetYs), yMax = Math.max(...targetYs);
115
+            const clampLow = 1.5;
116
+            const clampHigh = 20;
117
+            let yMin = Math.min(...targetYs);
118
+            let yMax = Math.max(...targetYs);
119
+            if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh;
120
+            else if (yMin > -clampLow) yMin = -clampLow;
121
+            if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh;
122
+            else if (yMax < clampLow) yMax = clampLow;
93 123
             const guessYsList = [];
124
+            const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches;
94 125
 
95 126
             function drawAxes() {
96 127
                 ctx.clearRect(0, 0, width, height);
... ...
@@ -111,32 +142,30 @@
111 142
                     ctx.moveTo(x0 - 5, py);
112 143
                     ctx.lineTo(x0 + 5, py);
113 144
                 });
114
-                ctx.strokeStyle = "black";
145
+                ctx.lineWidth = 1;
146
+                ctx.strokeStyle = darkTheme ? "white" : "black";
115 147
                 ctx.stroke();
116 148
             }
117 149
             
118
-            function drawCurve(ys, color) {
150
+            function drawCurve(ys, color, width = 4) {
119 151
                 ctx.beginPath();
120 152
                 for (let i = 0; i < xs.length; i++) {
121 153
                     const px = mapX(xs[i]), py = mapY(ys[i]);
122 154
                     if (i === 0) ctx.moveTo(px, py);
123 155
                     else ctx.lineTo(px, py);
124 156
                 }
157
+                ctx.lineWidth = width;
125 158
                 ctx.strokeStyle = color;
126 159
                 ctx.stroke();
127 160
             }
128 161
             
129 162
             function redrawAll() {
130 163
                 drawAxes();
131
-                drawCurve(targetYs, "green");
132
-                guessYsList.forEach((ys, idx) => {
133
-                    const color = idx < guessYsList.length - 1 ? "rgba(255,0,0,0.3)" : "red";
134
-                    drawCurve(ys, color);
135
-                });
164
+                drawCurve(targetYs, darkTheme ? "lime" : "green");
165
+                guessYsList.forEach((ys, idx) => drawCurve(ys, "red", (idx < guessYsList.length - 1) ? 1 : 4));
136 166
             }
137 167
             
138
-            drawAxes();
139
-            drawCurve(targetYs, "green");
168
+            redrawAll();
140 169
             
141 170
             const grid = document.getElementById("grid");
142 171
             for (let i = 0; i < maxGuesses; i++) {
... ...
@@ -152,7 +181,7 @@
152 181
                 const inp = document.getElementById("guessInput").value.trim().split(/\s+/);
153 182
                 document.getElementById("message").textContent = "";
154 183
                 if (inp.length !== tl) {
155
-                    document.getElementById("message").textContent = `Exactly ${tl} tokens.`;
184
+                    document.getElementById("message").textContent = `Use exactly ${tl} tokens.`;
156 185
                     return;
157 186
                 }
158 187
                 for (const t of inp) {
... ...
@@ -170,7 +199,6 @@
170 199
                 }
171 200
                 if (closeEnough(ys, targetYs)) {
172 201
                     fillRow(inp, "green");
173
-                    alert("Correct.");
174 202
                     document.getElementById("submitBtn").disabled = true;
175 203
                     return;
176 204
                 }
... ...
@@ -179,14 +207,14 @@
179 207
                 redrawAll();
180 208
                 currentRow++;
181 209
                 if (currentRow >= maxGuesses) {
182
-                    alert("Out of guesses.");
183 210
                     document.getElementById("submitBtn").disabled = true;
184 211
                 }
185 212
             };
186 213
             
187 214
             function closeEnough(a, b) {
188 215
                 for (let i = 0; i < a.length; i++) {
189
-                    if (isNaN(a[i]) || Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false;
216
+                    if (isNaN(a[i]) && !isNaN(b[i])) return false;
217
+                    if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false;
190 218
                 }
191 219
                 return true;
192 220
             }
... ...
@@ -204,13 +232,13 @@
204 232
                 const freq = {};
205 233
                 for (const t of target) freq[t] = (freq[t] || 0) + 1;
206 234
                 const colors = Array(tl).fill("grey");
207
-                for (let i = 0; i < tl; i++) {
235
+                for (let i = 0; i < tokens.length; i++) {
208 236
                     if (tokens[i] === target[i]) {
209 237
                         colors[i] = "green";
210 238
                         freq[tokens[i]]--;
211 239
                     }
212 240
                 }
213
-                for (let i = 0; i < tl; i++) {
241
+                for (let i = 0; i < tokens.length; i++) {
214 242
                     if (colors[i] === "grey" && freq[tokens[i]] > 0) {
215 243
                         colors[i] = "yellow";
216 244
                         freq[tokens[i]]--;
217 245