mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 18:10:47 -04:00
buttonbox
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
// button Box Service store
|
||||
// Purpose: Owns persisted button-box state shape, normalization, and atomic file read/write operations.
|
||||
// Scope: Keeps runtime behavior unchanged by isolating state persistence mechanics from reward/HTTP orchestration.
|
||||
const fs = require('fs');
|
||||
|
||||
function createButtonBoxStore(deps) {
|
||||
const {
|
||||
logger,
|
||||
getRewardById,
|
||||
listRewards,
|
||||
dataDir,
|
||||
storePath,
|
||||
buttonCount,
|
||||
storeVersion,
|
||||
} = deps;
|
||||
|
||||
let state = null;
|
||||
|
||||
function createDefaultButton(id) {
|
||||
return {
|
||||
id,
|
||||
count: 0,
|
||||
rewardId: null,
|
||||
rewardName: null,
|
||||
rewardNumber: null,
|
||||
goal: null,
|
||||
lastIncrementAt: null,
|
||||
lastRewardAt: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultState() {
|
||||
return {
|
||||
version: storeVersion,
|
||||
updatedAt: Date.now(),
|
||||
buttons: Array.from({ length: buttonCount }, (_, idx) => createDefaultButton(idx + 1)),
|
||||
effects: {},
|
||||
};
|
||||
}
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function assignNewReward(button, options = {}) {
|
||||
const allRewards = listRewards();
|
||||
if (!allRewards.length) throw new Error('No rewards configured');
|
||||
const excludeRewardId = typeof options.excludeRewardId === 'string' ? options.excludeRewardId : null;
|
||||
|
||||
const usedByOtherButtons = new Set(
|
||||
state.buttons
|
||||
.filter((entry) => entry.id !== button.id)
|
||||
.map((entry) => entry.rewardId)
|
||||
.filter((rewardId) => typeof rewardId === 'string' && rewardId.length > 0),
|
||||
);
|
||||
const uniqueCandidates = allRewards.filter(
|
||||
(reward) => !usedByOtherButtons.has(reward.id) && (!excludeRewardId || reward.id !== excludeRewardId),
|
||||
);
|
||||
const fallbackUniqueCandidates = allRewards.filter((reward) => !usedByOtherButtons.has(reward.id));
|
||||
const pool = uniqueCandidates.length
|
||||
? uniqueCandidates
|
||||
: fallbackUniqueCandidates.length
|
||||
? fallbackUniqueCandidates
|
||||
: allRewards.filter((reward) => !excludeRewardId || reward.id !== excludeRewardId);
|
||||
const reward = pool[Math.floor(Math.random() * pool.length)] || null;
|
||||
if (!reward) throw new Error('No rewards configured');
|
||||
|
||||
button.rewardId = reward.id;
|
||||
button.rewardName = reward.name || null;
|
||||
button.rewardNumber = reward.number;
|
||||
button.goal = reward.goal;
|
||||
button.count = 0;
|
||||
}
|
||||
|
||||
function ensureRewardAssignments() {
|
||||
if (!state) return;
|
||||
const seen = new Set();
|
||||
state.buttons.forEach((button) => {
|
||||
const reward = getRewardById(button.rewardId);
|
||||
if (reward && !seen.has(reward.id)) {
|
||||
seen.add(reward.id);
|
||||
button.rewardName = reward.name || null;
|
||||
button.rewardNumber = reward.number;
|
||||
button.goal = reward.goal;
|
||||
if (!Number.isFinite(button.count) || button.count < 0) {
|
||||
button.count = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
assignNewReward(button);
|
||||
if (button.rewardId) {
|
||||
seen.add(button.rewardId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeLoaded(raw = {}) {
|
||||
const base = createDefaultState();
|
||||
const sourceButtons = Array.isArray(raw.buttons) ? raw.buttons : [];
|
||||
const buttons = base.buttons.map((button, idx) => {
|
||||
const loaded = sourceButtons[idx] || {};
|
||||
return {
|
||||
...button,
|
||||
count: Number.isFinite(loaded.count) ? Math.max(0, Math.floor(loaded.count)) : 0,
|
||||
rewardId: typeof loaded.rewardId === 'string' ? loaded.rewardId : null,
|
||||
rewardName: typeof loaded.rewardName === 'string' ? loaded.rewardName : null,
|
||||
rewardNumber: Number.isFinite(loaded.rewardNumber) ? Math.floor(loaded.rewardNumber) : null,
|
||||
goal: Number.isFinite(loaded.goal) ? Math.max(1, Math.floor(loaded.goal)) : null,
|
||||
lastIncrementAt: Number.isFinite(loaded.lastIncrementAt) ? loaded.lastIncrementAt : null,
|
||||
lastRewardAt: Number.isFinite(loaded.lastRewardAt) ? loaded.lastRewardAt : null,
|
||||
};
|
||||
});
|
||||
|
||||
const effects = raw.effects && typeof raw.effects === 'object' ? raw.effects : {};
|
||||
return {
|
||||
version: storeVersion,
|
||||
updatedAt: Number.isFinite(raw.updatedAt) ? raw.updatedAt : Date.now(),
|
||||
buttons,
|
||||
effects,
|
||||
};
|
||||
}
|
||||
|
||||
function writeState() {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
const next = {
|
||||
...state,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const tempPath = `${storePath}.${process.pid}.${Date.now()}.tmp`;
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
||||
fs.renameSync(tempPath, storePath);
|
||||
state = next;
|
||||
}
|
||||
|
||||
function loadState() {
|
||||
if (state) return state;
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(storePath, 'utf8'));
|
||||
state = normalizeLoaded(raw);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
logger.warn('Failed to load button box store', err.message);
|
||||
}
|
||||
state = createDefaultState();
|
||||
}
|
||||
ensureRewardAssignments();
|
||||
writeState();
|
||||
return state;
|
||||
}
|
||||
|
||||
function getState() {
|
||||
loadState();
|
||||
return state;
|
||||
}
|
||||
|
||||
function getStateClone() {
|
||||
return clone({ buttons: getState().buttons });
|
||||
}
|
||||
|
||||
return {
|
||||
loadState,
|
||||
writeState,
|
||||
getState,
|
||||
getStateClone,
|
||||
clone,
|
||||
assignNewReward,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createButtonBoxStore,
|
||||
};
|
||||
Reference in New Issue
Block a user