mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
even more more barcode game stuff
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -18,8 +18,8 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-Bv_Ho3il.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-esPFU1Dr.css">
|
||||
<script type="module" crossorigin src="/assets/index-I6c2my6z.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CBt8gbRK.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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