初始化仓库:友乐/gameabc 房卡游戏平台脚手架(client + server + docs)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
# 01 · 服务端环境与框架基础
|
||||
|
||||
本篇建立**全局认知**:子游戏运行在什么环境里、平台框架由哪些对象构成、一个数据包如何被路由到你的代码、一个房间从生到死经历哪些阶段。读懂这一篇,后面的接入与协议才有坐标。
|
||||
|
||||
> 举例以麻将类房卡游戏为主,但本篇所有机制对任意子游戏一致。
|
||||
|
||||
> **命名说明**:代码示例里的类/文件/变量名(如 `GameStateManager` 等)均为**示例、可自定**;只有平台接缝上的名字(包字段、`export`/`import` 钩子名、平台 API 如 `cls_mod.new`/`min_loadJsFile`/`sendpack_toseat`)是契约。详见 [README「命名约定」](./README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 运行环境:一套代码,两个运行时
|
||||
|
||||
子游戏代码**同时运行在两套环境**,写每一行都要同时考虑:
|
||||
|
||||
| 运行时 | 场景 | 模块加载方式 | 是否有 `require` |
|
||||
|--------|------|--------------|------------------|
|
||||
| **Node.js** | 本地、单元测试 | `require()` | 有 |
|
||||
| **浏览器 / 友乐平台** | 线上部署 | 由 `mod.js` 用 `min_loadJsFile` 统一加载为**全局对象** | **没有** |
|
||||
|
||||
由此引出两条硬约束(详见 04):
|
||||
|
||||
1. **严格 ES5**:用 `var`/`function`,禁止 `let`/`const`/箭头函数/模板串/`class`/`Promise` 等;线上浏览器/友乐运行时不保证 ES6+。
|
||||
2. **`require` 只能在文件开头的守卫块内**:
|
||||
|
||||
```js
|
||||
// ✅ 正确:集中在顶部守卫块;运行时按全局名引用
|
||||
if (typeof require !== 'undefined') {
|
||||
var GameStateManager = require('./dataStructures/GameStateManager.js');
|
||||
}
|
||||
// ... 之后直接用全局名 GameStateManager.xxx() ——浏览器由 mod.js 加载为同名全局
|
||||
```
|
||||
|
||||
浏览器运行时没有 `require`,任何**无守卫的、函数体内的中途 `require`** 都会抛 `ReferenceError: require is not defined`,导致功能崩溃。跨模块统一**按全局名引用**:Node 由守卫块 `var X = require(...)` 得到,浏览器由 `mod.js` 加载的同名全局解析,二者靠共享全局作用域下的同名 `var` 统一。
|
||||
|
||||
### 前后端是物理分离的两端
|
||||
|
||||
- 客户端跑在用户浏览器,服务端跑在 Node。两端**不是函数调用,而是 WebSocket/HTTP 收发 JSON 包**,全程异步。
|
||||
- 前端代码不走 npm 构建,**前后端共享代码靠文件复制同步**(见 04 的 shared 同步流程)。
|
||||
- **服务器权威**:凡房卡、胜负、积分、房间状态等关键数据/操作,一律以服务器为准,前端只负责显示,不能"前端说什么就是什么"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 平台代码 vs 子游戏代码
|
||||
|
||||
```
|
||||
server/ ← 平台框架(除子游戏容器目录外,全部禁改)
|
||||
├── packet.js 总收发包入口、应用列表、最终 SendPack
|
||||
├── applist.js 加载各应用(server / youle / update)
|
||||
├── class/ 框架基础类
|
||||
│ ├── class.app.js 应用基类 cls_app(按 route 路由到模块)
|
||||
│ ├── class.mod.js 模块基类 cls_mod(按 rpc 路由到方法)
|
||||
│ └── class.desk.js / ... 桌、牌等基础类
|
||||
├── youle/ 友乐应用(appname = "youle")
|
||||
│ ├── app.js 创建 youle_app
|
||||
│ └── server_room/ 房间模块 youle_room(routename = "room")
|
||||
│ ├── class.room.js 房间对象 o_room(含 seatlist、发包方法)
|
||||
│ ├── class.player.js 座位/玩家对象(含 conmode/fromid)
|
||||
│ ├── class.export.js 框架对外服务:check_player / deduct_roomcard / save_grade ...
|
||||
│ └── class.import.js 框架回调子游戏:makewar_deskwar / get_disbandRoom / player_* ...
|
||||
└── <游戏容器目录>/ ← 子游戏容器(目录名由接入方自定,非框架强制)
|
||||
└── <你的游戏>/ ← 子游戏(唯一可编辑目录)
|
||||
├── mod.js 模块入口:创建模块、加载文件、定义 RPC 方法
|
||||
├── export.js 子游戏对平台暴露的一组回调接口(按需实现,见 02)
|
||||
├── import.js 子游戏对平台服务的封装接口(本项目 4 个)
|
||||
└── ... 你的玩法业务代码
|
||||
```
|
||||
|
||||
> **容器目录名不是框架约定**:容器目录名由接入方自定。框架**不按目录名找子游戏**——运行时由 `youle_room.app[o_room.o_game.modename]` **按模块名**定位(见 §5)。接入一个新游戏只需两步:① 在 `server/youle/app.js` 里加一行 `min_loadJsFile("<容器目录>/<你的游戏>/mod.js", ...)` 加载它;② 在 `mod.js` 里用唯一的 `modname/routename` 注册。因此容器目录换成任何不与框架冲突的名字都可以(`app.js` 是唯一必须触碰的平台文件,属游戏注册接入点)。
|
||||
|
||||
**一句话**:平台提供"网络 + 房间 + 玩家 + 房卡 + 战绩"的地基,子游戏只往自己的目录里填"玩法"。两边的接缝就是 **export / import**(详见 02)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心对象模型
|
||||
|
||||
理解五个对象的层级关系,就理解了框架的数据骨架:
|
||||
|
||||
```
|
||||
app (youle_app) 一个应用,appname = "youle"
|
||||
└── modlist[] 应用下挂的所有模块
|
||||
├── youle_room 平台房间模块,routename = "room"
|
||||
└── mod_<你的游戏> 你的子游戏模块,routename = "<你的游戏>"
|
||||
|
||||
o_room 房间对象(平台创建,一桌一个)
|
||||
├── roomcode / roomtype / asetcount / battlestate ... 平台基础数据
|
||||
├── seatlist[] 座位列表,长度=满桌人数
|
||||
│ └── seatlist[seat] = o_player 座位上的玩家对象
|
||||
│ ├── playerid / nickname / onstate
|
||||
│ └── conmode / fromid 连接方式与连接ID(发包要用)
|
||||
├── method.sendpack_toseat(msg, seat) 平台发包方法
|
||||
├── method.sendpack_toother(msg, seat)
|
||||
└── o_desk ★ 子游戏的牌桌对象(由你在 makewar 里创建并挂上)
|
||||
├── o_room 反向引用回房间
|
||||
└── data.* ★ 你的对局状态全挂这里(房间隔离的落点)
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- **`o_room` 由平台创建并持有**;`seatlist[seat]` 是玩家对象,发包所需的 `conmode`/`fromid` 就在它上面。
|
||||
- **`o_desk` 由子游戏创建**(在 `makewar` 中),并与 `o_room` 建立双向引用:`o_room.o_desk = desk; desk.o_room = o_room;`。
|
||||
- **对局状态一律挂在 `o_room.o_desk.data.*`**。这是"房间隔离"的物理落点:每桌一份,互不串扰(见 04)。
|
||||
|
||||
> 举例:麻将把游戏状态放在 `o_room.o_desk.data.roomAdapter.gameState`,把操作队列放在 `o_room.o_desk.data.operationQueue`。换成斗地主、跑得快也是同一套落点,只是 `data.*` 下的字段不同。
|
||||
|
||||
---
|
||||
|
||||
## 4. 三层路由:一个包如何到达你的函数
|
||||
|
||||
平台把客户端发来的包,按 `app → route → rpc` 三级精确投递到子游戏的某个函数。
|
||||
|
||||
```
|
||||
{ app: "youle", route: "<你的游戏>", rpc: "playCard", data: { ... } }
|
||||
```
|
||||
|
||||
| 级 | 在哪 | 依据 | 动作 |
|
||||
|----|------|------|------|
|
||||
| 1 | `packet.js` `packet_face.ReceivePack` | `pack.app` | 在 `applist` 里找到 `appname == pack.app` 的应用,调 `app.ReceivePack(pack)` |
|
||||
| 2 | `class/class.app.js` `cls_app.ReceivePack` | `pack.route` | 在 `app.modlist` 里找到 `routename == pack.route` 的模块,调 `mod.DoPack(pack)` |
|
||||
| 3 | `class/class.mod.js` `cls_mod.DoPack` | `pack.rpc` | 若 `mod[pack.rpc]` 存在,调用 `mod[pack.rpc](pack)` |
|
||||
|
||||
证据:
|
||||
- 第 1 级:`server/packet.js` `ReceivePack` 按 `pack.app == applist[i].appname` 分发。
|
||||
- 第 2 级:`server/class/class.app.js` `ReceivePack` 按 `pack.route == modlist[i].routename` 调 `DoPack`。
|
||||
- 第 3 级:`server/class/class.mod.js` `DoPack` 按 `_msg.rpc` 调 `_obj_mod[_msg.rpc](_msg)`。
|
||||
|
||||
**对开发者的含义**:你在 `mod.js` 里写下 `mod_<游戏>.playCard = function(pack){...}`,前端只要发 `{route:"<你的游戏>", rpc:"playCard", ...}`,框架就会自动调到它——**无需自己写路由分发**,更不要在一个总入口里用 `switch(action)` 做二次路由(一个操作对应一个 RPC 方法)。
|
||||
|
||||
### ⚠️ 关于 `DoPack` 的返回值(重要)
|
||||
|
||||
`cls_app.ReceivePack` 在拿到 `DoPack` 的返回值后,确实会执行一次 `app.SendPack(repack)`。但这个回发:
|
||||
|
||||
- 只回给**当前请求的那条连接**,且不经过"按座位定向"的处理;
|
||||
- 在浏览器/友乐链路下**不是子游戏向前端推送状态的可靠通道**。
|
||||
|
||||
因此本项目的铁律是:**子游戏 RPC handler 一律通过 `o_room.method.sendpack_toseat / sendpack_toother` 主动推送**来下发结果,**不依赖 `return`**。这条直接推导出 03 的"成败标志必须放在主动推送的 `data.success` 里"。
|
||||
|
||||
---
|
||||
|
||||
## 5. 模块注册与全局暴露
|
||||
|
||||
子游戏模块用框架基类创建,并自动注册进应用:
|
||||
|
||||
```js
|
||||
// cls_mod.new(模块名, 路由名, 所属应用)
|
||||
var mod_<你的游戏> = mod_<你的游戏> || cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app);
|
||||
```
|
||||
|
||||
`cls_mod.new` 做了三件事:把模块 push 进 `app.modlist`、在 `app` 上挂 `app[模块名]=mod`、并通过 `OutputMod` 把模块**暴露为全局名**(`global[modname]`)。这套"双运行时全局暴露"是浏览器/友乐能按全局名找到模块的基础。
|
||||
|
||||
> 双运行时陷阱:凡"构造函数 + 单例实例"式模块,`module.exports` 之后必须**无条件**把全局名暴露出去(不要塞进 `else` 分支),否则线上浏览器拿不到该全局,报 `xxx is not a function`,而 Node 测试却测不出来。
|
||||
|
||||
`export.js` / `import.js` 在各自文件末尾把接口对象挂到 `mod.export` / `mod.import`,`mod.js` 只负责按顺序加载它们(见 02)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 房间生命周期
|
||||
|
||||
一个房间从创建到回收,平台与子游戏分工如下。**理解每个阶段"谁调用谁",是接入的关键**:
|
||||
|
||||
```
|
||||
创建房间
|
||||
平台调 export.get_needroomcard / get_asetcount / get_needroomcard_joinroom
|
||||
→ 决定房卡与总局数(此时还没有 o_desk,子游戏不需要知道谁坐在哪)
|
||||
|
||||
满桌/房主开战
|
||||
平台调 export.makewar(o_room, o_game_config)
|
||||
→ 子游戏创建 o_desk、初始化对局状态、返回开战数据包
|
||||
→ 平台把开战包下发所有客户端(对应前端 StartWar)
|
||||
→ o_room.battlestate = 1
|
||||
|
||||
对局进行中
|
||||
客户端发 RPC → mod[rpc](pack) → 子游戏处理 → 主动推送结果
|
||||
|
||||
断线重连 / 中途加入
|
||||
平台调 export.get_deskinfo(o_room, seat)
|
||||
→ 子游戏返回该座位「当前完整快照」(对应前端 Reconnect)
|
||||
|
||||
每小局结算
|
||||
第一小局结算时:子游戏调 import.deduct_roomcard(o_room) 扣房卡(仅此一次)
|
||||
|
||||
大局(整场)结束
|
||||
子游戏算完最终战绩后调 import.save_grade(o_room, ...)
|
||||
→ 平台保存战绩并「自动释放房间」,子游戏无需再回收房间本身
|
||||
|
||||
解散房间
|
||||
平台调 export.get_disbandRoom(o_room) 取解散数据包下发
|
||||
|
||||
玩家中途进出
|
||||
平台调 export.player_enter / player_leave 通知子游戏处理
|
||||
```
|
||||
|
||||
**子游戏自己创建的东西,自己负责回收**:定时器、托管/决策状态、各类缓存等,必须能按房间定位,并在小局结束、解散、开新局时清理干净,禁止泄漏到下一局或别的房间(见 04 的房间隔离)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 小结
|
||||
|
||||
- 一套代码两个运行时 → 强制 ES5 + 守卫块 require。
|
||||
- 对象层级:`app → mod`、`o_room → seatlist[seat](o_player) / o_desk(.data.*)`。
|
||||
- 三层路由:`app(按app) → route(按route) → rpc(按rpc)`,框架自动投递,你只写 `mod.rpc 方法`。
|
||||
- `DoPack` 的 `return` 不是可靠下发通道 → **一律主动 `sendpack_toseat` 推送**。
|
||||
- 房间生命周期由 export/import 接缝串起:`makewar` 开局、`get_deskinfo` 重连、`deduct_roomcard` 首局扣卡、`save_grade` 终局保存并自动回收。
|
||||
|
||||
下一篇 [02-子游戏接入与开发流程](./02-子游戏接入与开发流程.md) 讲:这三个文件具体怎么写、8+4 个接口各做什么、一次操作的完整数据流。
|
||||
</content>
|
||||
Reference in New Issue
Block a user