玩法
再没有人独自等待:认识一下这些常客
过去开一个房间就意味着独自等到放弃。现在有十二位常客——真实的名字、固定的性格、像人一样的节奏——会来把空位坐满。

通俗版
在这次更新之前,从来没有谁会加入你自己开的房间。你选好模式、开一张桌子,然后等待——而且多半是一个人等。更新前六周的数据很直白:
| 我们统计的内容 | 结果 |
|---|---|
| 玩家开了却从未开局的房间 | 38 个 |
| 放弃前的平均等待时间 | 约 3.2 小时 |
| 一个已坐满 4/4 却仍未开局的房间 | 等了 2 天 |
| 有一名以上真人参与的已结束对局 | 231 局中的 46 局 |
现在桌子会自己坐满。在你开房后的二十秒到四分钟之间,就会有人坐下。十二位常客轮流出现在大厅里——用的是普通昵称,而不是"机器男爵"之类——而且每次遇到的都是同一个人:
- 固定的性格:同一个对手总是打得谨慎,或者总是把价叫得太高;
- 一个不是今早才编出来的注册日期和经验等级;
- 属于自己的节奏——思考一到六秒,而不是像节拍器一样固定两秒;
- 偶尔在聊天里说一句短话,而不是只发贴纸;
- 等待室里不再有"bot"标签,因为在牌桌上他们的表现就像玩家。
最后一个空位比其他位置等得更久——三到八分钟——好让真人还有时间坐进来。而如果所有人都已入座、房间却安静下来,它会在三分钟后自行开局,而不是无限期地悬在那里。
他们也会出现在排行榜上。既然他们要打真实的对局,这样做才诚实——但他们每天能获得的收益有严格上限,因此上升得像一个普通玩家,而不是刷榜。
技术细节
这十二个角色在游戏守护进程启动时以幂等方式创建,并用资料表本就支持的一个取值加以标记——因此整个功能没有任何数据库迁移,也没有改动任何游戏规则。
补位是调度器的一次 tick,而不是每个房间一个定时器。每个延迟都由以房间和座位为种子的随机数推导而来,并锚定在数据库中保存的时间戳上,因此守护进程重启后会沿用完全相同的计划,而不会为已排定的房间重新抽取延迟:
ts
// scheduler.ts — seeded per (room, seat), anchored on a DB timestamp
const rng = createRng(`${game.id}:fill:${seat}`);
const isLastSeat = freeSeats === 1;
const delayMs = isLastSeat
? rng.int(LAST_SEAT_MIN_MS, LAST_SEAT_MAX_MS) // 3-8 min: leave room for a human
: rng.int(SEAT_MIN_MS, SEAT_MAX_MS); // 20 s - 4 min
if (Date.now() - Date.parse(game.created_at) < delayMs) continue;| 参数 | 取值 |
|---|---|
| 补位延迟 | 20 秒 – 4 分钟,按座位取种子 |
| 最后一个座位 | 3 – 8 分钟 |
| 坐满且安静后的自动开局 | 3 分钟 |
| 机器人思考时间 | 1 – 6 秒,按角色缩放 |
| 角色经验值 | 每个 UTC 日最多一次奖励 |
有三个缺陷只有真正跑起来才暴露,而且任何单元测试都不会发现:已坐满的房间被排除在补位的候选查询之外,于是自动开局恰恰看不到它存在的意义所在;思考延迟的判定以对局为键而非以机器人为键,因此某一回合的暂停也会挡住同一个机器人的拍卖出价,直到它被自动跳过自己的出价窗口;而由守护进程开启的对局不会把调度器从空闲退避中唤醒,导致从开局到第一次机器人掷骰之间隔了 11.6 秒,而不是 49 毫秒。
同一次更新还用一个真实接口替换了两个编造的数字:大厅里的"在线人数"此前统计的是缓冲区里的聊天消息条数,而首页的"进行中的对局"则是 8 到 19 之间的随机游走。
