Adding battery %, memory MB, and CPU temp gauges per bot in the final game. Only the local player's bot is shown in a panel overlaid on the top-left corner of the grid. Two consequences: battery death (permanent stop) and cooldown (temporary freeze when CPU or memory maxes out, then auto-resume).
| Resource | Random start | Dijkstra start | Max | Change per tick |
|---|---|---|---|---|
| Battery % | 100 | 100 | 100 | -1% every move |
| Memory MB | 0 MB | 128 MB | 256 MB | 0 (random) / +5-10 MB (Dijkstra) |
| CPU C | 50 | 70 | 100 | +/-3-5 random (random) / +3-5 only (Dijkstra) |
- Battery death: grayscale filter on bot sprite + red OFFLINE badge in panel
- Cooldown trigger: red tint (CSS filter) on bot sprite + yellow "COOLING DOWN" message in panel
- Cooldown: bot fully frozen; resources drain back to starting values per tick; then auto-resumes
- Panel: always visible from page load (shows starting values before game starts)
- Phase reset: all resources reset to starting values at each phase transition
- Movement type change: resources reset immediately to new starting values
- Panel: shows only local player's bot (window.currentBotId)
Add three fields to defaultBot (lines 81-93):
battery_pct: 100,
memory_mb: 0,
cpu_temp: 50,These serialize to Firebase automatically because the entire bot object is passed unfiltered to update() in replace_bot (firebase-sync.js line 295).
Add module-level flag: let is_cooling_down = false;
Reset it inside stopMovingBot_virtual so it never bleeds across phases.
Add exported helper getStartingResources(movement_type) — returns { battery_pct: 100, memory_mb, cpu_temp }:
"random"->{ memory_mb: 0, cpu_temp: 50 }"dijkstra"->{ memory_mb: 128, cpu_temp: 70 }
Add updateBotResources(bot) — mutates bot in place (consistent with how move_bot/turn_bot in grid.js mutate in place):
- battery:
Math.max(0, bot.battery_pct - 1) - memory: unchanged if random;
Math.min(256, bot.memory_mb + randInt(5,10))if dijkstra - cpu:
clamp(bot.cpu_temp + randSignedInt(3,5), 0, 100)if random;Math.min(100, bot.cpu_temp + randInt(3,5))if dijkstra
Add drainResourcesToStart(bot) — called each tick during cooldown. Decreases cpu by randInt(3,5) and memory by randInt(5,10) toward starting values. When both reach starting values, sets is_cooling_down = false and removes the .bot-overheating class from the bot DOM element.
Add exported updateResourcePanel(bot) — updates panel DOM elements from bot fields. Shows/hides #coolingBadge (orange, "COOLING DOWN") based on is_cooling_down and #offlineBadge (red, "OFFLINE") based on battery_pct <= 0. Guards with early return if elements don't exist. Also exposed as window.updateResourcePanel (same pattern as window.startMovingBot etc.).
Modified move() inside startMovingBot_virtual:
function move() {
const bot = window.grid.bots[bot_id]?.[0];
if (!bot) return;
if (bot.battery_pct <= 0) {
stopMovingBot_virtual(bot_id);
updateResourcePanel(bot);
return;
}
if (is_cooling_down) {
drainResourcesToStart(bot); // sets is_cooling_down = false when done, removes bot-overheating class
if (window.emitReplaceBot) window.emitReplaceBot(bot);
updateResourcePanel(bot);
return;
}
const next_move = window.grid.get_next_move_using_policies(bot_id, 1);
if (!next_move) return;
const { bot: updated_bot } = window.grid.apply_next_move_to_bot(
bot_id,
next_move,
);
updateBotResources(updated_bot);
if (updated_bot.cpu_temp >= 100 || updated_bot.memory_mb >= 256) {
is_cooling_down = true;
// Apply red tint to bot sprite
const botEl = document.getElementById(`bot-${bot_id}`);
if (botEl) botEl.classList.add("bot-overheating");
}
if (window.emitReplaceBot) window.emitReplaceBot(updated_bot);
updateResourcePanel(updated_bot);
}In window.onPhaseChanged within the if (phase > prevPhase) block (line 130), after clear_grid(), add:
if (window.currentBotId && window.grid?.bots[window.currentBotId]) {
const bot = window.grid.bots[window.currentBotId][0];
Object.assign(bot, getStartingResources(bot.movement_type), {
battery_pct: 100,
});
if (window.emitReplaceBot) window.emitReplaceBot(bot);
window.updateResourcePanel?.(bot);
}Import getStartingResources from ./bot-movement.js.
In the radio change handler (lines 133-149), after update_bot_movement_type:
const bot = window.grid.bots[window.currentBotId][0];
Object.assign(bot, getStartingResources(value), { battery_pct: 100 });
updateResourcePanel(bot);Import getStartingResources, updateResourcePanel from ./bot-movement.js.
Add #resourcePanel as the first child inside #gridContainer (line 187). #gridContainer has position: relative (test-style.css line 70) so an absolutely-positioned child anchors to its top-left corner without affecting layout:
<div id="gridContainer" ...>
<div id="resourcePanel">
<div class="resource-row">
<span class="resource-label">Battery</span>
<div class="resource-bar-track">
<div class="resource-bar" id="bar-battery"></div>
</div>
<span class="resource-value" id="val-battery">100%</span>
</div>
<div class="resource-row">
<span class="resource-label">Memory</span>
<div class="resource-bar-track">
<div class="resource-bar" id="bar-memory"></div>
</div>
<span class="resource-value" id="val-memory">0 MB</span>
</div>
<div class="resource-row">
<span class="resource-label">CPU Temp</span>
<div class="resource-bar-track">
<div class="resource-bar" id="bar-cpu"></div>
</div>
<span class="resource-value" id="val-cpu">50C</span>
</div>
<div id="coolingBadge" style="display:none;">COOLING DOWN</div>
<div id="offlineBadge" style="display:none;">OFFLINE</div>
</div>
<!-- existing grid objects append here -->
</div>Add at the bottom of the file:
#resourcePanel {
position: absolute;
top: 8px;
left: 8px;
z-index: 10;
background: rgba(0, 0, 0, 0.65);
border-radius: 6px;
padding: 8px 10px;
min-width: 160px;
pointer-events: none;
}
.resource-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.resource-label {
color: #ccc;
font-size: 11px;
width: 56px;
flex-shrink: 0;
}
.resource-bar-track {
flex: 1;
height: 8px;
background: #444;
border-radius: 4px;
overflow: hidden;
}
.resource-bar {
height: 100%;
background: #4caf50;
border-radius: 4px;
transition: width 0.3s;
}
.resource-bar.warning {
background: #ff9800;
}
.resource-bar.danger {
background: #e53935;
}
.resource-value {
color: #fff;
font-size: 11px;
width: 44px;
text-align: right;
flex-shrink: 0;
}
#coolingBadge {
color: #000;
background: #ff9800;
border-radius: 4px;
text-align: center;
font-size: 12px;
font-weight: bold;
padding: 2px 6px;
margin-top: 4px;
}
#offlineBadge {
color: #fff;
background: #c00;
border-radius: 4px;
text-align: center;
font-size: 12px;
font-weight: bold;
padding: 2px 6px;
margin-top: 4px;
}
/* Red tint overlay during cooldown */
.bot-overheating {
filter: sepia(1) saturate(5) hue-rotate(-20deg) opacity(0.85);
}
.bot-offline {
filter: grayscale(100%) opacity(0.5);
}In setupNewBot (line 103), after drawBot returns DOM_ID, apply grayscale for dead bots:
if (bot.battery_pct <= 0) {
document.getElementById(DOM_ID)?.classList.add("bot-offline");
}In onReplaceBot (line 185), after setupNewBot(bot), update panel for local bot:
if (bot.id === window.currentBotId) window.updateResourcePanel?.(bot);In setupSelectedBot (line 87), populate the panel on initial bot registration:
window.updateResourcePanel?.(bot);| File | Change |
|---|---|
virtual-board/grid.js |
Add battery_pct, memory_mb, cpu_temp to defaultBot |
virtual-board/bot-movement.js |
Resource helpers + panel fn + modified move() + is_cooling_down flag |
virtual-board/phase-manager.js |
Reset resources on phase advance; import getStartingResources |
virtual-board/test-index.js |
Reset resources on radio change; import helpers |
virtual-board/game-setup.js |
updateResourcePanel calls + .bot-offline class in setupNewBot |
virtual-board/virtualMode.html |
Add #resourcePanel inside #gridContainer |
virtual-board/virtualMode-style.css |
Panel, bar, badge, .bot-offline CSS |
- Serve:
python3 -m http.server 8080, open two tabs, join room. - Panel visible on load with starting values (100%, 0 MB, 50C) before clicking Start Moving.
- Switch radio to Dijkstra: panel immediately shows 128 MB / 70C.
- Phase 1 (random): battery drops 1%/tick, memory stays 0, CPU fluctuates. Panel updates every 500ms.
- Phase 2 (Dijkstra): battery drops, memory climbs 5-10 MB/tick, CPU climbs.
- Max out CPU or memory: bot freezes, red tint appears on sprite, orange "COOLING DOWN" badge shown in panel. Values drain per tick back to starting values, then bot resumes and tint/badge clear automatically.
- Battery hits 0: bot permanently frozen, sprite greyscale, red OFFLINE badge in panel.
- Phase transition (1->2): resources reset to starting values for newly selected movement type.
- Other player's bot moving: local panel unchanged.
Currently, a player using "Utility-based planning" (Dijkstra) must manually click "Load bot info!" before the game can start. This pre-loading step blocks the Start Moving button for all players until every Dijkstra user has processed the graph. The UX is confusing and introduces an asymmetric delay.
The desired behavior: the game starts for everyone at the same time regardless of planning type. The Dijkstra bot simply stays still on the grid while its graph computes in the background; once done, it begins moving normally. The "Load bot info!" button and the require_graph Firebase gate are removed entirely.
The entire requireGraphRef system (require_graph in Firebase, requires_graph_load on VirtualGrid, the Firebase listener that disables the Start button, and reset_default_require_graph) exists solely to enforce pre-loading. All of it can be removed or bypassed.
virtual-board/test-index.js
- Remove the
loadBotButtonclick handler (lines 123–130). - In the Dijkstra radio selection handler (lines 138–143): remove the calls to
window.grid.reset_default_require_graph()andwindow.live_updates.reset_default_require_graph(), and removebody.setAttribute("needs-loading", ""). The radio handler just needs to callclaim_dijkstraas it already does. - The
noMovementguard inchangeMovingBotsButtonalready checksbot.movement_type === null— no change needed there.
firebase-sync.js
- Remove the
onValue(this.requireGraphRef, ...)listener (lines 174–189) that disableschangeMovingBotsButtonand shows/hidesloadBotButton. This is the key gate to remove. - Keep
requireGraphRefdeclared (it's still used byreset_default_require_graphon the sync class, called fromstart_game_for_everyone) — or remove that call too. The safest approach: just remove theonValuelistener so the button is never blocked.
firebase-game-flow.js
- In
stop_moving_bot(lines 22–36): remove the block that re-disableschangeMovingBotsButtonbased onnum_requireand shows "Load bot info!" — since that button and flow no longer exist. - In
start_game_for_everyone(line 42): removeset(this.requireGraphRef, {})— no need to clear it.
virtual-board/virtualMode.html
- Remove or hide the
#loadBotButtonelement entirely (line 182).
virtual-board/test-style.css
- Remove the
body:not([needs-loading]) #loadBotButton { display: none; }rule (lines 190–192) — dead after button is removed.
Instead of pre-loading synchronously on button click, trigger update_all_coin_graphs in the background when the bot starts moving, and hold the bot in place until it finishes.
virtual-board/bot-movement.js
The startMovingBot function sets up a setInterval(move, 500) tick. Modify it so that when the bot's movement_type is Dijkstra, it first kicks off graph computation asynchronously, then begins the interval only after the graphs are ready.
Since update_all_coin_graphs is synchronous but blocking, wrap it in a setTimeout(..., 0) to yield to the browser before computing, then start the interval in the callback. This keeps the UI responsive (the timer starts, other bots move) while the Dijkstra bot just hasn't started its interval yet.
Concrete change in startMovingBot (bot-movement.js):
// Before (simplified):
intervals[bot_id] = setInterval(move, 500);
// After:
if (
window.grid.bots[bot_id]?.[0]?.movement_type ===
MOVEMENT_VALUES.DIJKSTRA.value
) {
setTimeout(() => {
window.grid.update_all_coin_graphs(bot_id);
window.grid.change_require_graph(bot_id, false);
intervals[bot_id] = setInterval(move, 500);
}, 0);
} else {
intervals[bot_id] = setInterval(move, 500);
}MOVEMENT_VALUES is already a global set by grid.js (plain <script>), so it's accessible in bot-movement.js.
virtual-board/grid.js
- No changes needed.
update_all_coin_graphsand the movement tick (get_next_move_using_policies→distance_to_object) work correctly once the graphs exist; the bot just won't have an interval firing until they're ready.
| File | Change |
|---|---|
virtual-board/bot-movement.js |
Branch on Dijkstra in startMovingBot: compute graphs async via setTimeout, then start interval |
virtual-board/test-index.js |
Remove loadBotButton handler; remove reset_default_require_graph calls and needs-loading attribute from radio handler |
firebase-sync.js |
Remove onValue(requireGraphRef) listener that disables the Start button |
firebase-game-flow.js |
Remove set(requireGraphRef, {}) in start_game_for_everyone; remove require_graph button-disable logic in stop_moving_bot |
virtual-board/virtualMode.html |
Remove #loadBotButton element |
virtual-board/test-style.css |
Remove needs-loading CSS rule |
- Serve:
python3 -m http.server 8080, open two browser tabs, join the same room. - In tab A: place a bot, select "Goal-based planning" (random). In tab B: place a bot, select "Utility-based planning" (Dijkstra).
- Confirm "Load bot info!" button is gone for both players.
- Both players click "Start Moving!" — confirm the game starts immediately for both without any extra step.
- Tab A's bot (random) begins moving at the first tick. Tab B's bot (Dijkstra) stays still for one tick while graphs compute, then begins moving.
- Verify no JS errors in the console during graph computation.
- Run through all 3 phases to confirm the flow works consistently across phase transitions.
The final game has 3 phases. Before a phase can start, the player clicks "Start Moving!" — which already has a guard that blocks progression if the player's bot has no movement type selected (showing the "Oops!" modal). The request is to extend this same guard to also require that the grid has at least 1 coin (reward) and at least 1 obstacle placed before the player can signal ready.
The "Oops!" modal (#needToSelectMovementModal) already exists in virtualMode.html and has a single text body line. We'll append a second reason to that body and expand the JS guard to cover the new rule.
This constraint applies only to the final game (when window.tutorial is falsy). Tutorial phases should not be affected.
Current modal body (line 108–110):
<div class="modal-body text-black">
Need to select a type of movement for your bot!
</div>Replace with two named reason paragraphs so JS can show/hide them independently:
<div class="modal-body text-black">
<p id="oopsNoMovement" class="mb-2">
Need to select a type of movement for your bot!
</p>
<p id="oopsNoRewardOrObstacle" class="mb-0" style="display:none;">
The grid needs at least <strong>1 reward</strong> and
<strong>1 obstacle</strong> before you can start!
</p>
</div>Current guard (lines 106–113):
changeMovingBotsButton.addEventListener("click", async () => {
const bot = window.grid.bots[window.currentBotId]?.[0];
if (!bot || bot.movement_type === null) {
needToSelectMovementHandler.show();
return;
}
await changeMovingBotsHandler();
});Replace with:
changeMovingBotsButton.addEventListener("click", async () => {
const bot = window.grid.bots[window.currentBotId]?.[0];
const noMovement = !bot || bot.movement_type === null;
const missingItems =
!window.tutorial &&
(Object.keys(window.grid.coins).length < 1 ||
Object.keys(window.grid.obstacles).length < 1);
if (noMovement || missingItems) {
document.getElementById("oopsNoMovement").style.display = noMovement
? ""
: "none";
document.getElementById("oopsNoRewardOrObstacle").style.display =
missingItems ? "" : "none";
needToSelectMovementHandler.show();
return;
}
await changeMovingBotsHandler();
});Key points:
- Both conditions can be true simultaneously; both paragraphs will show.
- The
!window.tutorialguard skips the coin/obstacle check in tutorial pages. - Coin and obstacle counts come from
Object.keys(window.grid.coins).lengthandObject.keys(window.grid.obstacles).length— these are already the canonical counts used elsewhere in the codebase (grid.js).
| File | Change |
|---|---|
virtual-board/virtualMode.html |
Split modal body into two <p> tags with IDs |
virtual-board/test-index.js |
Extend changeMovingBotsButton click guard |
No other files need changes. The Firebase sync layer, phase-manager, and bot-movement are unaffected — the guard fires before any of them are invoked.
- Serve the app:
python3 -m http.server 8080 - Open
http://localhost:8080/virtual-board/doodlebotGame.htmland create/join a room with 2 players. - No bot + no reward/obstacle → click "Start Moving!" → modal shows both messages.
- Bot placed, movement selected, but no coin → modal shows only the reward/obstacle message.
- Bot placed, movement selected, but no obstacle → modal shows only the reward/obstacle message.
- Bot placed, movement selected, 1+ coin, 1+ obstacle → modal does not appear, phase starts normally.
- Open a tutorial page → verify the coin/obstacle check is skipped (modal only appears for missing movement type).