Engineering Journal · TypeSafe Jev · System One

让 AI 自己玩游戏:
Jev 集成开发实战指南

以一个真实开发过程为蓝本:俄罗斯方块如何从「AI 玩不起来」迭代到「每分钟消十几行」。本文讲透三件事——如何为智能体设计游戏接口、如何集成 Jev、如何组织 Jev 的输入与输出——读完你可以把同样的方法迁移到任何游戏或应用。

面向零经验开发者单文件 · 零依赖含完整代码含实测数据2026-09-23
01 / 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 负责在选项中判断

API 一瞥

整个集成只涉及一个 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(带真/假两面的判断,返回倾向值)。你的全部工作就是把游戏问题「翻译」成这三类问题。

02 / 设计哲学

代码玩游戏,Jev 做判断

这句话是整个项目的灵魂,也是社区所有成功案例(官方 Tetris demo、Doom bot、Pac-Man demo、开源项目 trungdq88/jev-tetris)共同遵循的模式:

The Golden Rule

"Code owns the game, Jev supplies the judgment." —— 所有确定性计算(枚举、模拟、评估、执行动画)由代码完成;Jev 只在「已经算好的选项」之间做价值判断。

具体拆成三条铁律:

铁律一:代码枚举,不要让 Jev 想象

不要把棋盘丢给模型说「你看着办」。Jev 没有持久的工作记忆,让它做空间旋转推理、心算十几步落点,既慢又不可靠。代码应该穷举所有合法选择,模拟每个选择的后果,把结果做成选项表交给它。

铁律二:数字转词,不要让 Jev 算术

System One 对文本的「语义感觉」远强于算术。holes: 2 不如 "two holes";bumpiness: 7 不如 "slightly uneven"。把所有量化指标转成有语义的词(下文 §5 给出完整实现)。

铁律三:一次调用决策一个「完整决策点」

决策点的粒度应该是「一个方块的最终落点」,而不是「一次按键」。前者选项有限、后果明确、一次调用搞定;后者会把一个大决策拆成十几个犹豫不决的小调用——我们 v1 就死在这里(§3)。

错误心智模型(LLM 式 agent 循环) Jev 看生棋盘 自己想象落点 问一个动作 再问下一个… → 空间推理靠脑补 · 一次按键一次调用 · 概率摊平 · 决策瘫痪 正确心智模型(code-enumerated placement) 代码枚举落点 模拟每种后果 Jev 选一个 代码执行动画 → 确定性计算交给代码 · 一次调用定落点 · 概率集中于最优选项
图 1 · 两种心智模型的对比。上面的循环是我们真实踩过的坑。
03 / 反面教材

我们第一个失败版本(v1)

先看错误做法,你才知道正确做法为什么长那样。v1 的设计非常「直觉」:把 Jev 当成一个 LLM agent,让它像人打游戏一样每按一次键就问一次。

v1 的问题设计

state 是生棋盘文本 + 当前方块的旋转形状;question 是一个 Choice,选项是五个抽象动作:

// v1:错误的组织方式(保留下来做对照实验用)
state: "当前棋盘 20 行文本…… 当前方块:T(可旋转)……"

questions: {
  action: {
    type: "choice",
    instructions: { goal: "你正在逐步玩俄罗斯方块……每步之后会再次询问你。" },
    criteria: {
      left:      "左移一格",
      right:     "右移一格",
      rotate:    "顺时针旋转 90°",
      soft_drop: "下落一行",
      hard_drop: "立刻锁定(不可逆)"
    }
  }
}

实测决策日志:AI 做不出决定

游戏跑起来后,Jev 的决策日志是这样的(真实数据):

#36 conf 13% · risk 0.71 · 416ms · 942 tok
soft_drop
32%
hard_drop
23%
right
22%
#34 conf 20% · risk 0.68 · 402ms · 942 tok
soft_drop
36%
hard_drop
26%
right
17%

三个致命症状,每个都有明确的根因:

症状数据表现根因分析
概率摊平,置信度极低 置信度 9–23%,top2 选项概率只差几个百分点 「下一步往哪动」取决于再往后几步,单看当前状态五个动作都「合理」。问题本身欠约束,Jev 只能平均分配概率。
行为退化成磨蹭 60 秒 74 次调用,0 消行,反复 soft_drop 提示词里 hard_drop 标注了「不可逆」,模型天然回避不可逆动作;而每次 soft_drop 后局面几乎没变,下一步的回答也几乎不变。
成本高、无进展 74 次调用 × 942 tok ≈ 7 万 token,得分 112 agent 式 while 循环正是官方文档明确列出的反模式:每次调用都携带全部上下文却只推进一小步。
教训

如果你发现 Jev 的概率分布长期摊平、置信度上不去,先怀疑问题本身设计错了,而不是怀疑模型能力。摊平的概率不是「模型不确定」,是「你的问题没有给模型可以抓住的抓手」。

04 / 正确架构

v2 的决策闭环

v2 把决策粒度从「一次按键」提升为「一个方块的最终落点」,并把所有空间计算移进代码。每个新方块到来时执行以下闭环:

STEP 1 · 代码 枚举合法落点 所有旋转 × 所有列 → 12~34 个 STEP 2 · 代码 模拟每个落点的后果 锁定→消行→数洞→量高度/表面 STEP 3 · 代码 数字全部转成词 "two lines" / "flat" / "one hole" STEP 4 · JEV(唯一一次调用) placement Choice(30+ 落点选项) 并行附问:strategy / board_health / next_piece_fits STEP 5 · 代码 消费答案并执行 choice→落点·概率→备选·执行动画 STEP 6 · 代码 防御性检查 答案校验·步数上限·退避·错误兜底 LOOP 下一块到来,回到 STEP 1 每块 1 次调用,240~400ms 职责划分 代码:确定性计算 + 执行 Jev:唯一的「品味」判断
图 2 · v2 每方块决策闭环。六个步骤里只有 STEP 4 是一次 Jev 调用。

Step 1–2:枚举与模拟(核心代码)

对当前方块的每一种旋转、每一列,模拟「硬降到最低合法位置 → 锁定 → 消行 → 统计新局面」,产出一个落点对象列表:

// 枚举当前方块的每个合法落点(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 个带完整后果描述的选项里挑一个」这个封闭问题。封闭问题的概率分布才有意义,置信度才有校准价值。这个套路可以直接搬到棋类(枚举合法着法)、资源管理类(枚举可行操作)等任何游戏。

05 / 组织输入

state 的设计规范

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 状态这些统统不要放。每次调用只放这一次判断需要的信息。

06 / 组织输出使用

questions 与答案消费

questions 是你向 Jev 提问的全部接口。v2 用了三个原语、四个问题,一次调用并行返回:

主问题:placement Choice

这是决策核心。每个落点转成一个 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"
  };
}

priorities:用一句话列表注入策略

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.",
];
为什么 priorities 用英文写

实测与文档都表明: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 选项分布;可做「次优备选」:首选执行失败时用第二名。
confidence0~1置信度低于阈值时可触发保守策略(如选启发式最优项)。注意它反映的是「这个判断值得信吗」,不是「选项好不好」。
score0~1连续评估信号,适合画趋势图或触发策略切换(本项目中 health 恶化 → 优先 survive)。
noul0~1 倾向二元判断的软版本:0.62 表示「更倾向 true 但不坚决」,可做阈值告警。
07 / 工程落地

CORS、代理与防御性编程

坑一:浏览器无法直连 API(实测)

这是本文档最「值钱」的一条实测结论。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 可以放环境变量(更安全),也可以继续填在页面里由代理转发(本地实验够用)。

坑二:没有防御性编程的 AI 玩家活不过十分钟

生产可用的 Jev 循环必须内建这些保险丝:

保险丝实现为什么需要
步数上限每方块最多 N 步动作(v1 遗留模式里 30 步强制 hard_drop)模型可能陷入重复动作死循环(v1 实测反复 soft_drop)。
指数退避429 / 529 时 sleep 1s → 2s → 4s… 重试API 有速率限制;官方文档明确 529 是过载。
答案校验choice 不在候选里 → 退化到概率最高项 → 再退化到启发式最优生成式判断永远有小概率输出无效 id。
错误面板CORS/网络错误给出可操作的提示文案用户最常见的第一坑就是没启动代理。
可观测性每次调用记录:耗时、token、概率 top3、置信度、与启发式的一致率没有日志你根本无法诊断「AI 为什么这么玩」(本文 §3 的诊断全靠它)。
08 / 智能体接口

为外部智能体暴露接口

「留接口让 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 有无限思考时间
};

接口设计四原则

  1. 观测必须可序列化:getState() 返回纯 JSON,不含 DOM、Canvas、函数引用。智能体可能在完全不同的进程里消费它。
  2. 动作分两层:低层 applyAction(按键级,灵活)+ 高层 executePlacement(意图级,省事)。外部智能体如果只想「选落点」,不应该被迫写移动循环。
  3. 把难的计算做成接口:getPlacements() 把枚举+模拟+词化一次给全。外部智能体(哪怕是个简单的随机脚本)拿到选项表就能玩出可看的水准。
  4. 事件推送代替轮询:onEvent("lock", cb) 让智能体在方块锁定时才做下一次决策,而不是定时盲扫。

驱动示例:20 行让任意模型玩游戏

// 伪代码:任何能发 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 的数据就是这么测出来的。

09 / 实测数据

v1 vs v2:同一环境各 60 秒

指标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 策略,优先「不增高」落点

两个值得一提的行为细节:

下一步实验方向

10 / 迁移指南

把方法用到你的游戏 / 应用

最后把全文收敛成一张可执行的 checklist。无论你做的是俄罗斯方块、棋类、模拟经营还是任何「有状态、有选择」的应用,流程都是同一套:

第一步:找到「决策点」并降低它的粒度

第二步:写枚举器(代码的活,不是 AI 的活)

第三步:把后果翻译成「词」

第四步:写 priorities 和投机问题

第五步:消费答案 + 保险丝

第六步:暴露智能体接口

一句话总结

为智能体设计应用的本质是:把一个开放问题压缩成一组封闭选项,把封闭选项的后果翻译成语义词,然后把最后一步「品味判断」交给 Jev。代码永远拥有游戏;Jev 永远只做选择。

11 / 附录

文件清单与参考资料

本项目文件

文件说明
tetris-jev/index.html单文件零依赖俄罗斯方块:完整玩法 + 内置 Jev 玩家(v2 落点决策 / v1 逐步动作可切换)+ 决策日志 + window.TetrisAPI 智能体接口。
tetris-jev/jev-proxy.mjs70 行零依赖 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 玩家」,观察决策日志

参考资料

12 / 讨论

代码枚举了一切,Jev 还有什么意义?

项目做到 v2 之后,一个无法回避的质疑浮出水面:「程序已经把所有落点的结果都遍历了,那还用 Jev 干嘛?是不是没有意义?」这个问题值得认真回答,因为它触及整个集成范式的合法性。本节是这场讨论的完整记录。

枚举了结果 ≠ 能判断好坏

代码遍历解决的是「每个选择会发生什么」,但没有解决「哪个结果更好」。这是两件分开的事:

有人会说:用启发式公式 -0.51h + 0.76c − 0.36o − 0.18b 打分不就行了?但注意,那四个权重本身就是别人拍脑袋固化的判断——公式不是「正确答案」,只是把某个人(或某次实验)的品味冻成了数字。它在大量局面下和真人高手的选择并不一致。

插曲:「67% 一致率」到底是什么意思

决策日志里有一个「与启发式一致率 67%」的指标,容易被误读,在这里讲清楚。每来一个方块:

  1. 代码枚举全部合法落点(约 30 个);
  2. 用上面的四参数公式给每个落点打分,得分最高者记为「启发式首选」;
  3. Jev 从同样的落点集合中选一个;
  4. 两者选中同一个落点记一次「一致」,否则记「不同」。

67% = 一致次数 ÷ 总方块数,即每 10 个方块里约 7 个选择相同、3 个不同。解读这个指标的三条纪律:

Jev 的三个真实价值

价值说明本项目证据
判断是上下文敏感的,权重不是 堆到 15 层时「活下去」压倒一切;堆得低时应该「搭 Tetris 等 I 块」。固定公式只有一套权重,Jev 能读着局面语义动态切换价值取向。 v2 日志:堆到临界时自动切换 survive 策略,优先「不增高」落点。
策略用自然语言注入,不用改代码 priorities 那 6 句话就是「价值观配置」,改几行英文就能换打法风格。启发式要改风格得重新调权重——而调权重本身是没有标准答案的苦活。 priorities 中「消行收益远大于小代价时可例外」被正确执行(选过「消一行但引入一个洞」的落点)。
范式可迁移 换到没有成熟启发式的领域(策略游戏、资源调度、运营决策),「代码枚举 + 模型判断」就是唯一可行的结构。俄罗斯方块只是载体。 §10 的迁移 checklist 全部成立。

诚实的结论

在俄罗斯方块这个特定游戏上,质疑基本成立:纯 Dellacherie 级启发式大概率比 Jev 玩得好,而且零成本零延迟。如果目标是「最强俄罗斯方块 AI」,正确答案是强化学习或精调的启发式,不是 Jev。

但这个项目的目标从来不是造最强俄罗斯方块 AI——是验证「代码枚举 + 模型判断」这个集成范式。俄罗斯方块恰好是最诚实的试验场:规则简单、有公开基准、判断质量可以直接比分量化。

由此引出的下一步实验

质疑指出了最有价值的后续工作:把纯启发式玩家加进页面做同环境对照,量化 Jev 到底比公式强还是弱、在哪些局面(危高抢救?Tetris 搭建?)有真实优势。有了基线,「智能不智能」就从手感变成数据——再据此决定是改进 Jev 的输入,还是承认这个游戏就该用启发式。