mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
barcode game colors!
This commit is contained in:
@@ -316,6 +316,7 @@ module.exports = {
|
||||
id: GAME_ID,
|
||||
title: 'Most items',
|
||||
description: 'Scan as many different known objects as possible.',
|
||||
themeColor: { r: 16, g: 185, b: 129 },
|
||||
createInitialState,
|
||||
normalizeState,
|
||||
activate,
|
||||
|
||||
@@ -301,6 +301,7 @@ module.exports = {
|
||||
id: GAME_ID,
|
||||
title: 'Scan quest',
|
||||
description: 'Scan one or two requested objects in order.',
|
||||
themeColor: { r: 34, g: 211, b: 238 },
|
||||
createInitialState,
|
||||
normalizeState,
|
||||
activate,
|
||||
|
||||
@@ -319,6 +319,7 @@ module.exports = {
|
||||
id: GAME_ID,
|
||||
title: 'Scans per second',
|
||||
description: 'Count every scan for five minutes and save the world record.',
|
||||
themeColor: { r: 245, g: 158, b: 11 },
|
||||
createInitialState,
|
||||
normalizeState,
|
||||
activate,
|
||||
|
||||
@@ -195,6 +195,24 @@ function getGameDefinition(gameId) {
|
||||
return GAMES_BY_ID[String(gameId || '')] || null;
|
||||
}
|
||||
|
||||
function normalizeRgbChannel(value) {
|
||||
if (!Number.isFinite(value)) return null;
|
||||
return Math.max(0, Math.min(255, Math.round(value)));
|
||||
}
|
||||
|
||||
function getGameThemeColor(definition) {
|
||||
const raw = definition?.themeColor;
|
||||
const r = normalizeRgbChannel(raw?.r);
|
||||
const g = normalizeRgbChannel(raw?.g);
|
||||
const b = normalizeRgbChannel(raw?.b);
|
||||
|
||||
// Games own their visual identity, but the socket payload should always be a
|
||||
// small safe RGB object. Invalid or missing values fall back to neutral text
|
||||
// colors in the browser instead of leaking arbitrary styling data into React.
|
||||
if (r === null || g === null || b === null) return null;
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function getKnownObjects() {
|
||||
try {
|
||||
const snapshot = getRegistrySnapshot();
|
||||
@@ -842,6 +860,7 @@ function buildStatePayload(socket = null) {
|
||||
id: game.id,
|
||||
title: game.title,
|
||||
description: game.description,
|
||||
themeColor: getGameThemeColor(game),
|
||||
voteCount: voteCounts[game.id] || 0,
|
||||
active: game.id === store.runningGameId,
|
||||
selected: game.id === store.selectedGameId,
|
||||
@@ -869,13 +888,16 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
|
||||
if (store.phase === 'running' && runningGame) {
|
||||
return {
|
||||
...runningGame,
|
||||
themeColor: getGameThemeColor(runningDefinition),
|
||||
participants,
|
||||
};
|
||||
}
|
||||
if (store.phase === 'results') {
|
||||
const resultDefinition = getGameDefinition(store.resultGameId);
|
||||
return {
|
||||
id: store.resultGameId,
|
||||
title: getGameDefinition(store.resultGameId)?.title || 'Results',
|
||||
title: resultDefinition?.title || 'Results',
|
||||
themeColor: getGameThemeColor(resultDefinition),
|
||||
status: 'results',
|
||||
participants,
|
||||
display: {
|
||||
@@ -892,6 +914,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
|
||||
return {
|
||||
id: store.selectedGameId,
|
||||
title: selectedDefinition?.title || 'Starting',
|
||||
themeColor: getGameThemeColor(selectedDefinition),
|
||||
status: 'starting',
|
||||
participants,
|
||||
display: {
|
||||
@@ -909,6 +932,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
|
||||
return {
|
||||
id: store.selectedGameId,
|
||||
title: selectedDefinition?.title || 'Join game',
|
||||
themeColor: getGameThemeColor(selectedDefinition),
|
||||
status: 'joining',
|
||||
participants,
|
||||
display: {
|
||||
@@ -926,6 +950,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
|
||||
return {
|
||||
id: store.selectedGameId,
|
||||
title: 'Voting',
|
||||
themeColor: getGameThemeColor(selectedDefinition),
|
||||
status: 'voting',
|
||||
participants,
|
||||
display: {
|
||||
@@ -944,6 +969,7 @@ function buildLifecycleGameState(store, { now, selectedDefinition, runningDefini
|
||||
return {
|
||||
id: null,
|
||||
title: 'Barcode games',
|
||||
themeColor: null,
|
||||
status: 'idle',
|
||||
participants,
|
||||
display: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const fsp = require('fs/promises');
|
||||
const Fuse = require('fuse.js');
|
||||
const { Ollama } = require('ollama');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('overseerControl');
|
||||
@@ -36,7 +35,6 @@ const overseerConfig = config.overseerControl || {};
|
||||
const RUN_MODE_AUTONOMOUS = 'autonomous';
|
||||
const RUN_MODE_DIRECT_ADDRESS = 'directAddress';
|
||||
const RUN_MODES = new Set([RUN_MODE_AUTONOMOUS, RUN_MODE_DIRECT_ADDRESS]);
|
||||
const DIRECT_ADDRESS_FUZZY_THRESHOLD = 0.3;
|
||||
const enabled = Boolean(overseerConfig.enabled);
|
||||
const observeOnly = overseerConfig.observeOnly !== false;
|
||||
const name = String(overseerConfig.name || DEFAULT_NAME).trim() || DEFAULT_NAME;
|
||||
@@ -53,42 +51,18 @@ const runWhileNoPeopleOnline = Boolean(overseerConfig.runWhileNoPeopleOnline);
|
||||
const profileImageUrl = String(overseerConfig.profileImageUrl || '').trim() || null;
|
||||
const ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null;
|
||||
|
||||
function normalizeDirectAddressText(value) {
|
||||
function normalizeNameMentionText(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
// Direct-address matching is intentionally word-oriented. Replacing
|
||||
// punctuation with spaces lets "overseer," and "overseer:" behave like the
|
||||
// same invocation phrase without letting punctuation affect Fuse scores.
|
||||
// Mention detection is intentionally literal to the configured name. The
|
||||
// only normalization here is presentation-level cleanup so punctuation and
|
||||
// repeated whitespace do not decide whether the configured name appears.
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
const directAddressName = normalizeDirectAddressText(name);
|
||||
const directAddressNameWords = directAddressName.split(' ').filter(Boolean);
|
||||
const directAddressAliases = (() => {
|
||||
const aliases = new Set();
|
||||
if (directAddressName) aliases.add(directAddressName);
|
||||
|
||||
// The default name includes a leading article, but users naturally shorten
|
||||
// "The Overseer" to "Overseer". Keeping this as an alias makes the direct
|
||||
// invocation humane while still requiring the match to be at the very start
|
||||
// of the chat message.
|
||||
if (directAddressNameWords[0] === 'the' && directAddressNameWords.length > 1) {
|
||||
aliases.add(directAddressNameWords.slice(1).join(' '));
|
||||
}
|
||||
|
||||
return Array.from(aliases).map((label) => ({
|
||||
label,
|
||||
wordCount: label.split(' ').filter(Boolean).length,
|
||||
}));
|
||||
})();
|
||||
const directAddressFuse = new Fuse(directAddressAliases, {
|
||||
includeScore: true,
|
||||
ignoreLocation: true,
|
||||
threshold: DIRECT_ADDRESS_FUZZY_THRESHOLD,
|
||||
keys: [{ name: 'label', weight: 1 }],
|
||||
});
|
||||
const normalizedConfiguredName = normalizeNameMentionText(name);
|
||||
|
||||
const runtime = {
|
||||
timer: null,
|
||||
@@ -324,66 +298,15 @@ function normalizeChatDraft(text) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function isConfiguredNameAddress(text) {
|
||||
const clean = String(text || '').trimStart();
|
||||
if (!clean) return false;
|
||||
function mentionsConfiguredName(text) {
|
||||
if (!normalizedConfiguredName) return false;
|
||||
const normalizedMessage = normalizeNameMentionText(text);
|
||||
if (!normalizedMessage) return false;
|
||||
|
||||
const lowerText = clean.toLowerCase();
|
||||
const lowerName = name.toLowerCase();
|
||||
if (!lowerText.startsWith(lowerName)) return false;
|
||||
|
||||
const nextChar = clean.charAt(name.length);
|
||||
if (!nextChar) return true;
|
||||
|
||||
// The character after the configured name must separate the invocation from
|
||||
// the rest of the message. This keeps "The Overseer, help" and "the overseer
|
||||
// help" valid while preventing accidental triggers such as "The Overseerish".
|
||||
return /[\s,.:;!?-]/.test(nextChar);
|
||||
}
|
||||
|
||||
function buildLeadingDirectAddressCandidates(text) {
|
||||
const words = normalizeDirectAddressText(text).split(' ').filter(Boolean);
|
||||
if (!words.length) return [];
|
||||
|
||||
const maxAliasWords = directAddressAliases.reduce((max, alias) => Math.max(max, alias.wordCount), 1);
|
||||
const maxWords = Math.min(words.length, Math.max(2, maxAliasWords + 1));
|
||||
const candidates = [];
|
||||
|
||||
for (let count = 1; count <= maxWords; count += 1) {
|
||||
// Each candidate is made only from the start of the message. This preserves
|
||||
// the "starts with the bot name" contract even though the actual comparison
|
||||
// is fuzzy.
|
||||
candidates.push(words.slice(0, count).join(' '));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function isFuzzyConfiguredNameAddress(text) {
|
||||
if (!directAddressAliases.length) return false;
|
||||
|
||||
const candidates = buildLeadingDirectAddressCandidates(text);
|
||||
for (const candidate of candidates) {
|
||||
const exactAlias = directAddressAliases.find((alias) => alias.label === candidate);
|
||||
if (exactAlias) return true;
|
||||
|
||||
const best = directAddressFuse.search(candidate)[0];
|
||||
if (!best || Number(best.score) > DIRECT_ADDRESS_FUZZY_THRESHOLD) continue;
|
||||
|
||||
const alias = best.item;
|
||||
const candidateWords = candidate.split(' ').filter(Boolean);
|
||||
if (candidateWords.length !== alias.wordCount) continue;
|
||||
|
||||
// Fuse can rate very short partial phrases surprisingly well. Requiring the
|
||||
// candidate to be close to the alias length keeps first words like "the" or
|
||||
// "over" from waking a multi-word bot while still allowing typos such as
|
||||
// "overseer" -> "oversear" or "the overseer" -> "teh overseer".
|
||||
if (Math.abs(candidate.length - alias.label.length) > 2) continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Pad both sides with spaces so a configured name only matches as a whole
|
||||
// normalized phrase. For example, "The Overseer" matches "hey, The Overseer"
|
||||
// but not "The Overseerish".
|
||||
return ` ${normalizedMessage} `.includes(` ${normalizedConfiguredName} `);
|
||||
}
|
||||
|
||||
function summarizeResult(result) {
|
||||
@@ -850,7 +773,7 @@ subscribe('chat:message', ({ payload } = {}) => {
|
||||
return;
|
||||
}
|
||||
if (!directAddressMode) return;
|
||||
if (!isConfiguredNameAddress(text) && !isFuzzyConfiguredNameAddress(text)) return;
|
||||
if (!mentionsConfiguredName(text)) return;
|
||||
void runDirectAddressCycle('direct_address_chat');
|
||||
});
|
||||
verificationEvents.on('change', () => evaluateSchedulerGate('online vote update'));
|
||||
|
||||
Reference in New Issue
Block a user