训练设置

  • 算法:stable-baselines3 DQN。学习率 1e-4,回放缓冲区 10 万,批大小 32,每 4 步训练一次,目标网络每 1 万步更新一次。
  • 探索:在最初 20% 的时间里,把随机比例从 1 降到 0.02。
  • 策略网络:由于观测值是 0~7,SB3 不把它当作图像,而是选择 MlpPolicy。棋盘的 220 格被展平成一行输入。

奖励

每次方块固定时计算。

  • 消行:1 行 1 分,2 行 3 分,3 行 10 分,4 行 30 分
  • 放得越低,加得越多一点(落地高度×0.1)
  • 每新产生一个空洞 -5
  • 堆叠高度每上升一格 -2
  • 减去凹凸度(相邻列高度差之和)×0.02 和堆叠高度×0.03

结果

留下来的低层模型有两个:30 万步的模型(live_latest.zip.bak),以及在它基础上继续训练的 37 万步模型(live_latest.zip)。继续训练原本以 62 万步为目标,却在 37 万步时停了下来。

我们在同一个引擎上用种子 0~19 各跑了 20 局。模型每次都选择价值最高的动作。

  • 随机 7 个动作:平均 17 个方块,0 行
  • DQN 30 万步:平均 22 个方块,20 局共 2 行
  • DQN 37 万步:平均 20 个方块,0 行
37 万步 DQN 与随机的最终棋盘
37 万步 DQN 与随机的最终棋盘

为什么没学会

  • 几乎见不到消行奖励。随机乱按的话,20 局下来一行都消不掉。要消一行,得把好几个方块拼好,而这在探索过程中很少发生。进入训练的信号几乎只有空洞和高度的惩罚。
  • 一个方块要用 20 多个动作。位置好不好要到方块固定的瞬间才知道,很难分清之前的移动和旋转中哪一个做得好。
  • 由一个小型 MLP 来看展平后的 220 个数字。它很难读懂方块形状和位置关系,也不知道下一个方块是什么。

所以我们把动作减少到每个方块一次,并让 VLM 来辅助奖励。

这一阶段的实际代码

这是方块固定时引擎计算的奖励。

tetris/src/tetris_engine.cppcpp · 41 行
// Line clear reward (non-linear scaling)
double line_r = 0.0;
switch (cleared) {
    case 1: line_r = cfg_.line_reward_1; break;
    case 2: line_r = cfg_.line_reward_2; break;
    case 3: line_r = cfg_.line_reward_3; break;
    case 4: line_r = cfg_.line_reward_4; break;
    default: break;
}

// Height reward: lower placement = higher reward
double height_r = cfg_.height_reward_scale * landing_y;

const int holes_after = count_holes();
const int new_holes = holes_after - holes_before;
double hole_penalty = 0.0;
if (new_holes > 0) {
    hole_penalty = cfg_.hole_penalty_scale * static_cast<double>(new_holes);
}

// Stack height penalty: penalize increases of the board height (after line clears)
double height_penalty = 0.0;
if (cfg_.stack_height_penalty_scale > 0.0) {
    const int height_after = stack_height();
    const int dh = height_after - height_before;
    if (dh > 0) {
        height_penalty = cfg_.stack_height_penalty_scale * static_cast<double>(dh);
    }
}

// Surface shaping penalties (post line-clear)
double bump_penalty = 0.0;
if (cfg_.bumpiness_penalty_scale > 0.0) {
    bump_penalty = cfg_.bumpiness_penalty_scale * static_cast<double>(bumpiness());
}
double max_height_penalty = 0.0;
if (cfg_.max_height_penalty_scale > 0.0) {
    max_height_penalty = cfg_.max_height_penalty_scale * static_cast<double>(stack_height());
}

last_reward_ += line_r + height_r - hole_penalty - height_penalty - bump_penalty - max_height_penalty;

这是奖励大小和 DQN 设置。

config/rl_config.pypython · 18 行
    # ── 보상/패널티 스케일 (C++ 엔진으로 전달) ───────────────────────────────
    # 한쪽으로 몰빵되는 탑쌓기를 억제하려면 아래 2개를 켜는 게 효과적
    stack_height_penalty_scale: float = 2.0   # 스택 높이 "증가분" 패널티 (권장 0.5~5.0)
    # bumpiness는 값 범위가 커서(최대 수백) 스케일을 너무 크게 주면 보상이 붕괴합니다.
    bumpiness_penalty_scale: float = 0.02     # 인접 열 높이 차이 합 패널티 (권장 0.005~0.05)
    max_height_penalty_scale: float = 0.03    # 절대 스택 높이 패널티 (권장 0.0~0.1)


@dataclass(frozen=True)
class DQNConfig:
    timesteps: int = 300_000
    learning_rate: float = 1e-4
    buffer_size: int = 100_000
    batch_size: int = 32
    train_freq: int = 4
    target_update_interval: int = 10_000
    exploration_fraction: float = 0.2
    exploration_final_eps: float = 0.02

下一篇是“3. 每个方块只选一次的宏动作与 VLM 奖励”。