feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
@@ -44,7 +44,7 @@ function makeStore(location: 'outside' | 'inside', stage = 0, infinite = 0): Pla
|
||||
return store;
|
||||
}
|
||||
|
||||
function makeCommands(store: PlatformStore): {
|
||||
function makeCommands(store: PlatformStore): {
|
||||
readonly commands: PlatformCommands;
|
||||
readonly sent: OutboundEnvelope[];
|
||||
} {
|
||||
@@ -57,56 +57,56 @@ function makeCommands(store: PlatformStore): {
|
||||
}),
|
||||
sent,
|
||||
};
|
||||
}
|
||||
|
||||
test('invite query and binding preserve exact legacy fields and leading zeros',()=>{
|
||||
const store=makeStore('outside'),{commands,sent}=makeCommands(store);
|
||||
commands.getPlayerInvitecode();commands.bindInvitecode('00123');
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_player_invitecode',data:{agentid:'agent-from-config',playerid:430511,unionid:'test_unionid',openid:'o1'}},
|
||||
{app:'youle',route:'agent',rpc:'binding_invitecode',data:{agentid:'agent-from-config',playerid:430511,invitecode:'00123'}},
|
||||
]);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerInvitecode(),/logged-in/);
|
||||
assert.throws(()=>makeCommands(makeStore('inside')).commands.bindInvitecode('1'),/logged-in/);
|
||||
});
|
||||
|
||||
test('invite responses publish even unchanged codes, update only on success, and reject malformed data atomically',()=>{
|
||||
const store=makeStore('outside');let updates=0;store.subscribe(()=>updates++);
|
||||
store.applyInvitecode({invitecode:''});store.applyInvitecode({invitecode:''});
|
||||
assert.equal(updates,2);
|
||||
store.applyInviteBinding({state:0,invitecode:'00123',error:'绑定成功'});
|
||||
assert.equal(store.getState().players.entities[430511].invitecode,'00123');
|
||||
store.applyInviteBinding({state:1,error:'邀请码无效'});
|
||||
assert.equal(store.getState().players.entities[430511].invitecode,'00123');
|
||||
const before=store.getState();
|
||||
assert.throws(()=>store.applyInvitecode({}),/invitecode/);
|
||||
assert.throws(()=>store.applyInviteBinding({state:0,invitecode:123,error:'bad'}),/invitecode/);
|
||||
assert.throws(()=>store.applyInviteBinding({state:0,invitecode:'wrong'}),/error/);
|
||||
for(const state of ['',null,true,'invalid','0.5']){
|
||||
assert.throws(()=>store.applyInviteBinding({state,invitecode:'wrong',error:'bad'}),/state/);
|
||||
}
|
||||
assert.equal(store.getState(),before);
|
||||
});
|
||||
}
|
||||
|
||||
test('VIP query and both entry forms use the exact agent protocol',()=>{
|
||||
test('invite query and binding preserve exact legacy fields and leading zeros',()=>{
|
||||
const store=makeStore('outside'),{commands,sent}=makeCommands(store);
|
||||
commands.getPlayerInvitecode();commands.bindInvitecode('00123');
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_player_invitecode',data:{agentid:'agent-from-config',playerid:430511,unionid:'test_unionid',openid:'o1'}},
|
||||
{app:'youle',route:'agent',rpc:'binding_invitecode',data:{agentid:'agent-from-config',playerid:430511,invitecode:'00123'}},
|
||||
]);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getPlayerInvitecode(),/logged-in/);
|
||||
assert.throws(()=>makeCommands(makeStore('inside')).commands.bindInvitecode('1'),/logged-in/);
|
||||
});
|
||||
|
||||
test('invite responses publish even unchanged codes, update only on success, and reject malformed data atomically',()=>{
|
||||
const store=makeStore('outside');let updates=0;store.subscribe(()=>updates++);
|
||||
store.applyInvitecode({invitecode:''});store.applyInvitecode({invitecode:''});
|
||||
assert.equal(updates,2);
|
||||
store.applyInviteBinding({state:0,invitecode:'00123',error:'绑定成功'});
|
||||
assert.equal(store.getState().players.entities[430511].invitecode,'00123');
|
||||
store.applyInviteBinding({state:1,error:'邀请码无效'});
|
||||
assert.equal(store.getState().players.entities[430511].invitecode,'00123');
|
||||
const before=store.getState();
|
||||
assert.throws(()=>store.applyInvitecode({}),/invitecode/);
|
||||
assert.throws(()=>store.applyInviteBinding({state:0,invitecode:123,error:'bad'}),/invitecode/);
|
||||
assert.throws(()=>store.applyInviteBinding({state:0,invitecode:'wrong'}),/error/);
|
||||
for(const state of ['',null,true,'invalid','0.5']){
|
||||
assert.throws(()=>store.applyInviteBinding({state,invitecode:'wrong',error:'bad'}),/state/);
|
||||
}
|
||||
assert.equal(store.getState(),before);
|
||||
});
|
||||
|
||||
test('VIP query and both entry forms use the exact agent protocol',()=>{
|
||||
const store=makeStore('outside'),{commands,sent}=makeCommands(store);
|
||||
commands.getVipRooms(['123456']);commands.joinVipRoom('123456',null);commands.joinVipRoom(900001,{city:'测试'});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'getInfoByShortCode',data:{agentid:'agent-from-config',gameid:'game-from-config',shortcodeList:['123456']}});
|
||||
assert.deepEqual(sent[1],{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:'123456',joinmode:0,location:null,ip:store.getState().players.entities[430511].ip}});
|
||||
assert.equal((sent[2].data as {roomcode:number}).roomcode,900001);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getVipRooms([]),/logged-in/);
|
||||
});
|
||||
|
||||
test('ranking and redemption use exact legacy fields, preserving card text',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
commands.getTreasureList();commands.redeemCard('00Ab-12');
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_treasurelist',data:{agentid:'agent-from-config',gameid:'game-from-config'}},
|
||||
{app:'youle',route:'agent',rpc:'topup_card',data:{agentid:'agent-from-config',playerid:430511,cardno:'00Ab-12'}},
|
||||
]);
|
||||
assert.throws(()=>commands.redeemCard(' '),/cardno/);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getTreasureList(),/logged-in/);
|
||||
});
|
||||
});
|
||||
|
||||
test('ranking and redemption use exact legacy fields, preserving card text',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
commands.getTreasureList();commands.redeemCard('00Ab-12');
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'get_treasurelist',data:{agentid:'agent-from-config',gameid:'game-from-config'}},
|
||||
{app:'youle',route:'agent',rpc:'topup_card',data:{agentid:'agent-from-config',playerid:430511,cardno:'00Ab-12'}},
|
||||
]);
|
||||
assert.throws(()=>commands.redeemCard(' '),/cardno/);
|
||||
assert.throws(()=>makeCommands(new PlatformStore()).commands.getTreasureList(),/logged-in/);
|
||||
});
|
||||
|
||||
test('task requests preserve legacy identity fields and reject unclaimable entries',()=>{
|
||||
const store=makeStore('outside');const {commands,sent}=makeCommands(store);
|
||||
@@ -261,23 +261,75 @@ test('command preconditions reject missing authentication, wrong room location,
|
||||
assert.equal(inside.sent.length, 0);
|
||||
assert.equal(battle.sent.length, 0);
|
||||
});
|
||||
|
||||
test('VIP rank and owned room requests preserve legacy exact fields',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
commands.getVipRankList(10);commands.joinOwnedRoom(123456,{city:'x'});
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'getVipRankList',data:{agentid:'agent-from-config',limit:10}},
|
||||
{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:123456,joinmode:1,location:{city:'x'},ip:'127.0.0.1'}}]);
|
||||
assert.throws(()=>commands.getVipRankList(-1));
|
||||
});
|
||||
|
||||
test('create command emits exact legacy packet and numeric keypad join stays numeric', () => {
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
const roomtype=[1,4,[7,8],{option:true}];
|
||||
commands.createRoom({roomtype,ip:'127.0.0.8',location:null});
|
||||
commands.joinRoom({roomcode:12345,ip:'127.0.0.8',location:null});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'create_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomtype,ip:'127.0.0.8',location:null}});
|
||||
assert.deepEqual(sent[1].data,{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:12345,ip:'127.0.0.8',location:null});
|
||||
assert.throws(()=>commands.createRoom({roomtype:[undefined],ip:'x',location:null}),/JSON/);
|
||||
assert.throws(()=>makeCommands(makeStore('inside')).commands.createRoom({roomtype,ip:'x',location:null}),/outside|lobby/);
|
||||
|
||||
test('VIP rank and owned room requests preserve legacy exact fields',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
commands.getVipRankList(10);commands.joinOwnedRoom(123456,{city:'x'});
|
||||
assert.deepEqual(sent,[
|
||||
{app:'youle',route:'agent',rpc:'getVipRankList',data:{agentid:'agent-from-config',limit:10}},
|
||||
{app:'youle',route:'agent',rpc:'self_join_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:123456,joinmode:1,location:{city:'x'},ip:'127.0.0.1'}}]);
|
||||
assert.throws(()=>commands.getVipRankList(-1));
|
||||
});
|
||||
|
||||
test('create command emits exact legacy packet and numeric keypad join stays numeric', () => {
|
||||
const {commands,sent}=makeCommands(makeStore('outside'));
|
||||
const roomtype=[1,4,[7,8],{option:true}];
|
||||
commands.createRoom({roomtype,ip:'127.0.0.8',location:null});
|
||||
commands.joinRoom({roomcode:12345,ip:'127.0.0.8',location:null});
|
||||
assert.deepEqual(sent[0],{app:'youle',route:'agent',rpc:'create_room',data:{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomtype,ip:'127.0.0.8',location:null}});
|
||||
assert.deepEqual(sent[1].data,{agentid:'agent-from-config',playerid:430511,gameid:'game-from-config',roomcode:12345,ip:'127.0.0.8',location:null});
|
||||
assert.throws(()=>commands.createRoom({roomtype:[undefined] as any,ip:'x',location:null}),/JSON/);
|
||||
assert.throws(()=>makeCommands(makeStore('inside')).commands.createRoom({roomtype,ip:'x',location:null}),/outside|lobby/);
|
||||
});
|
||||
|
||||
test('dissolution commands emit exact legacy room envelopes and await server state',()=>{
|
||||
const store=makeStore('inside',1);const data=fixture('player-login-room.json').data;const players=data.players as Record<string,unknown>[];players[0]={...players[1],playerid:999,nickname:'发起者'};store.applyLoginSuccess(parseLoginResponse(data));const {commands,sent}=makeCommands(store);
|
||||
commands.applyDissolution();assert.equal(store.getState().dissolution,undefined);
|
||||
store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,-1,null,null],countdown:60}});
|
||||
commands.agreeDissolution();commands.refuseDissolution();
|
||||
assert.deepEqual(sent.map(packet=>packet.rpc),['self_apply_free_room','self_agree_free_room','self_refuse_free_room']);
|
||||
for(const packet of sent)assert.deepEqual(packet,{app:'youle',route:'room',rpc:packet.rpc,data:{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001'}});
|
||||
assert.equal(store.getState().dissolution?.participants.find(p=>p.seat===1)?.state,-1);
|
||||
});
|
||||
test('dissolution commands reject pregame, already voted and duplicate active application',()=>{
|
||||
const first=makeCommands(makeStore('inside',0));assert.throws(()=>first.commands.applyDissolution(),/stage/);
|
||||
const store=makeStore('inside',1),{commands,sent}=makeCommands(store);
|
||||
store.applyDissolution('self_apply_free_room',{agreefree:{state:[null,0,null,null],countdown:1}});
|
||||
assert.throws(()=>commands.applyDissolution(),/vote/);assert.throws(()=>commands.agreeDissolution(),/waiting/);assert.throws(()=>commands.refuseDissolution(),/waiting/);assert.equal(sent.length,0);
|
||||
});
|
||||
|
||||
test('room chat and phrase commands preserve exact legacy envelopes and no speculative expression rpc',()=>{
|
||||
const store=makeStore('inside'),{commands,sent}=makeCommands(store);
|
||||
commands.sendRoomText('hello');commands.sendRoomText('notice',true);commands.sendRoomPhrase('phrase');commands.sendRoomVoice('https://audio',1.25);commands.sendRoomExpression(20);commands.changeRoom();
|
||||
assert.deepEqual(sent.map(e=>[e.route,e.rpc,e.data]),[
|
||||
['room','send_text',{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',text:'hello',type:0}],
|
||||
['room','send_text',{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',text:'notice',type:1}],
|
||||
['room','send_text',{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',text:'phrase'}],
|
||||
['room','send_voice',{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',voiceurl:'https://audio',time:1.25}],
|
||||
['room','send_phiz',{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',text:20}],
|
||||
['room','change_room',{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001'}],
|
||||
]);
|
||||
assert.throws(()=>commands.sendRoomExpression(0));assert.throws(()=>commands.sendRoomVoice('',1));
|
||||
});
|
||||
test('break room is allowed only for pregame owner and waits for authoritative response',()=>{
|
||||
const store=makeStore('inside',0),{commands,sent}=makeCommands(store);const before=store.getState();commands.breakRoom();assert.equal(sent[0].rpc,'self_break_room');assert.equal(store.getState(),before);
|
||||
assert.throws(()=>makeCommands(makeStore('inside',1)).commands.breakRoom());
|
||||
});
|
||||
|
||||
test('voice original string duration and surrender count are preserved on wire',()=>{
|
||||
const {commands,sent}=makeCommands(makeStore('inside'));commands.sendRoomVoice('url','1.2');commands.surrenderRoom(12.5);
|
||||
assert.deepEqual(sent[0].data,{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',voiceurl:'url',time:'1.2'});assert.equal(sent[1].rpc,'beanroom_surrender');assert.deepEqual(sent[1].data,{agentid:'agent-from-config',gameid:'game-from-config',playerid:430511,roomcode:'100001',count:12.5});
|
||||
});
|
||||
|
||||
test('live game projection can restrict commands but cannot override platform rules',()=>{
|
||||
const initial={revision:0,roundPhase:'waiting',selfParticipation:'waiting' as const,scoresBySeat:{},restrictions:{prepare:false as const,exit:false as const}};
|
||||
let projection:import('../../YouleNexus/assets/framework/sdk/contracts/game-room-projection.ts').GameRoomProjection|null=initial;
|
||||
const sent:OutboundEnvelope[]=[];const store=makeStore('inside',0);
|
||||
const commands=new PlatformCommands({runtimeConfig:RUNTIME_CONFIG,store,send:{send:message=>sent.push(message)},getRoomProjection:()=>projection});
|
||||
assert.throws(()=>commands.prepare(),/disallows.*prepare/);assert.throws(()=>commands.exitRoom(),/disallows.*exit/);assert.equal(sent.length,0);
|
||||
projection={...initial,revision:1,restrictions:{}};commands.prepare();commands.exitRoom();assert.deepEqual(sent.map(m=>m.rpc),['player_prepare','self_exit_room']);
|
||||
const battleStore=makeStore('inside',1);const battle=new PlatformCommands({runtimeConfig:RUNTIME_CONFIG,store:battleStore,send:{send:message=>sent.push(message)},getRoomProjection:()=>projection});
|
||||
assert.throws(()=>battle.exitRoom(),/stage=0/);assert.equal(sent.length,2);
|
||||
projection={...initial,revision:2,restrictions:{exit:false}};assert.throws(()=>battle.applyDissolution(),/disallows.*exit/);assert.equal(sent.length,2);
|
||||
projection=null;battle.applyDissolution();assert.equal(sent[2].rpc,'self_apply_free_room');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user