当猜数字游戏遇见飞书机器人

一个人的时候,有时会想写点小东西——不是为了有用,只是为了有趣。昨天晚上,我就在飞书上让 Hermes Agent 给我写了一个猜数字游戏。

玩法

系统随机生成一个四位不重复数字(0-9),你来猜。每次猜测,系统会告诉你两个数字:

  • A = 数字正确且位置正确
  • B = 数字正确但位置不对

比如秘密数是 1284,你猜 1234,结果是 2A2B——两个数字(1 和 4)位置正确,两个数字(2 和 3)数字对了但位置不对。

直到猜出 4A0B,游戏结束。

从终端到飞书

一开始它写的是一个终端交互版——用 input() 接收猜测,在终端里 print() 结果。这当然没问题,但我想在飞书聊天里直接玩,不需要 SSH 进去。

所以改了一版:把游戏逻辑模块化,用 JSON 文件持久化状态(秘密数和尝试次数)。这样每次你在飞书发一条消息,Hermes 就调一次 Python 脚本,读状态、做判断、写回状态、回复结果。整个过程对玩家来说是无感的,就像在和一个真人裁判对话。

源码

完整的游戏脚本,不到 60 行:

#!/usr/bin/env python3
"""猜数字游戏 — 飞书/聊天交互版(文件持久化)"""

import json
import os
import random

STATE_FILE = "/tmp/guess_number_state.json"


def _load_state() -> dict:
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE) as f:
            return json.load(f)
    return {"secret": None, "attempts": 0}


def _save_state(state: dict):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f)


def is_valid(guess: str) -> bool:
    return len(guess) == 4 and guess.isdigit() and len(set(guess)) == 4


def start() -> str:
    secret = "".join(random.sample("0123456789", 4))
    _save_state({"secret": secret, "attempts": 0})
    return (
        "🎯 猜数字游戏开始!\n"
        "规则:我已生成一个四位不重复数字(0-9)。\n"
        "- A = 数字对且位置对\n"
        "- B = 数字对但位置不对\n\n"
        "直接输入你的猜测(4位数字)即可开始!"
    )


def guess(guess_str: str) -> str:
    state = _load_state()
    secret = state.get("secret")

    if secret is None:
        return "游戏还没开始!请说「开始游戏」"

    if not is_valid(guess_str):
        return "输入错误!请输入4位不重复数字(0-9)。例如:1234"

    a = sum(g == s for g, s in zip(guess_str, secret))
    b = sum(g in secret for g in guess_str) - a
    state["attempts"] += 1

    if a == 4:
        msg = (
            f"🎉 恭喜!猜对了!\n"
            f"答案:{secret}\n"
            f"总共猜了 {state['attempts']} 次!\n"
            f"说「再来一局」重新开始"
        )
        _save_state({"secret": None, "attempts": 0})
        return msg

    _save_state(state)
    return f"结果:{a}A{b}B(第 {state['attempts']} 次)"

核心逻辑其实只有 evaluate 那一小段——sum(g == s ...) 算 A,sum(g in secret ...) - a 算 B。干净利落。

实战:1284 的四次对峙

游戏开始了。我并不知道秘密数是什么,只能靠每一次的反馈去逼近。

第一猜:4637

0A1B

四个数字只有一个存在,而且位置也不对。信息量很少,但至少排除了三个数字。

第二猜:1289

3A0B

这一下子从迷雾中走了出来。三个数字完全正确,且位置也对。唯一的遗憾是最后一位猜错了——但这也说明,那三个数字(1、2、8)已经锁定了。只是最后一位不是 9。

接下来就是一场耐心的排除。剩下那位,只可能从 {0,3,4,5,6,7} 里选。

第三猜:1287

3A0B

不是 7。

第四猜:1283

3A0B

不是 3。

第五猜:1284

🎉 恭喜!猜对了!答案:1284,总共猜了 5 次!

那一刻的感觉,就像终于拨开最后一层迷雾,看到了山顶的风景。五次,不多不少。从第二次猜中前三位开始,后面三次不过是在逐一验证那最后一个数字——用最小的代价,完成了最后的确认。

一点感想

猜数字的魅力就在于,它既需要逻辑推理,又保留了一点运气成分。你永远无法一步到位,但每一步都在缩小答案的可能空间。这种「越猜越近」的感觉,和调试代码时的二分查找、逐步缩小 bug 范围,本质上是同一种思维方式。

而把它搬到飞书机器人上之后,又多了一层趣味——像是多了一个随时陪你玩游戏的朋友。

如果你也想玩,说一句「开始游戏」就行。😄