24 点纸牌游戏:加减乘除凑出 24,一秒上头
24 点的乐趣全在「这四张牌到底有没有解」的抓心挠肝里。这个工具不只是随机发四张牌,发牌前会先用完整搜索确认有解,避免拿到一手死局;点牌、点亮运算符就能搭表达式,实时校验括号和答案,卡住还能一键看解法。先看成品,再讲怎么用华为云 CodeArts 码道 WebUI 一段话把它做出来、部署上线。
这个工具做什么
它把「24 点」做成一个即开即玩的单页小游戏:
- 发牌必可解:不是随便发四张,而是先跑
solve24全搜索,筛出确实能凑到 24 的组合。 - 点牌搭表达式:点击卡牌和
+ − × ÷ ( )按钮组成式子,用过的牌自动灰掉,不许连放两张数字。 - 实时校验:括号是否配对、表达式是否完整、数值是否等于 24,一输入就有反馈。
- 三档难度:简单 / 中等 / 困难对应不同的数值范围和倍数加成。
- 计时计分:越快解出分数越高,用提示会打折;连续得分记录在最近成绩里。
- 提示解法:卡住时一键展示可解的完整表达式,学完再继续。
- 重新发牌:再来一局,计时和牌面立即重置。
- 胜利特效:彩带 + 清晰的成功文案,成就感拉满。

码道开通(免费体验版)
开始之前,先免费开通码道体验版。不需要装任何软件,浏览器就能用:
https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwsf&sourcead=dmzntgwwbwz
开通后有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
进入 AI 开发者空间
- 浏览器打开 https://devstation.developer.huaweicloud.com/aidevspace?utm_source=opcsfsq&utm_medium=wbwz
- 弹窗「请选择要进入的版本」→ 点击 体验版
- 首次使用会跳到华为云登录页,扫码登录一次,后续浏览器会话复用
- 弹窗「临时凭据自动配置」→ 点击 同意,沙箱自动创建
进入 AI 开发者空间主界面后,先等左侧菜单加载出来,再找到 CodeArts,点击它右侧的浏览器图标,打开码道 WebUI 对话窗口:

打开后码道 WebUI 会先和你打招呼,下方就是需求输入框:

小贴士:体验版沙箱 3 小时后自动重置,生成完记得把
index.html下载保存。
任务描述
在码道 WebUI 输入框粘贴以下需求,回车发送:
开发一个「24 点纸牌游戏」单文件网页应用:随机抽 4 张扑克牌(用 1~13 表示数字),玩家用加减乘除和括号把四个数凑成 24;支持点击卡牌和运算符构建表达式、实时校验、提示与显示解法、计时和计分、选择难度、重新发牌,界面美观耐玩,手机和电脑都能用,所有代码写在一个 index.html 里。完成后用部署功能部署成可访问的网页应用,生成一个公网可访问的分享链接,并告诉我访问地址。

这轮码道具体做了什么
不是复述通用 AI 能力,而是它在这次项目里的实际动作:
- 在
/workspace/game24下创建单文件index.html,把求解搜索、发牌、表达式构建、计分和胜利特效全部放进一个文件,无第三方依赖。 - 写入代码后先做 JS 语法检查,再探测空闲端口 3002、启动
http.server并curl自检 HTTP 200。 - 用 DevBridge 创建公网隧道并验证内容,最后给出 8 小时有效的公网分享链接。
代码亮点
以下是 index.html 里值得关注的设计思路和实现细节。
1. 完整搜索求解,不是拍脑袋拼
solve24 把问题拆成递归:每次从当前数字列表里挑两个数做加减乘除,把结果和一个带括号的表达式塞回列表,直到只剩一个数,看它是否等于 24:
function solve24(nums){
function helper(arr){
if(arr.length===1){
if(Math.abs(arr[0].v-24)<EPS) return arr[0].e;
return null;
}
for(let i=0;i<arr.length;i++){
for(let j=0;j<arr.length;j++){
if(i===j) continue;
const a=arr[i], b=arr[j];
const rest=arr.filter((_,k)=>k!==i&&k!==j);
const cands=[
{v:a.v+b.v, e:`(${a.e}+${b.e})`},
{v:a.v-b.v, e:`(${a.e}-${b.e})`},
{v:a.v*b.v, e:`(${a.e}×${b.e})`}
];
if(Math.abs(b.v)>EPS) cands.push({v:a.v/b.v, e:`(${a.e}÷${b.e})`});
for(const c of cands){
const r=helper(rest.concat([{v:c.v,e:c.e}]));
if(r) return r;
}
}
}
return null;
}
return helper(nums.map(n=>({v:n,e:String(n)})));
}
用 1e-6 做浮点容差,避免 6/(1-5/8) 这类除不尽时被浮点误差误判。
2. 发牌先保证有解,玩起来才不窝火
deal 会先随机抽一组数,用 hasSolution 判断是否有解,没有就重抽,直到抽到可解组合:
function hasSolution(nums){ return solve24(nums)!==null; }
function deal(){
const cfg=DIFF_CFG[state.diff];
let tries=0, vals;
while(true){
vals=[randInt(cfg.min,cfg.max),randInt(cfg.min,cfg.max),randInt(cfg.min,cfg.max),randInt(cfg.min,cfg.max)];
tries++;
if(!cfg.ensureSolvable) break;
if(hasSolution(vals) || tries>2000) break;
}
state.cards=vals.map((v,i)=>({id:i,value:v,suit:SUITS[randInt(0,3)],used:false}));
state.tokens=[]; state.solved=false; state.hintUsed=false;
state.startTs=Date.now(); startTimer();
renderCards(); renderExpr(); setMsg('info','💡 新的一局开始!把四张牌凑成 24 吧。');
submitBtn.classList.remove('ready');
}
多一道搜索,换来的是用户永远拿不到「无解死局」。
3. 表达式求值带校验,括号和数值一次判清
玩家点出来的 token 最终拼成字符串,用 Function 做一次安全范围里的求值;validate 同时检查括号配对、表达式完整度、是否用完四张牌、是否等于 24:
function evalExpr(raw){
try{
const v=Function('"use strict";return ('+raw+')')();
if(typeof v==='number' && isFinite(v)) return v;
}catch(e){}
return null;
}
function validate(announce){
const used=state.cards.filter(c=>c.used).length;
if(balance()!==0){
setResult('bad','括号未配对','未完成'); submitBtn.classList.remove('ready'); return;
}
const raw=buildRawExpr();
const v=evalExpr(raw);
if(v===null){
setResult('wait','表达式不完整','未完成'); submitBtn.classList.remove('ready'); return;
}
const allUsed = used===4;
const is24 = Math.abs(v-24)<EPS;
if(is24 && allUsed){
setResult('ok',fmt(v),'= 24 ✓');
submitBtn.classList.add('ready');
if(announce) doWin();
}
}
4. 计分和连胜都写进 localStorage,关掉也不丢
doWin 用难度倍率减去耗时的公式算分,提示会打折,分数和连胜记录持久化保存:
function doWin(){
if(state.solved) return;
state.solved=true; stopTimer();
const elapsed=Math.floor((Date.now()-state.startTs)/1000);
const cfg=DIFF_CFG[state.diff];
let gain=Math.round(cfg.mult*100 - elapsed);
if(state.hintUsed) gain=Math.round(gain*0.4);
if(gain<10) gain=10;
state.score+=gain; state.streak+=1;
scoreEl.textContent=state.score; streakEl.textContent=state.streak;
const solStr=state.tokens.map(t=>{
if(t.type==='num'){ const c=state.cards[t.cardId]; return (FACE[c.value]||c.value)+c.suit; }
if(t.value==='('||t.value===')') return t.value;
return OP_DISPLAY[t.value];
}).join(' ');
setMsg('success',`🎉 太棒了!<span class="solution">${solStr} = 24</span> 用时 ${sec2str(elapsed)},+${gain} 分${state.hintUsed?'(用了提示)':''}`);
state.history.unshift(`+${gain}`);
if(state.history.length>5) state.history.pop();
historyEl.innerHTML='最近得分:'+state.history.map(h=>`<span>${h}</span>`).join(' ');
submitBtn.classList.remove('ready');
confetti();
}
直接下载试玩
本次生成的项目就是单文件 index.html,没有第三方依赖,把文件保存到本地后双击用浏览器打开即可运行。源码同步到公开 Demo Park 仓库:
- Demo Park 仓库:https://atomgit.com/deli007/demo_park
- 本案例目录:https://atomgit.com/deli007/demo_park/tree/main/codearts-24-game
总结
24 点小游戏核心就两件事:用递归搜索保证发牌有解,用带容差的表达式求值保证判定准确。solve24 每次合并两个数并保留带括号的表达式,最终返回一条完整解法;deal 在发牌前排掉死局,validate 在提交前检查括号、完整度和结果。想把工具再变强,可以接着做「多解提示」「限时挑战排行榜」或把运算符扩展到乘方和开方。
如果还没用过码道,可以免费开通体验版:https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwsf&sourcead=dmzntgwwbwz
- 点赞
- 收藏
- 关注作者
评论(0)