From 2025d444de43ac758fd01323034af90bcc0b80ec Mon Sep 17 00:00:00 2001
From: Ofer Shaal
Date: Sun, 24 May 2026 21:04:18 -0400
Subject: [PATCH] Add Pure Local Sensors experiment + first-load UX
improvements and fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Introduce 'Pure Local Sensors' mode (?pure-local=1) that zeros lf/lr track-orientation features so the brain only receives raw rays + speed.
- Add toggle in the π§ͺ Experiments panel with URL sync and restart helper.
- Improve first-load experience: collapse advanced panels, prominent Start button, one-time helpful hint after first start.
- Fix visual bug where #ab-hud (A/B HUD) leaked dark horizontal lines on first load and after A/B toggle cycles (now uses robust .ab-hud-visible class + early hide).
- Add dedicated ELI15 chapter for the pure-local experiment.
- Add planning documents under docs/plans/ for future RL mode and policy capability enhancements.
- Various ELI15 accuracy fixes and supporting screenshots for validation.
---
AI-Car-Racer/buttonResponse.js | 14 ++
AI-Car-Racer/car.js | 15 +-
AI-Car-Racer/eli15/chapters/lineage.js | 4 +-
AI-Car-Racer/eli15/chapters/neural-network.js | 30 +--
.../eli15/chapters/pure-local-experiment.js | 30 +++
AI-Car-Racer/eli15/chapters/sensors.js | 4 +-
.../eli15/chapters/what-is-this-project.js | 4 +-
AI-Car-Racer/eli15/chapters/why-cars-crash.js | 10 +-
AI-Car-Racer/eli15/index.js | 11 +-
AI-Car-Racer/eli15/tour.js | 4 +-
AI-Car-Racer/main.js | 81 ++++++-
AI-Car-Racer/sim-worker.js | 1 +
AI-Car-Racer/style.css | 24 +-
AI-Car-Racer/uiPanels.js | 109 ++++++++-
docs/plans/enhancing-policy-capabilities.md | 214 ++++++++++++++++++
docs/plans/reinforcement-learning-mode.md | 192 ++++++++++++++++
.../screenshots/ab-hud-toggle-fix.png | Bin 0 -> 116515 bytes
.../screenshots/first-load-after-fix.png | Bin 0 -> 116846 bytes
.../screenshots/first-load-fixed-abhud.png | Bin 0 -> 116515 bytes
...angle-checkpoints-left-focus-annotated.png | Bin 0 -> 197609 bytes
.../triangle-checkpoints-left-focus.png | Bin 0 -> 189244 bytes
.../triangle-v2-left-focus-annotated.png | Bin 0 -> 207775 bytes
.../screenshots/triangle-v2-left-focus.png | Bin 0 -> 198645 bytes
docs/validation/screenshots/two-lines.jpg | Bin 0 -> 8713 bytes
.../validation/screenshots/vv-after-start.png | Bin 0 -> 125712 bytes
docs/validation/screenshots/vv-first-load.png | Bin 0 -> 127103 bytes
.../vv-headed-03-triangle-10cps-annotated.png | Bin 0 -> 206584 bytes
.../vv-headed-03-triangle-10cps.png | Bin 0 -> 196801 bytes
.../screenshots/vv-improved-first-load.png | Bin 0 -> 154373 bytes
two-lines.jpg | Bin 0 -> 8713 bytes
30 files changed, 702 insertions(+), 45 deletions(-)
create mode 100644 AI-Car-Racer/eli15/chapters/pure-local-experiment.js
create mode 100644 docs/plans/enhancing-policy-capabilities.md
create mode 100644 docs/plans/reinforcement-learning-mode.md
create mode 100644 docs/validation/screenshots/ab-hud-toggle-fix.png
create mode 100644 docs/validation/screenshots/first-load-after-fix.png
create mode 100644 docs/validation/screenshots/first-load-fixed-abhud.png
create mode 100644 docs/validation/screenshots/triangle-checkpoints-left-focus-annotated.png
create mode 100644 docs/validation/screenshots/triangle-checkpoints-left-focus.png
create mode 100644 docs/validation/screenshots/triangle-v2-left-focus-annotated.png
create mode 100644 docs/validation/screenshots/triangle-v2-left-focus.png
create mode 100644 docs/validation/screenshots/two-lines.jpg
create mode 100644 docs/validation/screenshots/vv-after-start.png
create mode 100644 docs/validation/screenshots/vv-first-load.png
create mode 100644 docs/validation/screenshots/vv-headed-03-triangle-10cps-annotated.png
create mode 100644 docs/validation/screenshots/vv-headed-03-triangle-10cps.png
create mode 100644 docs/validation/screenshots/vv-improved-first-load.png
create mode 100644 two-lines.jpg
diff --git a/AI-Car-Racer/buttonResponse.js b/AI-Car-Racer/buttonResponse.js
index dc1c5a3..f28471a 100644
--- a/AI-Car-Racer/buttonResponse.js
+++ b/AI-Car-Racer/buttonResponse.js
@@ -8,6 +8,20 @@ function pauseGame(){
btn.classList.remove('start-cta');
}
window.__firstStart = false;
+
+ // Gentle first-time onboarding hint (only shown once)
+ if (!localStorage.getItem('seenFirstStartHint')) {
+ localStorage.setItem('seenFirstStartHint', '1');
+ setTimeout(() => {
+ try {
+ const hint = document.createElement('div');
+ hint.style.cssText = 'position:fixed;bottom:12px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.78);color:#ddd;padding:6px 14px;border-radius:4px;font-size:12px;z-index:9999;white-space:nowrap;';
+ hint.innerHTML = 'Demo running β cars are evolving. Use ββοΈ Customize Trackβ or π§ͺ Experiments for more options.';
+ document.body.appendChild(hint);
+ setTimeout(() => { if (hint && hint.parentNode) hint.parentNode.removeChild(hint); }, 7000);
+ } catch (_) {}
+ }, 1400);
+ }
// Halt / resume the worker's AI step loop too. Without this, sim-worker
// would keep burning CPU while the user has paused β and on resume the
// accumulator would stampede a huge backlog of physics steps at once.
diff --git a/AI-Car-Racer/car.js b/AI-Car-Racer/car.js
index 778e0e8..e867aa3 100644
--- a/AI-Car-Racer/car.js
+++ b/AI-Car-Racer/car.js
@@ -125,7 +125,14 @@ class Car{
// distance cue; see docs/plan/ruvector-proof/arch-a1/PROOF.md.
const cpList = checkPointList;
let lf = 0, lr = 0;
- if (cpList && cpList.length){
+
+ // When pureLocalSensors is active, we deliberately give the brain
+ // ZERO information about where the next checkpoint is.
+ // This is the "embodied local signals only" mode for comparison.
+ // Guard works in both main thread (window) and Web Worker (self).
+ const isPureLocal = (typeof window !== 'undefined' && window.pureLocalSensors) ||
+ (typeof self !== 'undefined' && self.pureLocalSensors);
+ if (!isPureLocal && cpList && cpList.length){
const passed = this.checkPointsPassed;
const nextIdx = passed.length === 0
? 0
@@ -139,11 +146,7 @@ class Car{
const s = Math.sin(this.angle), c = Math.cos(this.angle);
const lfRaw = dx * s + dy * c;
const lrRaw = dx * c - dy * s;
- // Canvas diagonal as track-invariant scale. `road` is a
- // global populated by main.js (or handleInit in the
- // worker); both paths set `right` and `bottom` = canvas
- // dims. Fallback constant guards the very-early frame
- // before handleInit lands.
+ // Canvas diagonal as track-invariant scale.
const W = (typeof road !== 'undefined' && road && road.right) ? (road.right - road.left) : 3200;
const H = (typeof road !== 'undefined' && road && road.bottom) ? (road.bottom - road.top) : 1800;
const D = Math.hypot(W, H);
diff --git a/AI-Car-Racer/eli15/chapters/lineage.js b/AI-Car-Racer/eli15/chapters/lineage.js
index e02dd92..05b6b7a 100644
--- a/AI-Car-Racer/eli15/chapters/lineage.js
+++ b/AI-Car-Racer/eli15/chapters/lineage.js
@@ -6,12 +6,12 @@ export default {
oneLiner: 'parentIds + getLineage() reconstruct a brain\'s family tree on demand.',
body: [
'
When a generation ends, the best car\'s brain gets archived β but we don\'t just',
- 'save the 92 weights. We also save which brains it came from. Each',
+ 'save the 244 weights. We also save which brains it came from. Each',
'archive entry carries a parentIds: string[]: the ids of the seeds the GA',
'warm-started this batch from. Those parents have their own parents stored',
'alongside their weights. String enough of those together and you have a family',
'tree of neural networks.
ruvectorBridge.js:1338 exposes getLineage(id, maxDepth = 6).',
'Starting from any brain id, it walks parentIds backwards. When a brain has',
'multiple parents (the batch was seeded from multiple retrievals), it picks the parent',
'with the highest fitness β "the line of descent we credit this genome to". The',
diff --git a/AI-Car-Racer/eli15/chapters/neural-network.js b/AI-Car-Racer/eli15/chapters/neural-network.js
index c5af0fa..ad3f364 100644
--- a/AI-Car-Racer/eli15/chapters/neural-network.js
+++ b/AI-Car-Racer/eli15/chapters/neural-network.js
@@ -1,28 +1,28 @@
// eli15/chapters/neural-network.js
-// The 92-weight 6β8β4 feed-forward network that decides W/A/S/D.
+// The 244-weight 10β16β4 feed-forward network that decides W/A/S/D.
export default {
id: 'neural-network',
- title: 'A brain made of 92 numbers',
- oneLiner: 'Six sensor inputs β eight hidden neurons β four pedal/steer outputs.',
+ title: 'A brain made of 244 numbers',
+ oneLiner: 'Ten sensor inputs β sixteen hidden neurons β four pedal/steer outputs.',
body: [
'
Every car carries a tiny neural network. It is almost comically small:',
- '6 inputs β 8 hidden neurons β 4 outputs. See it in',
+ '10 inputs β 16 hidden neurons β 4 outputs. See it in',
'network.js (the Level and NeuralNetwork classes) and',
- 'car.js:37-38 where the car wires it up.
',
+ 'car.js:41-42 where the car wires it up.
',
'
"A neuron" here is just a weighted sum with a threshold. It multiplies each input',
'by a weight, adds them all up, and compares the total to a bias.',
'If the sum beats the bias, the neuron fires a 1. Otherwise a 0. That\'s it β no fancy',
'activation function, no backprop, no gradients. The whole network can be serialised',
- 'as a flat Float32Array(92) β see brainCodec.js:2:',
- 'FLAT_LENGTH = 92. Where does 92 come from? For each layer with in',
+ 'as a flat Float32Array(244) β see brainCodec.js:5:',
+ 'FLAT_LENGTH = 244. Where does 244 come from? For each layer with in',
'inputs and out outputs you need in Γ out weights plus out biases:',
- '(6Γ8 + 8) + (8Γ4 + 4) = 56 + 36 = 92.
The four outputs are Boolean: forward, reverse, left, right.',
- 'The car presses the pedals whose neuron fired a 1. Combine that with the 5 rays',
- '(plus 1 bias) from sensors and you get the whole perception-to-action pipeline',
+ 'The car presses the pedals whose neuron fired a 1. Combine that with the 7 rays + 3 features',
+ ' from sensors + car and you get the whole perception-to-action pipeline',
'in ~50 lines of JS.
',
'
No learning happens inside the brain itself β the brain is just a lookup function.',
- 'Learning happens across generations, by the genetic algorithm tweaking those 92',
+ 'Learning happens across generations, by the genetic algorithm tweaking those 244',
'numbers.
',
'
Try it yourself
',
'
In phase 4, watch the four-box display in the right panel (the',
@@ -31,7 +31,7 @@ export default {
].join('\n'),
diagram: [
'
',
].join('\n'),
related: [
diff --git a/AI-Car-Racer/eli15/chapters/pure-local-experiment.js b/AI-Car-Racer/eli15/chapters/pure-local-experiment.js
new file mode 100644
index 0000000..616e2a4
--- /dev/null
+++ b/AI-Car-Racer/eli15/chapters/pure-local-experiment.js
@@ -0,0 +1,30 @@
+// Short chapter explaining the "Pure Local Signals" comparison experiment.
+// Registered in eli15/index.js and linked from the Experiments panel.
+export default {
+ id: 'pure-local-experiment',
+ title: 'Pure local signals vs track hints',
+ oneLiner: 'What happens when we remove the "next checkpoint" features and force the brain to drive from raw sensors only?',
+ body: [
+ '
This experiment lets you directly compare two versions of the same brain:
',
+ '
',
+ '
Normal mode β the network receives the usual 10 inputs: 7 ray readings + speed + two extra "track orientation" features (lf + lr). These two features quietly tell the brain the direction and distance to the next checkpoint in its own local frame.
',
+ '
Pure local mode (enable with the π§ͺ Experiments toggle or ?pure-local=1) β lf and lr are forced to zero. The brain only sees the raw rays + its own speed. No explicit hint about where the next gate is.
',
+ '
',
+ '
The goal is to understand how much the current system relies on those hidden "map-like" signals versus learning to drive from truly local, embodied perception β the kind a real car or robot would have.
',
+ '
Why this matters on hard tracks like Triangle
',
+ '
On easy tracks the extra features are convenient but not essential. On the Triangle the difference becomes dramatic because the critical 180Β° turn requires anticipation. A brain that only reacts to what its rays see right now often discovers the wall too late.
',
+ '
This mode is deliberately not "better" β it is a diagnostic tool. Use it (ideally with ?rv=0 for a clean GA baseline) to feel how much the network depends on the extra signals versus raw sensor data.
',
+ '
Try it yourself
',
+ '
',
+ '
Toggle "Pure local sensors (no lf/lr)" in the π§ͺ Experiments panel.
',
+ '
Watch the persistent ποΈ PURE LOCAL badge at the top.
',
+ '
Restart training and compare behavior (and the brain input bars) against a normal run.
',
+ '
Look at the brain visualization: the last two input bars will stay near zero.
',
+ '
',
+ ].join('\n'),
+ related: [
+ 'why-cars-crash',
+ 'sensors',
+ 'neural-network',
+ ],
+};
diff --git a/AI-Car-Racer/eli15/chapters/sensors.js b/AI-Car-Racer/eli15/chapters/sensors.js
index 0f426f9..c245c65 100644
--- a/AI-Car-Racer/eli15/chapters/sensors.js
+++ b/AI-Car-Racer/eli15/chapters/sensors.js
@@ -19,7 +19,7 @@ export default {
'rays β wide enough to hide an apex corner until the car is already on top of it.',
'Bumping to 7 closes those gaps to ~17Β°. Combined with speed and two track-orientation',
'features, that\'s the 10 inputs feeding the first hidden layer.',
- 'Look at car.js:42 and you\'ll see it: new NeuralNetwork([sensor.rayCount+3, 8, 4]).',
+ 'Look at car.js:42 and you\'ll see it: new NeuralNetwork([sensor.rayCount+3, 16, 4]).',
'
The rays are drawn on the training canvas as faint lines sticking out of each car.',
'Where a ray hits a wall, there\'s a tiny dot. That dot\'s distance is what the brain sees.
This is a car racing game where nobody writes the driving logic by hand. Each',
- 'car has a tiny neural network β 92 floating-point numbers, arranged as a',
- '6 β 8 β 4 topology β that reads from sensors (rays poking out of the car)',
+ 'car has a tiny neural network β 244 floating-point numbers, arranged as a',
+ '10 β 16 β 4 topology β that reads from sensors (rays poking out of the car)',
'and decides which pedals and steering to press. At the start of training the',
'networks are random, so the cars drive like drunk toddlers. A few make it a',
'little further than the others. Those are the "winners" of the generation.
1. The brain is a frozen reflex table, not an agent
',
- '
Every car\'s "brain" is the 92-number neural net from the',
- 'neural-network chapter. Once a car is born, those 92 numbers are locked',
+ '
Every car\'s "brain" is the 244-number neural net from the',
+ 'neural-network chapter. Once a car is born, those 244 numbers are locked',
'for its entire life. No learning, no memory, no "oh I bumped that wall, I\'ll try something',
'different next time." Each frame it does the same thing: feed sensor readings in the top,',
'read four booleans out the bottom. It\'s a lookup function dressed up as a driver.',
@@ -21,7 +21,7 @@ export default {
'
2. Generation 0 is a bag of dice rolls
',
'
When a fresh population is born, every weight and bias is a uniform random number',
- 'in [-1, 1] β see network.js:68: w[k] = Math.random()*2-1.',
+ 'in [-1, 1] β see network.js:73: w[k] = Math.random()*2-1.',
'That means ~1 in 16 gen-0 brains happen to roll a bias pattern that makes the',
'forward output neuron fire regardless of what the sensors say. Those cars are',
'"always-forward zombies" β they drive perfectly straight until the first corner',
@@ -32,9 +32,9 @@ export default {
'
3. The first half-decent car becomes the ancestor of everyone
',
'
Fitness is brutally simple: checkPointsCount + laps Γ cpLen',
- '(sim-worker.js:296). Whichever car grabs the first checkpoint first',
+ '(sim-worker.js:331). Whichever car grabs the first checkpoint first',
'wins the generation, even if it immediately crashes one metre later. The GA then copies',
- 'that winner\'s 92 weights, sprinkles a bit of mutation noise, and the whole next',
+ 'that winner\'s 244 weights, sprinkles a bit of mutation noise, and the whole next',
'generation is its near-siblings. After ~10 generations of this, the population has',
'locked in on whatever reflex the first lucky car happened to have β',
'even if that reflex is "hug the left wall at 0.3 Γ max speed." Escaping that local',
diff --git a/AI-Car-Racer/eli15/index.js b/AI-Car-Racer/eli15/index.js
index f43eeff..e522e09 100644
--- a/AI-Car-Racer/eli15/index.js
+++ b/AI-Car-Racer/eli15/index.js
@@ -34,13 +34,13 @@
loader: function () { return import('./chapters/what-is-this-project.js'); },
},
'sensors': {
- title: 'The car\'s eyes are five invisible rays',
+ title: 'The car\'s eyes are seven invisible rays',
oneLiner: 'Ray-cast sensors feed a number per ray into the neural network.',
loader: function () { return import('./chapters/sensors.js'); },
},
'neural-network': {
- title: 'A brain made of 92 numbers',
- oneLiner: 'Six sensor inputs β eight hidden neurons β four pedal/steer outputs.',
+ title: 'A brain made of 244 numbers',
+ oneLiner: 'Ten sensor inputs β sixteen hidden neurons β four pedal/steer outputs.',
loader: function () { return import('./chapters/neural-network.js'); },
},
'why-cars-crash': {
@@ -48,6 +48,11 @@
oneLiner: 'Four reasons: frozen reflexes, random gen-0, elite lock-in, and physics.',
loader: function () { return import('./chapters/why-cars-crash.js'); },
},
+ 'pure-local-experiment': {
+ title: 'Pure local signals vs track hints',
+ oneLiner: 'What happens when we remove the "next checkpoint" features and force the brain to drive from raw sensors only?',
+ loader: function () { return import('./chapters/pure-local-experiment.js'); },
+ },
'genetic-algorithm': {
title: 'Breeding brains instead of training them',
oneLiner: 'Copy the winners, nudge their weights, discard the losers. Repeat.',
diff --git a/AI-Car-Racer/eli15/tour.js b/AI-Car-Racer/eli15/tour.js
index 3f317a7..4131628 100644
--- a/AI-Car-Racer/eli15/tour.js
+++ b/AI-Car-Racer/eli15/tour.js
@@ -35,12 +35,12 @@
{
id: 'sensors',
anchor: '#inputCanvas',
- rationale: 'The five rays feeding the brain β this canvas shows what the car "sees".',
+ rationale: 'The seven rays feeding the brain β this canvas shows what the car "sees".',
},
{
id: 'neural-network',
anchor: '#inputCanvas',
- rationale: '92 numbers turning sensor rays into pedal + steer outputs.',
+ rationale: '244 numbers turning sensor rays into pedal + steer outputs.',
},
{
id: 'genetic-algorithm',
diff --git a/AI-Car-Racer/main.js b/AI-Car-Racer/main.js
index a102774..aca731c 100644
--- a/AI-Car-Racer/main.js
+++ b/AI-Car-Racer/main.js
@@ -269,6 +269,15 @@ if (new URLSearchParams(location.search).get('edit') === '1') {
window.__rvBridge.beginPhase4Trajectory(window.currentTrackVec || null);
}
} catch (e) { console.warn('[sona] beginTrajectory on auto-boot failed', e); }
+
+ // Ensure A/B HUD stays completely hidden on first load (it has dark
+ // backgrounds that were appearing as stray horizontal lines before any
+ // training or explicit A/B activation).
+ const abHudEl = document.getElementById('ab-hud');
+ if (abHudEl) {
+ abHudEl.hidden = true;
+ abHudEl.classList.remove('ab-hud-visible');
+ }
phase = 3;
nextPhase(); // β phase 4 (training)
}
@@ -291,6 +300,51 @@ if (localStorage.getItem("conservativeInit")){
// archiveBrain can record parent lineage and observe() can credit the seeds.
var rvDisabled = new URLSearchParams(location.search).get('rv') === '0';
var currentSeedIds = [];
+
+// Pure local sensors mode (experimental).
+// When enabled (?pure-local=1), the brain receives only the 7 ray readings + speed.
+// The two track-orientation features (lf, lr) are forced to 0.
+// This lets us compare "embodied local signals only" vs the current setup that
+// gives the network a soft "direction to next checkpoint" hint.
+var pureLocalSensors = new URLSearchParams(location.search).get('pure-local') === '1' ||
+ new URLSearchParams(location.search).get('pureLocal') === '1';
+window.pureLocalSensors = pureLocalSensors;
+
+if (pureLocalSensors) {
+ console.warn('%c[pure-local] Embodied / pure local signals mode active β lf + lr track-orientation features are forced to 0. The brain only receives the 7 raw ray readings + speed.', 'color:#f59e0b;font-weight:600');
+
+ // Persistent indicator (stays visible during training for easy comparison)
+ try {
+ const indicator = document.createElement('div');
+ indicator.id = 'pure-local-indicator';
+ indicator.textContent = 'ποΈ PURE LOCAL (no lf/lr)';
+ indicator.title = 'Brain receives only raw rays + speed. No "direction to next checkpoint" features. Use for embodied driving comparisons.';
+ indicator.style.cssText = 'position:fixed;top:6px;left:50%;transform:translateX(-50%);background:#431407;color:#fcd34d;padding:2px 14px;font-size:11px;border-radius:3px;z-index:99999;border:1px solid #78350f;white-space:nowrap;';
+ document.body.appendChild(indicator);
+ } catch (_) {}
+
+ // Lightweight metric logging for comparison (especially useful on Triangle)
+ if (!window.__pureLocalMetrics) {
+ window.__pureLocalMetrics = { bestCPSeen: 0, generations: 0 };
+ }
+ // Hook into genEnd for occasional reporting
+ const origHandleGenEnd = window.handleGenEnd;
+ if (origHandleGenEnd) {
+ window.handleGenEnd = function(m) {
+ const res = origHandleGenEnd.apply(this, arguments);
+ try {
+ if (window.pureLocalSensors && m && typeof m.checkPointsCount !== 'undefined') {
+ const cp = m.checkPointsCount | 0;
+ if (cp > window.__pureLocalMetrics.bestCPSeen) {
+ window.__pureLocalMetrics.bestCPSeen = cp;
+ console.log('%c[Pure Local] New best checkpoint reached: ' + cp, 'color:#f59e0b');
+ }
+ }
+ } catch (_) {}
+ return res;
+ };
+ }
+}
var generation = 0;
// Perf overlay β always on, disable with `?perf=0`. Reported ~6 Hz so the
@@ -375,6 +429,7 @@ function computeSensorStride(){
function perfRender(){
if (!perfEnabled) return;
+ if (window.__firstStart) return; // Don't show detailed status/perf on very first load until user clicks Start (keeps the initial view clean)
var hud = perfEnsureHud();
var frameDelta = perfAvg(perfBuf.frameDelta);
var sim = perfAvg(perfBuf.sim);
@@ -1188,7 +1243,8 @@ function performBegin(N){
type: 'init',
canvasW: canvas.width,
canvasH: canvas.height,
- borders, checkPointList
+ borders, checkPointList,
+ pureLocalSensors: !!window.pureLocalSensors
});
workerInited = true;
}
@@ -1338,6 +1394,14 @@ begin();
// doesn't start stepping before the user opts in.
if (window.__firstStart){
pause = true;
+
+ // First-visit UX polish: keep advanced experimental UI collapsed so the
+ // canvas and the big Start button are the clear focus. The user can open
+ // the π§ͺ Experiments panel or click "Customize Track" whenever they want.
+ try {
+ const exp = document.querySelector('details.rv-experiments');
+ if (exp) exp.open = false;
+ } catch (_) {}
}
animate();
@@ -1736,6 +1800,11 @@ window.__downloadCSV = function(label, rows){
var abHudB = document.getElementById('ab-hud-b');
var abHudDelta = document.getElementById('ab-hud-delta');
+ // Clean any stale inline styles from previous sessions/toggles
+ if (abHud) {
+ abHud.style.removeProperty('display');
+ }
+
var abEnabled = false;
var simWorkerB = null;
var bState = null;
@@ -1946,7 +2015,10 @@ window.__downloadCSV = function(label, rows){
if (abEnabled){
ensureCtxB();
if (canvasB) canvasB.hidden = false;
- if (abHud) abHud.hidden = false;
+ if (abHud) {
+ abHud.hidden = false;
+ abHud.classList.add('ab-hud-visible');
+ }
if (cDiv) cDiv.classList.add('ab-on');
spawnB();
// spawnB creates bState but workerReady lags the Worker's internal
@@ -1960,7 +2032,10 @@ window.__downloadCSV = function(label, rows){
} else {
teardownB();
if (canvasB) canvasB.hidden = true;
- if (abHud) abHud.hidden = true;
+ if (abHud) {
+ abHud.hidden = true;
+ abHud.classList.remove('ab-hud-visible');
+ }
if (cDiv) cDiv.classList.remove('ab-on');
}
}
diff --git a/AI-Car-Racer/sim-worker.js b/AI-Car-Racer/sim-worker.js
index 37cf4ce..78649e0 100644
--- a/AI-Car-Racer/sim-worker.js
+++ b/AI-Car-Racer/sim-worker.js
@@ -124,6 +124,7 @@ function handleInit(m) {
if (self.road.checkPointList && self.road.checkPointList.length) {
self.road.cpGrid.addSegments(self.road.checkPointList);
}
+ self.pureLocalSensors = !!m.pureLocalSensors;
}
// Build a car polygon at an arbitrary pose without allocating a Car (avoids
diff --git a/AI-Car-Racer/style.css b/AI-Car-Racer/style.css
index 9e249a6..376c67f 100644
--- a/AI-Car-Racer/style.css
+++ b/AI-Car-Racer/style.css
@@ -290,13 +290,19 @@ p { line-height: 1.5; margin: 0 0 .6em 0; }
top: calc(50% - 32px);
left: 8px;
z-index: 5;
- display: flex;
+ display: none; /* hidden by default β only shown when A/B comparison is explicitly enabled */
flex-direction: row;
gap: 6px;
pointer-events: none;
font: 11px/1.35 ui-monospace, Menlo, monospace;
color: #a8c8ff;
}
+
+/* When A/B mode is active, the canvasDiv gets .ab-on, and we also toggle
+ .ab-hud-visible on the HUD itself for reliable show/hide. */
+#ab-hud.ab-hud-visible {
+ display: flex;
+}
#ab-hud-b, #ab-hud-delta {
background: rgba(12, 14, 18, .88);
padding: 6px 9px;
@@ -1016,6 +1022,14 @@ label {
font-variant-numeric: tabular-nums;
}
.rv-seed-sources[hidden] { display: none; }
+/* one-time subtle highlight (light amber wash) on first non-zero archive_recall in run */
+.rv-seed-sources.rv-seed-first {
+ background: rgba(245, 158, 11, 0.10);
+ padding-left: 0.2em;
+ padding-right: 0.2em;
+ border-radius: 2px;
+ transition: background-color 2200ms ease-out;
+}
/* Master toggle β custom switch */
.rv-master-toggle {
@@ -1374,6 +1388,14 @@ label {
}
.rv-lineage-tooltip[hidden] { display: none; }
+/* Discoverability polish for static "Compare A/B" checkbox (first-load only, clears on use) */
+.rv-ab-toggle.rv-ab-first {
+ background: rgba(245, 158, 11, 0.12);
+ padding: 0.05em 0.35em;
+ border-radius: 3px;
+ transition: background-color 1200ms ease-out;
+}
+
/* A/B toggle strip */
.rv-abstrip {
margin-top: 0.5em;
diff --git a/AI-Car-Racer/uiPanels.js b/AI-Car-Racer/uiPanels.js
index 7e6db88..5ea2c69 100644
--- a/AI-Car-Racer/uiPanels.js
+++ b/AI-Car-Racer/uiPanels.js
@@ -61,7 +61,7 @@
// Makes the value-prop ("ruvector helps") visible without page reloads.
'',
// The reranker line, when visible, gets a badge pointing at the EMA chapter.
// The badge is a sibling of reranker text in the same line.
@@ -625,6 +625,10 @@
if (el.abToggle) {
el.abToggle.addEventListener('change', function () {
const enabled = !!el.abToggle.checked;
+ // clear first-load discoverability tint + set session flag (pure polish, no behavior change)
+ const abLabel = el.abToggle.parentElement;
+ if (abLabel) abLabel.classList.remove('rv-ab-first');
+ try { sessionStorage.setItem('rv-ab-seen', '1'); } catch (_) {}
try {
if (typeof window.__abSetEnabled === 'function') {
window.__abSetEnabled(enabled);
@@ -637,6 +641,14 @@
});
}
+ // one-time light amber tint on the A/B label for first-time discoverability (clears on interaction or via session flag)
+ if (el.abToggle) {
+ const abLabel = el.abToggle.parentElement;
+ if (abLabel && !sessionStorage.getItem('rv-ab-seen')) {
+ abLabel.classList.add('rv-ab-first');
+ }
+ }
+
// P3.B β mount the lineage viewer once; rendering is driven from tick().
// We don't *expand* the section by default β the DAG costs a real layout
// pass, and the panel has plenty of other rows. The expand toggle below
@@ -784,6 +796,8 @@
lastShift: null, // null until an observe() tick has a baseline to diff
};
+ let firstArchiveHighlight = false; // one-time subtle highlight when archive_recall first >0
+
function bridgeReadyLocal() {
if (window.rvDisabled) return false;
const b = window.__rvBridge;
@@ -828,10 +842,16 @@
const archive = s.archive_recall | 0;
const prior = s.localStorage_prior | 0;
const random = s.random_init | 0;
+ const total = archive + prior + random;
+ const archivePct = total > 0 ? Math.round((archive / total) * 100) : 0;
el.seedSourcesText.textContent =
- 'gen seed sources: archive ' + archive +
- ' Β· prior ' + prior +
+ 'ruvector seeded ' + archive + ' (' + archivePct + '%) similar Β· prior ' + prior +
' Β· random ' + random;
+ if (archive > 0 && !firstArchiveHighlight) {
+ firstArchiveHighlight = true;
+ el.seedSources.classList.add('rv-seed-first');
+ setTimeout(() => { try { el.seedSources.classList.remove('rv-seed-first'); } catch (_) {} }, 2800);
+ }
}
// Spearman's footrule over the union of ids. Ids present in only one list
@@ -1626,6 +1646,15 @@
details.innerHTML = [
'π§ͺ Experiments (RuLake-inspired toggles)',
'
',
+ '
',
+ ' ',
+ ' comparison mode β brain sees only rays + speed',
+ ' ',
+ '