以一个真实开发过程为蓝本:俄罗斯方块如何从「AI 玩不起来」迭代到「每分钟消十几行」。本文讲透三件事——如何为智能体设计游戏接口、如何集成 Jev、如何组织 Jev 的输入与输出——读完你可以把同样的方法迁移到任何游戏或应用。
Jev 是 TypeSafe(docs.typesafe.ai)提供的 System One 模型。名字借自心理学的「系统一 / 系统二」:系统一是快速、直觉的判断;系统二(传统 LLM 聊天模型)是缓慢、深思熟虑的推理。Jev 不生成文章,它只做一件事——给定一段状态描述和一组问题,返回带概率分布和置信度的结构化判断。
这决定了它和 LLM 的用法完全不同:
| 传统 LLM(Chat API) | Jev(System One API) | |
|---|---|---|
| 输入 | 自由对话,提示词工程没有硬性结构 | state(状态快照)+ questions(类型化问题) |
| 输出 | 自然语言文本,需要自己解析 | 结构化答案:选项、概率分布、置信度、分数 |
| 擅长 | 长文本生成、开放式推理、多步规划 | 单点判断:在明确选项中挑一个、给一个评分、回答真假 |
| 延迟/成本 | 秒级、贵 | 百毫秒级、极便宜(本文实测每步约 940 token,约几分钱一局) |
| 正确用法 | 把 LLM 当「大脑」,让它输出动作序列 | 把 Jev 当「直觉/品味」,代码负责计算与执行,Jev 负责在选项中判断 |
整个集成只涉及一个 endpoint:POST https://api.typesafe.ai/v1/systemone,Bearer 认证。请求体三要素:model、state、questions。
// 请求(示意,节选自本项目真实代码)
POST /v1/systemone
Authorization: Bearer <TYPESAFE_API_KEY>
{
"model": "jev-latest",
"state": { // 任意结构:字符串或 JSON 对象都行
game: {
board_rows_top_to_bottom: ["..........", "......##..", /* … 20 行 */],
current_piece: "T",
stack_height: "high",
holes_in_stack: "one hole"
}
},
"questions": { // 一次可并行问多个问题
placement: { type: "choice", instructions: {…}, criteria: {…} },
risk: { type: "score", instructions: "…" },
safe: { type: "noul", instructions: "…" }
}
}
// 响应
{
"answers": {
"placement": {
"choice": "p14", // 选中的选项 id
"probabilities": { "p14": 0.76, "p3": 0.11, … }, // 全选项概率分布
"confidence": 0.82 // 校准置信度
},
"risk": { "score": 0.31 },
"safe": { "noul": 0.62 }
},
"usage": { "input_tokens": 930, "output_tokens": 12 },
"model": "jev-1.13.0"
}
Jev 的三种问题原语覆盖了游戏决策的全部需求:Choice(在明确选项中挑一个)、Score(给一个 0–1 分数)、Noul(带真/假两面的判断,返回倾向值)。你的全部工作就是把游戏问题「翻译」成这三类问题。
这句话是整个项目的灵魂,也是社区所有成功案例(官方 Tetris demo、Doom bot、Pac-Man demo、开源项目 trungdq88/jev-tetris)共同遵循的模式:
"Code owns the game, Jev supplies the judgment." —— 所有确定性计算(枚举、模拟、评估、执行动画)由代码完成;Jev 只在「已经算好的选项」之间做价值判断。
具体拆成三条铁律:
不要把棋盘丢给模型说「你看着办」。Jev 没有持久的工作记忆,让它做空间旋转推理、心算十几步落点,既慢又不可靠。代码应该穷举所有合法选择,模拟每个选择的后果,把结果做成选项表交给它。
System One 对文本的「语义感觉」远强于算术。holes: 2 不如 "two holes";bumpiness: 7 不如 "slightly uneven"。把所有量化指标转成有语义的词(下文 §5 给出完整实现)。
决策点的粒度应该是「一个方块的最终落点」,而不是「一次按键」。前者选项有限、后果明确、一次调用搞定;后者会把一个大决策拆成十几个犹豫不决的小调用——我们 v1 就死在这里(§3)。
先看错误做法,你才知道正确做法为什么长那样。v1 的设计非常「直觉」:把 Jev 当成一个 LLM agent,让它像人打游戏一样每按一次键就问一次。
state 是生棋盘文本 + 当前方块的旋转形状;question 是一个 Choice,选项是五个抽象动作:
// v1:错误的组织方式(保留下来做对照实验用)
state: "当前棋盘 20 行文本…… 当前方块:T(可旋转)……"
questions: {
action: {
type: "choice",
instructions: { goal: "你正在逐步玩俄罗斯方块……每步之后会再次询问你。" },
criteria: {
left: "左移一格",
right: "右移一格",
rotate: "顺时针旋转 90°",
soft_drop: "下落一行",
hard_drop: "立刻锁定(不可逆)"
}
}
}
游戏跑起来后,Jev 的决策日志是这样的(真实数据):
三个致命症状,每个都有明确的根因:
| 症状 | 数据表现 | 根因分析 |
|---|---|---|
| 概率摊平,置信度极低 | 置信度 9–23%,top2 选项概率只差几个百分点 | 「下一步往哪动」取决于再往后几步,单看当前状态五个动作都「合理」。问题本身欠约束,Jev 只能平均分配概率。 |
| 行为退化成磨蹭 | 60 秒 74 次调用,0 消行,反复 soft_drop | 提示词里 hard_drop 标注了「不可逆」,模型天然回避不可逆动作;而每次 soft_drop 后局面几乎没变,下一步的回答也几乎不变。 |
| 成本高、无进展 | 74 次调用 × 942 tok ≈ 7 万 token,得分 112 | agent 式 while 循环正是官方文档明确列出的反模式:每次调用都携带全部上下文却只推进一小步。 |
如果你发现 Jev 的概率分布长期摊平、置信度上不去,先怀疑问题本身设计错了,而不是怀疑模型能力。摊平的概率不是「模型不确定」,是「你的问题没有给模型可以抓住的抓手」。
v2 把决策粒度从「一次按键」提升为「一个方块的最终落点」,并把所有空间计算移进代码。每个新方块到来时执行以下闭环:
对当前方块的每一种旋转、每一列,模拟「硬降到最低合法位置 → 锁定 → 消行 → 统计新局面」,产出一个落点对象列表:
// 枚举当前方块的每个合法落点(rotation × column),模拟锁定后的结果
function enumeratePlacements(engine) {
const type = engine.cur.type;
const before = gridStats(engine.grid); // 现局统计:高度/洞/粗糙度
const seen = new Set();
const placements = [];
SHAPES[type].forEach((m, rot) => {
for (let x = 0; x <= COLS - m[0].length; x++) {
if (collidesGrid(engine.grid, m, x, 0)) continue; // 该列放不下,跳过
let y = 0;
while (!collidesGrid(engine.grid, m, x, y + 1)) y++; // 硬降到最低合法位置
// 复制棋盘并锁定方块
const locked = engine.grid.map(r => r.slice());
for (let my = 0; my < m.length; my++)
for (let mx = 0; mx < m[my].length; mx++)
if (m[my][mx]) locked[y + my][x + mx] = type;
// 不同旋转可能产生相同终局(O/I/S/Z),去重
const key = locked.map(r => r.join("")).join("/");
if (seen.has(key)) continue;
seen.add(key);
// 模拟消行,统计落点后的局面
const { grid: afterGrid, cleared } = clearGridRows(locked);
const after = gridStats(afterGrid);
const p = {
id: `p${placements.length}`, // 稳定 id,Jev 的答案靠它映射回来
type, rot, x, y,
linesCleared: cleared,
holesCreated: Math.max(0, after.holes - before.holes),
heightDelta: after.maxHeight - before.maxHeight,
after,
};
placements.push(p);
}
});
return placements; // 典型规模:12~34 个落点
}
枚举把「玩俄罗斯方块」这个开放问题,压缩成了「从 30 个带完整后果描述的选项里挑一个」这个封闭问题。封闭问题的概率分布才有意义,置信度才有校准价值。这个套路可以直接搬到棋类(枚举合法着法)、资源管理类(枚举可行操作)等任何游戏。
state 支持纯字符串或任意嵌套的 JSON 对象。对象形式更好——它让 instructions 里可以用反引号路径精确引用(如 `game.current_piece`)。本项目的 state 结构:
function buildState(engine) {
const stats = gridStats(engine.grid);
return {
game: {
// 规则说明:给不知道游戏规则的模型
rules: "Standard Tetris. Board is 10 columns wide and 20 rows tall.
Rows fill left to right; a full row disappears.
The game is lost when the stack reaches the top.",
// 棋盘原文 + 图例:文本形态的原始证据,供模型核对
board_rows_top_to_bottom: engine.grid.map(r => r.join("")),
legend: "# is a filled cell, . is an empty cell.
The first row is the top of the board.",
// 预计算的量化指标 —— 注意:全部转成了词
column_heights_left_to_right: stats.heights,
stack_height: describeHeight(stats.maxHeight), // "medium"
holes_in_stack: describeHoles(stats.holes), // "one hole"
surface: describeSurface(stats.bumpiness), // "slightly uneven"
current_piece: engine.cur.type,
next_piece: engine.nextType,
lines_cleared_so_far: engine.lines,
},
};
}
state 的每个字段名都应该「读完就懂」。模型拿到 board_rows_top_to_bottom 不需要猜方向;holes_in_stack 不需要猜定义。宁可字段名长,也不要让模型从字段名反推语义。
把每个量化指标映射到一个人工设计的语义区间。这个映射本身就是你的「领域知识注入」:
// —— 数字转词:Jev 读文本强于算术 ——
function describeLines(n) {
return ["none", "one line", "two lines", "three lines", "four lines (a Tetris)"][n]
?? `${n} lines`;
}
function describeHoles(n) {
return n === 0 ? "none" : n === 1 ? "one hole"
: n === 2 ? "two holes" : "three or more holes";
}
function describeHeight(h) {
if (h <= 4) return "very low";
if (h <= 8) return "low";
if (h <= 12) return "medium";
if (h <= 15) return "high";
return "dangerously high, close to the top";
}
function describeSurface(b) { // b = bumpiness(相邻列高度差之和)
return b <= 4 ? "flat" : b <= 9 ? "slightly uneven"
: b <= 16 ? "bumpy" : "very jagged";
}
区间边界不是随便定的,它们编码了游戏策略:15 行以上标为 "dangerously high",是为了让 Jev 在「堆到临界」时能直接从字面读懂危险。当你调整游戏风格时,改的就是这些边界。
注意 state 里同时放了 board_rows_top_to_bottom(原始棋盘)和提炼后的指标。原则是:提炼指标承担 90% 的判断依据,原文用于模型在拿不准时交叉核对。只给原文,模型要做心算;只给提炼,模型无法发现指标没覆盖的细节(比如某个特定的井形)。
state 越大 token 越多、注意力越散。棋盘历史、已消行的过程动画、UI 状态这些统统不要放。每次调用只放这一次判断需要的信息。
questions 是你向 Jev 提问的全部接口。v2 用了三个原语、四个问题,一次调用并行返回:
这是决策核心。每个落点转成一个 criteria 对象——注意所有选项字段名完全一致,这让 Jev 可以逐字段横向比较:
// Choice criteria 条目:每个落点一个对象,字段名跨选项一致,全部用词不用数
function describePlacement(p) {
const m = SHAPES[p.type][p.rot];
const left = p.x + 1, right = p.x + m[0].length;
const where = left === right ? `column ${left}` : `columns ${left}-${right}`;
let hChange;
if (p.linesCleared > 0 && p.heightDelta < 0) hChange = "stack gets lower";
else if (p.heightDelta <= 0) hChange = "stack does not get taller";
else if (p.heightDelta === 1) hChange = "stack grows by one row";
else hChange = "stack grows by several rows";
return {
where, // "columns 4-6"
lines_cleared: describeLines(p.linesCleared), // "one line"
holes_created: describeHoles(p.holesCreated), // "none"
holes_uncovered: p.holesRemoved > 0 ? describeHoles(p.holesRemoved) : "none",
stack_height_after: describeHeight(p.after.maxHeight),
height_change: hChange,
surface_after: describeSurface(p.after.bumpiness),
wells_after: describeWells(p.after.wells), // "one deep well at column 9"
};
}
Choice 的 instructions.priorities 是一组按优先级排列的判据短句。它就是 Jev 的「价值观」,也是你调整 AI 风格的旋钮——想让它更激进或更保守,改这几行即可:
const PLACEMENT_PRIORITIES = [
"Clearing lines is good. Clearing more lines at once is better.",
"Do not create holes. A placement with holes_created of none beats one that
creates holes, unless the one with holes clears far more lines
or the stack is dangerously high.",
"Keep the stack low. Prefer a lower stack_height_after and a height_change
that does not grow the stack.",
"Keep the surface flat. Prefer surface_after of flat over slightly uneven,
bumpy, or very jagged.",
"One deep well is acceptable because the next I piece can fill it.
Several deep wells are bad.",
"When the stack is dangerously high, survival matters more than a clean surface.",
];
实测与文档都表明:Jev 的训练语料以英文为主,state 内容、criteria、priorities 全部用英文写,判断质量最稳。中文做游戏完全没问题,但提示词层面建议英文——这是「游戏实现语言」和「提示词语言」解耦的典型场景。
同一次调用里还附了三个「顺路问一嘴」的问题,成本几乎为零,但给决策日志和后续策略提供了丰富信号:
function buildQuestions(placements) {
const criteria = {};
for (const p of placements) criteria[p.id] = describePlacement(p);
return {
// ① 主问题:从 30+ 落点里挑一个
placement: {
type: "choice",
instructions: {
question: "Which placement of `game.current_piece` should the player
choose? Each option describes the board after that placement.",
priorities: PLACEMENT_PRIORITIES,
},
criteria, // { p0: {where, lines_cleared, …}, p1: {…}, … }
},
// ② 策略判断(choice over 固定策略集)
strategy: {
type: "choice",
instructions: "Looking at `game`, which strategy fits the current
situation best for the next few pieces?",
criteria: STRATEGY_OPTIONS, // build_clean / clear_lines / repair_surface / survive
},
// ③ 局面健康分(score)
board_health: {
type: "score",
instructions: "How healthy is the stack in `game` for a Tetris player
who wants to keep playing for a long time?",
criteria: HEALTH_LEVELS, // 给 score 提供锚点:Clean / Fine / Rough / Critical
},
// ④ 下一块是否有好去处(noul,真假倾向)
next_piece_fits: {
type: "noul",
instructions: "Given `game.column_heights_left_to_right` and `game.surface`,
is there an obvious clean spot for `game.next_piece`
after this move, without creating holes?",
criteria: { true: "A clean spot is easy to see.",
false: "The next piece will be awkward to place." },
},
};
}
答案回来后如何用?三条实践:
const a = data.answers.placement;
// 1. choice → 落点对象;choice 无效时退化到概率最高项,再退化到第一个落点
const byId = new Map(placements.map(p => [p.id, p]));
const ranked = Object.entries(a.probabilities || {})
.filter(([id]) => byId.has(id)).sort((x, y) => y[1] - x[1]);
const chosen = byId.get(a.choice) || byId.get(ranked[0]?.[0]) || placements[0];
// 2. probabilities → 记录 top3,写进决策日志(人可以看懂 AI 在犹豫什么)
this.appendLog({ top: ranked.slice(0, 3), confidence: a.confidence, … });
// 3. 与经典启发式对比(Felicitas: -0.51h + 0.76c - 0.36o - 0.18b),统计一致率
const hBest = placements.reduce((b, p) => (p.heuristic > b.heuristic ? p : b), placements[0]);
this.heuristicCount++; if (chosen.id === hBest.id) this.agreeCount++;
| 返回字段 | 类型 | 怎么用 |
|---|---|---|
choice | 选项 id | 映射回代码侧对象执行。必须做 存在性校验(模型可能返回不存在的 id)。 |
probabilities | {id: 0~1} | 展示 top 选项分布;可做「次优备选」:首选执行失败时用第二名。 |
confidence | 0~1 | 置信度低于阈值时可触发保守策略(如选启发式最优项)。注意它反映的是「这个判断值得信吗」,不是「选项好不好」。 |
score | 0~1 | 连续评估信号,适合画趋势图或触发策略切换(本项目中 health 恶化 → 优先 survive)。 |
noul | 0~1 倾向 | 二元判断的软版本:0.62 表示「更倾向 true 但不坚决」,可做阈值告警。 |
这是本文档最「值钱」的一条实测结论。api.typesafe.ai 的 CORS 是 origin 白名单模式:我们对 http://localhost:8000、localhost、null、https://example.com 甚至 https://console.typesafe.ai 逐一发了预检请求,全部被拒(无 Access-Control-Allow-Origin 头)。
纯前端页面无法直连 TypeSafe API。这其实印证了官方建议——API key 应放服务端。解法:架一个本地转发代理。
代理可以极简——零依赖 Node 脚本(Node 20+ 自带 fetch),完整代码只有 70 行:
#!/usr/bin/env node
// jev-proxy.mjs —— 浏览器 CORS 桥。用法: TYPESAFE_API_KEY=sk-xxx node jev-proxy.mjs 8787
import http from "node:http";
const PORT = Number(process.argv[2] || process.env.PORT || 8787);
const UPSTREAM = "https://api.typesafe.ai";
const ENV_KEY = process.env.TYPESAFE_API_KEY || "";
const CORS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Max-Age": "600",
};
http.createServer(async (req, res) => {
for (const [k, v] of Object.entries(CORS)) res.setHeader(k, v);
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
if (req.method !== "POST" || !req.url.startsWith("/v1/")) {
res.writeHead(404); res.end(JSON.stringify({ error: "only POST /v1/* is proxied" })); return;
}
const chunks = [];
for await (const c of req) chunks.push(c);
const body = Buffer.concat(chunks);
// 客户端自带 Authorization 则优先,否则用环境变量里的 key
const headers = { "Content-Type": "application/json" };
headers["Authorization"] = req.headers["authorization"]
|| (ENV_KEY ? `Bearer ${ENV_KEY}` : "");
try {
const upstream = await fetch(UPSTREAM + req.url, { method: "POST", headers, body });
const respBody = Buffer.from(await upstream.arrayBuffer());
res.writeHead(upstream.status, {
"Content-Type": upstream.headers.get("content-type") || "application/json",
});
res.end(respBody);
} catch (err) {
res.writeHead(502); res.end(JSON.stringify({ error: "upstream_failed" }));
}
}).listen(PORT, () => console.log(`Jev proxy on http://localhost:${PORT}`));
页面里把 API 地址填 http://localhost:8787 即可。key 可以放环境变量(更安全),也可以继续填在页面里由代理转发(本地实验够用)。
生产可用的 Jev 循环必须内建这些保险丝:
| 保险丝 | 实现 | 为什么需要 |
|---|---|---|
| 步数上限 | 每方块最多 N 步动作(v1 遗留模式里 30 步强制 hard_drop) | 模型可能陷入重复动作死循环(v1 实测反复 soft_drop)。 |
| 指数退避 | 429 / 529 时 sleep 1s → 2s → 4s… 重试 | API 有速率限制;官方文档明确 529 是过载。 |
| 答案校验 | choice 不在候选里 → 退化到概率最高项 → 再退化到启发式最优 | 生成式判断永远有小概率输出无效 id。 |
| 错误面板 | CORS/网络错误给出可操作的提示文案 | 用户最常见的第一坑就是没启动代理。 |
| 可观测性 | 每次调用记录:耗时、token、概率 top3、置信度、与启发式的一致率 | 没有日志你根本无法诊断「AI 为什么这么玩」(本文 §3 的诊断全靠它)。 |
「留接口让 AI 能自己玩」不只有内置 Jev 一条路。把游戏引擎的观测、动作、事件三要素暴露到 window 上,任何外部智能体(Playwright/Puppeteer 脚本、你的 harness 插件、另一个 LLM)都能驱动游戏。这是本项目 v2 的完整接口:
window.TetrisAPI = {
// —— 观测(Read)——
getState: () => engine.getState(), // 结构化 JSON:棋盘/当前块/分数/消行数
getStateText: () => engine.getStateText(), // 文本形态,适合直接粘给任何 LLM
getPlacements: () => { // ★ 帮智能体做完所有难的计算
const ps = enumeratePlacements(engine);
return ps.map(p => ({ id: p.id, rot: p.rot, x: p.x,
...describePlacement(p), heuristic: p.heuristic }));
},
// —— 动作(Write)——
applyAction: (a) => engine.applyAction(a), // left/right/rotate/soft_drop/hard_drop
executePlacement: (p) => { // ★ 高层动作:直接执行一个落点
let guard = 0;
while (engine.cur && engine.cur.rot !== p.rot && guard++ < 4)
engine.applyAction("rotate");
const dx = p.x - engine.cur.x;
for (let i = 0; i < Math.abs(dx); i++)
engine.applyAction(dx > 0 ? "right" : "left");
return engine.applyAction("hard_drop");
},
// —— 事件(Subscribe)——
onEvent: (evt, cb) => engine.on(evt, cb), // lock / clear / gameover / score
// —— 控制 ——
start: () => {…}, reset: () => engine.reset(),
isGameOver: () => engine.over,
setGravity: (on) => engine.setGravity(!!on), // 关重力 = AI 有无限思考时间
};
getState() 返回纯 JSON,不含 DOM、Canvas、函数引用。智能体可能在完全不同的进程里消费它。applyAction(按键级,灵活)+ 高层 executePlacement(意图级,省事)。外部智能体如果只想「选落点」,不应该被迫写移动循环。getPlacements() 把枚举+模拟+词化一次给全。外部智能体(哪怕是个简单的随机脚本)拿到选项表就能玩出可看的水准。onEvent("lock", cb) 让智能体在方块锁定时才做下一次决策,而不是定时盲扫。// 伪代码:任何能发 HTTP 请求的智能体都能这样玩
const page = await browser.newPage();
await page.goto("file:///path/to/index.html");
await page.evaluate(() => TetrisAPI.start() || TetrisAPI.setGravity(false));
while (!(await page.evaluate(() => TetrisAPI.isGameOver()))) {
const placements = await page.evaluate(() => TetrisAPI.getPlacements());
const state = await page.evaluate(() => TetrisAPI.getStateText());
// 把 state + placements 发给任意决策者:Jev / LLM / 你自己写的启发式
const pick = await decideWithYourAgent(state, placements);
await page.evaluate((p) => TetrisAPI.executePlacement(p), pick);
}
这个接口层同时是测试钩子:自动化测试可以用它确定性地下棋、验证计分;也可以用它跑「Jev vs 启发式 vs 随机」的对照实验——本文 §9 的数据就是这么测出来的。
| 指标 | v1 · 逐步动作循环 | v2 · 落点决策 |
|---|---|---|
| 置信度 | 9–23%(概率摊平) | 76–90%(偶尔低至 33%,多为平局局面) |
| 60 秒产出 | 74 次调用,0 消行,112 分 | 69 块全部落定,消 12 行,3094 分 |
| 单次延迟 | 约 400ms | 约 240–400ms(每块仅 1 次) |
| Token 效率 | ≈ 7 万 tok / 60s,无进展 | ≈ 6.6 万 tok / 60s,稳定推进 |
| 与经典启发式一致率 | 无法衡量(判断无锚点) | 67% |
| 策略适应性 | 无 | 堆到临界时自动切换 survive 策略,优先「不增高」落点 |
两个值得一提的行为细节:
probabilities 做 beam search:同时模拟 top-3 落点往后看一块,提升长线规划;PLACEMENT_PRIORITIES 做风格消融(激进消行 vs 保守堆叠)。最后把全文收敛成一张可执行的 checklist。无论你做的是俄罗斯方块、棋类、模拟经营还是任何「有状态、有选择」的应用,流程都是同一套:
getPlacements())——外部智能体直接复用你的全部计算。为智能体设计应用的本质是:把一个开放问题压缩成一组封闭选项,把封闭选项的后果翻译成语义词,然后把最后一步「品味判断」交给 Jev。代码永远拥有游戏;Jev 永远只做选择。
| 文件 | 说明 |
|---|---|
tetris-jev/index.html | 单文件零依赖俄罗斯方块:完整玩法 + 内置 Jev 玩家(v2 落点决策 / v1 逐步动作可切换)+ 决策日志 + window.TetrisAPI 智能体接口。 |
tetris-jev/jev-proxy.mjs | 70 行零依赖 Node CORS 代理,桥接浏览器与 TypeSafe API。 |
# 1. 启动代理(key 放环境变量,或在页面里填)
TYPESAFE_API_KEY=sk-xxxx node tetris-jev/jev-proxy.mjs 8787
# 2. 浏览器打开 index.html,API 地址填 http://localhost:8787
# 3. 点「▶ 启动 Jev 玩家」,观察决策日志
项目做到 v2 之后,一个无法回避的质疑浮出水面:「程序已经把所有落点的结果都遍历了,那还用 Jev 干嘛?是不是没有意义?」这个问题值得认真回答,因为它触及整个集成范式的合法性。本节是这场讨论的完整记录。
代码遍历解决的是「每个选择会发生什么」,但没有解决「哪个结果更好」。这是两件分开的事:
有人会说:用启发式公式 -0.51h + 0.76c − 0.36o − 0.18b 打分不就行了?但注意,那四个权重本身就是别人拍脑袋固化的判断——公式不是「正确答案」,只是把某个人(或某次实验)的品味冻成了数字。它在大量局面下和真人高手的选择并不一致。
决策日志里有一个「与启发式一致率 67%」的指标,容易被误读,在这里讲清楚。每来一个方块:
67% = 一致次数 ÷ 总方块数,即每 10 个方块里约 7 个选择相同、3 个不同。解读这个指标的三条纪律:
| 价值 | 说明 | 本项目证据 |
|---|---|---|
| 判断是上下文敏感的,权重不是 | 堆到 15 层时「活下去」压倒一切;堆得低时应该「搭 Tetris 等 I 块」。固定公式只有一套权重,Jev 能读着局面语义动态切换价值取向。 | v2 日志:堆到临界时自动切换 survive 策略,优先「不增高」落点。 |
| 策略用自然语言注入,不用改代码 | priorities 那 6 句话就是「价值观配置」,改几行英文就能换打法风格。启发式要改风格得重新调权重——而调权重本身是没有标准答案的苦活。 |
priorities 中「消行收益远大于小代价时可例外」被正确执行(选过「消一行但引入一个洞」的落点)。 |
| 范式可迁移 | 换到没有成熟启发式的领域(策略游戏、资源调度、运营决策),「代码枚举 + 模型判断」就是唯一可行的结构。俄罗斯方块只是载体。 | §10 的迁移 checklist 全部成立。 |
在俄罗斯方块这个特定游戏上,质疑基本成立:纯 Dellacherie 级启发式大概率比 Jev 玩得好,而且零成本零延迟。如果目标是「最强俄罗斯方块 AI」,正确答案是强化学习或精调的启发式,不是 Jev。
但这个项目的目标从来不是造最强俄罗斯方块 AI——是验证「代码枚举 + 模型判断」这个集成范式。俄罗斯方块恰好是最诚实的试验场:规则简单、有公开基准、判断质量可以直接比分量化。
质疑指出了最有价值的后续工作:把纯启发式玩家加进页面做同环境对照,量化 Jev 到底比公式强还是弱、在哪些局面(危高抢救?Tetris 搭建?)有真实优势。有了基线,「智能不智能」就从手感变成数据——再据此决定是改进 Jev 的输入,还是承认这个游戏就该用启发式。