猜函-小游戏

前几天终于成功的解决了服务器连不上codex的问题(什么),于是就有了这个网站:**Guess The Function / 猜这个函数**。 玩法很直接,页面上先给一条目标函数的曲线,玩家往输入框里猜。系统会把你的猜测和真正的目标函数做比较,算出一个匹配度分数;如果分数足够高,就算你猜中了。如果还差一点,也没关系,页面下面会留下你的猜测记录和分数变化,方便你一点点往正确答案逼近。目前使用的是信噪比(SNR): ![](/static/uploads/images/1203dc1ed0fb883f002413635f4872bb.jpg) 这个网站现在有两个模式。练习模式适合随手玩,想换题就换题;每日挑战则会在每天中午切一题,大家面对的是同一个函数,猜出来之后还能上榜。 ![](/static/uploads/images/7d8a1d10794bcac6a23a4c7f93c02f7c.png) ## 函数是怎么生成出来的 最开始做这个网站时,一个很自然的思路是手写几十个表达式放进题库里,随机抽一道就行。但这样很快会碰到两个问题:一是题目数量有限,玩几轮就熟了;二是函数的风格会越来越像出题人自己,变化不够大。 所以后来把它改成了“表达式树生成器”。简单说,系统不是先想好字符串,而是先递归地长出一棵语法树,最后再把这棵树重新拼成可读的表达式。 代码里先定了一个生成深度范围: ```js const MIN_GENERATION_DEPTH = 4; const MAX_GENERATION_DEPTH = 5; function generationShouldStop(depth) { if (depth < MIN_GENERATION_DEPTH) { return false; } if (depth >= MAX_GENERATION_DEPTH) { return true; } return randomSource() < Math.min(0.22 + depth * 0.1, 0.82); } ``` 如果当前这一层决定不再往下展开,系统就会生成一个“叶子节点”。叶子节点不是只有 `x`,而是会从几个基础形态里挑一个出来,比如 `x`、`-x`、`3*x`、`x/2`、`x+4` 这种: ```js function buildLeafNode() { const coefficient = randomCoefficient(5, 2); const constant = randomCoefficient(5); return randomChoice([ makeVariableNode(), makeUnaryNode("-", makeVariableNode()), makeNumberNode(constant), makeBinaryNode("*", makeNumberNode(coefficient), makeVariableNode()), makeBinaryNode("/", makeVariableNode(), makeNumberNode(coefficient)), makeBinaryNode("+", makeVariableNode(), makeNumberNode(constant)), makeBinaryNode("-", makeVariableNode(), makeNumberNode(constant)), ]); } ``` 这里系数和常数都限制在不超过 5 的整数范围里。这样生成出来的函数虽然是随机的,但还保留了“人能猜”的手感,不会动不动冒出一个特别离谱的系数。 再往上一层,系统会决定当前节点到底长成什么样。它有时候会长成一个函数节点,比如 `sin(...)`、`cos(...)`、`exp(...)`;有时候会长成一个二元运算,比如加减乘除;还有时候会做一点偏移或者缩放: ```js function buildExpressionNode(depth) { if (generationShouldStop(depth)) { return buildLeafNode(); } const shape = randomChoice([ "func", "func", "binary", "binary", "binary", "scaled-func", "offset", ]); if (shape === "func") { return makeFunctionNode( randomChoice(["sin", "cos", "exp"]), buildFunctionArgumentNode(depth + 1) ); } if (shape === "scaled-func") { return makeBinaryNode( "*", makeNumberNode(randomCoefficient(5, 2)), makeFunctionNode(randomChoice(["sin", "cos", "exp"]), buildFunctionArgumentNode(depth + 1)) ); } if (shape === "offset") { return makeBinaryNode( randomChoice(["+", "-"]), buildExpressionNode(depth + 1), makeNumberNode(randomCoefficient(5)) ); } return makeBinaryNode( randomChoice(["+", "-", "*", "/"]), buildExpressionNode(depth + 1), buildExpressionNode(depth + 1) ); } ``` 这个网站现在对生成结果做了几层筛选。首先,它必须真的依赖 `x`,不能生成一个纯常数;其次,它至少要出现两个函数节点,也就是 `sin`、`cos`、`exp` 总次数不能太少。再往下,还有几条比较关键的限制:不允许函数互相嵌套,比如 `sin(cos(x))`;不允许函数参数退化成纯常数,比如 `sin(3)`;也不允许在分母里出现 `sin`、`cos`、`exp`。这些限制看起来有点“严格”,但它们的作用其实很直接,就是尽量避免题目长成一种虽然随机、却并不好猜的样子。 对应的校验大致是这样: ```js function isUsableGeneratedExpression(expression) { if (!expression.includes("x")) { return false; } if (!/(sin|cos|exp)/.test(expression)) { return false; } let ast; try { ast = parseExpression(expression); } catch (error) { return false; } ast = simplifyAst(ast); const depth = nodeDepth(ast); if (depth < MIN_GENERATION_DEPTH || depth > MAX_GENERATION_DEPTH) { return false; } if ( countFunctions(ast) < 2 || hasNestedFunction(ast) || hasConstantOnlyFunctionArgument(ast) || hasFunctionInDenominator(ast) ) { return false; } return true; } ``` 注意这里不是“生成时严格不准犯错”,而是“先生成,再淘汰”。这是一个很实用的思路。因为如果把所有规则都塞进生成阶段,代码会变得越来越拧巴;而先大胆生成,再统一过滤,整体会清楚很多。 另外,代码里还做了一步表达式化简。比如连续常数加减会被合并,`1*...`、`.../1` 这种写法会被消掉,双重负号也会被整理掉。这样最终展示给玩家的表达式更自然。 ```js function simplifyAst(node) { if (node.type === "number" || node.type === "variable") { return node; } if (node.type === "unary") { const argument = simplifyAst(node.argument); if (node.operator === "+") { return argument; } if (isNumberNode(argument)) { return makeNumberNode(-argument.value); } if (argument.type === "unary" && argument.operator === "-") { return argument.argument; } return makeUnaryNode(node.operator, argument); } // 下面处理常数折叠、1*x、x/1、连续加减项合并等情况 } ``` 最后,系统会反复尝试生成候选表达式,直到拿到一个通过校验的结果;如果连续很多次都不理想,就回退到一个保底函数: ```js for (let attempt = 0; attempt < 500; attempt += 1) { const expression = buildRandomCandidate(); if (isUsableGeneratedExpression(expression)) { return expression; } } return "sin(x)+cos(x)"; ``` 这段代码很朴素,但很重要。它让整个系统既有随机性,也有兜底,不会因为某一轮运气太差就直接出一题坏题。 ![](/static/uploads/images/49a03f3d3c11a54f42ce4fe430653f1a.png)

评论

留下评论

邮箱仅用于识别,不会公开

支持 KaTeX:$...$ 行内 · $$...$$ 独立公式
加载评论中...