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:
2026-09-09 03:13:18 +08:00
parent a7ec8aa6a7
commit 35216f75e7
1028 changed files with 835556 additions and 21415 deletions
@@ -96,7 +96,7 @@ function makeHandlers() {
}),
});
const connection = new ConnectionFixture();
const handlers = new PlatformHandlers({ store, gameSession, session, connection });
const handlers = new PlatformHandlers({ store, gameSession, session, connection,showMessage(){} });
const router = new Router({ session, platform: handlers, gameSession });
return {
store,
@@ -183,6 +183,7 @@ test('Router drains reentrant room entries FIFO so each module receives its own
});
const roomA = fixture('self-join-room.json');
const roomB = fixture('self-join-room.json');
roomA.deskwar=0;roomB.deskwar=0;
roomA.roomcode = 'room-A';
roomB.roomcode = 'room-B';
roomA.deskinfo = { round: 'A' };
@@ -285,6 +286,7 @@ test('PlatformHandlers commits every room event before one typed game event read
let received = 0;
const unsubscribe = setup.store.subscribe(() => { order.push('store'); });
setup.observeEvent((event, snapshot) => {
if(event.type==='room.started'){assertSnapshot(snapshot);return;}
received += 1;
order.push('game');
assert.equal(event.type, expectedType);
@@ -405,7 +407,7 @@ test('PlatformHandlers preserves complete switch payloads and builds the exact r
const setup = makeHandlers();
const roomData = { roomserver: '10.0.0.2:3088', epoch: 7 };
const agentData = {
agentserver: 'secure.example:443/path', opt: 'free_room', trace: { id: 'x' },
agentserver: 'secure.example:443/path', opt: 'self_exit_room', trace: { id: 'x' },
};
assert.equal(setup.router.dispatch({
@@ -489,7 +491,7 @@ test('unknown and every route-mismatched platform rpc leave all observable state
}
for (const [rpc, expectedRoute] of rpcsByRoute) {
for (const route of ['platform', 'agent', 'room'] as const) {
if (route === expectedRoute || rpc==='player_login') continue;
if (route === expectedRoute || rpc==='player_login'||rpc==='connect_agentserver'&&route==='room') continue;
assert.throws(
() => setup.router.dispatch({ route, rpc, data: {} }),
/route mismatch/i,
@@ -555,7 +557,7 @@ test('PlatformHandlers accepts case-insensitive WS/WSS schemes and preserves ful
extra: { trace: 1 },
};
const agentData = {
agentserver: 'WSS://secure.example:443/socket', opt: 'free_room', extra: { trace: 2 },
agentserver: 'WSS://secure.example:443/socket', opt: 'self_exit_room', extra: { trace: 2 },
};
setup.router.dispatch({ route: 'room', rpc: 'connect_roomserver', data: roomData });
@@ -675,3 +677,38 @@ test('kick_server attempts dispose, invalidate, mark, and stop once then aggrega
assert.equal(setup.getInvalidations(), 1);
assert.equal(setup.connection.stopCalls.length, 1);
});
test('active dispatcher receives dissolution and publishes immediate settlement contract without disposal',()=>{
const fixture=makeHandlers();fixture.session.markLoginPending();fixture.handlers.dispatch({route:'agent',rpc:'player_login',data:fixtureData()});
const events:PlatformToGameEvent[]=[];fixture.observeEvent(event=>events.push(event));
fixture.handlers.dispatch({route:'room',rpc:'self_apply_free_room',data:{agreefree:{state:[null,0,null,null],countdown:60}}});
assert.equal(fixture.store.getState().dissolution?.kind,'active');
fixture.handlers.dispatch({route:'room',rpc:'free_room',data:{freeNow:1,deskfree:{round:4},tips:'解散'}});
assert.deepEqual(events,[{type:'room.dissolution-confirmed',freeNow:true,deskfree:{round:4},tips:'解散'}]);
assert.equal(fixture.gameSession.state,'active');assert.equal(fixture.getDisposals(),0);
function fixtureData(){return JSON.parse(readFileSync(new URL('../fixtures/contracts/player-login-room.json',import.meta.url),'utf8')).data;}
});
test('room social ingress and confirmed break traverse Router and clear game ownership once',()=>{
const setup=makeHandlers();setup.router.dispatch({route:'agent',rpc:'player_login',data:fixture('player-login-room.json')});
setup.router.dispatch({route:'room',rpc:'send_text',data:{seat:1,text:'network',type:0}});
assert.equal(setup.store.getState().roomSocial?.history[0].text,'network');
setup.router.dispatch({route:'room',rpc:'change_seat',data:{seat1:1,seat2:0}});
const room=setup.store.getState().room;assert.ok(room.kind==='inside');assert.equal(room.selfSeat,0);
const before=setup.getDisposals();setup.router.dispatch({route:'room',rpc:'self_break_room',data:{roomcode:'100001'}});
assert.equal(setup.store.getState().room.kind,'outside');assert.equal(setup.store.getState().roomSocial,undefined);assert.equal(setup.getDisposals(),before+1);
setup.router.dispatch({route:'room',rpc:'self_break_room',data:{}});assert.equal(setup.getDisposals(),before+1);
});
test('all wire start paths preserve the original complete envelope after committed public state',()=>{
for(const rpc of ['self_makewar','other_makewar','player_prepare','other_join_room','self_join_room'] as const){
const setup=makeHandlers();const login=fixture('player-login-room.json');const players=login.players as any[];players[0]={...players[1],playerid:99,isprepare:0,onstate:0};players[1].isprepare=0;setup.session.handleLogin(login);
const events:PlatformToGameEvent[]=[];setup.observeEvent((event,snapshot)=>{events.push(event);if(event.type==='room.started'){assert.equal(snapshot.room.stage,1);if(rpc==='player_prepare')assert.equal(snapshot.seats.find(s=>s.seat===1)?.ready,true);}});
const data=rpc==='self_join_room'?{...fixture('self-join-room.json'),deskwar:1}:rpc==='other_join_room'?{seat:2,playerid:77,nickname:'new',avatar:'',sex:1,ip:'127.0.0.2',onstate:0,bean:90,isprepare:0,deskwar:1}:rpc==='player_prepare'?{seat:1,deskwar:1}:{unknownGameField:{cards:[1,2]}};
const message={route:rpc==='self_join_room'?'agent':'room',rpc,data,unknownEnvelopeField:42};
setup.handlers.dispatch(message);
const started=events.filter(e=>e.type==='room.started');assert.equal(started.length,1);assert.equal(started[0].sourceRpc,rpc);assert.equal(started[0].message,message);
if(rpc==='player_prepare')assert.deepEqual(events.map(e=>e.type),['room.player-ready','room.started']);
if(rpc==='other_join_room')assert.deepEqual(events.map(e=>e.type),['room.player-joined','room.started']);
}
});