界面迁移更新
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ActiveGame } from '../../YouleNexus/assets/framework/protocol/active-game.ts';
|
||||
import type { IGameModule, GameContext } from '../../YouleNexus/assets/framework/sdk/index.ts';
|
||||
|
||||
class MockGame implements IGameModule {
|
||||
readonly route = 'mock';
|
||||
events: string[] = [];
|
||||
onEnter(_ctx: GameContext): void { this.events.push('onEnter'); }
|
||||
onExit(): void { this.events.push('onExit'); }
|
||||
onReceive(rpc: string, _d: unknown): void { this.events.push(`onReceive:${rpc}`); }
|
||||
onReconnect(_d: unknown): void { this.events.push('onReconnect'); }
|
||||
}
|
||||
|
||||
test('初始: getGame 返回 null', () => {
|
||||
assert.equal(new ActiveGame().get(), null);
|
||||
});
|
||||
|
||||
test('setGame: 设置后 get 返回同一实例', () => {
|
||||
const slot = new ActiveGame();
|
||||
const g = new MockGame();
|
||||
slot.set(g);
|
||||
assert.equal(slot.get(), g);
|
||||
});
|
||||
|
||||
test('clear: 清空后 get 返回 null', () => {
|
||||
const slot = new ActiveGame();
|
||||
slot.set(new MockGame());
|
||||
slot.clear();
|
||||
assert.equal(slot.get(), null);
|
||||
});
|
||||
|
||||
test('set 覆盖前一个 game', () => {
|
||||
const slot = new ActiveGame();
|
||||
const g1 = new MockGame();
|
||||
const g2 = new MockGame();
|
||||
slot.set(g1);
|
||||
slot.set(g2);
|
||||
assert.equal(slot.get(), g2);
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildLoginRequest, parseLoginResponse } from '../../YouleNexus/assets/framework/protocol/login.ts';
|
||||
|
||||
const IDENTITY = { agentid: 1, gameid: 2, openid: 'o', nickname: 'n', avatar: 'a', sex: 1, province: 'p', city: 'c', unionid: 'u', version: 10001, channelid: 7, marketid: 9 };
|
||||
|
||||
test('buildLoginRequest 透传必备身份字段', () => {
|
||||
const d = buildLoginRequest(IDENTITY);
|
||||
assert.equal(d.agentid, 1);
|
||||
assert.equal(d.openid, 'o');
|
||||
assert.equal(d.marketid, 9);
|
||||
});
|
||||
|
||||
test('parseLoginResponse 提取核心字段 + 房间恢复标志', () => {
|
||||
const r = parseLoginResponse({ state: 0, playerid: 9, bean: 100, roomcode: 'ABCD', isbattle: 1, deskinfo: { x: 1 } });
|
||||
assert.equal(r.ok, true);
|
||||
assert.equal(r.playerid, 9);
|
||||
assert.equal(r.inRoom, true);
|
||||
assert.equal(r.hasBattle, true);
|
||||
assert.deepEqual(r.deskinfo, { x: 1 });
|
||||
});
|
||||
|
||||
test('parseLoginResponse:无 roomcode 时不在房间', () => {
|
||||
const r = parseLoginResponse({ state: 0, playerid: 9 });
|
||||
assert.equal(r.inRoom, false);
|
||||
assert.equal(r.hasBattle, false);
|
||||
});
|
||||
|
||||
test('parseLoginResponse:state!=0 视为失败', () => {
|
||||
const r = parseLoginResponse({ state: 1, playerid: -1 });
|
||||
assert.equal(r.ok, false);
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { typedSend, routeFor, type RpcRouteMap } from '../../YouleNexus/assets/framework/protocol/typed-send.ts';
|
||||
import { Route } from '../../YouleNexus/assets/framework/core/constants.ts';
|
||||
import { Rpc, type RpcName } from '../../YouleNexus/assets/framework/protocol/routes.ts';
|
||||
|
||||
/**
|
||||
* RPC → Route 映射(按协议分类):
|
||||
* - 房间生命周期/对局/解散/聊天/语音 → room
|
||||
* - 登录/连接/账号/资产/任务/社交/手机/VIP/列表 → agent
|
||||
* - joinRoom(H5 专用) → agent(与 joinRoom 业务一致)
|
||||
*/
|
||||
const RPC_ROUTE: RpcRouteMap = {
|
||||
// ---- 房间生命周期/对局/解散/聊天/语音 → room ----
|
||||
create_room: 'room', self_join_room: 'room', other_join_room: 'room',
|
||||
self_exit_room: 'room', other_exit_room: 'room',
|
||||
self_break_room: 'room', other_break_room: 'room',
|
||||
player_prepare: 'room', change_seat: 'room', change_room: 'room',
|
||||
switchRoomList: 'room',
|
||||
self_makewar: 'room', other_makewar: 'room', over_game: 'room',
|
||||
get_player_grade1: 'room', get_player_grade2: 'room',
|
||||
send_gift: 'room', other_send_gift: 'room',
|
||||
beanroom_surrender: 'room', send_phiz: 'room', broadcast: 'room',
|
||||
other_offline: 'room', other_online: 'room',
|
||||
self_apply_free_room: 'room', other_apply_free_room: 'room',
|
||||
self_agree_free_room: 'room', other_agree_free_room: 'room',
|
||||
self_refuse_free_room: 'room', other_refuse_free_room: 'room',
|
||||
free_room: 'room',
|
||||
send_voice: 'room', play_voice: 'room',
|
||||
send_text: 'room', receive_chat: 'room',
|
||||
call_phone: 'room', other_callphone: 'room',
|
||||
hangup_phone: 'room', other_hangup: 'room', kick_offline: 'room',
|
||||
// ---- agent(账号/资产/任务/社交/手机/反馈/VIP)----
|
||||
player_login: 'agent',
|
||||
kick_server: 'agent',
|
||||
connect_roomserver: 'agent',
|
||||
connect_agentserver: 'agent',
|
||||
agentserver_game: 'agent',
|
||||
update_roomcard: 'agent', update_bean: 'agent', update_charm: 'agent',
|
||||
giveCoin: 'agent', change_star: 'agent',
|
||||
topup_card: 'agent', get_paylist: 'agent', pay_succ: 'agent',
|
||||
get_treasurelist: 'agent',
|
||||
get_player_task: 'agent', player_finish_task: 'agent',
|
||||
get_task_award: 'agent', can_award: 'agent',
|
||||
binding_invitecode: 'agent', get_player_invitecode: 'agent',
|
||||
share_room: 'agent', get_share_room: 'agent', quick_enter_share_room: 'agent',
|
||||
advanced_roomlist: 'agent', advanced_createroom: 'agent', query_player2: 'agent',
|
||||
set_bankpwd: 'agent',
|
||||
binding_phone: 'agent', send_phone_checkcode: 'agent',
|
||||
send_phone_code_wechat: 'agent', submit_phoneinfo: 'agent',
|
||||
submit_location: 'agent', submit_opinion: 'agent',
|
||||
setSign: 'agent', getInfoByShortCode: 'agent', optBanList: 'agent',
|
||||
getShortCodeRankList: 'agent',
|
||||
setAllCharm: 'agent', getVipRankList: 'agent', playerBehavior: 'agent',
|
||||
setVipForbidSelect: 'agent', getPlayerWhiteList: 'agent', optWhiteList: 'agent',
|
||||
joinRoom: 'agent',
|
||||
};
|
||||
|
||||
test('RPC_ROUTE: 自映射契约(每个 rpc 在 Rpc 表里)', () => {
|
||||
for (const rpc of Object.keys(RPC_ROUTE) as RpcName[]) {
|
||||
assert.equal(RPC_ROUTE[rpc], RPC_ROUTE[rpc], `routeFor(${rpc}) 必有定义`);
|
||||
assert.ok(Rpc[rpc] === rpc, `Rpc.${rpc} 必须等于自身字符串`);
|
||||
}
|
||||
});
|
||||
|
||||
test('routeFor: 已知 rpc 返回正确 route', () => {
|
||||
assert.equal(routeFor('player_login', RPC_ROUTE), Route.agent);
|
||||
assert.equal(routeFor('self_join_room', RPC_ROUTE), Route.room);
|
||||
assert.equal(routeFor('over_game', RPC_ROUTE), Route.room);
|
||||
assert.equal(routeFor('joinRoom', RPC_ROUTE), Route.agent);
|
||||
});
|
||||
|
||||
test('routeFor: 未知 rpc 抛错(第二准则:不静默兜底)', () => {
|
||||
assert.throws(() => routeFor('unknown_rpc' as RpcName, RPC_ROUTE), /unknown_rpc/);
|
||||
});
|
||||
|
||||
test('typedSend: 返回包络结构(rpc/route/data)', () => {
|
||||
const env = typedSend(RPC_ROUTE, 'self_join_room', { seat: 1, roomcode: 'R1' });
|
||||
assert.equal(env.route, Route.room);
|
||||
assert.equal(env.rpc, 'self_join_room');
|
||||
assert.deepEqual(env.data, { seat: 1, roomcode: 'R1' });
|
||||
});
|
||||
|
||||
test('typedSend: 未知 rpc 抛错', () => {
|
||||
assert.throws(
|
||||
() => typedSend(RPC_ROUTE, 'totally_unknown' as RpcName, {}),
|
||||
/totally_unknown/,
|
||||
);
|
||||
});
|
||||
|
||||
test('typedSend: 全部 84 个 rpc 都能产出有效包络', () => {
|
||||
for (const rpc of Object.values(Rpc)) {
|
||||
const env = typedSend(RPC_ROUTE, rpc, {});
|
||||
assert.equal(env.rpc, rpc);
|
||||
assert.ok(env.route === Route.agent || env.route === Route.room,
|
||||
`${rpc} 应只走 agent 或 room 之一`);
|
||||
}
|
||||
});
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
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, player: makePlayer() }, { 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, player: makePlayer() }, { 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, player: makePlayer() }, { 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, player: makePlayer() }, { seat: 99 } as any),
|
||||
/seat=99/,
|
||||
);
|
||||
});
|
||||
|
||||
test('self_apply_free_room: state 进入解散投票中', () => {
|
||||
const room = makeRoom();
|
||||
roomHandlers.self_apply_free_room({ room, player: makePlayer() }, { 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, player: makePlayer() }, { 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, player: makePlayer() }, {} 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, player: makePlayer() }, {} 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, player: makePlayer() }, { seat: 1 } as any);
|
||||
// state 不变
|
||||
assert.equal(room.state.value.state, 0);
|
||||
});
|
||||
|
||||
test('other_online: 触发但不破坏 RoomStore', () => {
|
||||
const room = makeRoom();
|
||||
roomHandlers.other_online({ room, player: makePlayer() }, { 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, player: makePlayer() }, { 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, player: makePlayer() }, {} as any);
|
||||
roomHandlers.free_room({ room, player: makePlayer() }, { 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/);
|
||||
});
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts';
|
||||
import { ActiveGame } from '../../YouleNexus/assets/framework/protocol/active-game.ts';
|
||||
import type { InboundMessage } from '../../YouleNexus/assets/framework/core/types/envelope.ts';
|
||||
import type { IGameModule, GameContext } from '../../YouleNexus/assets/framework/sdk/index.ts';
|
||||
|
||||
class MockGame implements IGameModule {
|
||||
readonly route = 'mock';
|
||||
events: string[] = [];
|
||||
onEnter(_ctx: GameContext): void {}
|
||||
onExit(): void {}
|
||||
onReceive(rpc: string, _d: unknown): void { this.events.push(`onReceive:${rpc}`); }
|
||||
onReconnect(_d: unknown): void {}
|
||||
}
|
||||
|
||||
const msg = (route: string, rpc: string, data: unknown = {}): InboundMessage =>
|
||||
({ route, rpc, data } as InboundMessage);
|
||||
|
||||
test('platform route → 平台 handler(已注册)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const calls: Array<[string, unknown]> = [];
|
||||
const router = new Router({
|
||||
activeGame: slot,
|
||||
platformHandlers: { player_login: (d) => calls.push(['player_login', d]) },
|
||||
});
|
||||
router.dispatch(msg('agent', 'player_login', { state: 0 }));
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], 'player_login');
|
||||
});
|
||||
|
||||
test('platform route → 平台 handler 未注册 → 显式抛错(第二准则)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('agent', 'unknown_rpc')), /unknown_rpc/);
|
||||
});
|
||||
|
||||
test('非 platform route 但 activeGame 已注册 → onReceive', () => {
|
||||
const slot = new ActiveGame();
|
||||
const game = new MockGame();
|
||||
slot.set(game);
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
router.dispatch(msg('mock', 'deal_card', { n: 1 }));
|
||||
assert.deepEqual(game.events, ['onReceive:deal_card']);
|
||||
});
|
||||
|
||||
test('非 platform route 但 activeGame 未注册 → 显式抛错', () => {
|
||||
const slot = new ActiveGame();
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('mock', 'deal_card')), /mock.*deal_card/);
|
||||
});
|
||||
|
||||
test('route 与 activeGame.route 不匹配 → 显式抛错', () => {
|
||||
const slot = new ActiveGame();
|
||||
slot.set(new MockGame()); // route = 'mock'
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('other-game', 'rpc')), /other-game/);
|
||||
});
|
||||
|
||||
test('platform/agent/room 三 route 全部走平台 handler(对称)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const calls: string[] = [];
|
||||
const router = new Router({
|
||||
activeGame: slot,
|
||||
platformHandlers: {
|
||||
r1: () => calls.push('r1'),
|
||||
r2: () => calls.push('r2'),
|
||||
r3: () => calls.push('r3'),
|
||||
},
|
||||
});
|
||||
router.dispatch(msg('platform', 'r1'));
|
||||
router.dispatch(msg('agent', 'r2'));
|
||||
router.dispatch(msg('room', 'r3'));
|
||||
assert.deepEqual(calls, ['r1', 'r2', 'r3']);
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Rpc, type RpcName } from '../../YouleNexus/assets/framework/protocol/routes.ts';
|
||||
|
||||
test('Rpc 表: 每个值等于 key(自映射契约,避免错拼)', () => {
|
||||
// 每个 Rpc.X 必须 === 'X'(自映射契约;改任一即不一致)
|
||||
for (const [k, v] of Object.entries(Rpc)) {
|
||||
assert.equal(v, k, `Rpc.${k} 应等于 "${k}",实际 "${v}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Rpc 表: 至少覆盖核心业务场景', () => {
|
||||
const required: RpcName[] = [
|
||||
'player_login', 'kick_server', 'connect_roomserver', 'connect_agentserver',
|
||||
'create_room', 'self_join_room', 'other_join_room',
|
||||
'self_exit_room', 'other_exit_room', 'player_prepare', 'change_seat',
|
||||
'self_makewar', 'other_makewar',
|
||||
'self_apply_free_room', 'free_room',
|
||||
'send_text', 'receive_chat',
|
||||
'update_bean', 'update_roomcard',
|
||||
'joinRoom',
|
||||
];
|
||||
for (const rpc of required) {
|
||||
assert.ok(typeof Rpc[rpc] === 'string', `Rpc.${rpc} 必须是字符串`);
|
||||
}
|
||||
});
|
||||
|
||||
test('RpcName 联合类型: 字面量类型正确推导', () => {
|
||||
// 编译期类型测试:这里的赋值如果类型不对,tsc 会报错
|
||||
const sample: RpcName = 'player_login';
|
||||
assert.equal(sample, 'player_login');
|
||||
});
|
||||
|
||||
test('Rpc 表: 房间 handler 已覆盖的关键 rpc 都在白名单', () => {
|
||||
// 验证 room-handlers.ts 用的 14 个 rpc 都在 Rpc 表里
|
||||
const roomRpcs: RpcName[] = [
|
||||
'self_join_room', 'other_join_room',
|
||||
'self_exit_room', 'other_exit_room',
|
||||
'self_apply_free_room', 'other_apply_free_room',
|
||||
'self_agree_free_room', 'other_agree_free_room',
|
||||
'self_refuse_free_room', 'other_refuse_free_room',
|
||||
'other_offline', 'other_online',
|
||||
'change_seat', 'free_room',
|
||||
];
|
||||
for (const rpc of roomRpcs) {
|
||||
assert.ok(Rpc[rpc] === rpc, `Rpc.${rpc} 必须等于自身字符串`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user