Files
youle_cocos/cocoscreator_projects/framework-tests/protocol/room-handlers.test.ts
T
joywayerandClaude Opus 5 705c0389b5 feat(framework): 房间事件 handler(self/other join/exit/free_room/change_seat 等)
完整覆盖 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>
2026-08-31 20:51:51 +08:00

137 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/);
});