#!/usr/bin/env node const puppeteer = require('puppeteer'); async function checkRoomStatus(roomCode) { try { const response = await fetch(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`); if (response.ok) { const data = await response.json(); const roomData = data.body || data; if (process.env.DEBUG) { console.error('[API] Room data:', JSON.stringify(roomData, null, 2)); } return { exists: true, locked: roomData.locked || false, full: roomData.full || false, maxPlayers: roomData.maxPlayers || 8, minPlayers: roomData.minPlayers || 0 }; } return { exists: false }; } catch (e) { if (process.env.DEBUG) { console.error('[API] Error checking room:', e.message); } return { exists: false }; } } async function getPlayerCountFromAudience(roomCode) { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); let playerCount = null; // Enable CDP and listen for WebSocket frames BEFORE navigating const client = await page.target().createCDPSession(); await client.send('Network.enable'); client.on('Network.webSocketFrameReceived', ({ response }) => { if (response.payloadData && playerCount === null) { try { const data = JSON.parse(response.payloadData); // Check for bc:room with player count data let roomVal = null; if (data.opcode === 'client/welcome' && data.result?.entities?.['bc:room']) { roomVal = data.result.entities['bc:room'][1]?.val; } if (data.opcode === 'object' && data.result?.key === 'bc:room') { roomVal = data.result.val; } if (roomVal) { // Strategy 1: Game ended - use gameResults if (roomVal.gameResults?.players) { playerCount = roomVal.gameResults.players.length; if (process.env.DEBUG) { console.error('[SUCCESS] Found', playerCount, 'players from gameResults'); } } // Strategy 2: Game in progress - use analytics if (playerCount === null && roomVal.analytics) { const startAnalytic = roomVal.analytics.find(a => a.action === 'start'); if (startAnalytic?.value) { playerCount = startAnalytic.value; if (process.env.DEBUG) { console.error('[SUCCESS] Found', playerCount, 'players from analytics'); } } } } } catch (e) { // Ignore parse errors } } }); try { if (process.env.DEBUG) console.error('[2] Navigating to jackbox.tv...'); await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 }); if (process.env.DEBUG) console.error('[3] Waiting for form...'); await page.waitForSelector('input#roomcode', { timeout: 10000 }); await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); if (process.env.DEBUG) console.error('[4] Typing room code:', roomCode); const roomInput = await page.$('input#roomcode'); await roomInput.type(roomCode.toUpperCase(), { delay: 50 }); await new Promise(resolve => setTimeout(resolve, 2000)); if (process.env.DEBUG) console.error('[5] Typing name...'); const nameInput = await page.$('input#username'); await nameInput.type('CountBot', { delay: 30 }); if (process.env.DEBUG) console.error('[6] Waiting for JOIN AUDIENCE button...'); await page.waitForFunction(() => { const buttons = Array.from(document.querySelectorAll('button')); return buttons.some(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled); }, { timeout: 10000 }); if (process.env.DEBUG) console.error('[7] Clicking JOIN AUDIENCE...'); await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button')); const btn = buttons.find(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled); if (btn) btn.click(); }); // Wait for WebSocket messages if (process.env.DEBUG) console.error('[8] Waiting for player count...'); for (let i = 0; i < 20 && playerCount === null; i++) { await new Promise(resolve => setTimeout(resolve, 300)); } } finally { await browser.close(); } return playerCount; } async function getPlayerCount(roomCode) { if (process.env.DEBUG) console.error('[1] Checking room status via API...'); const roomStatus = await checkRoomStatus(roomCode); if (!roomStatus.exists) { if (process.env.DEBUG) console.error('[ERROR] Room does not exist'); return 0; } // If full, return maxPlayers if (roomStatus.full) { if (process.env.DEBUG) console.error('[1] Room is FULL - returning maxPlayers:', roomStatus.maxPlayers); return roomStatus.maxPlayers; } // If locked (game in progress), join as audience to get count if (roomStatus.locked) { if (process.env.DEBUG) console.error('[1] Room is LOCKED - joining as audience...'); try { const count = await getPlayerCountFromAudience(roomCode); if (count !== null) { return count; } } catch (e) { if (process.env.DEBUG) console.error('[ERROR] Failed to get count:', e.message); } // Fallback to maxPlayers if we couldn't get exact count if (process.env.DEBUG) console.error('[1] Could not get exact count, returning maxPlayers:', roomStatus.maxPlayers); return roomStatus.maxPlayers; } // Not locked (lobby open) - don't join, return minPlayers if (process.env.DEBUG) console.error('[1] Room is NOT locked (lobby) - returning minPlayers:', roomStatus.minPlayers); return roomStatus.minPlayers; } // Main const roomCode = process.argv[2]; if (!roomCode) { console.error('Usage: node jackbox-count.js '); process.exit(1); } getPlayerCount(roomCode.toUpperCase()) .then(count => { console.log(count); process.exit(0); }) .catch(err => { if (process.env.DEBUG) console.error('Error:', err.message); console.log(0); process.exit(0); });