Files
joywayer 35216f75e7 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.
2026-09-09 03:13:18 +08:00

55 lines
4.8 KiB
TypeScript

import {readFileSync} from 'node:fs';
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {PlatformStore} from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
import {parseLoginResponse,parseOtherOfflinePayload} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
function roomData(count=4):Record<string,unknown>{
const data=JSON.parse(readFileSync(new URL('../fixtures/contracts/player-login-room.json',import.meta.url),'utf8')).data;
const self=data.players[1];
data.players=Array.from({length:count},(_,seat)=>seat===1?self:{...self,playerid:500000+seat,nickname:`玩家${seat}`,onstate:0});
return data;
}
function setup(count=4){const store=new PlatformStore();store.applyLoginSuccess(parseLoginResponse(roomData(count)));return store;}
test('apply snapshots dynamic participants and deadline; all agree remains active pending free_room',()=>{
const store=setup(12);store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,...Array(11).fill(-1)],countdown:60}},1000);
const vote=store.getState().dissolution;assert.ok(vote?.kind==='active');assert.equal(vote.participants.length,12);assert.equal(vote?.initiator?.nickname,'玩家0');assert.equal(vote?.deadline,61000);
for(let seat=1;seat<12;seat++)store.applyDissolution('other_agree_free_room',{seat},5000);
assert.equal(store.getState().dissolution?.kind,'active');
store.playerOffline(parseOtherOfflinePayload({seat:0}));assert.equal(store.getState().dissolution?.kind,'active');
});
test('refusal result keeps participant snapshots and acknowledgement only clears vote',()=>{
const store=setup();store.applyDissolution('self_apply_free_room',{agreefree:{state:[-1,0,-1,-1],countdown:20}},0);
const room=store.getState().room;
store.applyDissolution('other_refuse_free_room',{seat:2},1);
const vote=store.getState().dissolution;assert.ok(vote?.kind==='result');assert.equal(vote.passed,false);assert.equal(vote.refusedSeat,2);
store.acknowledgeDissolution();assert.equal(store.getState().dissolution,null);assert.equal(store.getState().room,room);
});
test('server result interprets seats as states and writes zero roomcard, preserving deskfree',()=>{
const store=setup();store.applyDissolution('free_room',{seats:[0,1,-1,1],roomcard:0,deskfree:{rounds:[1]}},0);
const vote=store.getState().dissolution;assert.ok(vote?.kind==='result');assert.equal(vote.passed,true);assert.equal(vote?.initiator?.seat,0);assert.deepEqual(vote?.participants.map(p=>p.state),[0,1,-1,1]);assert.deepEqual(vote?.deskfree,{rounds:[1]});assert.equal(store.getState().players.entities[430511].roomcard,0);
});
test('reconnect restores vote atomically from agreefree; malformed source never commits',()=>{
const store=setup();const data=roomData();data.agreefree={state:[-1,1,0,-1],countdown:23};
let commits=0;store.subscribe(()=>commits++);const before=Date.now();store.applyLoginSuccess(parseLoginResponse(data));
const vote=store.getState().dissolution;assert.ok(vote?.kind==='active');assert.equal(commits,1);assert.equal(vote.initiator.seat,2);assert.ok(vote.deadline>=before+23000);
const state=store.getState();assert.throws(()=>store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,0,-1,-1],countdown:3}},0),/initiator/);assert.equal(store.getState(),state);
});
test('freeNow allows no seats and retains server tips without inventing countdown',()=>{
const store=setup();store.applyDissolution('free_room',{freeNow:1,tips:'强制解散',deskfree:{round:1}},0);
const vote=store.getState().dissolution;assert.ok(vote?.kind==='result');assert.equal(vote.freeNow,true);assert.equal(vote.initiator,null);assert.equal(vote.tips,'强制解散');
});
test('sparse room vote state preserves absolute seats and rejects malformed source atomically',()=>{
const data=roomData();(data.players as unknown[])[2]=null;const store=new PlatformStore();store.applyLoginSuccess(parseLoginResponse(data));
store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,-1,null,1],countdown:0}},99);
const vote=store.getState().dissolution;assert.deepEqual(vote?.participants.map(p=>p.seat),[0,1,3]);
const before=store.getState();for(const state of [[0,-1,-1,1],[0,-1],[0,-1,null,9]])assert.throws(()=>store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state,countdown:10}},0));
assert.throws(()=>store.applyDissolution('free_room',{seats:[0,1,null,1],roomcard:'bad'},0));assert.equal(store.getState(),before);
});
test('self vote is acknowledged by the server and a fresh login clears stale vote',()=>{
const store=setup();store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,-1,-1,-1],countdown:5}},0);
store.applyDissolution('self_agree_free_room',{},1);assert.equal(store.getState().dissolution?.participants[1].state,1);
store.applyLoginSuccess(parseLoginResponse(roomData()));assert.equal(store.getState().dissolution,undefined);
});