Skip to content

Turn GaugeGap into a twelve-lab science game#90

Merged
slavazeph-coder merged 7 commits into
mainfrom
feature/gaugegap-fun-layer
Jul 10, 2026
Merged

Turn GaugeGap into a twelve-lab science game#90
slavazeph-coder merged 7 commits into
mainfrom
feature/gaugegap-fun-layer

Conversation

@slavazeph-coder

Copy link
Copy Markdown
Owner

What this adds

Expands GaugeGap from eight simulations into a twelve-lab science arcade and adds a local progression loop.

New tactile experiments

  1. Ant Trail Architect — place food and obstacles while a leaderless colony discovers and reinforces routes.
  2. Traffic Tamer — control signal phases under rising demand, compare fixed timing with queue sensing, and minimize peak congestion.
  3. Life Painter — draw Conway cells, load glider/acorn/pulsar presets, and evolve long-lived computational patterns.
  4. Ecosystem Keeper — add plants, prey or predators and keep a live agent food web balanced through delayed feedback.

Retention and game layer

  • Local Foundry passport with no signup
  • XP and levels
  • One deterministic daily lab worth bonus XP
  • Visit streaks
  • Six unlockable achievements
  • Explored-state markers on experiment cards
  • Twelve distinct interaction verbs
  • Updated homepage, growth loop and experiment gallery

Scientific framing

Each new lab includes a mission, live score, interactive controls, underlying-model explanation and shareable URL settings. Existing BrainSNN, research tools and the first eight labs remain intact.

Validation requested

  • TypeScript/noEmit
  • deterministic tests
  • production Vite/Express build
  • MCP smoke test
  • pointer/touch interaction check
  • mobile passport and twelve-card selector check

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request expands the GaugeGap science arcade from eight to twelve live experiments by adding four new interactive labs (Ant Trail, Traffic Tamer, Life Painter, and Ecosystem Keeper) and introducing a gamified arcade passport system to track XP, levels, daily missions, streaks, and achievements. Feedback on these changes focuses on enhancing UX and performance: suggestions include using local time instead of UTC for consistent daily streak tracking, dynamically scaling the ant population without resetting the simulation, updating the Life Painter scoreboard on every step to prevent visual lag, optimizing distance calculations in the Ecosystem Lab using squared distances, and refactoring the entity update loop to resolve array mutation and performance issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +6 to +8
function dateKey(date = new Date()) {
return date.toISOString().slice(0, 10);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using date.toISOString() returns the date in UTC. This can cause daily streaks and missions to reset or fail to increment unexpectedly depending on the user's local timezone (e.g., if a user plays twice on consecutive local days but those visits fall on the same or non-consecutive UTC days). Formatting the date string using local time ensures consistent daily streak behavior.

function dateKey(date = new Date()) {
  const yyyy = date.getFullYear();
  const mm = String(date.getMonth() + 1).padStart(2, '0');
  const dd = String(date.getDate()).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

Comment on lines +107 to +111
function render() {
if (lastReset !== resetRef.current || antsRef.current.length !== controlsRef.current.count) {
lastReset = resetRef.current;
seedWorld();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Resetting the entire simulation (clearing placed obstacles and resetting deliveries to 0) when the user simply adjusts the "Ants" slider is a frustrating user experience. Instead, the ant population should be dynamically scaled up or down without resetting the world state.

Suggested change
function render() {
if (lastReset !== resetRef.current || antsRef.current.length !== controlsRef.current.count) {
lastReset = resetRef.current;
seedWorld();
}
function render() {
if (lastReset !== resetRef.current) {
lastReset = resetRef.current;
seedWorld();
} else if (antsRef.current.length !== controlsRef.current.count) {
const target = controlsRef.current.count;
if (antsRef.current.length < target) {
const extra = makeAnts(target - antsRef.current.length, nest, Date.now() % 100000);
antsRef.current = [...antsRef.current, ...extra];
} else {
antsRef.current = antsRef.current.slice(0, target);
}
}

Comment on lines +85 to +86
gridRef.current = next; generationCount += 1;
if (generationCount % 4 === 0) { let count = 0; for (let i = 0; i < next.length; i += 1) count += next[i]; setAlive(count); setGeneration(generationCount); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Updating the scoreboard only every 4 generations causes a visual mismatch when the simulation is paused or running slowly (e.g., if paused at generation 11, the scoreboard remains stuck at generation 8). Since counting the alive cells and updating the state is extremely fast, doing it on every step ensures the scoreboard is always accurate and in sync with the canvas.

Suggested change
gridRef.current = next; generationCount += 1;
if (generationCount % 4 === 0) { let count = 0; for (let i = 0; i < next.length; i += 1) count += next[i]; setAlive(count); setGeneration(generationCount); }
gridRef.current = next; generationCount += 1;
let count = 0;
for (let i = 0; i < next.length; i += 1) count += next[i];
setAlive(count);
setGeneration(generationCount);

Comment on lines +166 to +170
function nearest(entity, type, radius) {
let best = null; let bestDistance = radius;
entitiesRef.current.forEach((candidate) => { if (candidate.type !== type || candidate === entity) return; const distance = Math.hypot(candidate.x - entity.x, candidate.y - entity.y); if (distance < bestDistance) { best = candidate; bestDistance = distance; } });
return best;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Math.hypot is relatively slow in hot loops. Since nearest is called for every non-plant entity against all other entities every frame, comparing squared distances instead of actual distances avoids the expensive square root calculation and significantly improves performance, especially on mobile devices.

    function nearest(entity, type, radius) {
      let best = null;
      let bestDistSq = radius * radius;
      const entities = entitiesRef.current;
      for (let i = 0; i < entities.length; i++) {
        const candidate = entities[i];
        if (candidate.type !== type || candidate === entity) continue;
        const dx = candidate.x - entity.x;
        const dy = candidate.y - entity.y;
        const distSq = dx * dx + dy * dy;
        if (distSq < bestDistSq) {
          best = candidate;
          bestDistSq = distSq;
        }
      }
      return best;
    }

Comment on lines +181 to +195
const removed = new Set();
entitiesRef.current.forEach((entity, index) => {
entity.age += 1;
if (entity.type === 'plant') { entity.energy = Math.min(4, entity.energy + 0.002 * controls.sunlight); reproduce(entity, entity.energy > 2.8 ? 0.0015 * controls.sunlight : 0); return; }
const targetType = entity.type === 'prey' ? 'plant' : 'prey';
const target = nearest(entity, targetType, entity.type === 'prey' ? 90 : 125);
if (target) { const dx = target.x - entity.x; const dy = target.y - entity.y; const distance = Math.max(1, Math.hypot(dx, dy)); entity.vx += dx / distance * (entity.type === 'prey' ? 0.045 : 0.06); entity.vy += dy / distance * (entity.type === 'prey' ? 0.045 : 0.06); if (distance < 8) { removed.add(entitiesRef.current.indexOf(target)); entity.energy += entity.type === 'prey' ? 15 : 24; } }
entity.vx += (random() - 0.5) * 0.08; entity.vy += (random() - 0.5) * 0.08;
const max = entity.type === 'prey' ? 1.65 : 1.95; const magnitude = Math.max(0.01, Math.hypot(entity.vx, entity.vy)); if (magnitude > max) { entity.vx = entity.vx / magnitude * max; entity.vy = entity.vy / magnitude * max; }
entity.x = (entity.x + entity.vx * controls.speed + width) % width; entity.y = (entity.y + entity.vy * controls.speed + height) % height;
entity.energy -= (entity.type === 'prey' ? 0.022 : 0.035) * controls.metabolism * controls.speed;
if (entity.energy <= 0 || entity.age > 12000) removed.add(index);
reproduce(entity, entity.energy > (entity.type === 'prey' ? 62 : 85) ? 0.004 : 0);
});
entitiesRef.current = entitiesRef.current.filter((_, index) => !removed.has(index));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This entity update loop has three issues:

  1. Appending new entities directly to entitiesRef.current via reproduce inside the forEach loop is fragile and can lead to unexpected behaviors.
  2. Calling indexOf(target) inside the loop introduces an $O(N^2)$ complexity, which is inefficient.
  3. Storing indices in the removed Set is fragile compared to storing the object references directly.

Refactoring the loop to collect newborns in a separate array and using object references for removal resolves all three issues.

        const removed = new Set();
        const newborns = [];
        entitiesRef.current.forEach((entity) => {
          entity.age += 1;
          if (entity.type === 'plant') {
            entity.energy = Math.min(4, entity.energy + 0.002 * controls.sunlight);
            if (entity.energy > 2.8 && Math.random() < 0.0015 * controls.sunlight && entitiesRef.current.length + newborns.length < 420) {
              entity.energy *= 0.58;
              newborns.push(makeEntity(entity.type, entity.x + (random() - 0.5) * 14, entity.y + (random() - 0.5) * 14, random));
            }
            return;
          }
          const targetType = entity.type === 'prey' ? 'plant' : 'prey';
          const target = nearest(entity, targetType, entity.type === 'prey' ? 90 : 125);
          if (target) {
            const dx = target.x - entity.x;
            const dy = target.y - entity.y;
            const distance = Math.max(1, Math.hypot(dx, dy));
            entity.vx += dx / distance * (entity.type === 'prey' ? 0.045 : 0.06);
            entity.vy += dy / distance * (entity.type === 'prey' ? 0.045 : 0.06);
            if (distance < 8) {
              removed.add(target);
              entity.energy += entity.type === 'prey' ? 15 : 24;
            }
          }
          entity.vx += (random() - 0.5) * 0.08;
          entity.vy += (random() - 0.5) * 0.08;
          const max = entity.type === 'prey' ? 1.65 : 1.95;
          const magnitude = Math.max(0.01, Math.hypot(entity.vx, entity.vy));
          if (magnitude > max) {
            entity.vx = entity.vx / magnitude * max;
            entity.vy = entity.vy / magnitude * max;
          }
          entity.x = (entity.x + entity.vx * controls.speed + width) % width;
          entity.y = (entity.y + entity.vy * controls.speed + height) % height;
          entity.energy -= (entity.type === 'prey' ? 0.022 : 0.035) * controls.metabolism * controls.speed;
          if (entity.energy <= 0 || entity.age > 12000) {
            removed.add(entity);
          } else if (entity.energy > (entity.type === 'prey' ? 62 : 85) && Math.random() < 0.004 && entitiesRef.current.length + newborns.length < 420) {
            entity.energy *= 0.58;
            newborns.push(makeEntity(entity.type, entity.x + (random() - 0.5) * 14, entity.y + (random() - 0.5) * 14, random));
          }
        });
        entitiesRef.current = entitiesRef.current.filter((entity) => !removed.has(entity)).concat(newborns);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d9353a03d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +9 to +10
const values = (query.get('state') || '').split(',').map(Number);
return values.every(Number.isFinite) ? values : defaults;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require complete state before overriding lab defaults

When a user opens a new lab from the selector, daily mission, or landing card, the URL is set to ?lab=<id> while state is deliberately removed. With an empty state, ''.split(',').map(Number) becomes [0], passes this every(Number.isFinite) check, and prevents the defaults from being used; the remaining parameters become undefined/NaN (for example ants get smell/evaporation as NaN, traffic gets a NaN signal cycle, and the copied helper in WorldPlaygrounds.jsx breaks ecosystem speed/metabolism). Please reject missing or wrong-length state values before returning URL data.

Useful? React with 👍 / 👎.

@slavazeph-coder slavazeph-coder merged commit cef2cac into main Jul 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant