@@ -15,6 +15,7 @@ import {
1515 HostServerBootstrap ,
1616 INPUT_INGESTION_REJECTION_CODES ,
1717 InterestManager ,
18+ LoopbackTransport ,
1819 NetworkConditionSimulator ,
1920 NetworkingLayer ,
2021 PredictionReconciler ,
@@ -313,6 +314,92 @@ export async function run() {
313314 REPLICATION_MESSAGE_REJECTION_CODES . SNAPSHOT_TYPE_INVALID ,
314315 ) ;
315316
317+ // Level 12.4: one minimal playable multiplayer validation slice.
318+ const playableSessionId = 'session-playable-minimal' ;
319+ const [ playableHostTransport , playableClientTransport ] = LoopbackTransport . createLinkedPair (
320+ 'host-playable' ,
321+ 'client-playable' ,
322+ ) ;
323+ const playableHandshake = new HandshakeSimulator ( {
324+ sessionId : playableSessionId ,
325+ hostId : 'host-playable' ,
326+ clientId : 'client-playable' ,
327+ hostTransport : playableHostTransport ,
328+ clientTransport : playableClientTransport ,
329+ } ) ;
330+ assert . equal ( playableHandshake . begin ( { token : 'playable-token' } ) , true ) ;
331+ const playableHandshakeState = playableHandshake . update ( ) ;
332+ assert . equal ( playableHandshakeState . handshakeComplete , true ) ;
333+ assert . equal ( playableHandshakeState . host . state , SESSION_STATES . ACTIVE ) ;
334+ assert . equal ( playableHandshakeState . client . state , SESSION_STATES . ACTIVE ) ;
335+
336+ const playableServer = new AuthoritativeServerRuntime ( {
337+ sessionId : playableSessionId ,
338+ tickRateHz : 20 ,
339+ } ) ;
340+ const playableServerStarted = playableServer . start ( ) ;
341+ assert . equal ( playableServerStarted . runtimePhase , SERVER_RUNTIME_PHASES . RUNNING ) ;
342+
343+ const playableClient = new ClientReplicationApplicationLayer ( {
344+ sessionId : playableSessionId ,
345+ reconciliationStrategy : new ClientReconciliationStrategy ( ) ,
346+ } ) ;
347+ const playableStateReplication = new StateReplication ( ) ;
348+ let authoritativePlayableEntities = [ { id : 'player-1' , x : 0 , y : 0 } ] ;
349+
350+ const playableInputAccepted = playableServer . ingestClientInput ( {
351+ sessionId : playableSessionId ,
352+ clientId : 'client-playable' ,
353+ sequence : 0 ,
354+ inputType : 'move' ,
355+ payload : { dx : 3 , dy : 0 } ,
356+ sentAtMs : 1 ,
357+ } ) ;
358+ assert . equal ( playableInputAccepted . ok , true ) ;
359+
360+ playableServer . step ( 0.05 ) ;
361+ const playableInputs = playableServer . drainAcceptedInputs ( ) ;
362+ assert . equal ( playableInputs . length , 1 ) ;
363+ authoritativePlayableEntities = authoritativePlayableEntities . map ( ( entity ) => ( {
364+ ...entity ,
365+ x : entity . id === 'player-1'
366+ ? entity . x + ( playableInputs [ 0 ] . payload . dx ?? 0 )
367+ : entity . x ,
368+ y : entity . id === 'player-1'
369+ ? entity . y + ( playableInputs [ 0 ] . payload . dy ?? 0 )
370+ : entity . y ,
371+ } ) ) ;
372+
373+ const playableSnapshot = playableStateReplication . createSnapshot ( authoritativePlayableEntities , {
374+ tick : playableServer . getSnapshot ( ) . authoritativeTick ,
375+ } ) ;
376+ assert . equal ( playableClient . ingestReplicationEnvelope ( {
377+ sessionId : playableSessionId ,
378+ replicationSequence : 0 ,
379+ authoritativeTick : playableSnapshot . tick ,
380+ snapshotType : REPLICATION_SNAPSHOT_TYPES . FULL ,
381+ snapshot : {
382+ entities : playableSnapshot . entities ,
383+ despawned : playableSnapshot . despawned ,
384+ } ,
385+ sentAtMs : 5 ,
386+ } ) . ok , true ) ;
387+ const playableApplyResult = playableClient . applyPendingReplication ( ) ;
388+ assert . equal ( playableApplyResult . applied , 1 ) ;
389+ assert . equal ( playableApplyResult . ignored , 0 ) ;
390+ assert . equal (
391+ playableClient . getReplicatedStateSnapshot ( ) . find ( ( entity ) => entity . id === 'player-1' ) . x ,
392+ authoritativePlayableEntities [ 0 ] . x ,
393+ ) ;
394+
395+ const playableDisconnectState = playableHandshake . disconnect ( 'playable-cleanup' ) ;
396+ assert . equal ( playableDisconnectState . host . state , SESSION_STATES . DISCONNECTED ) ;
397+ assert . equal ( playableDisconnectState . client . state , SESSION_STATES . DISCONNECTED ) ;
398+ const playableServerStopped = playableServer . stop ( 'playable-cleanup' ) ;
399+ assert . equal ( playableServerStopped . runtimePhase , SERVER_RUNTIME_PHASES . STOPPED ) ;
400+ assert . equal ( playableServer . drainAcceptedInputs ( ) . length , 0 ) ;
401+ assert . equal ( playableClient . getReplicationStatus ( ) . pendingEnvelopes , 0 ) ;
402+
316403 const replication = new StateReplication ( ) ;
317404 const snapshot = replication . createSnapshot ( [ { id : 'npc-1' , x : 10 , y : 20 } ] , { tick : 3 } ) ;
318405 const encoded = replication . encodeSnapshot ( snapshot ) ;
0 commit comments