DKL9 GitList
Repositories
DKL9 home
functle
Code
Commits
Branches
Tags
Search
Tree:
1b84c09
Branches
Tags
master
functle
functle.html
Responsively adjust layout, dark theme, better generator
dkl9
commited
1b84c09
at 2025-209 17:19:08
functle.html
Blame
History
Raw
<!DOCTYPE html> <html> <head> <title>Functle</title> <style> body { font-family: sans-serif; } #plot { border: 1px solid grey; flex-shrink: 0; height: 400px; } #grid { border-collapse: separate; border-spacing: 3px 12px; margin-top: 10px; } #grid td { min-width: 40px; height: 40px; border: 1px solid #ccc; text-align: center; vertical-align: middle; font-weight: bold; } .grey { background: #3a3a3c; color: white; } .yellow { background: #b59f3b; color: white; } .green { background: #538e4e; color: white; } #message { color: red; margin-top: 5px; } .side { margin-left: 20px; } #guessInput { width: 6em; } @media (min-aspect-ratio: 4/3) { .container { display: flex; } } @media (prefers-color-scheme: dark) { body { color: white; background-color: black; } } </style> </head> <body> <div class="container"> <canvas id="plot" width="400" height="400"></canvas> <div class="side"> <div> <input id="guessInput" placeholder="token1 token2 ... token5"> <button id="submitBtn">Guess</button> <div id="message"></div> </div> <table id="grid"></table> </div> <div class="side"> <p>Guess the function based on its plot on the left. Tick marks on axes indicate <var>x</var> = ±1 and <var>y</var> = ±1. 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> <p>Enter your answer in postfix (reverse Polish) notation. For example, express 1 - <var>x</var> / (sin <var>x</var>) as "1 x x sin / -".</p> <p>The correct answer will be six tokens. Numerically equivalent rearranged answers will be accepted.</p> <p>Enhanced from a prototype by ChatGPT o4-mini.</p> </div> </div> <script> const allowed = { terminals: ["x", "2", "1"], unary: ["^2", "^3", "sin", "exp", "ln"], binary: ["+", "-", "*", "/"] }; const allTokens = [...allowed.terminals, ...allowed.unary, ...allowed.binary]; const tl = 5; function evalPostfix(tokens, x) { const s = []; for (const t of tokens) { if (t === "x") s.push(x); else if (t === "1") s.push(1); else if (t === "2") s.push(2); else if (allowed.unary.includes(t)) { if (s.length < 1) throw 0; const v = s.pop(); if (t === "^2") s.push(v ** 2); if (t === "^3") s.push(v ** 3); if (t === "sin") s.push(Math.sin(v)); if (t === "exp") s.push(Math.exp(v)); if (t === "ln") s.push(Math.log(v)); } else if (allowed.binary.includes(t)) { if (s.length < 2) throw 0; const b = s.pop(), a = s.pop(); if (t === "+") s.push(a + b); if (t === "-") s.push(a - b); if (t === "*") s.push(a * b); if (t === "/") s.push(a / b); } else throw 0; } if (s.length !== 1) throw 0; return s[0]; } function rand(l) { return l[Math.floor(l.length * Math.random())]; } function generateExpr(len = tl, force = true) { if (len == 1) { return force ? ["x"] : [rand(allowed.terminals)]; } else if (len == 2 || Math.random >= 0.2) { const e = generateExpr(len - 1, force); e.push(rand(evalPostfix(e, 0.6) == 1 ? ["sin", "exp"] : allowed.unary)); return e; } else { const k = Math.floor(1 + (len - 2) * Math.random()); const f = force && Math.random() > 0.5; const a = generateExpr(k, f); const b = generateExpr(len - 1 - k, force && !f); const one = evalPostfix(a, 0.6) == 1 || evalPostfix(b, 0.6) == 1; const e = a.concat(b) e.push(rand(one ? ["+", "-"] : allowed.binary)); return e; } } const target = generateExpr(); const maxGuesses = 6; let currentRow = 0; const width = 400, height = 400; const ctx = document.getElementById("plot").getContext("2d"); const bound = 10; const xs = []; const targetYs = []; for (let i = 0; i <= width; i++) { const x = bound * (2 * i / width - 1); xs.push(x); const yv = evalPostfix(target, x); targetYs.push(yv); } const clampLow = 1.5; const clampHigh = 20; let yMin = Math.min(...targetYs); let yMax = Math.max(...targetYs); if (isNaN(yMin) || yMin < -clampHigh) yMin = -clampHigh; else if (yMin > -clampLow) yMin = -clampLow; if (isNaN(yMax) || yMax > clampHigh) yMax = clampHigh; else if (yMax < clampLow) yMax = clampLow; const guessYsList = []; const darkTheme = matchMedia('(prefers-color-scheme: dark)').matches; function drawAxes() { ctx.clearRect(0, 0, width, height); ctx.beginPath(); const y0 = mapY(0); ctx.moveTo(0, y0); ctx.lineTo(width, y0); const x0 = mapX(0); ctx.moveTo(x0, 0); ctx.lineTo(x0, height); [1, -1].forEach(val => { const px = mapX(val); ctx.moveTo(px, y0 - 5); ctx.lineTo(px, y0 + 5); }); [1, -1].forEach(val => { const py = mapY(val); ctx.moveTo(x0 - 5, py); ctx.lineTo(x0 + 5, py); }); ctx.lineWidth = 1; ctx.strokeStyle = darkTheme ? "white" : "black"; ctx.stroke(); } function drawCurve(ys, color, width = 4) { ctx.beginPath(); for (let i = 0; i < xs.length; i++) { const px = mapX(xs[i]), py = mapY(ys[i]); if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.lineWidth = width; ctx.strokeStyle = color; ctx.stroke(); } function redrawAll() { drawAxes(); drawCurve(targetYs, darkTheme ? "lime" : "green"); guessYsList.forEach((ys, idx) => drawCurve(ys, "red", (idx < guessYsList.length - 1) ? 1 : 4)); } redrawAll(); const grid = document.getElementById("grid"); for (let i = 0; i < maxGuesses; i++) { const tr = document.createElement("tr"); for (let j = 0; j < tl; j++) { const td = document.createElement("td"); tr.appendChild(td); } grid.appendChild(tr); } document.getElementById("submitBtn").onclick = () => { const inp = document.getElementById("guessInput").value.trim().split(/\s+/); document.getElementById("message").textContent = ""; if (inp.length !== tl) { document.getElementById("message").textContent = `Use exactly ${tl} tokens.`; return; } for (const t of inp) { if (!allTokens.includes(t)) { document.getElementById("message").textContent = "Invalid token: " + t; return; } } let ys; try { ys = xs.map(x => evalPostfix(inp, x)); } catch (e) { document.getElementById("message").textContent = "Syntax error."; return; } if (closeEnough(ys, targetYs)) { fillRow(inp, "green"); document.getElementById("submitBtn").disabled = true; return; } markRow(inp); guessYsList.push(ys); redrawAll(); currentRow++; if (currentRow >= maxGuesses) { document.getElementById("submitBtn").disabled = true; } }; function closeEnough(a, b) { for (let i = 0; i < a.length; i++) { if (isNaN(a[i]) && !isNaN(b[i])) return false; if (Math.abs(a[i] - b[i]) > 0.1 * (Math.abs(b[i]) + 1)) return false; } return true; } function fillRow(tokens, cls) { const row = grid.rows[currentRow]; for (let i = 0; i < tl; i++) { const cell = row.cells[i]; cell.textContent = tokens[i]; cell.classList.add(cls); } } function markRow(tokens) { const freq = {}; for (const t of target) freq[t] = (freq[t] || 0) + 1; const colors = Array(tl).fill("grey"); for (let i = 0; i < tokens.length; i++) { if (tokens[i] === target[i]) { colors[i] = "green"; freq[tokens[i]]--; } } for (let i = 0; i < tokens.length; i++) { if (colors[i] === "grey" && freq[tokens[i]] > 0) { colors[i] = "yellow"; freq[tokens[i]]--; } } const row = grid.rows[currentRow]; for (let i = 0; i < tl; i++) { const cell = row.cells[i]; cell.textContent = tokens[i]; cell.classList.add(colors[i]); } } function mapX(x) { return (x + bound) / (2 * bound) * width; } function mapY(y) { return height - (y - yMin) / (yMax - yMin) * height; } </script> </body> </html>