【作品】联机游戏大厅:五子棋、中国象棋、反转棋(附源码下载)

阅读约 9 分钟
【作品】联机游戏大厅:五子棋、中国象棋、反转棋(附源码下载)

这是我刚交付给客户的一个小项目:网页版游戏大厅。打开链接,选一个游戏,开一个房间把房间号发给朋友,两个人就能对下;一个人也行,可以直接挑战电脑。

现在有三个游戏:五子棋、中国象棋、反转棋(黑白棋)。客户同意公开,所以我把代码整理了一份放在文末,可以下载自己跑,也可以拿去改。

游戏大厅:选一个游戏

图:大厅首页。

客户已经上线了:oyeyeyouxi.com。那个站设了进入密码,外人只能看到登录页,所以下面的截图都是我在本机跑源码截的。

它能做什么

  • 联机对战:创建房间会得到一个 4 位房间号,发给对方输入就能加入。房间可以设成公开(出现在大厅的“等你来的房间”列表里)或者仅限邀请。
  • 快速匹配:不想找人的时候点一下,会先加入正在等人的同类房间,没有就排队等下一个。
  • 人机对战:四档难度(入门 / 普通 / 困难 / 大师),可以选执黑还是执红、先手还是后手。
  • 悔棋、求和、认输:联机时每人每局最多悔棋 3 次、求和 3 次;人机对战每局可悔棋 5 次。
  • 每步限时和断线处理:五子棋和反转棋每步 60 秒,中国象棋 120 秒,超时判负(这几个数都能在 .env 里改);对局中掉线默认等 60 秒,不回来判负;回来之后能接着下。
  • 快捷聊天:九句固定的话(“你好呀”“好棋!”“再来一局?”这类),另外可以打字,限 40 个字、每分钟 20 条。
  • 三个游戏的规则:五子棋棋盘 13~20 路可选,自由规则(五连及以上获胜,黑先);中国象棋是标准走法,会提示将军,被将死或无子可动判输,连续 120 步没有吃子判和;反转棋 8×8,一方没地方下会自动跳过。

进到一个游戏里:创建、加入、快速匹配、人机对战

图:五子棋的房间页,下面是正在等人的公开房间。

五子棋人机对战

图:五子棋,我执黑,白棋已经连成四个。

中国象棋人机对战

图:中国象棋,最后一步的起点和终点有标记。

反转棋人机对战,小圆点是可以落子的位置

图:反转棋,小圆点提示可以下的位置。

下载和运行

源码包:game-lobby.zip,约 80KB,解压后是这样:

text
game-lobby/
├── server.py          # 服务端:网页 + WebSocket + 规则
├── index.html         # 游戏页面(含 AI,单文件)
├── login.html         # 密码登录页
├── gomoku.sh          # 后台运行脚本(带菜单)
├── nginx-gomoku.conf  # 反向代理示例
├── .env.example       # 配置模板
├── README.md
└── LICENSE            # MIT

不用 pip install 任何东西,需要 Python 3.7 以上(用到了 asyncio.run):

bash
cd game-lobby
python3 server.py

然后浏览器打开 http://localhost:8765。这样默认没有密码,只监听本机,自己试玩够用。

要后台长期跑、加进入密码:

bash
cp .env.example .env
chmod 600 .env        # 然后编辑 .env,填 GOMOKU_PASSWORD
./gomoku.sh           # 不带参数会出现菜单

我自己在本机用解压出来的这份包启动过,页面能正常打开、/healthz 返回 ok。包里不含我的 .env(里面有真实密码),nginx 配置里的域名和证书路径也换成了占位符。

结构:为什么只有两个主要文件

整个项目的思路和官网的表单推送一样:能不装的东西都不装。

  • 没有框架、没有数据库。 服务端只用标准库的 asyncio,房间、对局、封禁记录全放内存里。
  • 网页和 WebSocket 同一个进程、同一个端口。 GET / 发页面,/ws 走对局,nginx 一层反代就行。
  • 前端是一个 HTML 文件。 样式、脚本、三个游戏的 AI 都在里面,没有构建步骤,改完刷新就能看。

代价也很明显,放到后面“局限”里说。

几个值得讲的设计

1. 所有规则在服务端判,页面只说“我想下在这”

页面发给服务器的只有一句话:“我想下在 (x, y)”。落子合不合法、轮到谁、有没有赢、有没有超时,全部由 server.py 决定,再把结果广播给两个玩家。

这样做的好处是:就算有人改了页面脚本,也没法作弊——最多只是发一个服务端不认的走法,服务端直接拒绝。AI 例外:人机对战在浏览器里跑,因为对手是电脑,没有“防作弊”的必要,还能省服务器。

2. 手写 WebSocket,没有想象中复杂

用标准库没有现成的 WebSocket,所以握手和帧都是手写的。握手只需要算一个值:

python
key = hd.get('sec-websocket-key', '')
accept = base64.b64encode(hashlib.sha1((key + GUID).encode()).digest()).decode()

帧的解析要处理三件事:客户端发来的帧必须带掩码、消息可能被分片、长度有三种编码(7 位 / 16 位 / 64 位)。我加了两条保护:单条消息最大 4096 字节,每条连接用令牌桶限速(突发 20 条,每秒回 10 条)。服务端每 20 秒发一次心跳,60 秒没收到任何帧就当掉线。

写这个不难,但有一个坑要记住:nginx 反代 WebSocket 必须带 Upgrade 和 Connection 头,不然网页能开、对局连不上。示例配置里已经写好了。

3. 进入密码,而且“没登录就不下发游戏代码”

客户要求只让知道密码的人进。我做成:

  • 没登录时,访问 / 只返回 login.html,里面没有任何游戏代码;
  • POST /login 输对密码后,发一个 HttpOnly、SameSite=Strict 的登录 cookie(走 HTTPS 时加 Secure),之后才下发游戏页;
  • WebSocket 连接也靠这个 cookie 放行,没登录会收到“需要登录”然后被断开;
  • 密码比较用 hmac.compare_digest,每次输错还会让接口多等 1 秒。

连续输错会封 IP:默认输错 5 次封 1 小时,再次被封时长翻倍,最长一天(GOMOKU_MAX_FAIL、GOMOKU_BAN_SEC 可调)。gomoku.sh 菜单里能看到被封的 IP、手动解封,不用重启,也不影响正在进行的对局。这个管理接口只接受“本机直连”的请求,经过 nginx 的一律 404,公网访问不到。

这是一个共享密码,不是账号系统:知道密码的人都能进,适合“发给熟人朋友”的场景。

4. 其他防刷

这是公网服务,所以多叠了几层,都写在代码或配置里:

位置限制
服务端每个 IP 最多 20 条连接、每分钟最多新建 30 条 WebSocket、每分钟最多 120 次网页请求
服务端每个 IP 最多开 10 个房间,全站最多 1000 个房间
服务端WebSocket 握手时核对 Origin,拒绝别的网站的页面来连
服务端打字聊天去掉控制字符和看不见的字符(包括会改变文字方向的),昵称去掉 < >
nginxlimit_req 按 IP 限速:登录每分钟 10 次、页面 60 次、WebSocket 30 次;limit_conn 限并发

有一点要提醒:服务端按 X-Real-IP 统计真实 IP,只信任来自本机(nginx)的这个头。如果你直接把服务端口开到公网,别人可以伪造它。所以默认只监听 127.0.0.1,要对外一定前面放 nginx。

5. 中国象棋的规则写了两份,用走法计数互相校验

象棋规则最容易出错:蹩马腿、塞象眼、炮隔子打、将帅不能对面、送将不合法……

服务端要判定,页面又要显示可走的位置、给 AI 用,所以我把规则写了两份(Python 和 JavaScript),并且用 perft(走法计数) 互相校验:从开局出发,数一数走 N 步一共有多少种合法走法,两边必须一致。

我在本机把两份都跑了一遍:开局走 1、2、3 步,Python 和 JavaScript 的结果都是 44、1920、79666,两份一致,也和公开的象棋 perft 参考数值一致。

6. AI 放在浏览器的 Web Worker 里

AI 在 Web Worker 里计算,不卡页面,也不占服务器。三个游戏各有一套:

  • 五子棋:按五格窗口给局面打分,用负极大值搜索,再加“连续冲四”的必杀搜索(VCF)。“入门”和“普通”是在候选点里按权重随机挑,所以会漏招;“困难”最深 6 层、限时 1.5 秒,“大师”最深 24 层、限时 4.5 秒。
  • 中国象棋:迭代加深的 alpha-beta 搜索,加置换表、静态搜索、杀手走法和历史启发。“入门”只看一步再加随机扰动,“普通”看两步,“困难”最深 7 层、限时 1.5 秒,“大师”最深 20 层、限时 4.5 秒。
  • 反转棋:负极大值搜索,处理“一方没地方下要跳过”的情况。

各档的搜索深度、时间都集中写在代码里的 LEVELS 表里,想调强弱改那里就行。

部署到线上

反代的核心几段(完整的在 nginx-gomoku.conf):

nginx
# 对局用的 WebSocket:必须带 Upgrade / Connection 头
location /ws {
    limit_req zone=gomoku_ws burst=20 nodelay;
    limit_conn gomoku_conn 20;
    proxy_pass http://127.0.0.1:8765;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $http_host;          # 服务端用它核对 Origin
    proxy_set_header X-Real-IP $remote_addr;   # 没有这一行,所有玩家会被当成同一个 IP
    proxy_read_timeout 3600s;                  # 对局可能很久
    proxy_buffering off;
}

步骤:

  1. 把整个文件夹传到服务器,比如 /opt/game-lobby;
  2. cp .env.example .env,设密码,BIND=127.0.0.1、PORT=8765;
  3. 把 nginx-gomoku.conf 里的 game.example.com 换成你的域名,证书路径换成你的,放进 /etc/nginx/conf.d/;
  4. nginx -t && nginx -s reload,DNS 把域名指到服务器;
  5. ./gomoku.sh start。

如果前面还有 Cloudflare 这类代理,要确认它开着 WebSocket(大多数默认就开了),并且服务端拿到的“真实 IP”是访客而不是代理的地址,否则限流和封 IP 会误伤一片。这一点我没在你的环境里测,配完请自己试。

局限,用之前先知道

  1. 状态都在内存里。 重启服务,进行中的对局和封禁记录会清空;登录 cookie 也会失效,玩家要重新输一次密码。
  2. 只有一个进程。 适合朋友圈、小社群这种规模的小场子,不适合做成大平台。
  3. 没有账号、没有战绩、没有排行榜。 昵称是自己取的,不保证唯一。
  4. 共享密码不是账号。 想按人管理,要自己加账号体系。
  5. 页面里的品牌名“哦耶耶游戏”和底部“快建工作室”字样是原项目的,拿去用请自己换掉(index.html、login.html)。
  6. 我没有在多台设备、不同网络、长时间挂机的条件下做过完整的压力测试;上线前自己多开几个浏览器对下一局最稳妥。

想要一个自己的游戏大厅?

这类小而完整的网页项目正是我平时做的事:你说想要哪几个游戏、要不要密码、挂在哪个域名下,我来做、部署、交付源码。想聊聊的话,来官网留言。

出处

想要类似的东西?网站、Telegram 机器人、运维工具,说说你的需求。
发布需求

有想法或问题?去留言板留言 →