完整覆盖 C 规范 §6.4 对局流程的 14 个 room rpc: - self_join_room / other_join_room(填对应座位) - self_exit_room / other_exit_room(清空座位) - self/other_apply_free_room(state=1 投票中) - self/other_agree_free_room / refuse_free_room(状态由其他协议累计,本层 no-op) - other_offline / other_online(RoomStore 不持有,handler no-op) - change_seat(swap 两个座位) - free_room(state=2 已解散) 设计要点: - 纯逻辑函数 (ctx, data) => void,单测直接调,不依赖 NetClient/Router - 1 基座位 → 0 基数组下标;越界显式抛错(第二准则) - Object.freeze(handler表) 防止外部写入不安全 handler - 未注册 rpc 显式抛错(第二准则) - RoomState 增加 state 字段(0/1/2 三态:正常/投票中/已解散) 158/158 tests pass, typecheck exit 0。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
137 lines
5.4 KiB
TypeScript
137 lines
5.4 KiB
TypeScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { RoomStore } from '../../YouleNexus/assets/framework/platform/stores/room-store.ts';
|
||
import { PlayerStore } from '../../YouleNexus/assets/framework/platform/stores/player-store.ts';
|
||
import { roomHandlers } from '../../YouleNexus/assets/framework/protocol/room-handlers.ts';
|
||
import type { RoomRecoveryData } from '../../YouleNexus/assets/framework/platform/stores/room-store.ts';
|
||
|
||
function makeRoom() {
|
||
const room = new RoomStore();
|
||
room.applyRecovery({
|
||
roomcode: 'R1', isbattle: 1, roommode: 1,
|
||
seat: 1, isowner: 0,
|
||
players: [
|
||
null,
|
||
{ playerid: 100, nickname: '甲', bean: 10, roomcard: 0 } as any,
|
||
{ playerid: 200, nickname: '乙', bean: 20, roomcard: 0 } as any,
|
||
null,
|
||
],
|
||
} as RoomRecoveryData);
|
||
return room;
|
||
}
|
||
|
||
function makePlayer() {
|
||
const p = new PlayerStore();
|
||
p.applyLogin({ playerid: 100, nickname: '甲', bean: 10, roomcard: 0 } as any);
|
||
return p;
|
||
}
|
||
|
||
test('self_join_room: 已在房应被覆盖(再次入房罕见但支持)', () => {
|
||
const room = makeRoom();
|
||
// 假设自己是 seat=2 加入
|
||
roomHandlers.self_join_room({ room }, { seat: 2, playerid: 200 } as any);
|
||
assert.equal(room.state.value.seat, 2);
|
||
});
|
||
|
||
test('other_join_room: 第三方加入,players 数组对应座位填入', () => {
|
||
const room = new RoomStore();
|
||
room.applyRecovery({
|
||
roomcode: 'R1', seat: 1, isowner: 0,
|
||
players: [null, { playerid: 100 } as any, null, null],
|
||
} as RoomRecoveryData);
|
||
roomHandlers.other_join_room({ room }, { seat: 3, playerid: 300, nickname: '丙' } as any);
|
||
const v = room.state.value;
|
||
assert.equal(v.players[2]?.playerid, 300, 'seat=3 应填到 players[2]');
|
||
assert.equal(v.players[2]?.nickname, '丙');
|
||
});
|
||
|
||
test('self_exit_room: 自己座位清空', () => {
|
||
const room = makeRoom();
|
||
// 假设自己 seat=1
|
||
roomHandlers.self_exit_room({ room, player: makePlayer() }, { seat: 1 } as any);
|
||
assert.equal(room.state.value.players[0], null, 'seat=1(0基)应清空');
|
||
// 自己的 PlayerStore 不变(退出房间不抹掉登录态)
|
||
});
|
||
|
||
test('other_exit_room: 第三方座位清空', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.other_exit_room({ room }, { seat: 2 } as any);
|
||
assert.equal(room.state.value.players[1], null);
|
||
});
|
||
|
||
test('other_exit_room: 未知座位 → 显式抛错(第二准则)', () => {
|
||
const room = makeRoom();
|
||
assert.throws(
|
||
() => roomHandlers.other_exit_room({ room }, { seat: 99 } as any),
|
||
/seat=99/,
|
||
);
|
||
});
|
||
|
||
test('self_apply_free_room: state 进入解散投票中', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.self_apply_free_room({ room }, { applyer: 1 } as any);
|
||
assert.equal(room.state.value.state, 1);
|
||
});
|
||
|
||
test('other_apply_free_room: state 进入解散投票中', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.other_apply_free_room({ room }, { applyer: 2 } as any);
|
||
assert.equal(room.state.value.state, 1);
|
||
});
|
||
|
||
test('self/other_agree_free_room: 不改变 state,仅标记同意', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.self_apply_free_room({ room }, {} as any);
|
||
roomHandlers.self_agree_free_room({ room, player: makePlayer() }, {} as any);
|
||
// 协议无 agreeList 字段(state 保持 1=投票中),由其他机制累计
|
||
assert.equal(room.state.value.state, 1);
|
||
});
|
||
|
||
test('self/other_refuse_free_room: 不改变 state', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.self_apply_free_room({ room }, {} as any);
|
||
roomHandlers.self_refuse_free_room({ room, player: makePlayer() }, {} as any);
|
||
assert.equal(room.state.value.state, 1);
|
||
});
|
||
|
||
test('other_offline: 触发但不破坏 RoomStore(offline 状态由子游戏持有)', () => {
|
||
const room = makeRoom();
|
||
// RoomStore 不持有 offline 字段(handler 应是 no-op 或仅校验入参)
|
||
roomHandlers.other_offline({ room }, { seat: 1 } as any);
|
||
// state 不变
|
||
assert.equal(room.state.value.state, 0);
|
||
});
|
||
|
||
test('other_online: 触发但不破坏 RoomStore', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.other_online({ room }, { seat: 1 } as any);
|
||
assert.equal(room.state.value.state, 0);
|
||
});
|
||
|
||
test('change_seat: 交换两个座位(seat=1 ↔ seat=3)', () => {
|
||
const room = makeRoom();
|
||
// 初始: players = [null, 100, 200, null]
|
||
// change_seat(from=1, to=3) → i_from=0, i_to=2
|
||
// swap: [null, 100, 200, null] ↔ [200, 100, null, null]
|
||
roomHandlers.change_seat({ room }, { from: 1, to: 3 } as any);
|
||
const v = room.state.value;
|
||
// swap 结果:players[0]=200, players[1]=100, players[2]=null, players[3]=null
|
||
assert.equal(v.players[0]?.playerid, 200, '原 seat=3 的 200 现在在 seat=1(0基=0)');
|
||
assert.equal(v.players[1]?.playerid, 100, '原 seat=2 的 100 仍在原位');
|
||
assert.equal(v.players[2], null, '原 seat=1 的 null 现在在 seat=3(0基=2)');
|
||
});
|
||
|
||
test('free_room: 解散投票结束 → state=2(已解散,业务侧清场)', () => {
|
||
const room = makeRoom();
|
||
roomHandlers.self_apply_free_room({ room }, {} as any);
|
||
roomHandlers.free_room({ room }, { result: 1 } as any);
|
||
// state 字段语义: 0=正常 1=投票中 2=已解散
|
||
assert.equal(room.state.value.state, 2);
|
||
});
|
||
|
||
test('unknown rpc 不在 handler 表里 → 显式抛错(第二准则)', () => {
|
||
const room = makeRoom();
|
||
const h = roomHandlers as unknown as Record<string, (ctx: any, d: any) => void>;
|
||
// Object.freeze 后,h['unknown_rpc'] 是 undefined,调用会抛
|
||
assert.throws(() => h['unknown_rpc']({ room, player: makePlayer() }, {}), /unknown_rpc/);
|
||
}); |