mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
even more more barcode game stuff
This commit is contained in:
@@ -224,6 +224,9 @@ function getPublicState(rawState, context = {}) {
|
||||
const remainingMs = state.stepStartedAt
|
||||
? Math.max(0, REQUEST_TIMEOUT_MS - (now - state.stepStartedAt))
|
||||
: 0;
|
||||
const currentStep = state.currentQuest?.steps?.[state.progressIndex] || null;
|
||||
const totalSteps = state.currentQuest?.steps?.length || 0;
|
||||
const stepLabel = totalSteps > 1 ? `Step ${state.progressIndex + 1} of ${totalSteps}` : 'Find the object';
|
||||
return {
|
||||
id: GAME_ID,
|
||||
title: 'Scan quest',
|
||||
@@ -238,6 +241,28 @@ function getPublicState(rawState, context = {}) {
|
||||
},
|
||||
remainingMs,
|
||||
actionLabel: 'Start quest',
|
||||
display: {
|
||||
// The display payload is intentionally structured instead of HTML or
|
||||
// React component names. Games describe what should be shown, while each
|
||||
// client surface decides how large or compact that information should be.
|
||||
title: 'Scan quest',
|
||||
primary: currentStep ? `Scan ${currentStep.label}` : 'Add object barcodes',
|
||||
secondary: currentStep ? stepLabel : 'The registry needs at least one object',
|
||||
timer: state.stepStartedAt
|
||||
? {
|
||||
label: 'Time left',
|
||||
endsAt: state.stepStartedAt + REQUEST_TIMEOUT_MS,
|
||||
}
|
||||
: null,
|
||||
stats: [
|
||||
{ label: 'Completed', value: state.completedQuests },
|
||||
{ label: 'Quest points', value: getTopScores(state)[0]?.points || 0 },
|
||||
],
|
||||
results: state.recentEvents.slice(0, 3).map((event) => ({
|
||||
label: event.kind === 'timeout' ? 'Skipped' : event.kind,
|
||||
value: event.label || event.points || '',
|
||||
})),
|
||||
},
|
||||
scores: getTopScores(state),
|
||||
completedQuests: state.completedQuests,
|
||||
recentEvents: state.recentEvents,
|
||||
|
||||
@@ -223,6 +223,14 @@ function getPublicState(rawState, context = {}) {
|
||||
? calculateRate(state.scans.length, state.startedAt, elapsedEnd)
|
||||
: state.finalResult?.scansPerSecond || 0;
|
||||
const remainingMs = state.status === 'running' ? Math.max(0, (state.endsAt || now) - now) : 0;
|
||||
const worldRecordText = state.worldRecord
|
||||
? `${Number(state.worldRecord.scansPerSecond || 0).toFixed(2)} scans per second`
|
||||
: 'none yet';
|
||||
const primary = state.status === 'running'
|
||||
? `${Number(currentRate).toFixed(2)} scans per second`
|
||||
: state.finalResult
|
||||
? `${Number(state.finalResult.scansPerSecond || 0).toFixed(2)} scans per second`
|
||||
: 'Scan anything';
|
||||
|
||||
return {
|
||||
id: GAME_ID,
|
||||
@@ -242,6 +250,33 @@ function getPublicState(rawState, context = {}) {
|
||||
participants: Object.values(state.participantCounts || {}).sort((a, b) => (b.scanCount || 0) - (a.scanCount || 0)),
|
||||
recentRounds: state.recentRounds,
|
||||
actionLabel: state.status === 'running' ? 'Running' : 'Start round',
|
||||
display: {
|
||||
// This display description lets the scanner page and the driver panel
|
||||
// render the same game state at different scales without hardcoding
|
||||
// scans-per-second-specific UI branches in shared React components.
|
||||
title: 'Scans per second',
|
||||
primary,
|
||||
secondary: state.status === 'running'
|
||||
? `${state.scans.length} scans counted`
|
||||
: state.finalResult
|
||||
? `${state.finalResult.scanCount} scans in the last round`
|
||||
: 'Five minute scan challenge',
|
||||
timer: state.status === 'running' && state.endsAt
|
||||
? {
|
||||
label: 'Time left',
|
||||
endsAt: state.endsAt,
|
||||
}
|
||||
: null,
|
||||
stats: [
|
||||
{ label: 'Scans', value: state.scans.length },
|
||||
{ label: 'Current rate', value: Number(currentRate).toFixed(2) },
|
||||
{ label: 'World record', value: worldRecordText },
|
||||
],
|
||||
results: state.recentRounds.slice(0, 3).map((round) => ({
|
||||
label: 'Round',
|
||||
value: `${Number(round.scansPerSecond || 0).toFixed(2)} scans per second`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -258,23 +258,6 @@ function activateGame(draft, gameId, now) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetGame(draft, gameId, now) {
|
||||
const definition = getGameDefinition(gameId);
|
||||
if (!definition) return false;
|
||||
const currentState = ensureGameState(draft, gameId);
|
||||
const nextState = definition.reset
|
||||
? definition.reset(currentState, buildGameContext(draft, { now }))
|
||||
: definition.createInitialState();
|
||||
draft.games[gameId] = nextState;
|
||||
draft.activeGameId = gameId;
|
||||
addRecentEvent(draft, {
|
||||
kind: 'gameReset',
|
||||
gameId,
|
||||
title: definition.title,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeGameResult(result, fallbackState) {
|
||||
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
||||
return { state: fallbackState, awards: [] };
|
||||
@@ -349,7 +332,7 @@ function setVote(socket, gameId) {
|
||||
});
|
||||
|
||||
broadcastState();
|
||||
return { success: true, state: buildStatePayload() };
|
||||
return { success: true, state: buildStatePayload(socket) };
|
||||
}
|
||||
|
||||
function settleActiveGameIfNeeded() {
|
||||
@@ -404,29 +387,40 @@ function handleScan(scan) {
|
||||
broadcastState();
|
||||
}
|
||||
|
||||
function resetActiveGame() {
|
||||
const now = Date.now();
|
||||
let resetGameId = null;
|
||||
|
||||
withGameStore((draft) => {
|
||||
const gameId = draft.activeGameId;
|
||||
if (!getGameDefinition(gameId)) return;
|
||||
resetGameId = gameId;
|
||||
resetGame(draft, gameId, now);
|
||||
});
|
||||
|
||||
if (!resetGameId) return { error: 'no active barcode game' };
|
||||
broadcastState();
|
||||
return { success: true, state: buildStatePayload() };
|
||||
}
|
||||
|
||||
function topCounters(bucket = {}, limit = 5) {
|
||||
return Object.values(bucket || {})
|
||||
.sort((a, b) => (b.count || 0) - (a.count || 0))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function buildStatePayload() {
|
||||
function getPlayerForSocket(store, socket) {
|
||||
if (!socket) return null;
|
||||
const identity = getIdentitySummary(socket);
|
||||
const playerKey = normalizePlayerKey(identity, socket.id, '');
|
||||
const player = playerKey ? store.players?.[playerKey] || null : null;
|
||||
if (!player) {
|
||||
return {
|
||||
playerKey,
|
||||
nickname: identity.nickname || null,
|
||||
totalPoints: 0,
|
||||
rank: null,
|
||||
games: {},
|
||||
};
|
||||
}
|
||||
const rankedPlayers = Object.values(store.players || {})
|
||||
.filter((entry) => Number.isFinite(entry?.totalPoints) && entry.totalPoints > 0)
|
||||
.sort((a, b) => (b.totalPoints || 0) - (a.totalPoints || 0));
|
||||
const rank = rankedPlayers.findIndex((entry) => entry.playerKey === player.playerKey) + 1;
|
||||
return {
|
||||
playerKey: player.playerKey,
|
||||
nickname: player.nickname || identity.nickname || null,
|
||||
totalPoints: player.totalPoints || 0,
|
||||
rank: rank > 0 ? rank : null,
|
||||
games: player.games || {},
|
||||
};
|
||||
}
|
||||
|
||||
function buildStatePayload(socket = null) {
|
||||
settleActiveGameIfNeeded();
|
||||
const store = loadStore();
|
||||
const now = Date.now();
|
||||
@@ -452,6 +446,7 @@ function buildStatePayload() {
|
||||
})),
|
||||
activeGame,
|
||||
leaderboard,
|
||||
ownPlayer: getPlayerForSocket(store, socket),
|
||||
counters: {
|
||||
objects: topCounters(store.globalCounters?.objects),
|
||||
rovers: topCounters(store.globalCounters?.rovers),
|
||||
@@ -481,13 +476,20 @@ function ensureReadonlyGameState(store, gameId) {
|
||||
}
|
||||
|
||||
function broadcastState() {
|
||||
io.to(GAME_SOCKET_ROOM).emit('barcodeGame:state', buildStatePayload());
|
||||
const room = io.sockets.adapter.rooms.get(GAME_SOCKET_ROOM);
|
||||
if (!room) return;
|
||||
room.forEach((socketId) => {
|
||||
const socket = io.sockets.sockets.get(socketId);
|
||||
if (socket) {
|
||||
socket.emit('barcodeGame:state', buildStatePayload(socket));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('barcodeGame:subscribe', (_payload = {}, cb = () => {}) => {
|
||||
socket.join(GAME_SOCKET_ROOM);
|
||||
const state = buildStatePayload();
|
||||
const state = buildStatePayload(socket);
|
||||
socket.emit('barcodeGame:state', state);
|
||||
cb({ success: true, state });
|
||||
});
|
||||
@@ -501,14 +503,6 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('barcodeGame:resetActive', (_payload = {}, cb = () => {}) => {
|
||||
try {
|
||||
cb(resetActiveGame(socket));
|
||||
} catch (err) {
|
||||
logger.warn('Barcode game reset failed', { error: err.message });
|
||||
cb({ error: err.message || 'barcode game reset failed' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
subscribe('barcode.scanned', (event) => {
|
||||
@@ -524,7 +518,6 @@ subscribe('barcode.scanned', (event) => {
|
||||
module.exports = {
|
||||
buildStatePayload,
|
||||
handleScan,
|
||||
resetActiveGame,
|
||||
setVote,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user