
方式
llm_macro_client.py 是连接到 Qt 画面上的客户端。
- 从收到的棋盘中擦掉正在移动的方块,只留下已固定的方块。
- 把当前方块按每种旋转、每个 x 都试着落下。如果是 T 方块,就是 34 个位置。x 为负数的位置也包括在内。
- 对每个位置计算消除的行数、新空洞、堆在空洞上方的格子、凹凸度、最大高度、落地高度和深井,并打分。
- 再把下一个方块也试放一次,加上其最高分的 0.7 倍后重新排序。
- 把前 6 名连同文字棋盘一起给 LLM(Qwen2.5-3B GGUF)看,只让它选一个编号。LLM 无法编造坐标。
- 按选中的位置调整好旋转和移动后直接落下。
加上 --no-llm 时,不使用 LLM,直接用第一名。
仅靠启发式
C API 不提供下一个方块,所以这次测试去掉了第 4 步(多看一个方块),只使用第一名。相同种子 20 局,到 1,000 个方块时停止。
- 平均 940 个方块,370 行
- 20 局中有 16 局坚持到了 1,000 个方块。其余 4 局分别在 375、736、811、863 个方块时结束。
- 到达 1,000 个的 16 局中,有 12 局最后的堆叠高度在 6 格以下。
无需训练,仅凭一个打分公式就大幅领先 DQN(0 行)和 MoE(12 行)。

让 LLM 来选
接上 LLM,用种子 0 跑了一局,到 150 个方块为止。因为和五子棋训练共用 GPU,所以只跑了很短的一局。
- 150 次中有 147 次(98%)选择了第一名。第二名 2 次,第三名 1 次。
- 以 55 行、0 个空洞、高度 7 格结束。同样条件下只用启发式第一名是 58 行。
- 每次选择平均耗时 1.7 秒。只用启发式的话,20 局、1 万 8 千多个方块 14 秒就下完了。
提示词写着“如果 [0] 满足条件就选 0”,而候选又已经按分数排好,所以 LLM 几乎都跟着启发式走。在现在的结构里,LLM 并不能让棋盘变得更好,只会让速度变慢。
总结
- 低层 DQN:几乎见不到消行奖励,所以和随机一样。
- 宏 DQN:动作表示挡住了最左边一列,只会反复使用中间的两个位置。
- VLM 奖励与 VLM 直接选择:分辨不了棋盘,往中间堆。
- 候选计算与打分:无需训练就能超过 1,000 个方块。LLM 只起到确认第一名的作用。
接下来,我打算把宏动作改成实际可以放置的 162 个位置,并以启发式规划器下出的棋局为范例重新训练 DQN。
这一阶段的实际代码
这是一个位置的分数。
llm_macro_client.py
on_hole_w = 3.0 * max(0.2, 1.0 - max_h_global / 16.0)
score = (
+ 1.0 * cleared
- 4.0 * new_holes
- on_hole_w * cells_above_hole
- 0.36 * holes_after
- 1.00 * max_h # 0.5 → 1.0 (높이 누적 강하게 회피)
- 0.30 * bump_eff # 0.18 → 0.30 (양쪽 우물 패턴 회피)
- 0.70 * landing_height # 0.5 → 0.7 (깊게 박기 우선)
- 0.40 * other_well_sum
+ 0.30 * well_bonus # 0.5 → 0.3 (테트리스 욕심을 줄임)
)把当前方块在所有可以放置的位置都试着落下。
llm_macro_client.py
def enumerate_candidates(board: np.ndarray, ptype: int) -> list[dict]:
if ptype == 0:
return []
heights_before = column_heights(board)
hole_cols_before = column_hole_count(board, heights_before)
holes_before = sum(hole_cols_before)
nrot = _PIECE_NROT[ptype]
w = board.shape[1]
cands = []
for rot in range(nrot):
cells = PIECE_CELLS[ptype][rot]
min_dx = min(dx for dx, _ in cells)
max_dx = max(dx for dx, _ in cells)
x_lo = -min_dx
x_hi = w - 1 - max_dx
for x in range(x_lo, x_hi + 1):
r = evaluate_placement(
board,
ptype,
rot,
x,
heights_before=heights_before,
hole_cols_before=hole_cols_before,
holes_before=holes_before,
)
if r is not None:
cands.append(r)
cands.sort(key=lambda r: r["score"], reverse=True)
return cands这是给 LLM 的选择规则。
llm_macro_client.py
"Selection priorities (STRICT, in order):\n"
" 1) on_hole == 0 (NEVER stack on a column that already contains buried holes)\n"
" 2) new_holes == 0 (do not create new holes)\n"
" 3) cleared >= 1 (clear lines if possible)\n"
" 4) higher 'look' (1-step lookahead) wins ties\n"
" 5) lower max_h, then lower bump, then lower land_h.\n"
"If [0] already satisfies (1)+(2), just pick 0.\n"
f'Reply EXACTLY as: {{"choice": <integer 0..{len(candidates) - 1}>}}'下一篇是“6. 完整源代码与使用的模型”。
COMMENTS · CHAPTER 05
位置由代码计算,LLM 只负责选择 评论 0
还没有评论。阅读时有疑问或想补充的内容,欢迎留言。
发表评论前,请用 Google 开始并登记昵称和邮箱。
用 Google 开始