88 listQuizPlayersWithStatusForSession ,
99 unbindQuizSocketPresence ,
1010} from "./quiz-players-registry" ;
11- import { resetQuizRoomForSession } from "./lib/adepts-quiz-room-store" ;
11+ import { applySeatNickRosterToQuizRelay , resetQuizRoomForSession } from "./lib/adepts-quiz-room-store" ;
12+ import { broadcastAdeptsQuizSync } from "./adepts" ;
13+ import { seedPandoraFromQuiz } from "./game" ;
1214
1315function queryAdeptsRoleLower ( socket : Socket ) : string {
1416 const q = socket . handshake . query [ "adeptsRole" ] ;
@@ -42,6 +44,49 @@ interface ChatEntry {
4244 text : string ;
4345}
4446
47+ interface PandoraLottoPublicState {
48+ phase : "setup" | "drum" ;
49+ names : string [ ] ;
50+ drumPhase : "spinning" | "rolling" | "revealed" ;
51+ drumKey : number ;
52+ winnerIndex : number | null ;
53+ }
54+
55+ const DEFAULT_PANDORA_LOTTO_PUBLIC : PandoraLottoPublicState = {
56+ phase : "setup" ,
57+ names : [ ] ,
58+ drumPhase : "spinning" ,
59+ drumKey : 0 ,
60+ winnerIndex : null ,
61+ } ;
62+
63+ function sanitizePandoraLottoPublicPayload ( payload : unknown ) : PandoraLottoPublicState {
64+ const po = payload && typeof payload === "object" ? ( payload as Record < string , unknown > ) : { } ;
65+ const phase = po [ "phase" ] === "drum" ? "drum" : "setup" ;
66+ const rawNames = po [ "names" ] ;
67+ const names = Array . isArray ( rawNames )
68+ ? rawNames . map ( ( x ) => String ( x ?? "" ) . trim ( ) . slice ( 0 , 16 ) ) . filter ( Boolean ) . slice ( 0 , 12 )
69+ : [ ] ;
70+ const dp = po [ "drumPhase" ] ;
71+ const drumPhase =
72+ dp === "rolling" || dp === "revealed" || dp === "spinning" ? dp : "spinning" ;
73+ const rawKey = po [ "drumKey" ] ;
74+ const kn = typeof rawKey === "number" ? rawKey : Number ( rawKey ) ;
75+ const drumKey = Number . isInteger ( kn ) && kn >= 0 && kn < 1_000_000 ? kn : 0 ;
76+ const wiRaw = po [ "winnerIndex" ] ;
77+ let winnerIndex : number | null = null ;
78+ if ( wiRaw !== null && wiRaw !== undefined && names . length > 0 ) {
79+ const n = typeof wiRaw === "number" ? wiRaw : Number ( wiRaw ) ;
80+ if ( Number . isInteger ( n ) && n >= 0 && n < names . length ) winnerIndex = n ;
81+ }
82+ if ( phase === "setup" ) {
83+ return { phase : "setup" , names, drumPhase : "spinning" , drumKey, winnerIndex : null } ;
84+ }
85+ let win = winnerIndex ;
86+ if ( drumPhase === "spinning" ) win = null ;
87+ return { phase : "drum" , names, drumPhase, drumKey, winnerIndex : win } ;
88+ }
89+
4590interface QuizNavSession {
4691 gameStarted : boolean ;
4792 lastBoardIndex : number | null ;
@@ -51,6 +96,12 @@ interface QuizNavSession {
5196 adeptsWheelActive : boolean ;
5297 adeptsWheelReturnHref : string | null ;
5398 adeptsWheelCurrentTurnSeat : number ;
99+ pandoraRouletteActive : boolean ;
100+ pandoraRouletteReturnHref : string | null ;
101+ pandoraRouletteCurrentTurnSeat : number ;
102+ pandoraRoulettePlayerNames : string [ ] ;
103+ pandoraLottoActive : boolean ;
104+ pandoraLottoPublic : PandoraLottoPublicState | null ;
54105}
55106
56107const navBySession = new Map < string , QuizNavSession > ( ) ;
@@ -67,6 +118,12 @@ function getNav(sessionId: string): QuizNavSession {
67118 adeptsWheelActive : false ,
68119 adeptsWheelReturnHref : null ,
69120 adeptsWheelCurrentTurnSeat : 0 ,
121+ pandoraRouletteActive : false ,
122+ pandoraRouletteReturnHref : null ,
123+ pandoraRouletteCurrentTurnSeat : 0 ,
124+ pandoraRoulettePlayerNames : [ ] ,
125+ pandoraLottoActive : false ,
126+ pandoraLottoPublic : null ,
70127 } ;
71128 navBySession . set ( sessionId , s ) ;
72129 }
@@ -153,15 +210,21 @@ export function setupQuizNav(io: Server) {
153210 const n = getNav ( sessionId ) ;
154211 const po = payload && typeof payload === "object" ? ( payload as Record < string , unknown > ) : { } ;
155212 const raw = po [ "seatPlayerNicks" ] ;
156- n . seatPlayerNicks = Array . isArray ( raw )
157- ? raw . map ( ( x ) => String ( x ?? "" ) . trim ( ) . slice ( 0 , 64 ) ) . filter ( Boolean ) . slice ( 0 , 5 )
213+ const row = Array . isArray ( raw )
214+ ? raw . map ( ( x ) => String ( x ?? "" ) . trim ( ) . slice ( 0 , 64 ) ) . slice ( 0 , 5 )
158215 : [ ] ;
216+ while ( row . length < 5 ) row . push ( "" ) ;
217+ n . seatPlayerNicks = row ;
159218
160219 n . gameStarted = true ;
161220 n . lobbyEmojiLineIndex = - 1 ;
162221 if ( n . lastBoardIndex === null || n . lastBoardIndex < 0 || n . lastBoardIndex > MAX_BOARD ) {
163222 n . lastBoardIndex = 0 ;
164223 }
224+
225+ applySeatNickRosterToQuizRelay ( sessionId , n . seatPlayerNicks ) ;
226+ broadcastAdeptsQuizSync ( io , sessionId , socket ) ;
227+
165228 const out = lobbyPayload ( sessionId ) ;
166229 ns . to ( sessionId ) . emit ( "lobbyState" , out ) ;
167230 ns . to ( sessionId ) . emit ( "phase" , { boardIndex : out . boardIndex } ) ;
@@ -196,6 +259,132 @@ export function setupQuizNav(io: Server) {
196259 logger . info ( { sessionId, returnHref, currentTurnSeat } , "Quiz adepts wheel opened" ) ;
197260 } ) ;
198261
262+ socket . on ( "hostPandoraRouletteOpen" , ( payload : unknown ) => {
263+ if ( ! isQuizNavLobbyHost ( socket ) ) return ;
264+ const n = getNav ( sessionId ) ;
265+ const po = payload && typeof payload === "object" ? ( payload as Record < string , unknown > ) : { } ;
266+ const hrefRaw = po [ "returnHref" ] ;
267+ const returnHref =
268+ typeof hrefRaw === "string" && hrefRaw . length > 0 && hrefRaw . length < 2048 ? hrefRaw : null ;
269+ const rawSeat = po [ "currentTurnSeat" ] ;
270+ const seatNum = typeof rawSeat === "number" ? rawSeat : Number ( rawSeat ) ;
271+ const currentTurnSeat =
272+ Number . isInteger ( seatNum ) && seatNum >= 0 && seatNum <= 4 ? seatNum : 0 ;
273+ const rawNames = po [ "playerNames" ] ;
274+ const playerNames = Array . isArray ( rawNames )
275+ ? rawNames . map ( ( x ) => String ( x ?? "" ) . trim ( ) . slice ( 0 , 64 ) ) . slice ( 0 , 5 )
276+ : [ ] ;
277+ while ( playerNames . length < 5 ) playerNames . push ( "" ) ;
278+ if ( ! returnHref ) return ;
279+ seedPandoraFromQuiz ( sessionId , {
280+ playerNames,
281+ initialTurnSeat : currentTurnSeat ,
282+ } ) ;
283+ n . pandoraRouletteActive = true ;
284+ n . pandoraRouletteReturnHref = returnHref ;
285+ n . pandoraRouletteCurrentTurnSeat = currentTurnSeat ;
286+ n . pandoraRoulettePlayerNames = [ ...playerNames ] ;
287+ ns . to ( sessionId ) . emit ( "pandoraRouletteOpened" , {
288+ returnHref,
289+ currentTurnSeat,
290+ playerNames,
291+ } ) ;
292+ logger . info ( { sessionId, returnHref, currentTurnSeat } , "Quiz Pandora roulette opened" ) ;
293+ } ) ;
294+
295+ socket . on ( "hostPandoraLottoOpen" , ( ) => {
296+ if ( ! isQuizNavLobbyHost ( socket ) ) return ;
297+ const n = getNav ( sessionId ) ;
298+ n . pandoraLottoActive = true ;
299+ n . pandoraLottoPublic = { ...DEFAULT_PANDORA_LOTTO_PUBLIC } ;
300+ ns . to ( sessionId ) . emit ( "pandoraLottoOpened" , { } ) ;
301+ socket . emit ( "pandoraLottoOpened" , { } ) ;
302+ ns . to ( sessionId ) . emit ( "pandoraLottoPublicState" , n . pandoraLottoPublic ) ;
303+ socket . emit ( "pandoraLottoPublicState" , n . pandoraLottoPublic ) ;
304+ logger . info ( { sessionId } , "Quiz Pandora lotto opened" ) ;
305+ } ) ;
306+
307+ socket . on ( "hostPandoraLottoPublicSync" , ( payload : unknown ) => {
308+ if ( ! isQuizNavLobbyHost ( socket ) ) return ;
309+ const n = getNav ( sessionId ) ;
310+ if ( ! n . pandoraLottoActive ) return ;
311+ const next = sanitizePandoraLottoPublicPayload ( payload ) ;
312+ n . pandoraLottoPublic = next ;
313+ ns . to ( sessionId ) . emit ( "pandoraLottoPublicState" , next ) ;
314+ socket . emit ( "pandoraLottoPublicState" , next ) ;
315+ } ) ;
316+
317+ socket . on ( "hostPandoraLottoClose" , ( ) => {
318+ if ( ! isQuizNavLobbyHost ( socket ) ) return ;
319+ const n = getNav ( sessionId ) ;
320+
321+ let targetSessionId = sessionId ;
322+ let targetNav = n ;
323+
324+ if ( ! n . pandoraLottoActive ) {
325+ for ( const [ sid , nav ] of navBySession . entries ( ) ) {
326+ if ( nav . pandoraLottoActive ) {
327+ targetSessionId = sid ;
328+ targetNav = nav ;
329+ break ;
330+ }
331+ }
332+ if ( ! targetNav . pandoraLottoActive ) return ;
333+ }
334+
335+ targetNav . pandoraLottoActive = false ;
336+ targetNav . pandoraLottoPublic = null ;
337+ ns . to ( targetSessionId ) . emit ( "pandoraLottoReturn" , { } ) ;
338+ logger . info ( { sessionId, targetSessionId } , "Quiz Pandora lotto closed" ) ;
339+ } ) ;
340+
341+ socket . on ( "requestPandoraLottoState" , ( ) => {
342+ const nav = getNav ( sessionId ) ;
343+ if ( nav . pandoraLottoActive ) {
344+ socket . emit ( "pandoraLottoOpened" , { } ) ;
345+ const pub = nav . pandoraLottoPublic ?? { ...DEFAULT_PANDORA_LOTTO_PUBLIC } ;
346+ nav . pandoraLottoPublic = pub ;
347+ socket . emit ( "pandoraLottoPublicState" , pub ) ;
348+ }
349+ } ) ;
350+
351+ socket . on ( "hostPandoraRouletteReturn" , ( ) => {
352+ if ( ! isQuizNavLobbyHost ( socket ) ) return ;
353+ const n = getNav ( sessionId ) ;
354+
355+ let targetSessionId = sessionId ;
356+ let targetNav = n ;
357+
358+ if ( ! n . pandoraRouletteActive || ! n . pandoraRouletteReturnHref ) {
359+ for ( const [ sid , nav ] of navBySession . entries ( ) ) {
360+ if ( nav . pandoraRouletteActive && nav . pandoraRouletteReturnHref ) {
361+ targetSessionId = sid ;
362+ targetNav = nav ;
363+ break ;
364+ }
365+ }
366+ if ( ! targetNav . pandoraRouletteActive || ! targetNav . pandoraRouletteReturnHref ) return ;
367+ }
368+
369+ const href = targetNav . pandoraRouletteReturnHref ;
370+ targetNav . pandoraRouletteActive = false ;
371+ targetNav . pandoraRouletteReturnHref = null ;
372+ targetNav . pandoraRoulettePlayerNames = [ ] ;
373+ ns . to ( targetSessionId ) . emit ( "pandoraRouletteReturn" , { returnHref : href } ) ;
374+ logger . info ( { sessionId, targetSessionId, returnHref : href } , "Quiz Pandora roulette return" ) ;
375+ } ) ;
376+
377+ socket . on ( "requestPandoraRouletteState" , ( ) => {
378+ const n = getNav ( sessionId ) ;
379+ if ( n . pandoraRouletteActive && n . pandoraRouletteReturnHref ) {
380+ socket . emit ( "pandoraRouletteOpened" , {
381+ returnHref : n . pandoraRouletteReturnHref ,
382+ currentTurnSeat : n . pandoraRouletteCurrentTurnSeat ,
383+ playerNames : [ ...n . pandoraRoulettePlayerNames ] ,
384+ } ) ;
385+ }
386+ } ) ;
387+
199388 socket . on ( "hostAdeptsWheelReturn" , ( ) => {
200389 if ( ! isQuizNavLobbyHost ( socket ) ) return ;
201390 const n = getNav ( sessionId ) ;
@@ -274,6 +463,11 @@ export function setupQuizNav(io: Server) {
274463 n . lobbyEmojiLineIndex = - 1 ;
275464 n . adeptsWheelActive = false ;
276465 n . adeptsWheelReturnHref = null ;
466+ n . pandoraRouletteActive = false ;
467+ n . pandoraRouletteReturnHref = null ;
468+ n . pandoraRoulettePlayerNames = [ ] ;
469+ n . pandoraLottoActive = false ;
470+ n . pandoraLottoPublic = null ;
277471 resetQuizRoomForSession ( sessionId ) ;
278472 clearQuizPlayers ( ) ;
279473 ns . emit ( "quizLobbyRoster" , { allSessions : true , players : [ ] } ) ;
0 commit comments