二七王:前端子项目 A(地基与逻辑层)设计定案
前端拆为 A→F 六个子项目,本文定案 A:目录结构、常量层全量、 core 纯逻辑、codes/ui 布局求解器、shared 抽取与同步、单测框架、接入骨架。 两处取舍已定:求解器归子游戏 codes/ui;排序算法走服务端 shared/ 同源 (抽 7 个牌值编码与牌型分组函数,arith 改挂引用,现有 20 个测试为安全网)。 资源与精灵不阻塞编码——常量文件先行、即建资源的规格书,编辑器后期照建。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,360 @@
|
|||||||
|
# 二七王前端 · 子项目 A:地基与逻辑层 · 设计
|
||||||
|
|
||||||
|
> 日期:2026-08-26 状态:待实施
|
||||||
|
> 权威源:玩法 `server/games/erqiwang/docs/design/design.md`;协议 `server/games/erqiwang/docs/protocol/packet_protocol.md`;
|
||||||
|
> 界面规格 `docs_dev/二七王-UI资源与精灵清单.md`(下称**清单**);前端规范 `docs/client/development-guide/`。
|
||||||
|
> 本文只做「规格 → 代码结构」的落地设计,**不重新定义规则、不修订清单结论**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与定位
|
||||||
|
|
||||||
|
服务端二七王已完成(`class.arith/paiju/desk/export/import/mod` + 20 个测试脚本),协议与玩法文档定稿,清单已把全流程翻译成可执行的界面规格。前端尚未开工:`client/js/01_SubGame/` 只有三个平台转发壳,`codes/` 目录不存在。
|
||||||
|
|
||||||
|
整个前端按依赖顺序拆为 6 个子项目,各自独立走「设计 → 计划 → 实现 → 提交」:
|
||||||
|
|
||||||
|
| # | 子项目 | 内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| **A** | **地基与逻辑层(本文)** | 目录结构、常量层全量、纯逻辑核心、布局求解器、`shared/` 抽取与同步、单测框架、平台接入骨架 |
|
||||||
|
| B | 网络与数据镜像 | 发包封装、rpc 分发、`GameState` 服务端快照镜像、重连 `deskinfo` 复用同一条重画路径 |
|
||||||
|
| C | 牌桌主界面 | 顶部信息条、玩家位标记、底栏、手牌区、底牌区、三家已出牌区 |
|
||||||
|
| D | 阶段操作界面 | 叫分面板、选主/投降、埋牌条、出牌条、倒计时、提示气泡、状态提示条 |
|
||||||
|
| E | 弹窗与结算 | 小局/大局结算、冲关牌型叠加层、出牌历史、明牌、亮牌、底牌/扣底查看 |
|
||||||
|
| F | 建房与接入收尾 | 建房规则选项区、`SubGameHooks` 全接口、`PLAYER_INFO_LAYOUT` 同步、清单 §7.3 验收 18 条 |
|
||||||
|
|
||||||
|
### 1.1 前置条件的处理方式(已定)
|
||||||
|
|
||||||
|
清单 §7.4 把「美术出图 / 编辑器建精灵」列为开发前置条件。**本项目不采用等待策略**:
|
||||||
|
|
||||||
|
> **资源、精灵、图层、群组、布局、动画参数一律先在常量文件里定义**,代码只引用常量;后期按常量文件的注释在编辑器里手动创建对应资源与精灵。因此编辑器的建设进度**不阻塞任何编码工作**。
|
||||||
|
|
||||||
|
这与清单 §0 前言「代码里引用的 ID 必须与编辑器中实际建成的完全一致」并不冲突——次序倒过来了:**常量文件即建资源的规格书**,编辑器照它建。若建设过程中某个 ID 确需改动,改常量文件一处即可(业务代码零裸值,见 §4)。
|
||||||
|
|
||||||
|
清单 §0.3 已核实号段占用现状,规划的号段(精灵 1001–2999、图层 101–105/302–306、群组 201–250、图片 501+、声音 101+)**段内空闲**,可安全落地。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 本阶段范围
|
||||||
|
|
||||||
|
**交付**:
|
||||||
|
|
||||||
|
1. `codes/` 目录结构
|
||||||
|
2. 常量层全量落地(清单 §3 图片资源、§4 声音、§5 精灵结构、§6 布局配置、动画参数)
|
||||||
|
3. `core/` 纯逻辑模块(牌值编码、排序、标记推导、座位映射、精灵索引)
|
||||||
|
4. `codes/ui/LayoutSolver.js` 五型布局求解器
|
||||||
|
5. 服务端 `shared/cards.js` 抽取 + 前端只读同步副本 + 同步脚本
|
||||||
|
6. `client/tests/` 单测框架与全部用例
|
||||||
|
7. `codes/SubGameHooks.js` 空骨架 + `client/index.html` 加载段
|
||||||
|
|
||||||
|
**不在本阶段**:任何 UI 组件、任何 `SpriteManager` 渲染调用、任何收发包逻辑(B 起)。`LayoutSolver.apply()` 是唯一触碰 `SpriteManager` 的函数,且只有三行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
client/js/01_SubGame/codes/
|
||||||
|
config/ 纯数据:无函数、无副作用、不引用其他模块
|
||||||
|
Layers.js
|
||||||
|
Groups.js
|
||||||
|
ImageResources.js
|
||||||
|
SoundResources.js
|
||||||
|
Sprites_Table.js
|
||||||
|
Sprites_Cards.js
|
||||||
|
Sprites_Action.js
|
||||||
|
Sprites_Result.js
|
||||||
|
Sprites_CreateRoom.js
|
||||||
|
LayoutConstants.js
|
||||||
|
Layout_Table.js
|
||||||
|
Layout_Cards.js
|
||||||
|
Layout_Action.js
|
||||||
|
Layout_Result.js
|
||||||
|
Layout_CreateRoom.js
|
||||||
|
AnimConstants.js
|
||||||
|
core/ 纯逻辑:不碰精灵、无副作用、可 Node 单测
|
||||||
|
CardCodec.js
|
||||||
|
CardOrder.js
|
||||||
|
CardMark.js
|
||||||
|
SeatMap.js
|
||||||
|
SpriteIndex.js
|
||||||
|
shared/ 服务端 shared/ 的只读同步副本(禁止在前端修改)
|
||||||
|
cards.js
|
||||||
|
ui/
|
||||||
|
LayoutSolver.js
|
||||||
|
SubGameHooks.js 本阶段为空骨架
|
||||||
|
client/scripts/
|
||||||
|
sync_shared.cmd / sync_shared.ps1
|
||||||
|
client/tests/
|
||||||
|
_load.js _assert.js run.js test_*.js
|
||||||
|
```
|
||||||
|
|
||||||
|
单向依赖:`config`(无依赖)← `core` ← `ui` ← 后续 `net`/组件层。`shared/` 无依赖,被 `core` 使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 常量层
|
||||||
|
|
||||||
|
遵循 client 02 §2「资源与布局常量三件套」与清单 §6.10 的文件组织。**多文件用保护性声明**(`var EQW_Layout = EQW_Layout || {};`),加载后合并为一份。
|
||||||
|
|
||||||
|
### 4.1 文件与内容来源
|
||||||
|
|
||||||
|
| 文件 | 全局名 | 内容来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Layers.js` | `EQW_Layers` | 清单 §0.3 图层表:`TABLE_STATIC:101`、`HAND:102`、`TABLE_CARDS:103`、`ACTION:104`、`OVERLAY:105`、`POPUP_ASET_RESULT:302`、`POPUP_ACCOUNT:303`、`POPUP_HISTORY:304`、`POPUP_MINGPAI:305` |
|
||||||
|
| `Groups.js` | `EQW_Groups` | 清单 §0.3 群组表 201–250 |
|
||||||
|
| `ImageResources.js` | `EQW_Images` | 清单 §3 图片资源总表,**每条按 ImageResources.template 的要求注明用途 / 帧数 / 每帧含义 / 尺寸** |
|
||||||
|
| `SoundResources.js` | `EQW_Sounds` | 清单 §4——**整节 T-20 未定**,本阶段只建文件骨架与说明注释,不臆造条目(见 §11) |
|
||||||
|
| `Sprites_*.js` | `EQW_Sprites` | 清单 §5.1–§5.8,嵌套结构 `{ LAYER, 群组: { GROUP_ID, SPRITES: { 键名: ID } } }`(照 `GameView_Sprites.template.js`),每个精灵注明类型 / 用途 / 资源键 / 帧说明 |
|
||||||
|
| `LayoutConstants.js` | `EQW_Layout` | 清单 §6.3 `textStyle` 预设、§6.4 `CARD_SIZE` 三档、座位键常量 `SELF/LEFT/RIGHT` |
|
||||||
|
| `Layout_Table.js` | `EQW_Layout` | 清单 §6.5 常驻区 + §6.6 玩家位(含气泡朝向 `arrowRight`) |
|
||||||
|
| `Layout_Cards.js` | `EQW_Layout` | 清单 §6.7 牌区(手牌单排/双排、底牌、埋牌底牌、已出牌 `bySeat`、冲关牌) |
|
||||||
|
| `Layout_Action.js` | `EQW_Layout` | 清单 §6.8 阶段操作区 |
|
||||||
|
| `Layout_Result.js` | `EQW_Layout` | 清单 §6.9 结算区 |
|
||||||
|
| `Layout_CreateRoom.js` | `EQW_Layout` | 清单 §6.9b 建房规则选项 + `EQW_RoomOptions` 选项配置(§1.1 的 categories 数据) |
|
||||||
|
| `AnimConstants.js` | `EQW_Anim` | 发牌铺开时长与每张延迟(§1.2)、`selectedOffsetY`(§6.7)、`floatRise`/`floatDuration`(§6.8)、`toastDuration`(§6.8)、`liangpaiAutoClose`(§1.6)、开底 3 秒(§1.4) |
|
||||||
|
|
||||||
|
### 4.2 硬约束
|
||||||
|
|
||||||
|
- **布局文件是纯数据**:不得出现函数、不得引用其他模块、不得有副作用(client 02 §2、清单 §6.10)。`fan` 的间距算法、`bySeat` 合并、`attach` 换算全在 `LayoutSolver`。
|
||||||
|
- **业务代码零裸值**:精灵 ID / 群组 ID / 图层 ID / 资源 ID / 坐标尺寸 / 动画时长 / 事件名一律来自常量(前端红线)。
|
||||||
|
- **注释即建资源的规格**:图片资源与精灵的注释必须写到「据此就能准确创建」的程度(client 02 §2)。
|
||||||
|
- 数值来自清单,均为参考图 1280×720 目视实测估值(±5px),设计稿到位后只改这一层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. `core/` 纯逻辑层
|
||||||
|
|
||||||
|
全部为无副作用纯函数,不引用 `SpriteManager`、不引用引擎、不读全局状态。
|
||||||
|
|
||||||
|
### 5.1 `CardCodec`
|
||||||
|
|
||||||
|
```js
|
||||||
|
EQW_CardCodec.cardIdToFrame(cardId) // 清单 §0.4 的唯一权威转换,全前端只此一处实现
|
||||||
|
EQW_CardCodec.CARD_BACK_FRAME // 55
|
||||||
|
```
|
||||||
|
|
||||||
|
实现照清单 §0.4:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var n = cardId % 54;
|
||||||
|
if (n === 52) return 53; // 小王
|
||||||
|
if (n === 53) return 54; // 大王
|
||||||
|
return (3 - Math.floor(n / 13)) * 13 + (n % 13) + 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
花色段序与美术帧相反(服务端 1=方块…4=黑桃,美术帧 黑桃→红桃→梅花→方块),是最易静默画错牌的一处,故用清单 §0.4 的 10 条校验样例逐条锁死。
|
||||||
|
|
||||||
|
牌 id 的花色/点数解码**不在此重复实现**,一律调 `shared/cards.js` 的 `id_to_flower` / `id_to_number`(§7)。
|
||||||
|
|
||||||
|
### 5.2 `CardOrder`
|
||||||
|
|
||||||
|
```js
|
||||||
|
EQW_CardOrder.sort(cards, mainflower) // 返回新数组,从大到小,与服务端同序
|
||||||
|
```
|
||||||
|
|
||||||
|
内部直接调 `shared/cards.js` 的 `order_cards`(同源,见 §7),**不在前端另写排序**(清单 T-5)。前端只负责:
|
||||||
|
|
||||||
|
- 传入 `mainflower`:选主前传 `0`(仅固定主牌成主)、选主后传 `flower`
|
||||||
|
- 不修改入参数组(服务端 `order_cards` 原地排序,前端包一层 `concat()`)
|
||||||
|
|
||||||
|
选主后必须整体重排(正 2 / 正 7 升格、主花色普通牌并入主牌段,design §3)——重排由调用方在收到 `xuanzhu` 时触发(B 阶段),本阶段只提供函数。
|
||||||
|
|
||||||
|
### 5.3 `CardMark`
|
||||||
|
|
||||||
|
清单 §1.6 T-8 的三种牌面标记,前端据 `flower` 本地推导,服务端不下发:
|
||||||
|
|
||||||
|
```js
|
||||||
|
EQW_CardMark.markOf(cardId, mainflower, handCards)
|
||||||
|
// → 'tractor' | 'zheng' | 'zhu' | null (三者互斥,每张牌至多一个)
|
||||||
|
EQW_CardMark.marksOfHand(handCards, mainflower) // 批量,返回 { cardId: mark }
|
||||||
|
EQW_CardMark.countByFlower(cards) // 选主面板:{ flower: {count, pairs} }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `'tractor'`(红色 `拖` 圆标):该牌属于一组拖拉机 → 走 `shared` 的 `get_pairlist` + `get_tuolaji_list`
|
||||||
|
- `'zheng'`(橙色五角星):正 2 / 正 7,即选定花色的 2 和 7
|
||||||
|
- `'zhu'`(蓝色五角星):其余主牌(双王、副 2、副 7、主花色普通牌)
|
||||||
|
- 判定优先级:`tractor` > `zheng` > `zhu`(互斥且每张至多一个,见清单 §1.6)
|
||||||
|
|
||||||
|
`countByFlower` 供选主面板的中央大数字(该花色张数)与右上角标(对子数)——协议明确服务端不下发,前端据 `MyCards` 自算(清单 §1.5、协议 `ChooseMain` 注)。
|
||||||
|
|
||||||
|
### 5.4 `SeatMap`
|
||||||
|
|
||||||
|
清单 §0.1 的座位映射,**全前端唯一实现**:
|
||||||
|
|
||||||
|
```js
|
||||||
|
EQW_SeatMap.toDisplay(seat, mySeat) // → 'SELF' | 'RIGHT' | 'LEFT'
|
||||||
|
EQW_SeatMap.toSeat(displayKey, mySeat) // → 服务端座位号
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:`SELF = mySeat`、`RIGHT = (mySeat+1)%3`(下家)、`LEFT = (mySeat+2)%3`(上家)。依据服务端 `class.paiju.js:264` `get_nextseat = (seat+1)%3`。
|
||||||
|
|
||||||
|
### 5.5 `SpriteIndex`
|
||||||
|
|
||||||
|
精灵常量是按图层/群组嵌套的(照模板),而布局配置里 `attach.target` 写的是**键名**(清单 §6.1:「`target` 一律写键名而非数字 ID」)。故需一张扁平索引:
|
||||||
|
|
||||||
|
```js
|
||||||
|
EQW_SpriteIndex.build(spriteTrees) // 遍历嵌套结构 → { 键名: 精灵ID }
|
||||||
|
EQW_SpriteIndex.idOf(key) // 查 ID;查不到显式抛错,不返回 undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
**显式失败**:键名拼错时立即报错(工程总则 §7),而不是把 `undefined` 传给 `SpriteManager` 静默无效。
|
||||||
|
|
||||||
|
同名键在不同群组下重复(如三家结构相同的 `PLAY_*`)由清单 §5 的键名设计避免(`PLAY_SELF_*` / `PLAY_LEFT_*` / `PLAY_RIGHT_*`);`build` 遇到重复键**报错**而非后者覆盖前者。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. `ui/LayoutSolver.js` 布局求解器
|
||||||
|
|
||||||
|
### 6.1 接口
|
||||||
|
|
||||||
|
```js
|
||||||
|
EQW_LayoutSolver.solve(node, ctx) // → [{x, y, width, height}, ...],纯函数
|
||||||
|
EQW_LayoutSolver.apply(node, spriteIds, ctx) // solve 后逐个 SpriteManager.setPosition
|
||||||
|
```
|
||||||
|
|
||||||
|
- `node`:一份清单 §6.1 的布局配置(五型之一,可含 `bySeat`)
|
||||||
|
- `ctx`:
|
||||||
|
- `seat`:`'SELF' | 'LEFT' | 'RIGHT'`,用于 `bySeat` 合并(无 `bySeat` 时可省)
|
||||||
|
- `count`:运行时项数,供 `line` / `fan` / `grid`(清单 §6.1:`count` 由运行时数据给出,不进配置)
|
||||||
|
- `rects`:`{ 键名 → {x, y, width, height} }`,供 `attach` 查目标矩形
|
||||||
|
- 返回恒为**数组**(`point` / `attach` 长度为 1),调用方按索引摆精灵
|
||||||
|
|
||||||
|
`attach` 通过 `ctx.rects` 取目标矩形,而不是反查引擎——求解器因此保持纯函数、可在 Node 里完整单测。目标矩形由调用方提供(来自已解出的布局结果或常量里的固定 `x/y/w/h`)。
|
||||||
|
|
||||||
|
### 6.2 内部流程
|
||||||
|
|
||||||
|
```
|
||||||
|
solve(node, ctx)
|
||||||
|
1. mergeBySeat(node, ctx.seat) bySeat[seat] 覆盖外层 base,未列出的字段继承
|
||||||
|
2. 按 kind 分派:
|
||||||
|
point → 直接取 x/y/w/h
|
||||||
|
line → AlignmentUtils.distribute;有 items 时逐项取宽(itemWidth 失效)
|
||||||
|
fan → 先按 §6.1 公式算 spacing,再走同一个 distribute
|
||||||
|
grid → 逐行调 distributeHorizontally,fillOrder 决定填充次序
|
||||||
|
attach → corner 为空 → alignTo(目标内部对齐)
|
||||||
|
corner 有值 → 对应的 alignSpriteCorner*(贴目标外侧角)
|
||||||
|
3. 返回矩形数组
|
||||||
|
```
|
||||||
|
|
||||||
|
`fan` 的间距算法(清单 §6.1,**唯一实现,不散落**):
|
||||||
|
|
||||||
|
```
|
||||||
|
count <= 1 → spacing 不参与,按 anchor 摆一张
|
||||||
|
otherwise → raw = (maxWidth - itemWidth) / (count - 1) - itemWidth
|
||||||
|
spacing = clamp(raw, spacingMin, spacingMax)
|
||||||
|
```
|
||||||
|
|
||||||
|
`overlapFrom` 决定 z 序方向(`'left'` = 后面的牌盖住前面的;`'right'` 为左上家镜像),求解器**只输出坐标与 z 序次序**,实际 z 序由调用方按返回次序摆精灵。
|
||||||
|
|
||||||
|
### 6.3 参数命名
|
||||||
|
|
||||||
|
一律沿用框架 `AlignmentUtils` 的入参名(清单 §6.0),配置对象可原样喂进框架方法、中间零转换。对齐常量沿用 `AlignmentUtils.ALIGN`。精灵锚点恒在左上角,配置里的 `anchorX/anchorY` 是**对齐基准点**、不是精灵左上角坐标。
|
||||||
|
|
||||||
|
### 6.4 错误处理
|
||||||
|
|
||||||
|
未知 `kind`、缺必填参数、`attach` 的 `target` 在 `ctx.rects` 里查不到 → **显式抛错**(工程总则 §7),不返回 `{x:0,y:0}` 之类的兜底值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. `shared/` 抽取与同步
|
||||||
|
|
||||||
|
### 7.1 动机
|
||||||
|
|
||||||
|
前端排序(`CardOrder`)与手牌标记(`CardMark`)用的算法必须与服务端主牌序**完全一致**(清单 T-5:「对齐服务端主牌序,不另造一套」)。两处各写一份必然漂移,故按工程总则 §1(SSOT)抽为前后端同源的共享算法。
|
||||||
|
|
||||||
|
### 7.2 抽取边界
|
||||||
|
|
||||||
|
这些函数构成闭合依赖链,**不依赖对局状态、不依赖平台 API**:
|
||||||
|
|
||||||
|
```
|
||||||
|
order_cards ─→ id_to_code ─→ id_to_flower
|
||||||
|
└→ id_to_number
|
||||||
|
get_pairlist ─────→ id_to_code
|
||||||
|
get_tuolaji_list ─→ id_to_code
|
||||||
|
└→ is_continuous
|
||||||
|
trump_rank ───────→ id_to_code
|
||||||
|
```
|
||||||
|
|
||||||
|
共 7 个:`id_to_flower`、`id_to_number`、`id_to_code`、`order_cards`、`is_continuous`、`get_pairlist`、`get_tuolaji_list`、`trump_rank`。
|
||||||
|
|
||||||
|
### 7.3 服务端改动
|
||||||
|
|
||||||
|
1. 新建 `server/games/erqiwang/shared/cards.js`,全局名 `youle_erqiwang_shared_cards`,形态照既有文件:`var X = X || { ... }` + 尾部 `if (typeof module !== "undefined"){ module.exports = X; }`。函数内部的相互调用改走 shared 自身引用。
|
||||||
|
2. `class.arith.js` 中这 7 项改为**挂载 shared 的引用**(`id_to_code: youle_erqiwang_shared_cards.id_to_code`),对外 API 与全部既有调用点**零改动**。
|
||||||
|
3. `mod.js` 的 `min_loadJsFile` 序列里 `shared/cards.js` 排在 `class.arith.js` **之前**。
|
||||||
|
4. `test/_shim.js` 或各测试脚本按需 `require('../shared/cards.js')`,保证 Node 侧全局可用。
|
||||||
|
|
||||||
|
**安全网**:现有 20 个测试脚本原样全绿,即为这次重构的验收依据——不新增业务行为,只搬家。
|
||||||
|
|
||||||
|
### 7.4 前端同步
|
||||||
|
|
||||||
|
- `client/scripts/sync_shared.cmd` + `sync_shared.ps1`(仿既有 `build_spine_data.cmd` 范式):把 `server/games/erqiwang/shared/*.js` 拷到 `client/js/01_SubGame/codes/shared/`
|
||||||
|
- 副本**逐字节一致**——浏览器无 `module`,尾部守卫自动跳过,同一份文件两个运行时都能跑
|
||||||
|
- 前端副本**只读**(红线:`codes/shared/` 不在前端改,改服务端权威源后跑同步脚本)
|
||||||
|
- **防漂移守卫**:`client/tests/test_shared_sync.js` 断言两份文件内容逐字节相等,漂移即红
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 测试
|
||||||
|
|
||||||
|
照搬服务端 `test/` 的现有范式(`run.js` spawn 各用例独立进程 + `_assert.js` + `_shim.js`),新建 `client/tests/`。
|
||||||
|
|
||||||
|
### 8.1 加载机制
|
||||||
|
|
||||||
|
前端正式代码是浏览器全局脚本(`var X = {...}`,无 `module.exports`)。测试侧用 `_load.js` 以 `vm.runInThisContext(fs.readFileSync(path))` 把它们喂进 Node global——**不改正式代码**,符合 client 05 §10「正式代码禁止为测试而加逻辑」。
|
||||||
|
|
||||||
|
测试代码可用现代语法(只跑 Node、不上线),且不受 `.githooks/pre-commit` 的严格 ES5 拦截。
|
||||||
|
|
||||||
|
### 8.2 用例
|
||||||
|
|
||||||
|
| 文件 | 覆盖 |
|
||||||
|
|---|---|
|
||||||
|
| `test_cardcodec.js` | 清单 §0.4 的 **10 条校验样例**逐条;两副牌同帧;牌背常量;越界 id 的行为 |
|
||||||
|
| `test_cardorder.js` | 与服务端 `order_cards` 同序(正例);选主前后(`mainflower=0` vs `flower`)顺序变化;不改入参数组 |
|
||||||
|
| `test_cardmark.js` | 三种标记互斥且每张至多一个;拖拉机分组正例/反例(不连续的对子不成拖);`countByFlower` 的张数与对数 |
|
||||||
|
| `test_seatmap.js` | 三个 `mySeat` × 三个 `seat` 全组合;`toDisplay`/`toSeat` 互逆 |
|
||||||
|
| `test_spriteindex.js` | 嵌套结构展平正确;键名查不到显式抛错;重复键报错 |
|
||||||
|
| `test_layoutsolver.js` | 五型各自的坐标正确;`bySeat` 合并(覆盖与继承);`fan` 的 clamp 上下边界与 `count<=1`;`line` 的 `items` 不等宽;`grid` 的 `fillOrder`;`attach` 四角与内部对齐;未知 kind / 缺参 / target 缺失均抛错 |
|
||||||
|
| `test_shared_sync.js` | 前后端 `shared/cards.js` 逐字节一致 |
|
||||||
|
| `test_constants.js` | 号段合规:精灵 ∈ 1001–2999、群组 ≥201、图层 ∈ 101–200/301–400、图片 ≥501、声音 ≥101;**全局无重复 ID**;布局配置里每个 `attach.target` 都能在精灵索引里查到 |
|
||||||
|
|
||||||
|
`test_constants.js` 是清单 §0.3 与前端红线的机械化守卫——常量层有上百个 ID,人工核对不可靠。
|
||||||
|
|
||||||
|
正面 / 反面 / 边界用例都要覆盖(client 05 §10)。测试失败先用证据裁定「业务缺陷 vs 脚本缺陷」,禁止 skip / 软化断言 / 吞异常。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 平台接入骨架
|
||||||
|
|
||||||
|
- `codes/SubGameHooks.js`:从 `gameabc-framework/templates/subgame-entry/SubGameHooks.template.js` 复制,本阶段**保持空骨架**(各 hook 留空函数 + 用途注释),B/F 阶段逐个填充。三个转发壳已就位、原样不动。
|
||||||
|
- `client/index.html`:按 client 06 §3 的顺序加入 codes 加载段——**依赖模块 → `SubGameHooks` → 三文件转发壳**。本阶段的加载次序:
|
||||||
|
|
||||||
|
```
|
||||||
|
config/*(纯数据,最先)→ shared/cards.js → core/* → ui/LayoutSolver.js
|
||||||
|
→ SubGameHooks.js → 00_/01_/02_SubGame_*.js(已在文件中,位置不变)
|
||||||
|
```
|
||||||
|
|
||||||
|
验证标准:浏览器打开 `client/index.html`(HTTP 方式)控制台**无报错**,全局对象 `EQW_*` 均已就绪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 验收标准
|
||||||
|
|
||||||
|
1. `node client/tests/run.js` 全绿
|
||||||
|
2. `node server/games/erqiwang/test/run.js` 全绿(shared 抽取的安全网)
|
||||||
|
3. HTTP 打开 `client/index.html` 控制台无报错,`EQW_Layers/Groups/Images/Sprites/Layout/Anim/CardCodec/CardOrder/CardMark/SeatMap/SpriteIndex/LayoutSolver` 全部就绪
|
||||||
|
4. `git config core.hooksPath .githooks` 下提交通过机械红线校验(严格 ES5、可编辑范围)
|
||||||
|
5. 常量层与清单 §3 / §5 / §6 逐条对应,无遗漏、无臆造条目
|
||||||
|
6. 业务代码零裸值:`core/`、`ui/` 里不出现任何精灵 ID、坐标、时长的字面量
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 遗留与依赖
|
||||||
|
|
||||||
|
| 项 | 状态 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| **T-20 音效** | 清单唯一未决规格项(整套音效清单未定) | `SoundResources.js` 只建骨架与说明注释,**不臆造条目**;定案后补 |
|
||||||
|
| **D-8 判定结果动画** | 已定形态(对局中 + 结算前播放,靠 `curmultiple` 符号分辨),缺动画设计稿 | 动画参数位在 `AnimConstants.js` 预留注释;实际动画在 E 阶段 |
|
||||||
|
| 编辑器建资源 | 未开始 | 不阻塞(§1.1)。常量文件即建资源规格书 |
|
||||||
|
| 布局数值 | 参考图目视实测估值(±5px) | 设计稿到位后只改 `config/Layout_*.js` 一层 |
|
||||||
|
| `Game_Modify.PLAYER_INFO_LAYOUT` 同步 | 清单 §6.11 / T-28:转发壳配置区不能引用 `codes`,只能手工同步 | F 阶段做,并列入验收检查 |
|
||||||
Reference in New Issue
Block a user