@reicek/neataptic-ts 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/boundary-mapper.agent.md +29 -0
- package/.github/agents/docs-scout.agent.md +29 -0
- package/.github/agents/plan-scout.agent.md +29 -0
- package/.github/agents/solid-split.agent.md +138 -0
- package/.github/copilot-instructions.md +103 -0
- package/package.json +6 -3
- package/plans/ES2023 migration +13 -8
- package/plans/Evolution_Training_Interoperability_Contracts.md +1 -1
- package/plans/Interactive_Examples_and_Learning_Path.md +10 -2
- package/plans/Memory_Optimization.md +3 -3
- package/plans/README.md +63 -0
- package/plans/Roadmap.md +15 -3
- package/plans/asciiMaze_SOLID_split.done.md +130 -0
- package/plans/flappy_bird_SOLID_split.done.md +67 -0
- package/scripts/assets/theme.css +221 -34
- package/scripts/copy-examples.mjs +9 -5
- package/scripts/export-onnx.mjs +3 -3
- package/scripts/generate-bench-tables.mjs +10 -10
- package/scripts/generate-bench-tables.ts +10 -10
- package/scripts/generate-docs.ts +1415 -449
- package/scripts/render-docs-html.ts +15 -8
- package/src/README.md +101 -223
- package/src/architecture/README.md +57 -185
- package/src/architecture/layer/README.md +38 -38
- package/src/architecture/network/README.md +33 -31
- package/src/architecture/network/activate/README.md +77 -77
- package/src/architecture/network/connect/README.md +15 -13
- package/src/architecture/network/deterministic/README.md +7 -7
- package/src/architecture/network/evolve/README.md +44 -44
- package/src/architecture/network/gating/README.md +20 -20
- package/src/architecture/network/genetic/README.md +51 -51
- package/src/architecture/network/mutate/README.md +97 -97
- package/src/architecture/network/onnx/README.md +264 -264
- package/src/architecture/network/prune/README.md +39 -39
- package/src/architecture/network/remove/README.md +26 -26
- package/src/architecture/network/serialize/README.md +56 -56
- package/src/architecture/network/slab/README.md +61 -61
- package/src/architecture/network/standalone/README.md +24 -24
- package/src/architecture/network/stats/README.md +9 -9
- package/src/architecture/network/topology/README.md +46 -46
- package/src/architecture/network/training/README.md +21 -21
- package/src/methods/README.md +9 -87
- package/src/multithreading/README.md +8 -77
- package/src/multithreading/workers/README.md +2 -2
- package/src/multithreading/workers/browser/README.md +0 -6
- package/src/multithreading/workers/node/README.md +0 -3
- package/src/neat/README.md +562 -568
- package/src/utils/README.md +18 -18
- package/test/examples/asciiMaze/README.md +59 -59
- package/test/examples/asciiMaze/asciiMaze.e2e.test.ts +14 -9
- package/test/examples/asciiMaze/browser-entry/README.md +196 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.abort.services.ts +95 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.constants.ts +23 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.curriculum.services.ts +115 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.globals.services.ts +106 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.host.services.ts +157 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.services.ts +14 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.ts +129 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.types.ts +120 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.utils.ts +98 -0
- package/test/examples/asciiMaze/browser-entry.ts +10 -576
- package/test/examples/asciiMaze/dashboardManager/README.md +276 -0
- package/test/examples/asciiMaze/dashboardManager/archive/README.md +16 -0
- package/test/examples/asciiMaze/dashboardManager/archive/dashboardManager.archive.services.ts +267 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.constants.ts +35 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.services.ts +103 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.ts +181 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.types.ts +267 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.utils.ts +254 -0
- package/test/examples/asciiMaze/dashboardManager/live/README.md +14 -0
- package/test/examples/asciiMaze/dashboardManager/live/dashboardManager.live.services.ts +264 -0
- package/test/examples/asciiMaze/dashboardManager/telemetry/README.md +47 -0
- package/test/examples/asciiMaze/dashboardManager/telemetry/dashboardManager.telemetry.services.ts +513 -0
- package/test/examples/asciiMaze/dashboardManager.ts +13 -2335
- package/test/examples/asciiMaze/evolutionEngine/README.md +1058 -0
- package/test/examples/asciiMaze/evolutionEngine/curriculumPhase.ts +90 -0
- package/test/examples/asciiMaze/evolutionEngine/engineState.constants.ts +36 -0
- package/test/examples/asciiMaze/evolutionEngine/engineState.ts +58 -513
- package/test/examples/asciiMaze/evolutionEngine/engineState.types.ts +212 -0
- package/test/examples/asciiMaze/evolutionEngine/engineState.utils.ts +301 -0
- package/test/examples/asciiMaze/evolutionEngine/evolutionEngine.types.ts +445 -0
- package/test/examples/asciiMaze/evolutionEngine/evolutionLoop.ts +81 -50
- package/test/examples/asciiMaze/evolutionEngine/optionsAndSetup.ts +2 -4
- package/test/examples/asciiMaze/evolutionEngine/populationDynamics.ts +17 -33
- package/test/examples/asciiMaze/evolutionEngine/populationPruning.ts +1 -1
- package/test/examples/asciiMaze/evolutionEngine/rngAndTiming.ts +1 -2
- package/test/examples/asciiMaze/evolutionEngine/sampling.ts +1 -1
- package/test/examples/asciiMaze/evolutionEngine/scratchPools.ts +2 -5
- package/test/examples/asciiMaze/evolutionEngine/setupHelpers.ts +30 -37
- package/test/examples/asciiMaze/evolutionEngine/telemetryMetrics.ts +16 -58
- package/test/examples/asciiMaze/evolutionEngine/trainingWarmStart.ts +2 -2
- package/test/examples/asciiMaze/evolutionEngine.ts +55 -55
- package/test/examples/asciiMaze/fitness.ts +2 -2
- package/test/examples/asciiMaze/fitness.types.ts +65 -0
- package/test/examples/asciiMaze/interfaces.ts +64 -1352
- package/test/examples/asciiMaze/mazeMovement/README.md +356 -0
- package/test/examples/asciiMaze/mazeMovement/finalization/README.md +49 -0
- package/test/examples/asciiMaze/mazeMovement/finalization/mazeMovement.finalization.ts +138 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.constants.ts +101 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.services.ts +230 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.ts +299 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.types.ts +185 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.utils.ts +153 -0
- package/test/examples/asciiMaze/mazeMovement/policy/README.md +91 -0
- package/test/examples/asciiMaze/mazeMovement/policy/mazeMovement.policy.ts +467 -0
- package/test/examples/asciiMaze/mazeMovement/runtime/README.md +95 -0
- package/test/examples/asciiMaze/mazeMovement/runtime/mazeMovement.runtime.ts +354 -0
- package/test/examples/asciiMaze/mazeMovement/shaping/README.md +124 -0
- package/test/examples/asciiMaze/mazeMovement/shaping/mazeMovement.shaping.ts +459 -0
- package/test/examples/asciiMaze/mazeMovement.ts +12 -2978
- package/test/examples/flappy_bird/Trace-20260309T191949.json +24124 -0
- package/test/examples/flappy_bird/browser-entry/README.md +1129 -0
- package/test/examples/flappy_bird/browser-entry/browser-entry.host.utils.ts +4 -324
- package/test/examples/flappy_bird/browser-entry/browser-entry.network-view.utils.ts +6 -399
- package/test/examples/flappy_bird/browser-entry/browser-entry.playback.utils.ts +1 -717
- package/test/examples/flappy_bird/browser-entry/browser-entry.spawn.utils.ts +11 -31
- package/test/examples/flappy_bird/browser-entry/browser-entry.visualization.utils.ts +15 -893
- package/test/examples/flappy_bird/browser-entry/host/README.md +307 -0
- package/test/examples/flappy_bird/browser-entry/host/host.resize.service.ts +1 -295
- package/test/examples/flappy_bird/browser-entry/host/host.ts +562 -6
- package/test/examples/flappy_bird/browser-entry/host/resize/README.md +274 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.constants.ts +31 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.services.ts +360 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.ts +117 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.types.ts +63 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.utils.ts +250 -0
- package/test/examples/flappy_bird/browser-entry/network-view/README.md +399 -0
- package/test/examples/flappy_bird/browser-entry/network-view/network-view.topology.utils.ts +255 -0
- package/test/examples/flappy_bird/browser-entry/network-view/network-view.ts +802 -7
- package/test/examples/flappy_bird/browser-entry/playback/README.md +684 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/README.md +277 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/README.md +770 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.cache.services.ts +178 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.constants.ts +107 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.geometry.utils.ts +518 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.math.utils.ts +117 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.pulse.utils.ts +233 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.services.ts +211 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.ts +48 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.types.ts +212 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.utils.ts +81 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.cache.services.ts +96 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.constants.ts +62 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.services.ts +244 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.ts +53 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.types.ts +68 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.utils.ts +100 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/README.md +310 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.service.ts +92 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.services.ts +272 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.types.ts +39 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.utils.ts +493 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.constants.ts +1 -1
- package/test/examples/flappy_bird/browser-entry/playback/playback.frame-render.service.ts +4 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.snapshot.utils.ts +44 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.service.ts +39 -122
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.services.ts +272 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.types.ts +62 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.utils.ts +11 -4
- package/test/examples/flappy_bird/browser-entry/playback/playback.ts +409 -8
- package/test/examples/flappy_bird/browser-entry/playback/playback.types.ts +4 -12
- package/test/examples/flappy_bird/browser-entry/runtime/README.md +235 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.evolution-launch.service.ts +45 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.lifecycle.service.ts +81 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.startup.service.ts +74 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.ts +31 -121
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.types.ts +36 -0
- package/test/examples/flappy_bird/browser-entry/visualization/README.md +557 -0
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.constants.ts +110 -0
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.draw.service.ts +957 -19
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.legend.utils.ts +138 -3
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.topology.utils.ts +3 -27
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.ts +1 -23
- package/test/examples/flappy_bird/browser-entry/worker-channel/README.md +156 -0
- package/test/examples/flappy_bird/constants/README.md +1179 -0
- package/test/examples/flappy_bird/constants/constants.network-view.ts +24 -0
- package/test/examples/flappy_bird/constants/constants.palette.ts +7 -0
- package/test/examples/flappy_bird/constants/constants.starfield.ts +78 -3
- package/test/examples/flappy_bird/environment/README.md +143 -0
- package/test/examples/flappy_bird/environment/environment.observation.utils.ts +1 -19
- package/test/examples/flappy_bird/environment/environment.step.service.ts +3 -66
- package/test/examples/flappy_bird/evaluation/README.md +130 -0
- package/test/examples/flappy_bird/evaluation/evaluation.fitness.utils.ts +1 -1
- package/test/examples/flappy_bird/evaluation/evaluation.rollout.service.ts +5 -375
- package/test/examples/flappy_bird/evaluation/rollout/README.md +291 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.constants.ts +30 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.service.ts +58 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.services.ts +310 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.types.ts +56 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.utils.ts +368 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/README.md +618 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.playback.service.ts +7 -7
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.simulation.frame.service.ts +364 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.simulation.types.ts +14 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.simulation.utils.ts +4 -201
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.ts +184 -345
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.warm-start.service.ts +291 -0
- package/test/examples/flappy_bird/flappy.simulation.shared.utils.ts +5 -0
- package/test/examples/flappy_bird/simulation-shared/README.md +417 -0
- package/test/examples/flappy_bird/simulation-shared/observation/README.md +183 -0
- package/test/examples/flappy_bird/simulation-shared/observation/observation.features.utils.ts +301 -0
- package/test/examples/flappy_bird/simulation-shared/observation/observation.ts +9 -0
- package/test/examples/flappy_bird/simulation-shared/observation/observation.vector.utils.ts +59 -0
- package/test/examples/flappy_bird/simulation-shared/simulation-shared.observation.utils.ts +5 -403
- package/test/examples/flappy_bird/simulation-shared/simulation-shared.spawn.utils.ts +20 -6
- package/test/examples/flappy_bird/{evaluation/evaluation.statistics.utils.ts → simulation-shared/simulation-shared.statistics.utils.ts} +23 -8
- package/test/examples/flappy_bird/trainer/README.md +563 -0
- package/test/examples/flappy_bird/trainer/evaluation/README.md +199 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.constants.ts +9 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.services.ts +73 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.ts +165 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.types.ts +25 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.utils.ts +161 -0
- package/test/examples/flappy_bird/trainer/trainer.evaluation.service.ts +13 -0
- package/test/examples/flappy_bird/trainer/trainer.report.service.services.ts +181 -0
- package/test/examples/flappy_bird/trainer/trainer.report.service.ts +126 -0
- package/test/examples/flappy_bird/trainer/trainer.selection.utils.ts +89 -0
- package/test/examples/flappy_bird/trainer/trainer.ts +11 -553
- package/test/examples/flappy_bird/browser-entry/browser-entry.utils.ts +0 -12
- package/test/examples/flappy_bird/environment/environment.ts +0 -7
- package/test/examples/flappy_bird/evaluation/evaluation.ts +0 -7
- package/test/examples/flappy_bird/simulation-shared/simulation-shared.ts +0 -15
- package/test/examples/flappy_bird/trainer/trainer.statistics.utils.ts +0 -78
|
@@ -1,2339 +1,17 @@
|
|
|
1
|
-
// Rebuilt clean version with private fields
|
|
2
|
-
import { MazeUtils } from './mazeUtils';
|
|
3
|
-
import { MazeVisualization } from './mazeVisualization';
|
|
4
|
-
import { NetworkVisualization } from './networkVisualization';
|
|
5
|
-
import { colors } from './colors';
|
|
6
|
-
import { INetwork, IDashboardManager, IMazeRunResult } from './interfaces';
|
|
7
|
-
import type Neat from '../../../src/neat';
|
|
8
|
-
|
|
9
|
-
// Region: NEAT Runtime Type Interfaces ---------------------------------------
|
|
10
|
-
|
|
11
|
-
/** NEAT genome/network with runtime properties */
|
|
12
|
-
interface NeatGenome {
|
|
13
|
-
score?: number;
|
|
14
|
-
nodes?: Array<{ type?: string; [key: string]: unknown }>;
|
|
15
|
-
connections?: Array<{ enabled?: boolean; [key: string]: unknown }>;
|
|
16
|
-
[key: string]: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** NEAT species collection */
|
|
20
|
-
interface NeatSpecies {
|
|
21
|
-
length?: number;
|
|
22
|
-
[key: string]: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** NEAT instance with population and optional methods */
|
|
26
|
-
interface NeatInstance {
|
|
27
|
-
population?: NeatGenome[];
|
|
28
|
-
species?: NeatSpecies;
|
|
29
|
-
getOperatorStats?: () => unknown[];
|
|
30
|
-
getMutationStats?: () => Record<string, number>;
|
|
31
|
-
getNoveltyArchive?: () => unknown[];
|
|
32
|
-
[key: string]: unknown;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Operator stats entry from NEAT */
|
|
36
|
-
interface OperatorStatsEntry {
|
|
37
|
-
name: string;
|
|
38
|
-
success: number;
|
|
39
|
-
attempts: number;
|
|
40
|
-
accepted?: number;
|
|
41
|
-
total?: number;
|
|
42
|
-
[key: string]: unknown;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Region: Type Interfaces ----------------------------------------------------
|
|
46
|
-
type NumericTelemetryMap = Record<string, number | null | undefined>;
|
|
47
|
-
|
|
48
|
-
interface AsciiMazeComplexityStats extends NumericTelemetryMap {
|
|
49
|
-
meanNodes?: number | null;
|
|
50
|
-
meanConns?: number | null;
|
|
51
|
-
growthNodes?: number | null;
|
|
52
|
-
growthConns?: number | null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type MutationStatsMap = Record<string, unknown>;
|
|
56
|
-
|
|
57
|
-
interface DashboardTelemetry {
|
|
58
|
-
complexity?: AsciiMazeComplexityStats | null;
|
|
59
|
-
perf?: NumericTelemetryMap | null;
|
|
60
|
-
lineage?: NumericTelemetryMap | null;
|
|
61
|
-
diversity?: NumericTelemetryMap | null;
|
|
62
|
-
fronts?: ReadonlyArray<ReadonlyArray<unknown>> | null;
|
|
63
|
-
objectives?: NumericTelemetryMap | null;
|
|
64
|
-
hyper?: number | null;
|
|
65
|
-
mutationStats?: MutationStatsMap | null;
|
|
66
|
-
mutation?: { stats?: MutationStatsMap | null } | null;
|
|
67
|
-
species?: number | null;
|
|
68
|
-
saturationFraction?: number | null;
|
|
69
|
-
actionEntropy?: number | null;
|
|
70
|
-
populationMean?: number | null;
|
|
71
|
-
populationMedian?: number | null;
|
|
72
|
-
enabledConnRatio?: number | null;
|
|
73
|
-
bestFitness?: number | null;
|
|
74
|
-
bestFitnessDelta?: number | null;
|
|
75
|
-
topSpeciesSizes?: number[] | null;
|
|
76
|
-
noveltyArchiveSize?: number | null;
|
|
77
|
-
operatorAcceptance?: Array<{ name: string; acceptancePct: number }> | null;
|
|
78
|
-
topMutations?: Array<{ name: string; count: number }> | null;
|
|
79
|
-
trends?: {
|
|
80
|
-
fitness?: string | null;
|
|
81
|
-
nodes?: string | null;
|
|
82
|
-
conns?: string | null;
|
|
83
|
-
hyper?: string | null;
|
|
84
|
-
progress?: string | null;
|
|
85
|
-
species?: string | null;
|
|
86
|
-
} | null;
|
|
87
|
-
histories?: {
|
|
88
|
-
bestFitness?: number[];
|
|
89
|
-
nodes?: number[];
|
|
90
|
-
conns?: number[];
|
|
91
|
-
hyper?: number[];
|
|
92
|
-
progress?: number[];
|
|
93
|
-
species?: number[];
|
|
94
|
-
} | null;
|
|
95
|
-
timestamp?: number;
|
|
96
|
-
generation?: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Detailed stats structure produced inside the dashboard. */
|
|
100
|
-
interface AsciiMazeDetailedStats {
|
|
101
|
-
generation: number;
|
|
102
|
-
bestFitness: number | null;
|
|
103
|
-
bestFitnessDelta: number | null;
|
|
104
|
-
saturationFraction: number | null;
|
|
105
|
-
actionEntropy: number | null;
|
|
106
|
-
populationMean: number | null;
|
|
107
|
-
populationMedian: number | null;
|
|
108
|
-
enabledConnRatio: number | null;
|
|
109
|
-
complexity: AsciiMazeComplexityStats | null;
|
|
110
|
-
simplifyPhaseActive: boolean;
|
|
111
|
-
perf: NumericTelemetryMap | null;
|
|
112
|
-
lineage: NumericTelemetryMap | null;
|
|
113
|
-
diversity: NumericTelemetryMap | null;
|
|
114
|
-
speciesCount: number | null;
|
|
115
|
-
topSpeciesSizes: number[] | null;
|
|
116
|
-
objectives: NumericTelemetryMap | null;
|
|
117
|
-
paretoFrontSizes: number[] | null;
|
|
118
|
-
firstFrontSize: number;
|
|
119
|
-
hypervolume: number | null;
|
|
120
|
-
noveltyArchiveSize: number | null;
|
|
121
|
-
operatorAcceptance: Array<{ name: string; acceptancePct: number }> | null;
|
|
122
|
-
topMutations: Array<{ name: string; count: number }> | null;
|
|
123
|
-
mutationStats: MutationStatsMap | null;
|
|
124
|
-
trends: {
|
|
125
|
-
fitness: string | null;
|
|
126
|
-
nodes: string | null;
|
|
127
|
-
conns: string | null;
|
|
128
|
-
hyper: string | null;
|
|
129
|
-
progress: string | null;
|
|
130
|
-
species: string | null;
|
|
131
|
-
};
|
|
132
|
-
histories: {
|
|
133
|
-
bestFitness: number[];
|
|
134
|
-
nodes: number[];
|
|
135
|
-
conns: number[];
|
|
136
|
-
hyper: number[];
|
|
137
|
-
progress: number[];
|
|
138
|
-
species: number[];
|
|
139
|
-
};
|
|
140
|
-
timestamp: number;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Public snapshot returned by getLastTelemetry(). */
|
|
144
|
-
interface AsciiMazeTelemetrySnapshot {
|
|
145
|
-
generation: number;
|
|
146
|
-
bestFitness: number | null;
|
|
147
|
-
progress: number | null;
|
|
148
|
-
speciesCount: number | null;
|
|
149
|
-
gensPerSec: number;
|
|
150
|
-
timestamp: number;
|
|
151
|
-
details: AsciiMazeDetailedStats | null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
1
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* ASCII dashboard for the maze NEAT example. Tracks current best genome,
|
|
158
|
-
* bounded histories (fitness, complexity, hypervolume, progress, species),
|
|
159
|
-
* archives solved mazes with efficiency stats, and emits telemetry events.
|
|
2
|
+
* Compatibility entrypoint for the dedicated dashboardManager module.
|
|
160
3
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
4
|
+
* The real implementation now lives under `dashboardManager/dashboardManager.ts`.
|
|
5
|
+
* This file remains so existing imports such as `./dashboardManager` continue
|
|
6
|
+
* to resolve without changes.
|
|
164
7
|
*/
|
|
165
|
-
export class DashboardManager implements IDashboardManager {
|
|
166
|
-
/** Allow additional properties for extensibility */
|
|
167
|
-
[key: string]: unknown;
|
|
168
|
-
|
|
169
|
-
#solvedMazes: Array<{
|
|
170
|
-
maze: string[];
|
|
171
|
-
result: IMazeRunResult;
|
|
172
|
-
network: INetwork;
|
|
173
|
-
generation: number;
|
|
174
|
-
}> = [];
|
|
175
|
-
#solvedMazeKeys: Set<string> = new Set();
|
|
176
|
-
#currentBest: {
|
|
177
|
-
result: IMazeRunResult;
|
|
178
|
-
network: INetwork;
|
|
179
|
-
generation: number;
|
|
180
|
-
} | null = null;
|
|
181
|
-
#lastTelemetry: DashboardTelemetry | null = null;
|
|
182
|
-
#lastBestFitness: number | null = null;
|
|
183
|
-
#bestFitnessHistory: number[] = [];
|
|
184
|
-
#complexityNodesHistory: number[] = [];
|
|
185
|
-
#complexityConnsHistory: number[] = [];
|
|
186
|
-
#hypervolumeHistory: number[] = [];
|
|
187
|
-
#progressHistory: number[] = [];
|
|
188
|
-
#speciesCountHistory: number[] = [];
|
|
189
|
-
#lastDetailedStats: AsciiMazeDetailedStats | null = null;
|
|
190
|
-
#runStartTs: number | null = null;
|
|
191
|
-
#perfStart: number | null = null;
|
|
192
|
-
#lastGeneration: number | null = null;
|
|
193
|
-
#lastUpdateTs: number | null = null;
|
|
194
|
-
|
|
195
|
-
#logFn: (...args: unknown[]) => void;
|
|
196
|
-
#clearFn: () => void;
|
|
197
|
-
#archiveFn?: (...args: unknown[]) => void;
|
|
198
|
-
|
|
199
|
-
static #HISTORY_MAX = 500;
|
|
200
|
-
static #FRAME_INNER_WIDTH = 148;
|
|
201
|
-
static #LEFT_PADDING = 7;
|
|
202
|
-
static #RIGHT_PADDING = 1;
|
|
203
|
-
static #STAT_LABEL_WIDTH = 28;
|
|
204
|
-
static #ARCHIVE_SPARK_WIDTH = 64; // spark width in archive blocks
|
|
205
|
-
static #GENERAL_SPARK_WIDTH = 64; // spark width in live panel
|
|
206
|
-
static #SOLVED_LABEL_WIDTH = 22; // label width in archive stats
|
|
207
|
-
static #HISTORY_EXPORT_WINDOW = 200; // samples exported in telemetry details
|
|
208
|
-
static #SPARK_BLOCKS = Object.freeze([
|
|
209
|
-
'▁',
|
|
210
|
-
'▂',
|
|
211
|
-
'▃',
|
|
212
|
-
'▄',
|
|
213
|
-
'▅',
|
|
214
|
-
'▆',
|
|
215
|
-
'▇',
|
|
216
|
-
'█',
|
|
217
|
-
]);
|
|
218
|
-
static #DELTA_EPSILON = 1e-9;
|
|
219
|
-
static #TOP_OPERATOR_LIMIT = 6;
|
|
220
|
-
static #TOP_MUTATION_LIMIT = 8;
|
|
221
|
-
static #TOP_SPECIES_LIMIT = 5;
|
|
222
|
-
static #LAYER_INFER_LOOP_MULTIPLIER = 4;
|
|
223
|
-
static #LABEL_PATH_EFF = 'Path efficiency';
|
|
224
|
-
static #LABEL_PATH_OVER = 'Path overhead';
|
|
225
|
-
static #LABEL_UNIQUE = 'Unique cells visited';
|
|
226
|
-
static #LABEL_REVISITS = 'Cells revisited';
|
|
227
|
-
static #LABEL_STEPS = 'Steps';
|
|
228
|
-
static #LABEL_FITNESS = 'Fitness';
|
|
229
|
-
static #LABEL_ARCH = 'Architecture';
|
|
230
|
-
static #FRAME_SINGLE_LINE_CHAR = '═';
|
|
231
|
-
static #FRAME_BRIDGE_TOP = '╦════════════╦';
|
|
232
|
-
static #FRAME_BRIDGE_BOTTOM = '╩════════════╩';
|
|
233
|
-
static #EVOLVING_SECTION_LINE = '══════════════════════';
|
|
234
|
-
static get FRAME_INNER_WIDTH() {
|
|
235
|
-
return DashboardManager.#FRAME_INNER_WIDTH;
|
|
236
|
-
}
|
|
237
|
-
static get LEFT_PADDING() {
|
|
238
|
-
return DashboardManager.#LEFT_PADDING;
|
|
239
|
-
}
|
|
240
|
-
static get RIGHT_PADDING() {
|
|
241
|
-
return DashboardManager.#RIGHT_PADDING;
|
|
242
|
-
}
|
|
243
|
-
static get CONTENT_WIDTH() {
|
|
244
|
-
return (
|
|
245
|
-
DashboardManager.#FRAME_INNER_WIDTH -
|
|
246
|
-
DashboardManager.#LEFT_PADDING -
|
|
247
|
-
DashboardManager.#RIGHT_PADDING
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
static get STAT_LABEL_WIDTH() {
|
|
251
|
-
return DashboardManager.#STAT_LABEL_WIDTH;
|
|
252
|
-
}
|
|
253
|
-
static get HISTORY_MAX() {
|
|
254
|
-
return DashboardManager.#HISTORY_MAX;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Create a new DashboardManager.
|
|
259
|
-
*
|
|
260
|
-
* @param clearFn Function that clears the live dashboard region (terminal or DOM). Required.
|
|
261
|
-
* @param logFn Function used for streaming live panel lines. Required.
|
|
262
|
-
* @param archiveFn Optional function used to prepend/append solved-maze archive blocks (separate area / element).
|
|
263
|
-
*
|
|
264
|
-
* Defensive notes:
|
|
265
|
-
* - Non-function arguments are coerced to no-ops to avoid runtime crashes in mixed environments (browser / node tests).
|
|
266
|
-
* - All three functions are stored as private fields (#clearFn, #logFn, #archiveFn) for later reuse.
|
|
267
|
-
*/
|
|
268
|
-
constructor(
|
|
269
|
-
clearFn: () => void,
|
|
270
|
-
logFn: (...args: unknown[]) => void,
|
|
271
|
-
archiveFn?: (...args: unknown[]) => void,
|
|
272
|
-
) {
|
|
273
|
-
const noop = () => {};
|
|
274
|
-
this.#clearFn = typeof clearFn === 'function' ? clearFn : noop;
|
|
275
|
-
this.#logFn = typeof logFn === 'function' ? logFn : noop;
|
|
276
|
-
this.#archiveFn = typeof archiveFn === 'function' ? archiveFn : undefined;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Optional log function for dashboard messages (implements IDashboardManager) */
|
|
280
|
-
get logFunction(): ((msg: string) => void) | undefined {
|
|
281
|
-
return this.#logFn;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** Emit a blank padded line inside the frame to avoid duplication. */
|
|
285
|
-
#logBlank(): void {
|
|
286
|
-
this.#logFn(
|
|
287
|
-
`${colors.blueCore}║${NetworkVisualization.pad(
|
|
288
|
-
' ',
|
|
289
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
290
|
-
' ',
|
|
291
|
-
)}${colors.blueCore}║${colors.reset}`,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Format a single statistic line (label + value) framed for the dashboard.
|
|
297
|
-
*
|
|
298
|
-
* Educational goals:
|
|
299
|
-
* - Demonstrates consistent alignment via fixed label column width.
|
|
300
|
-
* - Centralizes color application so other helpers (`#appendSolvedPathStats`, etc.) remain lean.
|
|
301
|
-
* - Shows simple, allocation‑aware string building without external libs.
|
|
302
|
-
*
|
|
303
|
-
* Steps:
|
|
304
|
-
* 1. Canonicalize label (ensure trailing colon) for uniform appearance.
|
|
305
|
-
* 2. Pad label to `labelWidth` (left aligned) creating a fixed column.
|
|
306
|
-
* 3. Normalize the value to string (numbers preserved; null/undefined become literal strings for transparency).
|
|
307
|
-
* 4. Compose colored content segment (`label` + single space + `value`).
|
|
308
|
-
* 5. Left/right pad inside the frame content width and wrap with vertical border glyphs.
|
|
309
|
-
*
|
|
310
|
-
* Performance notes:
|
|
311
|
-
* - O(L) where L = composed string length; dominated by `padEnd` + `NetworkVisualization.pad`.
|
|
312
|
-
* - Avoids template churn inside loops by keeping construction linear.
|
|
313
|
-
* - No truncation: labels longer than `labelWidth` intentionally overflow to surface overly verbose labels during development.
|
|
314
|
-
*
|
|
315
|
-
* Determinism: Pure formatting; no external state or randomness.
|
|
316
|
-
* Reentrancy: Safe; relies only on parameters and static sizing constants.
|
|
317
|
-
* Edge cases: Empty label yields just a colon after canonicalization (":"); nullish values become "null" / "undefined" explicitly.
|
|
318
|
-
*
|
|
319
|
-
* @param label Descriptive metric label (colon appended if missing).
|
|
320
|
-
* @param value Metric value (string or number) displayed after a space.
|
|
321
|
-
* @param colorLabel ANSI / style token for the label portion.
|
|
322
|
-
* @param colorValue ANSI / style token for the value portion.
|
|
323
|
-
* @param labelWidth Fixed width for the label column (default derives from class constant).
|
|
324
|
-
* @returns Fully framed, colorized line ready for logging.
|
|
325
|
-
* @example
|
|
326
|
-
* const line = (dashboard as any)["#formatStat"]("Fitness", 12.34);
|
|
327
|
-
* // => "║ Fitness: 12.34 ... ║" (color codes omitted here)
|
|
328
|
-
*/
|
|
329
|
-
#formatStat(
|
|
330
|
-
label: string,
|
|
331
|
-
value: string | number,
|
|
332
|
-
colorLabel = colors.neonSilver,
|
|
333
|
-
colorValue = colors.cyanNeon,
|
|
334
|
-
labelWidth = DashboardManager.#STAT_LABEL_WIDTH,
|
|
335
|
-
): string {
|
|
336
|
-
// Step 1: Canonicalize label (ensure colon exactly once at end)
|
|
337
|
-
const canonicalLabel = label.endsWith(':') ? label : `${label}:`;
|
|
338
|
-
|
|
339
|
-
// Step 2: Fixed-width left-aligned label column
|
|
340
|
-
const paddedLabel = canonicalLabel.padEnd(labelWidth, ' ');
|
|
341
|
-
|
|
342
|
-
// Step 3: Normalize value to string (explicit String coercion for transparency)
|
|
343
|
-
const valueString = typeof value === 'number' ? `${value}` : String(value);
|
|
344
|
-
|
|
345
|
-
// Step 4: Compose colored inner content segment
|
|
346
|
-
const coloredContent = `${colorLabel}${paddedLabel}${colorValue} ${valueString}${colors.reset}`;
|
|
347
|
-
|
|
348
|
-
// Step 5: Wrap with frame borders & horizontal padding
|
|
349
|
-
const leftPadSpaces = ' '.repeat(DashboardManager.LEFT_PADDING);
|
|
350
|
-
const framed = `${
|
|
351
|
-
colors.blueCore
|
|
352
|
-
}║${leftPadSpaces}${NetworkVisualization.pad(
|
|
353
|
-
coloredContent,
|
|
354
|
-
DashboardManager.CONTENT_WIDTH,
|
|
355
|
-
' ',
|
|
356
|
-
'left',
|
|
357
|
-
)}${' '.repeat(DashboardManager.RIGHT_PADDING)}${colors.blueCore}║${
|
|
358
|
-
colors.reset
|
|
359
|
-
}`;
|
|
360
|
-
return framed;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Convert the tail of a numeric series into a compact Unicode sparkline.
|
|
365
|
-
*
|
|
366
|
-
* Educational intent: illustrates how a simple per-frame trend visualization
|
|
367
|
-
* can be produced without external dependencies, while keeping allocation
|
|
368
|
-
* costs minimal for frequent refreshes (every generation / UI frame).
|
|
369
|
-
*
|
|
370
|
-
* Steps:
|
|
371
|
-
* 1. Slice the most recent `width` samples (via `MazeUtils.tail`) — bounded O(width).
|
|
372
|
-
* 2. Filter out non‑finite samples (defensive; telemetry may contain NaN during warmup).
|
|
373
|
-
* 3. Scan once to derive `minValue` / `maxValue` (range baseline).
|
|
374
|
-
* 4. Map each sample to an index in the precomputed block ramp (#SPARK_BLOCKS).
|
|
375
|
-
* 5. Append corresponding block characters into a single result string.
|
|
376
|
-
*
|
|
377
|
-
* Performance notes:
|
|
378
|
-
* - Single pass min/max + single pass mapping: O(n) with n = min(series.length, width).
|
|
379
|
-
* - No intermediate arrays beyond the tail slice (which reuses existing util) & final string builder.
|
|
380
|
-
* - Uses descriptive local names to keep code educational; hot path is still trivial compared to rendering.
|
|
381
|
-
* - Avoids `Math.min(...spread)` / `Array.prototype.map` to prevent temporary arrays & GC churn.
|
|
382
|
-
*
|
|
383
|
-
* Determinism: Pure function of input array slice (no randomness, no external state).
|
|
384
|
-
* Reentrancy: Safe; no shared mutable scratch used.
|
|
385
|
-
* Edge cases: Returns empty string for empty / all non‑finite input; collapses zero range to uniform block.
|
|
386
|
-
*
|
|
387
|
-
* @param series Numeric history (older -> newer) to visualize.
|
|
388
|
-
* @param width Maximum number of most recent samples to encode (default 32); values <= 0 produce ''.
|
|
389
|
-
* @returns Sparkline string (length <= width). Empty string when insufficient valid data.
|
|
390
|
-
* @example
|
|
391
|
-
* // Given recent fitness scores
|
|
392
|
-
* const spark = dashboardManager["#buildSparkline"]([10,11,11.5,12,13], 4); // -> e.g. "▃▄▆█"
|
|
393
|
-
*/
|
|
394
|
-
#buildSparkline(series: number[], width = 32): string {
|
|
395
|
-
// Fast exits for invalid / trivial scenarios
|
|
396
|
-
if (!Array.isArray(series) || !series.length || width <= 0) return '';
|
|
397
|
-
|
|
398
|
-
// Step 1: Tail slice (bounded) — relies on existing utility for consistency
|
|
399
|
-
const tailSlice = MazeUtils.tail<number>(series, width);
|
|
400
|
-
const sampleCount = tailSlice.length;
|
|
401
|
-
if (!sampleCount) return '';
|
|
402
|
-
|
|
403
|
-
// Step 2: Filter non-finite values in-place by compaction to avoid new array
|
|
404
|
-
let writeIndex = 0;
|
|
405
|
-
for (let readIndex = 0; readIndex < sampleCount; readIndex++) {
|
|
406
|
-
const sampleValue = tailSlice[readIndex];
|
|
407
|
-
if (Number.isFinite(sampleValue)) {
|
|
408
|
-
tailSlice[writeIndex++] = sampleValue;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (writeIndex === 0) return '';
|
|
412
|
-
|
|
413
|
-
// Step 3: Compute min/max over the compacted prefix [0, writeIndex)
|
|
414
|
-
let minValue = Infinity;
|
|
415
|
-
let maxValue = -Infinity;
|
|
416
|
-
for (let scanIndex = 0; scanIndex < writeIndex; scanIndex++) {
|
|
417
|
-
const value = tailSlice[scanIndex];
|
|
418
|
-
if (value < minValue) minValue = value;
|
|
419
|
-
if (value > maxValue) maxValue = value;
|
|
420
|
-
}
|
|
421
|
-
// Use a small epsilon to guard against zero or near-zero ranges so
|
|
422
|
-
// normalization remains stable (avoids divide-by-zero and huge
|
|
423
|
-
// normalized values when min ~= max). Prefer the class constant for
|
|
424
|
-
// easy tuning in one place.
|
|
425
|
-
let valueRange = maxValue - minValue;
|
|
426
|
-
if (Math.abs(valueRange) < DashboardManager.#DELTA_EPSILON) {
|
|
427
|
-
valueRange = DashboardManager.#DELTA_EPSILON;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Step 4: Map each sample to a block index
|
|
431
|
-
const blocks = DashboardManager.#SPARK_BLOCKS;
|
|
432
|
-
const blocksCount = blocks.length - 1; // highest ramp position index
|
|
433
|
-
let sparkline = '';
|
|
434
|
-
for (let encodeIndex = 0; encodeIndex < writeIndex; encodeIndex++) {
|
|
435
|
-
const normalized = (tailSlice[encodeIndex] - minValue) / valueRange; // [0,1]
|
|
436
|
-
const blockIndex = Math.min(
|
|
437
|
-
blocksCount,
|
|
438
|
-
Math.max(0, Math.floor(normalized * blocksCount)),
|
|
439
|
-
);
|
|
440
|
-
sparkline += blocks[blockIndex];
|
|
441
|
-
}
|
|
442
|
-
return sparkline;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Create a lightweight key for a maze (dedupe solved mazes). */
|
|
446
|
-
#getMazeKey(maze: string[]): string {
|
|
447
|
-
return maze.join('');
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** Wrapper to append solved archive block (public logic retained from original). */
|
|
451
|
-
#appendSolvedToArchive(
|
|
452
|
-
solved: {
|
|
453
|
-
maze: string[];
|
|
454
|
-
result: IMazeRunResult;
|
|
455
|
-
network: INetwork;
|
|
456
|
-
generation: number;
|
|
457
|
-
},
|
|
458
|
-
displayNumber: number,
|
|
459
|
-
): void {
|
|
460
|
-
if (!this.#archiveFn) return;
|
|
461
|
-
const blockLines: string[] = [];
|
|
462
|
-
this.#appendSolvedHeader(blockLines, solved, displayNumber);
|
|
463
|
-
this.#appendSolvedSparklines(blockLines, solved.network);
|
|
464
|
-
this.#appendSolvedMaze(blockLines, solved);
|
|
465
|
-
this.#appendSolvedPathStats(blockLines, solved);
|
|
466
|
-
// Architecture now included in sparklines section for consolidated solved summary (avoids duplication).
|
|
467
|
-
this.#appendSolvedFooterAndEmit(blockLines);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* redraw
|
|
472
|
-
*
|
|
473
|
-
* Clear + repaint the live dashboard frame while updating rich stats snapshot.
|
|
474
|
-
*
|
|
475
|
-
* Steps (delegated to focused helpers for readability & GC awareness):
|
|
476
|
-
* 1. beginFrameRefresh: clear terminal region & print static frame header.
|
|
477
|
-
* 2. printCurrentBestSection: conditionally render evolving section (network, maze, stats, progress).
|
|
478
|
-
* 3. updateDetailedStatsSnapshot: build/export metrics & sparklines using scratch arrays (bounded histories).
|
|
479
|
-
* 4. Emit a spacer line to preserve the original layout rhythm.
|
|
480
|
-
*
|
|
481
|
-
* Performance considerations:
|
|
482
|
-
* - Reuses `#scratch` arrays to avoid per-frame allocations when deriving top lists.
|
|
483
|
-
* - Histories are already bounded (HISTORY_MAX) so sparkline work is O(width).
|
|
484
|
-
* - Early exit when no telemetry & no current best yet.
|
|
485
|
-
*
|
|
486
|
-
* Determinism: Purely formatting & aggregation (no randomness).
|
|
487
|
-
* Reentrancy: Not reentrant (mutates internal state and shared scratch buffers). One instance per run.
|
|
488
|
-
* @param currentMaze Maze currently being evolved.
|
|
489
|
-
* @param neat Optional NEAT implementation instance for population-level stats.
|
|
490
|
-
*/
|
|
491
|
-
redraw(currentMaze: string[], neat?: unknown): void {
|
|
492
|
-
// Update the high-resolution last-update timestamp when a redraw happens.
|
|
493
|
-
this.#lastUpdateTs = globalThis.performance?.now?.() ?? Date.now();
|
|
494
|
-
this.#beginFrameRefresh();
|
|
495
|
-
if (this.#currentBest) this.#printCurrentBestSection(currentMaze);
|
|
496
|
-
this.#updateDetailedStatsSnapshot(neat); // updates #lastDetailedStats (used by getLastTelemetry())
|
|
497
|
-
this.#logBlank(); // spacer preserving legacy visual rhythm
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** Shared scratch allocations reused across redraw cycles to reduce GC churn. */
|
|
501
|
-
#scratch: {
|
|
502
|
-
scores: number[];
|
|
503
|
-
speciesSizes: number[];
|
|
504
|
-
operatorStats: OperatorStatsEntry[];
|
|
505
|
-
mutationEntries: [string, number][];
|
|
506
|
-
} = { scores: [], speciesSizes: [], operatorStats: [], mutationEntries: [] };
|
|
507
|
-
|
|
508
|
-
/** Clear & print static frame top. (Step 1 of redraw) */
|
|
509
|
-
#beginFrameRefresh(): void {
|
|
510
|
-
this.#clearFn();
|
|
511
|
-
this.#printTopFrame();
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Build the rich detailed stats snapshot consumed by external telemetry observers.
|
|
516
|
-
*
|
|
517
|
-
* Educational overview:
|
|
518
|
-
* This method aggregates multiple orthogonal evolution signals (fitness trends, structural complexity,
|
|
519
|
-
* diversity, Pareto front geometry, operator acceptance, mutation frequencies, species distribution, etc.) into
|
|
520
|
-
* one immutable plain object assigned to `#lastDetailedStats`. It is invoked once per redraw cycle (not per
|
|
521
|
-
* individual genome evaluation) to amortize cost and keep UI refresh predictable.
|
|
522
|
-
*
|
|
523
|
-
* Steps (high‑level):
|
|
524
|
-
* 1. Guard: if we have neither telemetry nor a current best candidate, skip work (no data yet).
|
|
525
|
-
* 2. Destructure relevant sub-snapshots from the raw telemetry (complexity, perf, lineage, diversity, objectives...).
|
|
526
|
-
* 3. Derive population statistics via `#computePopulationStats` (mean/median/species/enabled ratio) and patch gaps
|
|
527
|
-
* with the current best's fitness / species count as reasonable fallbacks for early generations.
|
|
528
|
-
* 4. Generate sparkline trend strings for tracked bounded histories (fitness, nodes, conns, hypervolume, progress, species).
|
|
529
|
-
* 5. Derive Pareto front size metrics & novelty archive size (defensive wrappers to tolerate optional APIs).
|
|
530
|
-
* 6. Compute operator acceptance, top mutation operator counts, and largest species sizes (scratch-buffer reuse inside helpers).
|
|
531
|
-
* 7. Compute best fitness delta (difference vs previous sample) for quick “is improving” signal.
|
|
532
|
-
* 8. Assemble and assign a consolidated snapshot object with timestamps & derived boolean flags (e.g. simplifyPhaseActive).
|
|
533
|
-
*
|
|
534
|
-
* Performance notes:
|
|
535
|
-
* - All history arrays are already bounded (HISTORY_MAX); sparkline generation is O(width) each.
|
|
536
|
-
* - Sorting work (operator / mutation / species) is limited to top-N extraction with small fixed caps (config constants).
|
|
537
|
-
* - Uses defensive optional chaining + nullish coalescing to avoid cascading throws; a single try/catch wraps overall build.
|
|
538
|
-
* - Allocations: one snapshot object + a handful of small arrays (top lists). Histories are sliced lazily via helper.
|
|
539
|
-
*
|
|
540
|
-
* Determinism: Pure aggregation of previously captured deterministic data. No RNG usage.
|
|
541
|
-
* Reentrancy: Not reentrant; mutates `#lastDetailedStats`. Acceptable because a single dashboard instance services one run.
|
|
542
|
-
* Failure handling: Any unexpected error aborts this build silently (stats are opportunistic, UI remains functional).
|
|
543
|
-
*
|
|
544
|
-
* @param neat Optional NEAT engine instance (used for population stats, operator stats, novelty archive size, species sizes).
|
|
545
|
-
*/
|
|
546
|
-
#updateDetailedStatsSnapshot(neat?: unknown): void {
|
|
547
|
-
const telemetry = this.#lastTelemetry;
|
|
548
|
-
// Step 1: Early guard when no data yet (avoids unnecessary object churn)
|
|
549
|
-
if (!telemetry && !this.#currentBest) return;
|
|
550
|
-
try {
|
|
551
|
-
// Step 2: Pull out nested telemetry domains with safe optional access
|
|
552
|
-
const complexitySnapshot = telemetry?.complexity;
|
|
553
|
-
const perfSnapshot = telemetry?.perf;
|
|
554
|
-
const lineageSnapshot = telemetry?.lineage;
|
|
555
|
-
const diversitySnapshot = telemetry?.diversity;
|
|
556
|
-
const rawFrontsArray = Array.isArray(telemetry?.fronts)
|
|
557
|
-
? telemetry.fronts
|
|
558
|
-
: null;
|
|
559
|
-
const objectivesSnapshot = telemetry?.objectives;
|
|
560
|
-
const hypervolumeValue = telemetry?.hyper;
|
|
561
|
-
const mutationStatsObj: MutationStatsMap | null =
|
|
562
|
-
telemetry?.mutationStats ?? telemetry?.mutation?.stats ?? null;
|
|
563
|
-
|
|
564
|
-
// Current best scalar metrics (fitness + auxiliary run stats)
|
|
565
|
-
const bestFitnessValue = this.#currentBest?.result?.fitness;
|
|
566
|
-
const saturationFractionValue =
|
|
567
|
-
this.#currentBest?.result?.saturationFraction;
|
|
568
|
-
const actionEntropyValue = this.#currentBest?.result?.actionEntropy;
|
|
569
|
-
|
|
570
|
-
// Step 3: Population-level summary (fills in early-run blanks with best fitness/species when needed)
|
|
571
|
-
const populationStats = this.#computePopulationStats(neat);
|
|
572
|
-
if (
|
|
573
|
-
populationStats.mean == null &&
|
|
574
|
-
typeof bestFitnessValue === 'number'
|
|
575
|
-
) {
|
|
576
|
-
populationStats.mean = +bestFitnessValue.toFixed(2);
|
|
577
|
-
}
|
|
578
|
-
if (
|
|
579
|
-
populationStats.median == null &&
|
|
580
|
-
typeof bestFitnessValue === 'number'
|
|
581
|
-
) {
|
|
582
|
-
populationStats.median = +bestFitnessValue.toFixed(2);
|
|
583
|
-
}
|
|
584
|
-
if (
|
|
585
|
-
populationStats.speciesCount == null &&
|
|
586
|
-
typeof telemetry?.species === 'number'
|
|
587
|
-
) {
|
|
588
|
-
populationStats.speciesCount = telemetry.species;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Step 4: Sparklines for bounded histories
|
|
592
|
-
const sparkWidth = DashboardManager.#GENERAL_SPARK_WIDTH;
|
|
593
|
-
const sparklines = {
|
|
594
|
-
fitness:
|
|
595
|
-
this.#buildSparkline(this.#bestFitnessHistory, sparkWidth) || null,
|
|
596
|
-
nodes:
|
|
597
|
-
this.#buildSparkline(this.#complexityNodesHistory, sparkWidth) ||
|
|
598
|
-
null,
|
|
599
|
-
conns:
|
|
600
|
-
this.#buildSparkline(this.#complexityConnsHistory, sparkWidth) ||
|
|
601
|
-
null,
|
|
602
|
-
hyper:
|
|
603
|
-
this.#buildSparkline(this.#hypervolumeHistory, sparkWidth) || null,
|
|
604
|
-
progress:
|
|
605
|
-
this.#buildSparkline(this.#progressHistory, sparkWidth) || null,
|
|
606
|
-
species:
|
|
607
|
-
this.#buildSparkline(this.#speciesCountHistory, sparkWidth) || null,
|
|
608
|
-
} as const;
|
|
609
|
-
|
|
610
|
-
// Step 5: Pareto + novelty archive metrics
|
|
611
|
-
const firstFrontSize = rawFrontsArray?.[0]?.length || 0;
|
|
612
|
-
const paretoFrontSizes = rawFrontsArray
|
|
613
|
-
? rawFrontsArray.map((front) => front?.length ?? 0)
|
|
614
|
-
: null;
|
|
615
|
-
const neatInstance = neat as NeatInstance;
|
|
616
|
-
const noveltyArchiveSize = this.#safeInvoke<number | null>(
|
|
617
|
-
() =>
|
|
618
|
-
neatInstance?.getNoveltyArchive
|
|
619
|
-
? (neatInstance.getNoveltyArchive()?.length ?? null)
|
|
620
|
-
: null,
|
|
621
|
-
null,
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
// Step 6: Operator acceptance, mutation frequencies, species distribution
|
|
625
|
-
const operatorAcceptance = this.#computeOperatorAcceptance(neat);
|
|
626
|
-
const topMutations = this.#computeTopMutations(mutationStatsObj);
|
|
627
|
-
const topSpeciesSizes = this.#computeTopSpeciesSizes(neat);
|
|
628
|
-
|
|
629
|
-
// Step 7: Best fitness delta (vs prior sample) — small improvement signal
|
|
630
|
-
const bestFitnessDelta = (() => {
|
|
631
|
-
if (typeof bestFitnessValue !== 'number') return null;
|
|
632
|
-
const previousSample = this.#bestFitnessHistory.at(-2) ?? null;
|
|
633
|
-
if (previousSample == null) return null;
|
|
634
|
-
return +(bestFitnessValue - previousSample).toFixed(3);
|
|
635
|
-
})();
|
|
636
|
-
|
|
637
|
-
// Step 8: Consolidated snapshot assignment
|
|
638
|
-
this.#lastDetailedStats = {
|
|
639
|
-
generation: this.#currentBest?.generation || 0,
|
|
640
|
-
bestFitness:
|
|
641
|
-
typeof bestFitnessValue === 'number' ? bestFitnessValue : null,
|
|
642
|
-
bestFitnessDelta,
|
|
643
|
-
saturationFraction:
|
|
644
|
-
typeof saturationFractionValue === 'number'
|
|
645
|
-
? saturationFractionValue
|
|
646
|
-
: null,
|
|
647
|
-
actionEntropy:
|
|
648
|
-
typeof actionEntropyValue === 'number' ? actionEntropyValue : null,
|
|
649
|
-
populationMean: populationStats.mean,
|
|
650
|
-
populationMedian: populationStats.median,
|
|
651
|
-
enabledConnRatio: populationStats.enabledRatio,
|
|
652
|
-
complexity: complexitySnapshot || null,
|
|
653
|
-
simplifyPhaseActive: (() => {
|
|
654
|
-
if (!complexitySnapshot) return false;
|
|
655
|
-
const nodesDecrease =
|
|
656
|
-
typeof complexitySnapshot.growthNodes === 'number' &&
|
|
657
|
-
complexitySnapshot.growthNodes < 0;
|
|
658
|
-
const connsDecrease =
|
|
659
|
-
typeof complexitySnapshot.growthConns === 'number' &&
|
|
660
|
-
complexitySnapshot.growthConns < 0;
|
|
661
|
-
return nodesDecrease || connsDecrease;
|
|
662
|
-
})(),
|
|
663
|
-
perf: perfSnapshot || null,
|
|
664
|
-
lineage: lineageSnapshot || null,
|
|
665
|
-
diversity: diversitySnapshot || null,
|
|
666
|
-
speciesCount: populationStats.speciesCount,
|
|
667
|
-
topSpeciesSizes,
|
|
668
|
-
objectives: objectivesSnapshot || null,
|
|
669
|
-
paretoFrontSizes,
|
|
670
|
-
firstFrontSize,
|
|
671
|
-
hypervolume:
|
|
672
|
-
typeof hypervolumeValue === 'number' ? hypervolumeValue : null,
|
|
673
|
-
noveltyArchiveSize,
|
|
674
|
-
operatorAcceptance,
|
|
675
|
-
topMutations,
|
|
676
|
-
mutationStats: mutationStatsObj || null,
|
|
677
|
-
trends: sparklines,
|
|
678
|
-
histories: {
|
|
679
|
-
bestFitness: this.#sliceHistoryForExport(this.#bestFitnessHistory),
|
|
680
|
-
nodes: this.#sliceHistoryForExport(this.#complexityNodesHistory),
|
|
681
|
-
conns: this.#sliceHistoryForExport(this.#complexityConnsHistory),
|
|
682
|
-
hyper: this.#sliceHistoryForExport(this.#hypervolumeHistory),
|
|
683
|
-
progress: this.#sliceHistoryForExport(this.#progressHistory),
|
|
684
|
-
species: this.#sliceHistoryForExport(this.#speciesCountHistory),
|
|
685
|
-
},
|
|
686
|
-
timestamp: Date.now(),
|
|
687
|
-
};
|
|
688
|
-
} catch {
|
|
689
|
-
// Snapshot production is optional; swallow to keep UI resilient.
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Aggregate basic population-wide statistics (mean & median fitness, species count, enabled connection ratio).
|
|
695
|
-
*
|
|
696
|
-
* Educational intent:
|
|
697
|
-
* Centralizes lightweight descriptive statistics needed for trend visualization and telemetry export
|
|
698
|
-
* while demonstrating reuse of a shared scratch array to minimize per-generation allocations.
|
|
699
|
-
*
|
|
700
|
-
* Steps:
|
|
701
|
-
* 1. Guard & early return if the provided engine instance lacks a population array.
|
|
702
|
-
* 2. Reuse `#scratch.scores` (cleared in-place) to collect numeric fitness scores.
|
|
703
|
-
* 3. Count total vs enabled connections across genomes to derive an enabled ratio (structural sparsity signal).
|
|
704
|
-
* 4. Compute mean in a single pass over the scores scratch array.
|
|
705
|
-
* 5. Clone & sort scores (ascending) to compute median (keeps original order intact for any other readers).
|
|
706
|
-
* 6. Derive species count defensively (array length or null when absent).
|
|
707
|
-
* 7. Return a small plain object (all numbers formatted to 2 decimals where derived) — consumers may patch
|
|
708
|
-
* missing values later (see fallback logic in `#updateDetailedStatsSnapshot`).
|
|
709
|
-
*
|
|
710
|
-
* Complexity:
|
|
711
|
-
* - Score collection: O(G) with G = population size.
|
|
712
|
-
* - Connection scan: O(E) with E = total number of connection entries (linear).
|
|
713
|
-
* - Sorting for median: O(G log G) — acceptable for modest populations; if G became very large, a selection
|
|
714
|
-
* algorithm (nth_element style) could replace the sort (document trade-offs first if changed).
|
|
715
|
-
*
|
|
716
|
-
* Performance notes:
|
|
717
|
-
* - Reuses a single scores scratch array (cleared via length reset) to avoid churn.
|
|
718
|
-
* - Uses numeric formatting only at final aggregation (minimizes intermediate string creation).
|
|
719
|
-
* - Avoids repeated optional chaining in inner loops by shallow local references.
|
|
720
|
-
*
|
|
721
|
-
* Determinism: Pure function of the provided `neat.population` snapshot (iteration order is respected).
|
|
722
|
-
* Reentrancy: Safe; scratch array is instance-scoped but method is not expected to be invoked concurrently.
|
|
723
|
-
* Edge cases: Empty / missing population returns all-null fields; division by zero guarded by connection count checks.
|
|
724
|
-
*
|
|
725
|
-
* @param neat NEAT-like engine instance exposing `population`, optional `species` collection.
|
|
726
|
-
* @returns Object with `mean`, `median`, `speciesCount`, `enabledRatio` (each nullable when not derivable).
|
|
727
|
-
*/
|
|
728
|
-
#computePopulationStats(neat?: unknown): {
|
|
729
|
-
mean: number | null;
|
|
730
|
-
median: number | null;
|
|
731
|
-
speciesCount: number | null;
|
|
732
|
-
enabledRatio: number | null;
|
|
733
|
-
} {
|
|
734
|
-
const neatInstance = neat as NeatInstance;
|
|
735
|
-
// Step 1: Guard for absent / malformed population
|
|
736
|
-
if (
|
|
737
|
-
!neatInstance ||
|
|
738
|
-
!Array.isArray(neatInstance.population) ||
|
|
739
|
-
neatInstance.population.length === 0
|
|
740
|
-
) {
|
|
741
|
-
return {
|
|
742
|
-
mean: null,
|
|
743
|
-
median: null,
|
|
744
|
-
speciesCount: null,
|
|
745
|
-
enabledRatio: null,
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Step 2: Reuse scratch array for fitness scores (clear via length assignment)
|
|
750
|
-
const { scores } = this.#scratch;
|
|
751
|
-
scores.length = 0;
|
|
752
|
-
|
|
753
|
-
// Step 3: Scan genomes collecting scores & connection enablement stats
|
|
754
|
-
let enabledConnectionsCount = 0;
|
|
755
|
-
let totalConnectionsCount = 0;
|
|
756
|
-
for (const genome of neatInstance.population) {
|
|
757
|
-
if (typeof genome?.score === 'number') scores.push(genome.score);
|
|
758
|
-
const genomeConns = genome?.connections;
|
|
759
|
-
if (Array.isArray(genomeConns)) {
|
|
760
|
-
for (const connection of genomeConns) {
|
|
761
|
-
totalConnectionsCount++;
|
|
762
|
-
if (connection?.enabled !== false) enabledConnectionsCount++;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Step 4 & 5: Mean and median computation
|
|
768
|
-
let mean: number | null = null;
|
|
769
|
-
let median: number | null = null;
|
|
770
|
-
if (scores.length) {
|
|
771
|
-
let sum = 0;
|
|
772
|
-
for (let scoreIndex = 0; scoreIndex < scores.length; scoreIndex++) {
|
|
773
|
-
sum += scores[scoreIndex];
|
|
774
|
-
}
|
|
775
|
-
mean = +(sum / scores.length).toFixed(2);
|
|
776
|
-
|
|
777
|
-
// Clone before sort to preserve potential external reliance on original order (defensive)
|
|
778
|
-
const sortedScores = [...scores].sort((a, b) => a - b);
|
|
779
|
-
const middleIndex = Math.floor(sortedScores.length / 2);
|
|
780
|
-
const medianRaw =
|
|
781
|
-
sortedScores.length % 2 === 0
|
|
782
|
-
? (sortedScores[middleIndex - 1] + sortedScores[middleIndex]) / 2
|
|
783
|
-
: sortedScores[middleIndex];
|
|
784
|
-
median = +medianRaw.toFixed(2);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Step 6: Enabled ratio (null when no connections observed)
|
|
788
|
-
const enabledRatio = totalConnectionsCount
|
|
789
|
-
? +(enabledConnectionsCount / totalConnectionsCount).toFixed(2)
|
|
790
|
-
: null;
|
|
791
|
-
|
|
792
|
-
// Step 7: Species count (nullable)
|
|
793
|
-
const speciesCount = Array.isArray(neatInstance.species)
|
|
794
|
-
? (neatInstance.species.length ?? null)
|
|
795
|
-
: null;
|
|
796
|
-
|
|
797
|
-
// Step 8: Return aggregate
|
|
798
|
-
return { mean, median, speciesCount, enabledRatio };
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Derive a ranked list of operator acceptance percentages from the (optional) evolution engine.
|
|
803
|
-
*
|
|
804
|
-
* Educational focus:
|
|
805
|
-
* - Demonstrates defensive integration with a loosely‑typed external API (`getOperatorStats`).
|
|
806
|
-
* - Shows how to reuse an instance scratch buffer to avoid per‑refresh allocations.
|
|
807
|
-
* - Illustrates compact ranking logic (copy + sort + slice) while preserving original raw snapshot.
|
|
808
|
-
*
|
|
809
|
-
* Acceptance definition:
|
|
810
|
-
* acceptancePct = (success / max(1, attempts)) * 100 (formatted to 2 decimals)
|
|
811
|
-
* A zero attempts count is clamped to 1 to avoid division by zero; this treats a (success>0, attempts==0)
|
|
812
|
-
* anomaly as full success rather than NaN — acceptable for a resilience‑biased dashboard.
|
|
813
|
-
*
|
|
814
|
-
* Steps:
|
|
815
|
-
* 1. Guard: verify `neat.getOperatorStats` is a function (else return null to signal absence of data).
|
|
816
|
-
* 2. Safe invoke inside try/catch (engines may throw while stats are initializing).
|
|
817
|
-
* 3. Filter raw entries into `#scratch.operatorStats` keeping only { name:string, success:number, attempts:number }.
|
|
818
|
-
* 4. Create a ranked copy sorted by descending acceptance ratio (stable for ties in modern JS engines).
|
|
819
|
-
* 5. Map the top N (`#TOP_OPERATOR_LIMIT`) into a lightweight exported shape `{ name, acceptancePct }`.
|
|
820
|
-
* 6. Return `null` when no valid entries remain after filtering (downstream rendering can simply skip the block).
|
|
821
|
-
*
|
|
822
|
-
* Complexity:
|
|
823
|
-
* - Let K be the number of operator entries. Filtering O(K); sort O(K log K); slice/map O(min(N, K)).
|
|
824
|
-
* - K is typically tiny (single digits), so the impact per redraw is negligible.
|
|
825
|
-
*
|
|
826
|
-
* Performance notes:
|
|
827
|
-
* - Scratch buffer cleared via length reset (no new array each call).
|
|
828
|
-
* - Only one extra array allocation (`rankedCopy`) for isolation of sort side‑effects.
|
|
829
|
-
* - Formatting (toFixed) deferred until final mapping to limit transient string creation.
|
|
830
|
-
*
|
|
831
|
-
* Determinism: Pure given the operator stats snapshot (no randomness). Relies on stable `Array.prototype.sort` for tie ordering.
|
|
832
|
-
* Reentrancy: Safe under single‑threaded assumption; scratch buffer is reused but not shared across concurrent calls.
|
|
833
|
-
* Edge cases & error handling:
|
|
834
|
-
* - Missing API / thrown error => null.
|
|
835
|
-
* - Malformed entries (missing numeric fields) silently excluded.
|
|
836
|
-
* - Division by zero avoided via denominator clamp.
|
|
837
|
-
* - Empty post‑filter set => null (consistent sentinel).
|
|
838
|
-
*
|
|
839
|
-
* @param neat Optional engine exposing `getOperatorStats(): Array<{name:string, success:number, attempts:number}>`.
|
|
840
|
-
* @returns Array of top operators with acceptance percentages or null when unavailable / no data.
|
|
841
|
-
* @example
|
|
842
|
-
* const acceptance = (dashboard as any)["#computeOperatorAcceptance"](neatInstance);
|
|
843
|
-
* // => [ { name: 'mutateAddNode', acceptancePct: 62.5 }, ... ] or null
|
|
844
|
-
*/
|
|
845
|
-
#computeOperatorAcceptance(
|
|
846
|
-
neat?: unknown,
|
|
847
|
-
): Array<{ name: string; acceptancePct: number }> | null {
|
|
848
|
-
const neatInstance = neat as NeatInstance;
|
|
849
|
-
if (typeof neatInstance?.getOperatorStats !== 'function') return null;
|
|
850
|
-
|
|
851
|
-
let rawOperatorStats: unknown;
|
|
852
|
-
try {
|
|
853
|
-
rawOperatorStats = neatInstance.getOperatorStats();
|
|
854
|
-
} catch {
|
|
855
|
-
return null; // Defensive: treat transient failures as absence of data.
|
|
856
|
-
}
|
|
857
|
-
if (!Array.isArray(rawOperatorStats) || rawOperatorStats.length === 0)
|
|
858
|
-
return null;
|
|
859
|
-
|
|
860
|
-
// Step 3: Populate scratch buffer with only well-formed entries.
|
|
861
|
-
const scratchBuffer = this.#scratch.operatorStats;
|
|
862
|
-
scratchBuffer.length = 0; // in-place clear
|
|
863
|
-
for (const operatorStat of rawOperatorStats) {
|
|
864
|
-
if (
|
|
865
|
-
operatorStat &&
|
|
866
|
-
typeof operatorStat.name === 'string' &&
|
|
867
|
-
typeof operatorStat.success === 'number' &&
|
|
868
|
-
typeof operatorStat.attempts === 'number'
|
|
869
|
-
) {
|
|
870
|
-
scratchBuffer.push(operatorStat);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (!scratchBuffer.length) return null;
|
|
874
|
-
|
|
875
|
-
// Step 4: Sort copy (preserve original ordering in scratch for potential reuse / future augmentation).
|
|
876
|
-
const rankedCopy = [...scratchBuffer].sort((leftStat, rightStat) => {
|
|
877
|
-
const leftAcceptance = leftStat.success / Math.max(1, leftStat.attempts);
|
|
878
|
-
const rightAcceptance =
|
|
879
|
-
rightStat.success / Math.max(1, rightStat.attempts);
|
|
880
|
-
// Descending order; tie-break maintains stable relative ordering due to JS sort stability in modern engines.
|
|
881
|
-
if (rightAcceptance !== leftAcceptance)
|
|
882
|
-
return rightAcceptance - leftAcceptance;
|
|
883
|
-
return 0;
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
// Step 5: Map top-N into exported simplified objects.
|
|
887
|
-
const limit = Math.min(
|
|
888
|
-
DashboardManager.#TOP_OPERATOR_LIMIT,
|
|
889
|
-
rankedCopy.length,
|
|
890
|
-
);
|
|
891
|
-
const acceptanceList: Array<{ name: string; acceptancePct: number }> = [];
|
|
892
|
-
for (let rankIndex = 0; rankIndex < limit; rankIndex++) {
|
|
893
|
-
const rankedStat = rankedCopy[rankIndex];
|
|
894
|
-
const acceptancePct = +(
|
|
895
|
-
(100 * rankedStat.success) /
|
|
896
|
-
Math.max(1, rankedStat.attempts)
|
|
897
|
-
).toFixed(2);
|
|
898
|
-
acceptanceList.push({ name: rankedStat.name, acceptancePct });
|
|
899
|
-
}
|
|
900
|
-
return acceptanceList.length ? acceptanceList : null;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* Produce a ranked list of the most frequent mutation operators observed so far.
|
|
905
|
-
*
|
|
906
|
-
* Educational focus:
|
|
907
|
-
* - Demonstrates reuse of an in-place scratch tuple array to avoid allocation churn.
|
|
908
|
-
* - Shows defensive extraction from a loosely-typed stats object (filtering only numeric counts).
|
|
909
|
-
* - Illustrates a simple top-N selection pattern (sort + bounded slice) with explicit caps.
|
|
910
|
-
*
|
|
911
|
-
* Steps:
|
|
912
|
-
* 1. Guard: return null if `mutationStats` is not a plain object.
|
|
913
|
-
* 2. Clear and repopulate `#scratch.mutationEntries` with `[name, count]` tuples for numeric fields.
|
|
914
|
-
* 3. Early return null when no valid entries collected (simplifies downstream rendering conditions).
|
|
915
|
-
* 4. Sort the scratch array in-place by descending count (highest frequency first) using descriptive comparator param names.
|
|
916
|
-
* 5. Take the top N (bounded by `#TOP_MUTATION_LIMIT`) and map to output objects `{ name, count }`.
|
|
917
|
-
* 6. Return the resulting array (guaranteed non-empty) or null if absent.
|
|
918
|
-
*
|
|
919
|
-
* Complexity:
|
|
920
|
-
* - Collection: O(K) with K = enumerable keys on `mutationStats`.
|
|
921
|
-
* - Sort: O(K log K); K is typically modest (dozens at most) so overhead is negligible.
|
|
922
|
-
* - Slice/map: O(min(N, K)).
|
|
923
|
-
*
|
|
924
|
-
* Performance notes:
|
|
925
|
-
* - Scratch array is reused (length reset) preventing repeated allocation of tuple arrays each frame.
|
|
926
|
-
* - In-place sort avoids cloning (`[...entries]`) found in earlier version, eliminating one transient array.
|
|
927
|
-
* - Comparator accesses tuple indices directly, avoiding destructuring overhead in the hot call.
|
|
928
|
-
*
|
|
929
|
-
* Determinism: Pure transformation of the provided stats snapshot; no randomness.
|
|
930
|
-
* Reentrancy: Safe for single-threaded invocation pattern; scratch state is not shared across instances.
|
|
931
|
-
* Edge cases:
|
|
932
|
-
* - Non-object or empty object => null.
|
|
933
|
-
* - Non-numeric values silently skipped.
|
|
934
|
-
* - Negative counts retained (still sorted numerically) under the assumption they signal net effects; could be filtered if undesired.
|
|
935
|
-
*
|
|
936
|
-
* @param mutationStats Arbitrary object mapping mutation operator names to numeric invocation counts.
|
|
937
|
-
* @returns Array of top mutation operators (name + count) or null when no data.
|
|
938
|
-
* @example
|
|
939
|
-
* const top = (dashboard as any)["#computeTopMutations"]({ addNode: 42, addConn: 17 });
|
|
940
|
-
* // => [ { name: 'addNode', count: 42 }, { name: 'addConn', count: 17 } ]
|
|
941
|
-
*/
|
|
942
|
-
#computeTopMutations(
|
|
943
|
-
mutationStats: MutationStatsMap | null,
|
|
944
|
-
): Array<{ name: string; count: number }> | null {
|
|
945
|
-
// Step 1: Guard for invalid container
|
|
946
|
-
if (!mutationStats) return null;
|
|
947
|
-
|
|
948
|
-
// Step 2: Populate scratch with numeric entries only
|
|
949
|
-
const mutationEntriesScratch = this.#scratch.mutationEntries;
|
|
950
|
-
mutationEntriesScratch.length = 0;
|
|
951
|
-
for (const [mutationName, occurrenceCount] of Object.entries(
|
|
952
|
-
mutationStats,
|
|
953
|
-
)) {
|
|
954
|
-
if (
|
|
955
|
-
typeof occurrenceCount === 'number' &&
|
|
956
|
-
Number.isFinite(occurrenceCount)
|
|
957
|
-
) {
|
|
958
|
-
mutationEntriesScratch.push([mutationName, occurrenceCount]);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Step 3: Early return when no numeric stats present
|
|
963
|
-
if (!mutationEntriesScratch.length) return null;
|
|
964
|
-
|
|
965
|
-
// Step 4: In-place sort descending by count
|
|
966
|
-
mutationEntriesScratch.sort(
|
|
967
|
-
(leftEntry, rightEntry) => rightEntry[1] - leftEntry[1],
|
|
968
|
-
);
|
|
969
|
-
|
|
970
|
-
// Step 5: Map top-N to output objects
|
|
971
|
-
const limit = Math.min(
|
|
972
|
-
DashboardManager.#TOP_MUTATION_LIMIT,
|
|
973
|
-
mutationEntriesScratch.length,
|
|
974
|
-
);
|
|
975
|
-
const topMutations: Array<{ name: string; count: number }> = [];
|
|
976
|
-
for (let rankIndex = 0; rankIndex < limit; rankIndex++) {
|
|
977
|
-
const [mutationName, occurrenceCount] = mutationEntriesScratch[rankIndex];
|
|
978
|
-
topMutations.push({ name: mutationName, count: occurrenceCount });
|
|
979
|
-
}
|
|
980
|
-
return topMutations;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
/**
|
|
984
|
-
* Compute the sizes (member counts) of the largest species (Top-N) in the current population snapshot.
|
|
985
|
-
*
|
|
986
|
-
* Educational focus:
|
|
987
|
-
* - Demonstrates reuse of an integer scratch array to avoid new allocations every redraw.
|
|
988
|
-
* - Highlights a simple pattern for extracting a Top-N ranking from a small set (in-place sort + bounded copy).
|
|
989
|
-
* - Shows defensive handling of loosely-typed engine data (species objects may omit `members`).
|
|
990
|
-
*
|
|
991
|
-
* Steps:
|
|
992
|
-
* 1. Guard: return null when `neat.species` is absent or empty.
|
|
993
|
-
* 2. Repopulate `#scratch.speciesSizes` with numeric member counts (fallback 0 when ambiguous).
|
|
994
|
-
* 3. In-place sort scratch array descending (largest first).
|
|
995
|
-
* 4. Copy the first N (`#TOP_SPECIES_LIMIT`) values into a new output array for immutability to callers.
|
|
996
|
-
* 5. Return the ranked sizes or null when no data.
|
|
997
|
-
*
|
|
998
|
-
* Complexity:
|
|
999
|
-
* - Let S = species count. Population: O(S). Sort: O(S log S). Copy: O(min(S, N)). S is typically modest, so cost is trivial.
|
|
1000
|
-
*
|
|
1001
|
-
* Performance notes:
|
|
1002
|
-
* - Reuses a single scratch array (cleared via length assignment) to avoid allocation churn.
|
|
1003
|
-
* - In-place sort avoids creating an additional clone (`[...scratch]`), reducing temporary memory.
|
|
1004
|
-
* - Output array is sized at most `#TOP_SPECIES_LIMIT` (small, bounded allocation) for downstream display safety.
|
|
1005
|
-
*
|
|
1006
|
-
* Determinism: Pure function of the `neat.species` snapshot (ordering depends only on numerical counts; stable for equal sizes because JS sort is stable in modern engines but equal sizes preserve original order).
|
|
1007
|
-
* Reentrancy: Safe under single-threaded invocation pattern (scratch array reused but not shared concurrently).
|
|
1008
|
-
* Edge cases:
|
|
1009
|
-
* - Missing / non-array / empty species list => null.
|
|
1010
|
-
* - Species object missing `members` => treated as size 0.
|
|
1011
|
-
* - Negative member counts (unexpected) retained and sorted numerically; could be filtered if a real engine produced them.
|
|
1012
|
-
*
|
|
1013
|
-
* @param neat Optional NEAT-like engine instance exposing an array `species` with `members` arrays.
|
|
1014
|
-
* @returns Array of top species sizes (descending) or null when no species present.
|
|
1015
|
-
* @example
|
|
1016
|
-
* const sizes = (dashboard as any)["#computeTopSpeciesSizes"](neat);
|
|
1017
|
-
* // => [34, 21, 10] (up to 5 elements) or null
|
|
1018
|
-
*/
|
|
1019
|
-
#computeTopSpeciesSizes(neat?: unknown): number[] | null {
|
|
1020
|
-
const neatInstance = neat as NeatInstance;
|
|
1021
|
-
// Step 1: Guard for absence / emptiness
|
|
1022
|
-
if (
|
|
1023
|
-
!Array.isArray(neatInstance?.species) ||
|
|
1024
|
-
neatInstance.species.length === 0
|
|
1025
|
-
)
|
|
1026
|
-
return null;
|
|
1027
|
-
|
|
1028
|
-
// Step 2: Populate scratch with member counts
|
|
1029
|
-
const speciesSizesScratch = this.#scratch.speciesSizes;
|
|
1030
|
-
speciesSizesScratch.length = 0; // clear
|
|
1031
|
-
for (const speciesEntry of neatInstance.species) {
|
|
1032
|
-
// Fallback to 0 when members array missing / non-array
|
|
1033
|
-
const sizeValue = Array.isArray(speciesEntry?.members)
|
|
1034
|
-
? speciesEntry.members.length
|
|
1035
|
-
: 0;
|
|
1036
|
-
speciesSizesScratch.push(sizeValue);
|
|
1037
|
-
}
|
|
1038
|
-
if (!speciesSizesScratch.length) return null; // defensive (should not occur if earlier guard passed)
|
|
1039
|
-
|
|
1040
|
-
// Step 3: In-place descending sort
|
|
1041
|
-
speciesSizesScratch.sort((leftSize, rightSize) => rightSize - leftSize);
|
|
1042
|
-
|
|
1043
|
-
// Step 4: Bounded copy to output (immutability for consumers)
|
|
1044
|
-
const limit = Math.min(
|
|
1045
|
-
DashboardManager.#TOP_SPECIES_LIMIT,
|
|
1046
|
-
speciesSizesScratch.length,
|
|
1047
|
-
);
|
|
1048
|
-
const topSpeciesSizes: number[] = [];
|
|
1049
|
-
for (let rankIndex = 0; rankIndex < limit; rankIndex++) {
|
|
1050
|
-
topSpeciesSizes.push(speciesSizesScratch[rankIndex]);
|
|
1051
|
-
}
|
|
1052
|
-
return topSpeciesSizes;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
/** Safe invoke wrapper returning fallback on throw. */
|
|
1056
|
-
#safeInvoke<T>(fn: () => T, fallback: T): T {
|
|
1057
|
-
try {
|
|
1058
|
-
return fn();
|
|
1059
|
-
} catch {
|
|
1060
|
-
return fallback;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* Append the top header lines for a solved maze archive block.
|
|
1066
|
-
*
|
|
1067
|
-
* Format mirrors other framed sections: a top border, a centered label line
|
|
1068
|
-
* identifying the solved ordinal and generation, and one spacer line to
|
|
1069
|
-
* visually separate from subsequent sparkline + maze content.
|
|
1070
|
-
*
|
|
1071
|
-
* We keep this lean: no dynamic width calculations beyond centering, and we
|
|
1072
|
-
* avoid extra temporary arrays (push directly into the provided accumulator).
|
|
1073
|
-
*
|
|
1074
|
-
* @param blockLines Accumulator array mutated by appending formatted lines.
|
|
1075
|
-
* @param solved Object containing result + generation metadata.
|
|
1076
|
-
* @param displayNumber 1-based solved maze index for user-friendly labeling.
|
|
1077
|
-
*/
|
|
1078
|
-
#appendSolvedHeader(
|
|
1079
|
-
blockLines: string[],
|
|
1080
|
-
solved: {
|
|
1081
|
-
maze: string[];
|
|
1082
|
-
result: IMazeRunResult;
|
|
1083
|
-
network: INetwork;
|
|
1084
|
-
generation: number;
|
|
1085
|
-
},
|
|
1086
|
-
displayNumber: number,
|
|
1087
|
-
): void {
|
|
1088
|
-
/**
|
|
1089
|
-
* Educational / formatting notes:
|
|
1090
|
-
* - Uses a fixed inner frame width to keep all archive sections visually aligned regardless of maze size.
|
|
1091
|
-
* - Centers a dynamic title string without allocating intermediate arrays (direct pushes to accumulator).
|
|
1092
|
-
* - Fitness value is formatted to two decimals only when finite; otherwise 'n/a' is displayed for clarity.
|
|
1093
|
-
* - Keeps allocation footprint minimal: a handful of short-lived strings (no joins over arrays).
|
|
1094
|
-
*
|
|
1095
|
-
* Steps:
|
|
1096
|
-
* 1. Resolve & validate sizing constants (frame inner width).
|
|
1097
|
-
* 2. Push a top border line (full width of heavy box characters).
|
|
1098
|
-
* 3. Build a descriptive centered title including solved ordinal, generation, and fitness.
|
|
1099
|
-
* 4. Compute left/right padding to center the title (favor left bias on odd extra space for stable layout).
|
|
1100
|
-
* 5. Push the centered title line with color accents.
|
|
1101
|
-
* 6. Push a spacer line to visually separate header from subsequent sparkline/stat/maze content.
|
|
1102
|
-
*
|
|
1103
|
-
* Determinism: Pure formatting based on provided parameters and static constants.
|
|
1104
|
-
* Reentrancy: Safe; only mutates the provided `blockLines` accumulator.
|
|
1105
|
-
* Edge cases:
|
|
1106
|
-
* - Extremely long title (e.g., unexpectedly large generation number) will be clipped visually by frame borders (allowed; signals anomaly).
|
|
1107
|
-
* - Non-numeric / NaN fitness gracefully downgrades to 'n/a'.
|
|
1108
|
-
*/
|
|
1109
|
-
const innerWidth = DashboardManager.FRAME_INNER_WIDTH; // Step 1
|
|
1110
|
-
|
|
1111
|
-
// Step 2: Top border line
|
|
1112
|
-
blockLines.push(
|
|
1113
|
-
`${colors.blueCore}╔${NetworkVisualization.pad(
|
|
1114
|
-
'═'.repeat(innerWidth),
|
|
1115
|
-
innerWidth,
|
|
1116
|
-
'═',
|
|
1117
|
-
)}╗${colors.reset}`,
|
|
1118
|
-
);
|
|
1119
|
-
|
|
1120
|
-
// Step 3: Title components (defensive numeric handling)
|
|
1121
|
-
const { result, generation } = solved;
|
|
1122
|
-
const rawFitness = result?.fitness;
|
|
1123
|
-
const formattedFitness =
|
|
1124
|
-
typeof rawFitness === 'number' && Number.isFinite(rawFitness)
|
|
1125
|
-
? rawFitness.toFixed(2)
|
|
1126
|
-
: 'n/a';
|
|
1127
|
-
const title = ` SOLVED #${Math.max(
|
|
1128
|
-
1,
|
|
1129
|
-
displayNumber,
|
|
1130
|
-
)} (GEN ${generation}) FITNESS ${formattedFitness} `;
|
|
1131
|
-
|
|
1132
|
-
// Step 4: Centering math
|
|
1133
|
-
const leftPaddingSize = Math.max(
|
|
1134
|
-
0,
|
|
1135
|
-
Math.floor((innerWidth - title.length) / 2),
|
|
1136
|
-
);
|
|
1137
|
-
const rightPaddingSize = Math.max(
|
|
1138
|
-
0,
|
|
1139
|
-
innerWidth - title.length - leftPaddingSize,
|
|
1140
|
-
);
|
|
1141
|
-
|
|
1142
|
-
// Step 5: Centered title line
|
|
1143
|
-
blockLines.push(
|
|
1144
|
-
`${colors.blueCore}║${' '.repeat(leftPaddingSize)}${
|
|
1145
|
-
colors.orangeNeon
|
|
1146
|
-
}${title}${colors.blueCore}${' '.repeat(rightPaddingSize)}║${
|
|
1147
|
-
colors.reset
|
|
1148
|
-
}`,
|
|
1149
|
-
);
|
|
1150
|
-
|
|
1151
|
-
// Step 6: Spacer line
|
|
1152
|
-
blockLines.push(
|
|
1153
|
-
`${colors.blueCore}║${NetworkVisualization.pad(' ', innerWidth, ' ')}║${
|
|
1154
|
-
colors.reset
|
|
1155
|
-
}`,
|
|
1156
|
-
);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
/**
|
|
1160
|
-
* Append solved-run sparklines plus a one-line architecture summary.
|
|
1161
|
-
*
|
|
1162
|
-
* Additions over the original implementation:
|
|
1163
|
-
* - Includes current network architecture (layer sizes) using arrow formatting ("I <=> H1 <=> ... <=> O").
|
|
1164
|
-
* - Consolidates architecture here (removed separate later architecture line to avoid duplication).
|
|
1165
|
-
* - Retains aligned label formatting via shared `#formatStat` helper for consistency.
|
|
1166
|
-
*
|
|
1167
|
-
* Architecture derivation:
|
|
1168
|
-
* - Uses `#deriveArchitecture` (returns e.g. "6 - 6 - 5 - 4").
|
|
1169
|
-
* - Converts hyphen-delimited form to bi-directional arrow form replacing " - " with " <=> " for clearer layer transitions.
|
|
1170
|
-
* - Skips line when result is 'n/a'.
|
|
1171
|
-
*
|
|
1172
|
-
* Steps:
|
|
1173
|
-
* 1. Build architecture string (if derivable) and push as first line.
|
|
1174
|
-
* 2. For each tracked history series build a sparkline (bounded width) and push if non-empty.
|
|
1175
|
-
* 3. Emit a trailing blank framed line as a visual separator before maze rendering.
|
|
1176
|
-
*
|
|
1177
|
-
* Determinism: Pure formatting/read-only usage of snapshot histories & network.
|
|
1178
|
-
* Reentrancy: Safe; only mutates provided accumulator.
|
|
1179
|
-
* Edge cases: Empty histories yield omitted lines; architecture omitted when unknown.
|
|
1180
|
-
*
|
|
1181
|
-
* @param blockLines Accumulator mutated in place.
|
|
1182
|
-
* @param network Network whose architecture will be summarized; optional (can be nullish).
|
|
1183
|
-
*/
|
|
1184
|
-
#appendSolvedSparklines(blockLines: string[], network?: INetwork): void {
|
|
1185
|
-
const solvedLabelWidth = DashboardManager.#SOLVED_LABEL_WIDTH;
|
|
1186
|
-
const solvedStat = (label: string, value: string) =>
|
|
1187
|
-
this.#formatStat(
|
|
1188
|
-
label,
|
|
1189
|
-
value,
|
|
1190
|
-
colors.neonSilver,
|
|
1191
|
-
colors.cyanNeon,
|
|
1192
|
-
solvedLabelWidth,
|
|
1193
|
-
);
|
|
1194
|
-
const pushIf = (label: string, value: string | null | undefined) => {
|
|
1195
|
-
if (value) blockLines.push(solvedStat(label, value));
|
|
1196
|
-
};
|
|
1197
|
-
|
|
1198
|
-
// Step 1: Architecture summary
|
|
1199
|
-
if (network) {
|
|
1200
|
-
let architectureRaw = 'n/a';
|
|
1201
|
-
try {
|
|
1202
|
-
architectureRaw = this.#deriveArchitecture(network);
|
|
1203
|
-
} catch {
|
|
1204
|
-
architectureRaw = 'n/a';
|
|
1205
|
-
}
|
|
1206
|
-
if (architectureRaw !== 'n/a') {
|
|
1207
|
-
const arrowArchitecture = architectureRaw
|
|
1208
|
-
.split(/\s*-\s*/)
|
|
1209
|
-
.join(' <=> ');
|
|
1210
|
-
pushIf(DashboardManager.#LABEL_ARCH, arrowArchitecture);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Step 2: Trend sparklines (bounded width)
|
|
1215
|
-
const archiveWidth = DashboardManager.#ARCHIVE_SPARK_WIDTH;
|
|
1216
|
-
pushIf(
|
|
1217
|
-
'Fitness trend',
|
|
1218
|
-
this.#buildSparkline(this.#bestFitnessHistory, archiveWidth),
|
|
1219
|
-
);
|
|
1220
|
-
pushIf(
|
|
1221
|
-
'Nodes trend',
|
|
1222
|
-
this.#buildSparkline(this.#complexityNodesHistory, archiveWidth),
|
|
1223
|
-
);
|
|
1224
|
-
pushIf(
|
|
1225
|
-
'Conns trend',
|
|
1226
|
-
this.#buildSparkline(this.#complexityConnsHistory, archiveWidth),
|
|
1227
|
-
);
|
|
1228
|
-
pushIf(
|
|
1229
|
-
'Hypervol trend',
|
|
1230
|
-
this.#buildSparkline(this.#hypervolumeHistory, archiveWidth),
|
|
1231
|
-
);
|
|
1232
|
-
pushIf(
|
|
1233
|
-
'Progress trend',
|
|
1234
|
-
this.#buildSparkline(this.#progressHistory, archiveWidth),
|
|
1235
|
-
);
|
|
1236
|
-
pushIf(
|
|
1237
|
-
'Species trend',
|
|
1238
|
-
this.#buildSparkline(this.#speciesCountHistory, archiveWidth),
|
|
1239
|
-
);
|
|
1240
|
-
|
|
1241
|
-
// Step 3: Spacer line
|
|
1242
|
-
blockLines.push(
|
|
1243
|
-
`${colors.blueCore}║${NetworkVisualization.pad(
|
|
1244
|
-
' ',
|
|
1245
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
1246
|
-
' ',
|
|
1247
|
-
)}${colors.blueCore}║${colors.reset}`,
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
/**
|
|
1252
|
-
* Append a centered maze visualization for a newly solved maze.
|
|
1253
|
-
*
|
|
1254
|
-
* The visualization is produced by `MazeVisualization.visualizeMaze`, which
|
|
1255
|
-
* returns either a multi‑line string or an array of row strings. We normalize
|
|
1256
|
-
* the output to an array and then emit each row framed inside the dashboard
|
|
1257
|
-
* box. Rows are padded horizontally to the fixed `FRAME_INNER_WIDTH` so that
|
|
1258
|
-
* varying maze sizes (small corridors vs larger layouts) remain visually
|
|
1259
|
-
* centered and consistent with surrounding stats blocks.
|
|
1260
|
-
*
|
|
1261
|
-
* Steps (educational):
|
|
1262
|
-
* 1. Determine the terminal path position (last coordinate) – used to draw the agent end state.
|
|
1263
|
-
* 2. Generate a textual maze representation (string[] or string) including the path highlight.
|
|
1264
|
-
* 3. Normalize to an array of raw row strings (split on newlines if needed).
|
|
1265
|
-
* 4. Pad each row to the inner frame width (acts as horizontal centering) and push framed lines to `blockLines`.
|
|
1266
|
-
*
|
|
1267
|
-
* Performance & ES2023 notes:
|
|
1268
|
-
* - Uses `Array.prototype.at(-1)` for the final path coordinate (clearer than `path[path.length-1]`).
|
|
1269
|
-
* - Avoids the previous join/split round‑trip (now pads & pushes in a single pass), reducing temporary string allocations.
|
|
1270
|
-
* - Relies on local constants to minimize repeated property lookups (`innerWidth`).
|
|
1271
|
-
*
|
|
1272
|
-
* Determinism: purely formatting; does not mutate input arrays or rely on random state.
|
|
1273
|
-
* Reentrancy: safe (no shared scratch buffers used here).
|
|
1274
|
-
*
|
|
1275
|
-
* @param blockLines - Accumulated output lines for the solved maze archive block (mutated in place by appending framed rows).
|
|
1276
|
-
* @param solved - Object containing the raw `maze` character grid and a `result` with a `path` of `[x,y]` coordinates.
|
|
1277
|
-
* @remarks The `result.path` is expected to include the start cell; if empty, a fallback position `[0,0]` is used (rare edge case for defensive coding in examples).
|
|
1278
|
-
*/
|
|
1279
|
-
#appendSolvedMaze(
|
|
1280
|
-
blockLines: string[],
|
|
1281
|
-
solved: {
|
|
1282
|
-
maze: string[];
|
|
1283
|
-
result: { path?: ReadonlyArray<readonly [number, number]> } & Record<
|
|
1284
|
-
string,
|
|
1285
|
-
unknown
|
|
1286
|
-
>;
|
|
1287
|
-
},
|
|
1288
|
-
): void {
|
|
1289
|
-
// Step 1: Determine final position on the solved path (fallback to [0,0] if path missing)
|
|
1290
|
-
const pathCoordinates = solved.result.path as
|
|
1291
|
-
| ReadonlyArray<readonly [number, number]>
|
|
1292
|
-
| undefined;
|
|
1293
|
-
const endPosition = pathCoordinates?.at(-1) ?? [0, 0];
|
|
1294
|
-
|
|
1295
|
-
// Step 2: Produce visualization (string or string[]). Pass the path so it can be highlighted.
|
|
1296
|
-
const visualization = MazeVisualization.visualizeMaze(
|
|
1297
|
-
solved.maze,
|
|
1298
|
-
endPosition as [number, number],
|
|
1299
|
-
(pathCoordinates ?? []) as [number, number][],
|
|
1300
|
-
);
|
|
1301
|
-
|
|
1302
|
-
// Step 3: Normalize to array of lines.
|
|
1303
|
-
const rawLines: string[] = Array.isArray(visualization)
|
|
1304
|
-
? visualization
|
|
1305
|
-
: (visualization as string).split('\n');
|
|
1306
|
-
|
|
1307
|
-
// Step 4: Pad & frame each line (acts as centering); push directly to accumulator.
|
|
1308
|
-
const innerWidth = DashboardManager.FRAME_INNER_WIDTH; // local alias for micro-clarity
|
|
1309
|
-
for (const rawLine of rawLines) {
|
|
1310
|
-
const paddedRow = NetworkVisualization.pad(rawLine, innerWidth, ' ');
|
|
1311
|
-
blockLines.push(
|
|
1312
|
-
`${colors.blueCore}║${NetworkVisualization.pad(
|
|
1313
|
-
paddedRow,
|
|
1314
|
-
innerWidth,
|
|
1315
|
-
' ',
|
|
1316
|
-
)}${colors.blueCore}║${colors.reset}`,
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
/**
|
|
1322
|
-
* Compute and append human‑readable path efficiency statistics for a solved maze.
|
|
1323
|
-
*
|
|
1324
|
-
* Metrics exposed (educational rationale):
|
|
1325
|
-
* - Path efficiency: optimal BFS distance vs actual traversed length – demonstrates how close evolution came to shortest path.
|
|
1326
|
-
* - Path overhead: percent longer than optimal – highlights wasted exploration after reaching a viable route.
|
|
1327
|
-
* - Unique cells visited / revisits: proxy for exploration vs dithering; useful to tune mutation operators.
|
|
1328
|
-
* - Steps: raw action count taken (often equals `pathLength`).
|
|
1329
|
-
* - Fitness: final scalar used for selection (displayed with two decimals for compactness).
|
|
1330
|
-
*
|
|
1331
|
-
* Steps:
|
|
1332
|
-
* 1. Derive aggregated path metrics via `#computePathMetrics` (encapsulates BFS optimal distance + visitation stats).
|
|
1333
|
-
* 2. Format each metric with consistent label width & colors using `formatStat` (keeps styling centralized).
|
|
1334
|
-
* 3. Push each formatted line to the `blockLines` accumulator.
|
|
1335
|
-
*
|
|
1336
|
-
* Performance notes:
|
|
1337
|
-
* - Single metrics object reused for string interpolation (no intermediate arrays created).
|
|
1338
|
-
* - Uses template literals directly; minimal extra allocations beyond the final output strings.
|
|
1339
|
-
* - Order is fixed to preserve snapshot diff stability for external log parsers.
|
|
1340
|
-
*
|
|
1341
|
-
* Determinism: relies on deterministic BFS + pure counting; no randomness.
|
|
1342
|
-
* Reentrancy: safe; no shared mutable scratch state.
|
|
1343
|
-
*
|
|
1344
|
-
* @param blockLines - Accumulator array mutated by appending formatted stat lines.
|
|
1345
|
-
* @param solved - Object holding the `maze` layout and `result` containing at least `path`, `steps`, and `fitness`.
|
|
1346
|
-
*/
|
|
1347
|
-
#appendSolvedPathStats(
|
|
1348
|
-
blockLines: string[],
|
|
1349
|
-
solved: { maze: string[]; result: IMazeRunResult },
|
|
1350
|
-
): void {
|
|
1351
|
-
// Step 1: Derive metrics (single call encapsulates BFS + visitation stats)
|
|
1352
|
-
const metrics = this.#computePathMetrics(solved.maze, solved.result);
|
|
1353
|
-
|
|
1354
|
-
// Local alias for consistent label width
|
|
1355
|
-
const labelWidth = DashboardManager.#SOLVED_LABEL_WIDTH;
|
|
1356
|
-
const solvedStat = (label: string, value: string) =>
|
|
1357
|
-
this.#formatStat(
|
|
1358
|
-
label,
|
|
1359
|
-
value,
|
|
1360
|
-
colors.neonSilver,
|
|
1361
|
-
colors.cyanNeon,
|
|
1362
|
-
labelWidth,
|
|
1363
|
-
);
|
|
1364
|
-
|
|
1365
|
-
// Step 2 & 3: Format and append in stable order
|
|
1366
|
-
blockLines.push(
|
|
1367
|
-
solvedStat(
|
|
1368
|
-
DashboardManager.#LABEL_PATH_EFF,
|
|
1369
|
-
`${metrics.optimalLength}/${metrics.pathLength} (${metrics.efficiencyPct}%)`,
|
|
1370
|
-
),
|
|
1371
|
-
);
|
|
1372
|
-
blockLines.push(
|
|
1373
|
-
solvedStat(
|
|
1374
|
-
DashboardManager.#LABEL_PATH_OVER,
|
|
1375
|
-
`${metrics.overheadPct}% longer than optimal`,
|
|
1376
|
-
),
|
|
1377
|
-
);
|
|
1378
|
-
blockLines.push(
|
|
1379
|
-
solvedStat(
|
|
1380
|
-
DashboardManager.#LABEL_UNIQUE,
|
|
1381
|
-
`${metrics.uniqueCellsVisited}`,
|
|
1382
|
-
),
|
|
1383
|
-
);
|
|
1384
|
-
blockLines.push(
|
|
1385
|
-
solvedStat(
|
|
1386
|
-
DashboardManager.#LABEL_REVISITS,
|
|
1387
|
-
`${metrics.revisitedCells} times`,
|
|
1388
|
-
),
|
|
1389
|
-
);
|
|
1390
|
-
blockLines.push(
|
|
1391
|
-
solvedStat(DashboardManager.#LABEL_STEPS, `${metrics.totalSteps}`),
|
|
1392
|
-
);
|
|
1393
|
-
blockLines.push(
|
|
1394
|
-
solvedStat(
|
|
1395
|
-
DashboardManager.#LABEL_FITNESS,
|
|
1396
|
-
`${metrics.fitnessValue.toFixed(2)}`,
|
|
1397
|
-
),
|
|
1398
|
-
);
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
/** Emit footer & send archive block to logger. */
|
|
1402
|
-
static #CACHED_SOLVED_FOOTER_BORDER: string | null = null;
|
|
1403
|
-
|
|
1404
|
-
/**
|
|
1405
|
-
* Append the solved-archive footer border and emit the accumulated block to the archive logger.
|
|
1406
|
-
*
|
|
1407
|
-
* Implementation notes:
|
|
1408
|
-
* - Reuses an internal cached bottom-border string to avoid recomputing the padded border on every solved maze.
|
|
1409
|
-
* - Emits the block as a single joined payload for efficiency; falls back to a line-wise append if the
|
|
1410
|
-
* archive function throws or is not compatible with the single-string API.
|
|
1411
|
-
* - Clears the provided `blockLines` accumulator in-place after emission so callers (and tests) can reuse the
|
|
1412
|
-
* same array as a scratch buffer, reducing GC churn in tight loops.
|
|
1413
|
-
*
|
|
1414
|
-
* Steps (inline):
|
|
1415
|
-
* 1. Ensure cached border exists (lazy-init).
|
|
1416
|
-
* 2. Append the bottom border to the provided accumulator.
|
|
1417
|
-
* 3. Attempt single-string emission with `{ prepend: true }`.
|
|
1418
|
-
* 4. On failure, fallback to line-by-line emission using the archive function or a no-op.
|
|
1419
|
-
* 5. Clear the accumulator for reuse.
|
|
1420
|
-
*
|
|
1421
|
-
* @param blockLines Mutable accumulator of framed lines representing a solved maze archive block. This
|
|
1422
|
-
* function will append the closing border and emit the payload; the array will be emptied on return.
|
|
1423
|
-
* @example
|
|
1424
|
-
* const lines: string[] = [];
|
|
1425
|
-
* // ... various helpers push frame header, stats, maze rows into `lines` ...
|
|
1426
|
-
* (dashboard as any)["#appendSolvedFooterAndEmit"](lines);
|
|
1427
|
-
*/
|
|
1428
|
-
#appendSolvedFooterAndEmit(blockLines: string[]): void {
|
|
1429
|
-
// Step 1: Lazy-initialize a cached bottom-border string to avoid repeated pad/repeat work.
|
|
1430
|
-
const innerFrameWidth = DashboardManager.FRAME_INNER_WIDTH;
|
|
1431
|
-
if (DashboardManager.#CACHED_SOLVED_FOOTER_BORDER === null) {
|
|
1432
|
-
// Build once and reuse; this avoids allocating a new long string on every solved maze.
|
|
1433
|
-
DashboardManager.#CACHED_SOLVED_FOOTER_BORDER = `${
|
|
1434
|
-
colors.blueCore
|
|
1435
|
-
}╚${NetworkVisualization.pad(
|
|
1436
|
-
'═'.repeat(innerFrameWidth),
|
|
1437
|
-
innerFrameWidth,
|
|
1438
|
-
'═',
|
|
1439
|
-
)}╝${colors.reset}`;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
// Step 2: Append cached bottom border to the provided accumulator (no intermediate arrays).
|
|
1443
|
-
blockLines.push(DashboardManager.#CACHED_SOLVED_FOOTER_BORDER);
|
|
1444
|
-
|
|
1445
|
-
// Step 3: Prefer a single-string emission for efficiency (smaller call overhead and fewer allocations).
|
|
1446
|
-
try {
|
|
1447
|
-
// Favor the original API shape: archiveFn(payload, { prepend: true }). Use a permissive type cast
|
|
1448
|
-
// because test harnesses may provide different shapes.
|
|
1449
|
-
const archiveFnCast = this.#archiveFn as (
|
|
1450
|
-
payload: string,
|
|
1451
|
-
options?: { prepend?: boolean },
|
|
1452
|
-
) => void;
|
|
1453
|
-
archiveFnCast(blockLines.join('\n'), { prepend: true });
|
|
1454
|
-
|
|
1455
|
-
// Step 5: Clear the accumulator in-place to allow caller reuse (reduces GC pressure in tests).
|
|
1456
|
-
blockLines.length = 0;
|
|
1457
|
-
return;
|
|
1458
|
-
} catch {
|
|
1459
|
-
// Step 4: Fallback to line-wise appends when the single-string API fails.
|
|
1460
|
-
const archiveAppend = this.#archiveFn ?? (() => {});
|
|
1461
|
-
|
|
1462
|
-
// Use a conventional indexed loop with a descriptive iterator variable to avoid short-name warnings.
|
|
1463
|
-
for (let lineIndex = 0; lineIndex < blockLines.length; lineIndex++) {
|
|
1464
|
-
archiveAppend(blockLines[lineIndex]);
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// Clear the accumulator for reuse by the caller.
|
|
1468
|
-
blockLines.length = 0;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
/**
|
|
1473
|
-
* Compute derived path metrics for a solved (or partially solved) maze run.
|
|
1474
|
-
*
|
|
1475
|
-
* Metrics returned (educational focus):
|
|
1476
|
-
* - optimalLength: Shortest possible path length (BFS over encoded maze). Provides a baseline for efficiency.
|
|
1477
|
-
* - pathLength: Actual traversed path length (steps between first & last coordinate). Used for overhead calculations.
|
|
1478
|
-
* - efficiencyPct: (optimal / actual * 100) clamped to 100%. Indicates how close the agent was to an optimal route.
|
|
1479
|
-
* - overheadPct: Percent the actual path exceeds optimal ((actual/optimal)*100 - 100). Negative values are clamped to 0 in practice by optimal <= path.
|
|
1480
|
-
* - uniqueCellsVisited: Distinct grid cells in the path – proxy for exploration breadth.
|
|
1481
|
-
* - revisitedCells: Times a cell coordinate was encountered after the first visit – proxy for dithering / loops.
|
|
1482
|
-
* - totalSteps: Reported step counter from the result object (may equal pathLength, but kept separate for clarity / future divergence like wait actions).
|
|
1483
|
-
* - fitnessValue: Raw fitness scalar copied through for convenience (avoids re-threading the original result where only metrics are needed).
|
|
1484
|
-
*
|
|
1485
|
-
* Steps:
|
|
1486
|
-
* 1. Locate start 'S' and exit 'E' positions in the maze (single pass each via MazeUtils helpers).
|
|
1487
|
-
* 2. Run BFS to obtain the optimal shortest path length between S and E (O(C) with C = cell count).
|
|
1488
|
-
* 3. Derive actual path length from provided coordinate list (defensive against empty / single-node path).
|
|
1489
|
-
* 4. Compute efficiency & overhead percentages with divide-by-zero guards (fallback to 0.0 when ambiguous).
|
|
1490
|
-
* 5. Count unique vs revisited cells in a single pass through the path (O(P) with P = pathLength+1 nodes).
|
|
1491
|
-
* 6. Return an immutable plain object used by formatting helpers.
|
|
1492
|
-
*
|
|
1493
|
-
* Complexity:
|
|
1494
|
-
* - BFS: O(C) where C = maze cell count.
|
|
1495
|
-
* - Path scan: O(P) where P = number of coordinates in path.
|
|
1496
|
-
* - Overall: O(C + P) per invocation, acceptable for archive-time formatting (not in a hot inner evolution loop).
|
|
1497
|
-
*
|
|
1498
|
-
* Determinism: Fully deterministic given identical maze + path (no randomness, stable BFS ordering assumed from MazeUtils implementation).
|
|
1499
|
-
* Reentrancy: Safe (allocates only local structures: Set + return object).
|
|
1500
|
-
* Memory: Extra allocations are bounded (Set size <= P). Suitable for occasional solved-maze archival.
|
|
1501
|
-
*
|
|
1502
|
-
* Edge cases handled:
|
|
1503
|
-
* - Empty or single-coordinate path: pathLength coerces to 0; efficiency & overhead emit '0.0'.
|
|
1504
|
-
* - Unreachable BFS (negative / non-positive optimalLength): treated as 0 for ratios (prevents NaN/Infinity).
|
|
1505
|
-
* - Division by zero avoided via guards; percentages formatted with one decimal place.
|
|
1506
|
-
*
|
|
1507
|
-
* @param maze Maze layout as array of row strings containing 'S' and 'E'.
|
|
1508
|
-
* @param result Evaluation result containing at least { path, steps, fitness }.
|
|
1509
|
-
* @returns Object with path + efficiency metrics (see description).
|
|
1510
|
-
* @example
|
|
1511
|
-
* const metrics = dashboard.#computePathMetrics(maze, { path, steps: path.length, fitness });
|
|
1512
|
-
* console.log(metrics.efficiencyPct); // e.g. '87.5'
|
|
1513
|
-
*/
|
|
1514
|
-
#computePathMetrics(
|
|
1515
|
-
maze: string[],
|
|
1516
|
-
result: { path: [number, number][]; steps: number; fitness: number },
|
|
1517
|
-
): {
|
|
1518
|
-
optimalLength: number;
|
|
1519
|
-
pathLength: number;
|
|
1520
|
-
efficiencyPct: string;
|
|
1521
|
-
overheadPct: string;
|
|
1522
|
-
uniqueCellsVisited: number;
|
|
1523
|
-
revisitedCells: number;
|
|
1524
|
-
totalSteps: number;
|
|
1525
|
-
fitnessValue: number;
|
|
1526
|
-
} {
|
|
1527
|
-
// Step 1: Resolve start & exit coordinates
|
|
1528
|
-
const startPosition = MazeUtils.findPosition(maze, 'S');
|
|
1529
|
-
const exitPosition = MazeUtils.findPosition(maze, 'E');
|
|
1530
|
-
|
|
1531
|
-
// Step 2: Compute optimal shortest path via BFS (may return <=0 if unreachable)
|
|
1532
|
-
const bfsLength = MazeUtils.bfsDistance(
|
|
1533
|
-
MazeUtils.encodeMaze(maze),
|
|
1534
|
-
startPosition,
|
|
1535
|
-
exitPosition,
|
|
1536
|
-
);
|
|
1537
|
-
const optimalLength = typeof bfsLength === 'number' ? bfsLength : 0;
|
|
1538
|
-
|
|
1539
|
-
// Step 3: Derive actual path length (edges traversed); guard against empty path
|
|
1540
|
-
const rawPathLength = Math.max(0, result.path.length - 1);
|
|
1541
|
-
|
|
1542
|
-
// Step 4: Efficiency & overhead (guard divide-by-zero and invalid optimal)
|
|
1543
|
-
let efficiencyPct = '0.0';
|
|
1544
|
-
let overheadPct = '0.0';
|
|
1545
|
-
if (rawPathLength > 0 && optimalLength > 0) {
|
|
1546
|
-
const efficiency = Math.min(1, optimalLength / rawPathLength) * 100;
|
|
1547
|
-
efficiencyPct = efficiency.toFixed(1);
|
|
1548
|
-
const overhead = (rawPathLength / optimalLength) * 100 - 100;
|
|
1549
|
-
overheadPct = overhead.toFixed(1);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// Step 5: Count unique vs revisits
|
|
1553
|
-
const uniqueCells = new Set<string>();
|
|
1554
|
-
let revisitedCells = 0;
|
|
1555
|
-
for (const [cellX, cellY] of result.path) {
|
|
1556
|
-
const key = `${cellX},${cellY}`;
|
|
1557
|
-
if (uniqueCells.has(key)) revisitedCells++;
|
|
1558
|
-
else uniqueCells.add(key);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Step 6: Return immutable metrics object
|
|
1562
|
-
return {
|
|
1563
|
-
optimalLength,
|
|
1564
|
-
pathLength: rawPathLength,
|
|
1565
|
-
efficiencyPct,
|
|
1566
|
-
overheadPct,
|
|
1567
|
-
uniqueCellsVisited: uniqueCells.size,
|
|
1568
|
-
revisitedCells,
|
|
1569
|
-
totalSteps: result.steps,
|
|
1570
|
-
fitnessValue: result.fitness,
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
/**
|
|
1575
|
-
* Infer a compact, human‑readable architecture string (e.g. "3 - 5 - 2" or "4 - 8 - 8 - 1").
|
|
1576
|
-
*
|
|
1577
|
-
* Supports several internal network representations encountered in examples:
|
|
1578
|
-
* 1. Layered form: `network.layers = [layer0, layer1, ...]` where each layer has `nodes` or is an array.
|
|
1579
|
-
* 2. Flat node list: `network.nodes = [...]` each node declaring a `type` ('input' | 'hidden' | 'output'). Hidden layers are
|
|
1580
|
-
* approximated by a simple topological layering pass: we iteratively collect hidden nodes whose inbound connection sources
|
|
1581
|
-
* are already assigned to earlier layers. Remaining nodes after no progress count as a single ambiguous layer (safety / cycle guard).
|
|
1582
|
-
* 3. Scalar fallback: numeric `input` & `output` counts (no hidden layers) -> returns "I - O".
|
|
1583
|
-
*
|
|
1584
|
-
* Steps:
|
|
1585
|
-
* 1. Early null/undefined guard.
|
|
1586
|
-
* 2. If a layered structure exists (>=2 layers) derive each layer size in order and return immediately (fast path).
|
|
1587
|
-
* 3. Else if a flat node list exists, split into input / hidden / output categories.
|
|
1588
|
-
* 4. If no hidden nodes: use explicit numeric counts (prefer explicit `input`/`output` props if present).
|
|
1589
|
-
* 5. Perform iterative hidden layer inference with a safety iteration cap to avoid infinite loops for malformed cyclic graphs.
|
|
1590
|
-
* 6. Assemble final size list: input size, inferred hidden sizes, output size.
|
|
1591
|
-
* 7. Fallback: if only scalar counts available, return them; otherwise 'n/a'.
|
|
1592
|
-
*
|
|
1593
|
-
* Algorithmic notes:
|
|
1594
|
-
* - Hidden layering pass is O(H * E_in) where H = hidden nodes, E_in = mean in-degree, acceptable for formatting/UI.
|
|
1595
|
-
* - The safety cap (`hiddenCount * LAYER_INFER_LOOP_MULTIPLIER`) prevents pathological spins on cyclic graphs lacking
|
|
1596
|
-
* proper DAG layering; any leftover hidden nodes are grouped into a terminal bucket for transparency.
|
|
1597
|
-
* - We intentionally avoid mutating the original node objects (pure inspection) to keep side‑effects nil.
|
|
1598
|
-
*
|
|
1599
|
-
* Determinism: given a stable ordering of `network.nodes` and their connections, output is deterministic.
|
|
1600
|
-
* Reentrancy: safe; all state kept in local sets/arrays.
|
|
1601
|
-
*
|
|
1602
|
-
* @param networkInstance Arbitrary network-like object from examples or NEAT internals.
|
|
1603
|
-
* @returns Architecture string in the form "Input - Hidden... - Output" or 'n/a' if shape cannot be inferred.
|
|
1604
|
-
* @example
|
|
1605
|
-
* // Layered network
|
|
1606
|
-
* deriveArchitecture({ layers:[ {nodes:[1,2,3]}, {nodes:[4,5]}, {nodes:[6,7]} ] }) => "3 - 2 - 2"
|
|
1607
|
-
* @example
|
|
1608
|
-
* // Flat node list with inferred hidden tiers
|
|
1609
|
-
* deriveArchitecture({ nodes:[{type:'input'}, {type:'hidden'}, {type:'output'}] }) => "1 - 1 - 1"
|
|
1610
|
-
*/
|
|
1611
|
-
#deriveArchitecture(networkInstance: INetwork): string {
|
|
1612
|
-
const networkAny = networkInstance as unknown as Record<string, unknown>;
|
|
1613
|
-
// Step 1: Null/undefined quick exit
|
|
1614
|
-
if (!networkInstance) return 'n/a';
|
|
1615
|
-
|
|
1616
|
-
// Step 2: Layered representation (fast path)
|
|
1617
|
-
const layerArray = networkAny.layers;
|
|
1618
|
-
if (Array.isArray(layerArray) && layerArray.length >= 2) {
|
|
1619
|
-
const layerSizes: number[] = [];
|
|
1620
|
-
for (const layerRef of layerArray) {
|
|
1621
|
-
const size = Array.isArray(layerRef?.nodes)
|
|
1622
|
-
? layerRef.nodes.length
|
|
1623
|
-
: Array.isArray(layerRef)
|
|
1624
|
-
? layerRef.length
|
|
1625
|
-
: 0;
|
|
1626
|
-
layerSizes.push(size);
|
|
1627
|
-
}
|
|
1628
|
-
return layerSizes.join(' - ');
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
// Step 3: Flat node list representation
|
|
1632
|
-
const flatNodes = networkAny.nodes;
|
|
1633
|
-
if (Array.isArray(flatNodes)) {
|
|
1634
|
-
type NodeWithType = {
|
|
1635
|
-
type?: string;
|
|
1636
|
-
connections?: { in?: Array<{ from?: unknown }> };
|
|
1637
|
-
[key: string]: unknown;
|
|
1638
|
-
};
|
|
1639
|
-
const inputNodes = flatNodes.filter(
|
|
1640
|
-
(nodeItem: unknown) => (nodeItem as NodeWithType).type === 'input',
|
|
1641
|
-
);
|
|
1642
|
-
const outputNodes = flatNodes.filter(
|
|
1643
|
-
(nodeItem: unknown) => (nodeItem as NodeWithType).type === 'output',
|
|
1644
|
-
);
|
|
1645
|
-
const hiddenNodesAll = flatNodes.filter(
|
|
1646
|
-
(nodeItem: unknown) => (nodeItem as NodeWithType).type === 'hidden',
|
|
1647
|
-
);
|
|
1648
|
-
|
|
1649
|
-
// Step 4: No hidden nodes -> simple case
|
|
1650
|
-
if (!hiddenNodesAll.length) {
|
|
1651
|
-
if (
|
|
1652
|
-
typeof networkInstance.input === 'number' &&
|
|
1653
|
-
typeof networkInstance.output === 'number'
|
|
1654
|
-
) {
|
|
1655
|
-
return `${networkInstance.input} - ${networkInstance.output}`;
|
|
1656
|
-
}
|
|
1657
|
-
return `${inputNodes.length} - ${outputNodes.length}`;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
// Step 5: Iterative hidden layer inference
|
|
1661
|
-
const assignedNodes = new Set<unknown>(inputNodes);
|
|
1662
|
-
let remainingHidden = hiddenNodesAll.slice();
|
|
1663
|
-
const inferredHiddenSizes: number[] = [];
|
|
1664
|
-
const safetyLimit =
|
|
1665
|
-
hiddenNodesAll.length * DashboardManager.#LAYER_INFER_LOOP_MULTIPLIER;
|
|
1666
|
-
let iterationCounter = 0;
|
|
1667
|
-
while (remainingHidden.length && iterationCounter < safetyLimit) {
|
|
1668
|
-
iterationCounter++;
|
|
1669
|
-
const currentLayer = remainingHidden.filter((hiddenNode: unknown) => {
|
|
1670
|
-
const nodeWithConn = hiddenNode as NodeWithType;
|
|
1671
|
-
return nodeWithConn.connections?.in?.every((conn: unknown) =>
|
|
1672
|
-
assignedNodes.has((conn as { from?: unknown }).from),
|
|
1673
|
-
);
|
|
1674
|
-
});
|
|
1675
|
-
if (!currentLayer.length) {
|
|
1676
|
-
// Group unresolved remainder into one bucket (cycles / malformed graph)
|
|
1677
|
-
inferredHiddenSizes.push(remainingHidden.length);
|
|
1678
|
-
break;
|
|
1679
|
-
}
|
|
1680
|
-
inferredHiddenSizes.push(currentLayer.length);
|
|
1681
|
-
for (const nodeRef of currentLayer) assignedNodes.add(nodeRef);
|
|
1682
|
-
remainingHidden = remainingHidden.filter(
|
|
1683
|
-
(nodeCandidate: unknown) => !assignedNodes.has(nodeCandidate),
|
|
1684
|
-
);
|
|
1685
|
-
}
|
|
1686
|
-
return [
|
|
1687
|
-
`${inputNodes.length}`,
|
|
1688
|
-
...inferredHiddenSizes.map((hiddenSize) => `${hiddenSize}`),
|
|
1689
|
-
`${outputNodes.length}`,
|
|
1690
|
-
].join(' - ');
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
// Step 7: Numeric scalar fallback
|
|
1694
|
-
if (
|
|
1695
|
-
typeof networkInstance.input === 'number' &&
|
|
1696
|
-
typeof networkInstance.output === 'number'
|
|
1697
|
-
) {
|
|
1698
|
-
return `${networkInstance.input} - ${networkInstance.output}`;
|
|
1699
|
-
}
|
|
1700
|
-
return 'n/a';
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
/**
|
|
1704
|
-
* Ingest an evolution engine update (generation tick or improved candidate) and refresh live + archived displays.
|
|
1705
|
-
*
|
|
1706
|
-
* High‑level responsibilities:
|
|
1707
|
-
* 1. Lazy initialize timing anchors (wall clock + perf) on first call.
|
|
1708
|
-
* 2. Stash generation/time metadata for later telemetry sampling.
|
|
1709
|
-
* 3. Update the current best candidate reference.
|
|
1710
|
-
* 4. Archive a newly solved maze (once per unique layout) with rich stats.
|
|
1711
|
-
* 5. Pull the latest telemetry snapshot from the NEAT instance (if provided) and update bounded history buffers.
|
|
1712
|
-
* 6. Redraw the live ASCII dashboard (network summary, maze, stats, progress bar).
|
|
1713
|
-
* 7. Emit a structured telemetry payload (custom DOM event + postMessage + optional hook) for external consumers.
|
|
1714
|
-
*
|
|
1715
|
-
* Performance notes:
|
|
1716
|
-
* - History buffers are bounded (HISTORY_MAX) using push-with-trim helpers; memory growth is capped.
|
|
1717
|
-
* - Telemetry extraction takes only the last snapshot (`safeLast`) to minimize per-tick work.
|
|
1718
|
-
* - All formatting for the archive occurs only when a maze is first solved (amortized, infrequent).
|
|
1719
|
-
* - Uses `performance.now()` when available for higher‑resolution generation throughput metrics.
|
|
1720
|
-
*
|
|
1721
|
-
* Determinism: All state changes here are observational (no RNG). Ordering of history pushes is fixed.
|
|
1722
|
-
* Reentrancy: Not safe (instance maintains mutable internal single-run state). Use one instance per run.
|
|
1723
|
-
* Side‑effects: Console / logger output, optional DOM events (browser), optional parent frame messaging.
|
|
1724
|
-
*
|
|
1725
|
-
* @param maze - Current maze layout under evolution (array of row strings).
|
|
1726
|
-
* @param result - Candidate evaluation result (expects fields: fitness, path, success, progress, etc.).
|
|
1727
|
-
* @param network - Network associated with the candidate result.
|
|
1728
|
-
* @param generation - Current generation number reported by the engine.
|
|
1729
|
-
* @param neatInstance - Optional NEAT framework instance exposing `getTelemetry()` and optional stats helpers.
|
|
1730
|
-
* @example
|
|
1731
|
-
* dashboard.update(maze, evaluationResult, genome.network, generation, neat);
|
|
1732
|
-
*/
|
|
1733
|
-
update(
|
|
1734
|
-
maze: string[],
|
|
1735
|
-
result: IMazeRunResult,
|
|
1736
|
-
network: INetwork,
|
|
1737
|
-
generation: number,
|
|
1738
|
-
neatInstance?: Neat,
|
|
1739
|
-
): void {
|
|
1740
|
-
// Step 1: Lazy initialization of timing anchors
|
|
1741
|
-
if (this.#runStartTs == null) {
|
|
1742
|
-
this.#runStartTs = Date.now(); // wall‑clock anchor
|
|
1743
|
-
this.#perfStart = globalThis.performance?.now?.() ?? this.#runStartTs;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
// Step 2: Record generation & update timestamp
|
|
1747
|
-
this.#lastUpdateTs = globalThis.performance?.now?.() ?? Date.now();
|
|
1748
|
-
this.#lastGeneration = generation;
|
|
1749
|
-
|
|
1750
|
-
// Step 3: Update current best candidate reference
|
|
1751
|
-
this.#currentBest = { result, network, generation };
|
|
1752
|
-
|
|
1753
|
-
// Step 4: Archive newly solved maze (once per unique layout)
|
|
1754
|
-
if (result?.success) {
|
|
1755
|
-
const solvedMazeKey = this.#getMazeKey(maze);
|
|
1756
|
-
if (!this.#solvedMazeKeys.has(solvedMazeKey)) {
|
|
1757
|
-
this.#solvedMazes.push({ maze, result, network, generation });
|
|
1758
|
-
this.#solvedMazeKeys.add(solvedMazeKey);
|
|
1759
|
-
const displayOrdinal = this.#solvedMazes.length; // 1-based position
|
|
1760
|
-
this.#appendSolvedToArchive(
|
|
1761
|
-
{ maze, result, network, generation },
|
|
1762
|
-
displayOrdinal,
|
|
1763
|
-
);
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// Step 5: Pull latest telemetry snapshot & update bounded histories
|
|
1768
|
-
const telemetrySeriesCandidate = neatInstance?.getTelemetry?.();
|
|
1769
|
-
if (
|
|
1770
|
-
Array.isArray(telemetrySeriesCandidate) &&
|
|
1771
|
-
telemetrySeriesCandidate.length
|
|
1772
|
-
) {
|
|
1773
|
-
const telemetrySeries = telemetrySeriesCandidate as DashboardTelemetry[];
|
|
1774
|
-
this.#lastTelemetry =
|
|
1775
|
-
MazeUtils.safeLast<DashboardTelemetry>(telemetrySeries) ?? null;
|
|
1776
|
-
// Best fitness history (trend sparkline source)
|
|
1777
|
-
const latestFitness = this.#currentBest?.result?.fitness;
|
|
1778
|
-
if (typeof latestFitness === 'number') {
|
|
1779
|
-
this.#lastBestFitness = latestFitness;
|
|
1780
|
-
this.#bestFitnessHistory = MazeUtils.pushHistory(
|
|
1781
|
-
this.#bestFitnessHistory,
|
|
1782
|
-
latestFitness,
|
|
1783
|
-
DashboardManager.HISTORY_MAX,
|
|
1784
|
-
);
|
|
1785
|
-
}
|
|
1786
|
-
// Complexity histories (mean nodes / connections)
|
|
1787
|
-
const complexitySnapshot = this.#lastTelemetry?.complexity;
|
|
1788
|
-
if (complexitySnapshot) {
|
|
1789
|
-
if (typeof complexitySnapshot.meanNodes === 'number') {
|
|
1790
|
-
this.#complexityNodesHistory = MazeUtils.pushHistory(
|
|
1791
|
-
this.#complexityNodesHistory,
|
|
1792
|
-
complexitySnapshot.meanNodes,
|
|
1793
|
-
DashboardManager.HISTORY_MAX,
|
|
1794
|
-
);
|
|
1795
|
-
}
|
|
1796
|
-
if (typeof complexitySnapshot.meanConns === 'number') {
|
|
1797
|
-
this.#complexityConnsHistory = MazeUtils.pushHistory(
|
|
1798
|
-
this.#complexityConnsHistory,
|
|
1799
|
-
complexitySnapshot.meanConns,
|
|
1800
|
-
DashboardManager.HISTORY_MAX,
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
// Hypervolume (multi‑objective front quality)
|
|
1805
|
-
const hyperVolumeLatest = this.#lastTelemetry?.hyper;
|
|
1806
|
-
if (typeof hyperVolumeLatest === 'number') {
|
|
1807
|
-
this.#hypervolumeHistory = MazeUtils.pushHistory(
|
|
1808
|
-
this.#hypervolumeHistory,
|
|
1809
|
-
hyperVolumeLatest,
|
|
1810
|
-
DashboardManager.HISTORY_MAX,
|
|
1811
|
-
);
|
|
1812
|
-
}
|
|
1813
|
-
// Progress toward exit for current best
|
|
1814
|
-
const progressFraction = this.#currentBest?.result?.progress;
|
|
1815
|
-
if (typeof progressFraction === 'number') {
|
|
1816
|
-
this.#progressHistory = MazeUtils.pushHistory(
|
|
1817
|
-
this.#progressHistory,
|
|
1818
|
-
progressFraction,
|
|
1819
|
-
DashboardManager.HISTORY_MAX,
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
1822
|
-
// Species count history
|
|
1823
|
-
const speciesCountSnapshot = this.#lastTelemetry?.species;
|
|
1824
|
-
if (typeof speciesCountSnapshot === 'number') {
|
|
1825
|
-
this.#speciesCountHistory = MazeUtils.pushHistory(
|
|
1826
|
-
this.#speciesCountHistory,
|
|
1827
|
-
speciesCountSnapshot,
|
|
1828
|
-
DashboardManager.HISTORY_MAX,
|
|
1829
|
-
);
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
// Step 6: Redraw live dashboard view
|
|
1834
|
-
this.redraw(maze, neatInstance);
|
|
1835
|
-
|
|
1836
|
-
// Step 7: Emit external telemetry payload (event + postMessage + optional hook)
|
|
1837
|
-
try {
|
|
1838
|
-
const elapsedMs =
|
|
1839
|
-
this.#perfStart != null && globalThis.performance?.now
|
|
1840
|
-
? globalThis.performance.now() - this.#perfStart
|
|
1841
|
-
: this.#runStartTs
|
|
1842
|
-
? Date.now() - this.#runStartTs
|
|
1843
|
-
: 0;
|
|
1844
|
-
const generationsPerSecond =
|
|
1845
|
-
elapsedMs > 0 ? generation / (elapsedMs / 1000) : 0;
|
|
1846
|
-
const payload = {
|
|
1847
|
-
type: 'asciiMaze:telemetry',
|
|
1848
|
-
generation,
|
|
1849
|
-
bestFitness: this.#lastBestFitness,
|
|
1850
|
-
progress: this.#currentBest?.result?.progress ?? null,
|
|
1851
|
-
speciesCount: this.#speciesCountHistory.at(-1) ?? null,
|
|
1852
|
-
gensPerSec: +generationsPerSecond.toFixed(3),
|
|
1853
|
-
timestamp: Date.now(),
|
|
1854
|
-
details: this.#lastDetailedStats || null,
|
|
1855
|
-
};
|
|
1856
|
-
if (typeof window !== 'undefined') {
|
|
1857
|
-
try {
|
|
1858
|
-
window.dispatchEvent(
|
|
1859
|
-
new CustomEvent('asciiMazeTelemetry', { detail: payload }),
|
|
1860
|
-
);
|
|
1861
|
-
} catch {
|
|
1862
|
-
// Swallow event dispatch errors
|
|
1863
|
-
}
|
|
1864
|
-
try {
|
|
1865
|
-
if (window.parent && window.parent !== window)
|
|
1866
|
-
window.parent.postMessage(payload, '*');
|
|
1867
|
-
} catch {
|
|
1868
|
-
// Swallow postMessage errors
|
|
1869
|
-
}
|
|
1870
|
-
interface AsciiMazeWindow extends Window {
|
|
1871
|
-
asciiMazeLastTelemetry?: unknown;
|
|
1872
|
-
}
|
|
1873
|
-
(window as AsciiMazeWindow).asciiMazeLastTelemetry = payload; // polling surface
|
|
1874
|
-
}
|
|
1875
|
-
try {
|
|
1876
|
-
interface DashboardWithHook {
|
|
1877
|
-
_telemetryHook?: (payload: unknown) => void;
|
|
1878
|
-
}
|
|
1879
|
-
const dashboardWithHook = this as unknown as DashboardWithHook;
|
|
1880
|
-
if (dashboardWithHook._telemetryHook) {
|
|
1881
|
-
dashboardWithHook._telemetryHook(payload);
|
|
1882
|
-
}
|
|
1883
|
-
} catch {
|
|
1884
|
-
// Swallow telemetry hook errors
|
|
1885
|
-
}
|
|
1886
|
-
} catch {
|
|
1887
|
-
/* swallow telemetry emission errors */
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
/**
|
|
1892
|
-
* Return the most recent telemetry snapshot including rich details.
|
|
1893
|
-
* Details may be null if not yet populated.
|
|
1894
|
-
* @returns Snapshot object with generation, fitness, progress and detail block.
|
|
1895
|
-
*/
|
|
1896
|
-
getLastTelemetry(): AsciiMazeTelemetrySnapshot {
|
|
1897
|
-
const elapsedMs =
|
|
1898
|
-
this.#perfStart != null && typeof performance !== 'undefined'
|
|
1899
|
-
? performance.now() - this.#perfStart
|
|
1900
|
-
: this.#runStartTs
|
|
1901
|
-
? Date.now() - this.#runStartTs
|
|
1902
|
-
: 0;
|
|
1903
|
-
const generation = this.#lastGeneration ?? 0;
|
|
1904
|
-
const gensPerSec = elapsedMs > 0 ? generation / (elapsedMs / 1000) : 0;
|
|
1905
|
-
return {
|
|
1906
|
-
generation,
|
|
1907
|
-
bestFitness: this.#lastBestFitness,
|
|
1908
|
-
progress: this.#currentBest?.result?.progress ?? null,
|
|
1909
|
-
speciesCount: MazeUtils.safeLast(this.#speciesCountHistory) ?? null,
|
|
1910
|
-
gensPerSec: +gensPerSec.toFixed(3),
|
|
1911
|
-
// Expose the last update time if available; convert high-resolution perf time to
|
|
1912
|
-
// wall-clock ms when possible so consumers receive an absolute timestamp.
|
|
1913
|
-
timestamp: this.#resolveLastUpdateWallMs(),
|
|
1914
|
-
details: this.#lastDetailedStats || null,
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
/**
|
|
1919
|
-
* Resolve the stored last-update timestamp to a wall-clock millisecond value.
|
|
1920
|
-
* If the stored value is a high-resolution perf.now() reading, convert it to
|
|
1921
|
-
* Date.now() anchored by the recorded `#runStartTs` / `#perfStart` pair. If no
|
|
1922
|
-
* last-update is available fall back to Date.now().
|
|
1923
|
-
*/
|
|
1924
|
-
#resolveLastUpdateWallMs(): number {
|
|
1925
|
-
if (this.#lastUpdateTs == null) return Date.now();
|
|
1926
|
-
// If we have both perfStart and runStart anchors then #lastUpdateTs is likely a perf.now() value.
|
|
1927
|
-
if (
|
|
1928
|
-
this.#perfStart != null &&
|
|
1929
|
-
typeof globalThis.performance?.now === 'function' &&
|
|
1930
|
-
this.#runStartTs != null
|
|
1931
|
-
) {
|
|
1932
|
-
return this.#runStartTs + (this.#lastUpdateTs - this.#perfStart);
|
|
1933
|
-
}
|
|
1934
|
-
// Otherwise #lastUpdateTs should already be a wall-clock timestamp.
|
|
1935
|
-
return this.#lastUpdateTs;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
/**
|
|
1939
|
-
* Print the static top frame (dashboard title header) once at construction / first redraw.
|
|
1940
|
-
*
|
|
1941
|
-
* Educational focus:
|
|
1942
|
-
* - Demonstrates consistent frame construction (symmetric width) using shared constants.
|
|
1943
|
-
* - Shows explicit centering math for a colored title while measuring width from an uncolored template.
|
|
1944
|
-
* - Avoids ad‑hoc IIFEs: clearer sequential steps improve readability for newcomers.
|
|
1945
|
-
*
|
|
1946
|
-
* Steps:
|
|
1947
|
-
* 1. Emit a solid single‑line top border (full inner width).
|
|
1948
|
-
* 2. Emit a bridge line (visual taper) using preconfigured characters.
|
|
1949
|
-
* 3. Center and print the title "ASCII maze" with color accents, preserving frame alignment.
|
|
1950
|
-
* 4. Emit a lower bridge line to transition into evolving content sections.
|
|
1951
|
-
*
|
|
1952
|
-
* Centering approach:
|
|
1953
|
-
* - We compute visible width using an uncolored template string (box glyph + spaces + raw title + trailing glyph).
|
|
1954
|
-
* - Remaining horizontal space is split; a slight left‑bias (ceil on left) improves stability with odd widths.
|
|
1955
|
-
* - ANSI color codes are injected only after padding is determined so they don't skew calculations.
|
|
1956
|
-
*
|
|
1957
|
-
* Performance notes:
|
|
1958
|
-
* - Only a handful of short-lived strings are created; cost is negligible (runs once per session).
|
|
1959
|
-
* - Uses direct `this.#logFn` calls (no intermediate array joins) to keep GC pressure minimal.
|
|
1960
|
-
*
|
|
1961
|
-
* Determinism: Pure formatting (no randomness). Reentrancy not required (idempotent semantics acceptable).
|
|
1962
|
-
* Edge cases: If the frame width shrinks below the label length, padding clamps to zero and label is still printed.
|
|
1963
|
-
*
|
|
1964
|
-
* @example
|
|
1965
|
-
* (dashboard as any)["#printTopFrame"](); // Emits header block
|
|
1966
|
-
*/
|
|
1967
|
-
#printTopFrame(): void {
|
|
1968
|
-
// Local alias for readability
|
|
1969
|
-
const innerWidth = DashboardManager.FRAME_INNER_WIDTH;
|
|
1970
|
-
|
|
1971
|
-
// Step 1: Solid top border line
|
|
1972
|
-
this.#logFn(
|
|
1973
|
-
`${colors.blueCore}╔${NetworkVisualization.pad(
|
|
1974
|
-
DashboardManager.#FRAME_SINGLE_LINE_CHAR,
|
|
1975
|
-
innerWidth,
|
|
1976
|
-
DashboardManager.#FRAME_SINGLE_LINE_CHAR,
|
|
1977
|
-
)}╗${colors.reset}`,
|
|
1978
|
-
);
|
|
1979
|
-
|
|
1980
|
-
// Step 2: Upper bridge line (visual accent)
|
|
1981
|
-
this.#logFn(
|
|
1982
|
-
`${colors.blueCore}╚${NetworkVisualization.pad(
|
|
1983
|
-
DashboardManager.#FRAME_BRIDGE_TOP,
|
|
1984
|
-
innerWidth,
|
|
1985
|
-
DashboardManager.#FRAME_SINGLE_LINE_CHAR,
|
|
1986
|
-
)}╝${colors.reset}`,
|
|
1987
|
-
);
|
|
1988
|
-
|
|
1989
|
-
// Step 3: Centered colored title line
|
|
1990
|
-
const uncoloredTemplate = '║ ASCII maze ║'; // Used only for visible width calculation
|
|
1991
|
-
const templateLength = uncoloredTemplate.length;
|
|
1992
|
-
const remainingSpace = innerWidth - templateLength;
|
|
1993
|
-
const leftPaddingCount = Math.max(0, Math.ceil(remainingSpace / 2)) + 1; // slight left bias (mirrors prior behavior)
|
|
1994
|
-
const rightPaddingCount = Math.max(0, remainingSpace - leftPaddingCount);
|
|
1995
|
-
const coloredTitleSegment = `║ ${colors.neonYellow}ASCII maze${colors.blueCore} ║`;
|
|
1996
|
-
const centeredTitleLine = `${colors.blueCore}${' '.repeat(
|
|
1997
|
-
leftPaddingCount,
|
|
1998
|
-
)}${coloredTitleSegment}${' '.repeat(rightPaddingCount)}${colors.reset}`;
|
|
1999
|
-
this.#logFn(centeredTitleLine);
|
|
2000
|
-
|
|
2001
|
-
// Step 4: Lower bridge line framing transition to evolving section
|
|
2002
|
-
this.#logFn(
|
|
2003
|
-
`${colors.blueCore}╔${NetworkVisualization.pad(
|
|
2004
|
-
DashboardManager.#FRAME_BRIDGE_BOTTOM,
|
|
2005
|
-
innerWidth,
|
|
2006
|
-
DashboardManager.#FRAME_SINGLE_LINE_CHAR,
|
|
2007
|
-
)}╗${colors.reset}`,
|
|
2008
|
-
);
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
/** Orchestrate printing of evolving section (network + maze + stats + progress). */
|
|
2012
|
-
#printCurrentBestSection(currentMaze: string[]): void {
|
|
2013
|
-
const generation = this.#currentBest!.generation;
|
|
2014
|
-
// Section delim lines
|
|
2015
|
-
const sectionLine = DashboardManager.#EVOLVING_SECTION_LINE;
|
|
2016
|
-
this.#logFn(
|
|
2017
|
-
`${colors.blueCore}╠${NetworkVisualization.pad(
|
|
2018
|
-
sectionLine,
|
|
2019
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
2020
|
-
'═',
|
|
2021
|
-
)}${colors.blueCore}╣${colors.reset}`,
|
|
2022
|
-
);
|
|
2023
|
-
this.#logFn(
|
|
2024
|
-
`${colors.blueCore}║${NetworkVisualization.pad(
|
|
2025
|
-
`${colors.orangeNeon}EVOLVING (GEN ${generation})`,
|
|
2026
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
2027
|
-
' ',
|
|
2028
|
-
)}${colors.blueCore}║${colors.reset}`,
|
|
2029
|
-
);
|
|
2030
|
-
this.#logFn(
|
|
2031
|
-
`${colors.blueCore}╠${NetworkVisualization.pad(
|
|
2032
|
-
sectionLine,
|
|
2033
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
2034
|
-
'═',
|
|
2035
|
-
)}${colors.blueCore}╣${colors.reset}`,
|
|
2036
|
-
);
|
|
2037
|
-
this.#logBlank();
|
|
2038
|
-
this.#printNetworkSummary();
|
|
2039
|
-
this.#printLiveMaze(currentMaze);
|
|
2040
|
-
this.#printLiveStats(currentMaze);
|
|
2041
|
-
this.#printProgressBar();
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
/** Print network summary visualization. */
|
|
2045
|
-
#printNetworkSummary(): void {
|
|
2046
|
-
this.#logBlank();
|
|
2047
|
-
this.#logFn(
|
|
2048
|
-
NetworkVisualization.visualizeNetworkSummary(this.#currentBest!.network),
|
|
2049
|
-
);
|
|
2050
|
-
this.#logBlank();
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
/**
|
|
2054
|
-
* Render (and frame) the live maze for the current best candidate.
|
|
2055
|
-
*
|
|
2056
|
-
* Educational focus:
|
|
2057
|
-
* - Demonstrates safe use of `Array.prototype.at(-1)` for final coordinate extraction.
|
|
2058
|
-
* - Streams framed rows directly to the logger (avoids building one large joined string).
|
|
2059
|
-
* - Normalizes flexible visualization return types (string or string[]) into a unified iteration path.
|
|
2060
|
-
*
|
|
2061
|
-
* Steps:
|
|
2062
|
-
* 1. Resolve the agent's last path position (fallback `[0,0]` if path absent) for end‑marker highlighting.
|
|
2063
|
-
* 2. Ask `MazeVisualization.visualizeMaze` for a textual representation containing the path overlay.
|
|
2064
|
-
* 3. Normalize the result into an array of raw row strings (split on newlines when a single string is returned).
|
|
2065
|
-
* 4. For each row, pad to the fixed frame width and emit a framed line (borders + color) via `#logFn`.
|
|
2066
|
-
* 5. Surround the block with blank spacer lines for visual separation from adjacent sections.
|
|
2067
|
-
*
|
|
2068
|
-
* Performance notes:
|
|
2069
|
-
* - Avoids intermediate `.map().join()` allocation; writes each row immediately (lower peak memory for large mazes).
|
|
2070
|
-
* - Uses a local `innerWidth` alias to prevent repeated static property lookups in the hot loop.
|
|
2071
|
-
* - Only allocates one padded string per row (the framing template is assembled inline).
|
|
2072
|
-
*
|
|
2073
|
-
* Determinism: Pure formatting based on current maze + candidate path (no randomness).
|
|
2074
|
-
* Reentrancy: Not designed for concurrent invocation but method is self‑contained (no shared scratch mutation).
|
|
2075
|
-
* Edge cases:
|
|
2076
|
-
* - Empty visualization yields just spacer lines.
|
|
2077
|
-
* - Extremely long rows are hard-clipped visually by frame padding (consistent with rest of dashboard design).
|
|
2078
|
-
*
|
|
2079
|
-
* @param currentMaze Current maze layout (array of row strings) being evolved.
|
|
2080
|
-
*/
|
|
2081
|
-
#printLiveMaze(currentMaze: string[]): void {
|
|
2082
|
-
// Step 1: Determine last path coordinate (agent end position)
|
|
2083
|
-
const pathCoordinates = this.#currentBest!.result.path as readonly [
|
|
2084
|
-
number,
|
|
2085
|
-
number,
|
|
2086
|
-
][];
|
|
2087
|
-
const endOfPathPosition = pathCoordinates?.at(-1) ?? [0, 0];
|
|
2088
|
-
|
|
2089
|
-
// Step 2: Obtain visualization (may be string or string[])
|
|
2090
|
-
const rawVisualization = MazeVisualization.visualizeMaze(
|
|
2091
|
-
currentMaze,
|
|
2092
|
-
endOfPathPosition as readonly [number, number],
|
|
2093
|
-
pathCoordinates,
|
|
2094
|
-
);
|
|
2095
|
-
|
|
2096
|
-
// Step 3: Normalize to array of lines
|
|
2097
|
-
const visualizationLines: readonly string[] = Array.isArray(
|
|
2098
|
-
rawVisualization,
|
|
2099
|
-
)
|
|
2100
|
-
? rawVisualization
|
|
2101
|
-
: rawVisualization.split('\n');
|
|
2102
|
-
|
|
2103
|
-
// Step 4: Emit framed & padded lines
|
|
2104
|
-
const innerWidth = DashboardManager.FRAME_INNER_WIDTH;
|
|
2105
|
-
this.#logBlank(); // leading spacer (Step 5a)
|
|
2106
|
-
for (const unpaddedRow of visualizationLines) {
|
|
2107
|
-
const paddedRow = NetworkVisualization.pad(unpaddedRow, innerWidth, ' ');
|
|
2108
|
-
this.#logFn(
|
|
2109
|
-
`${colors.blueCore}║${paddedRow}${colors.blueCore}║${colors.reset}`,
|
|
2110
|
-
);
|
|
2111
|
-
}
|
|
2112
|
-
this.#logBlank(); // trailing spacer (Step 5b)
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
static #INT32_SCRATCH_POOL: Int32Array[] = [];
|
|
2116
|
-
|
|
2117
|
-
/**
|
|
2118
|
-
* Render the live statistics block for the current best candidate.
|
|
2119
|
-
*
|
|
2120
|
-
* Enhancements over the original:
|
|
2121
|
-
* - Provides a concise JSDoc with parameter & example usage.
|
|
2122
|
-
* - Uses a small Int32Array pooling strategy for temporary numeric scratch space to reduce
|
|
2123
|
-
* short-lived allocation churn during frequent redraws.
|
|
2124
|
-
* - Employs descriptive local variable names and step-level inline comments for clarity.
|
|
2125
|
-
*
|
|
2126
|
-
* Steps:
|
|
2127
|
-
* 1. Guard and emit a small spacer when no current best candidate exists.
|
|
2128
|
-
* 2. Rent a temporary typed-array buffer to hold derived numeric summary values.
|
|
2129
|
-
* 3. Populate the buffer with fitness, steps, and progress (scaled where appropriate).
|
|
2130
|
-
* 4. Emit a small, framed summary via existing `#formatStat` helper to preserve dashboard styling.
|
|
2131
|
-
* 5. Delegate the detailed printing to `MazeVisualization.printMazeStats` (keeps single-responsibility).
|
|
2132
|
-
* 6. Return the rented buffer to the internal pool and emit a trailing spacer.
|
|
2133
|
-
*
|
|
2134
|
-
* @param currentMaze Current maze layout used to compute/print maze-specific stats.
|
|
2135
|
-
* @example
|
|
2136
|
-
* // invoked internally by `update()` during redraw
|
|
2137
|
-
* (dashboard as any)["#printLiveStats"](maze);
|
|
2138
|
-
*/
|
|
2139
|
-
#printLiveStats(currentMaze: string[]): void {
|
|
2140
|
-
// Step 1: Leading spacer for visual separation
|
|
2141
|
-
this.#logBlank();
|
|
2142
|
-
|
|
2143
|
-
// Defensive guard: if there's no current best candidate, just emit spacer and exit.
|
|
2144
|
-
const currentBestCandidate = this.#currentBest;
|
|
2145
|
-
if (!currentBestCandidate) {
|
|
2146
|
-
this.#logBlank();
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// Helper: rent a typed Int32Array of requested length from pool or allocate new.
|
|
2151
|
-
const rentInt32 = (requestedLength: number): Int32Array => {
|
|
2152
|
-
const pooled = DashboardManager.#INT32_SCRATCH_POOL.pop();
|
|
2153
|
-
if (pooled && pooled.length >= requestedLength)
|
|
2154
|
-
return pooled.subarray(0, requestedLength) as Int32Array;
|
|
2155
|
-
return new Int32Array(requestedLength);
|
|
2156
|
-
};
|
|
2157
|
-
|
|
2158
|
-
// Helper: return buffer to pool (clear view references by pushing the underlying buffer view).
|
|
2159
|
-
const releaseInt32 = (buffer: Int32Array) => {
|
|
2160
|
-
// Keep pool bounded to avoid unbounded memory growth.
|
|
2161
|
-
if (DashboardManager.#INT32_SCRATCH_POOL.length < 8) {
|
|
2162
|
-
DashboardManager.#INT32_SCRATCH_POOL.push(buffer);
|
|
2163
|
-
}
|
|
2164
|
-
};
|
|
2165
|
-
|
|
2166
|
-
// Step 2: Rent a small scratch buffer for numeric summaries: [fitnessScaled, steps, progressPct]
|
|
2167
|
-
const scratch = rentInt32(3);
|
|
2168
|
-
|
|
2169
|
-
// Step 3: Populate numeric summary values defensively.
|
|
2170
|
-
const reportedFitness = currentBestCandidate.result?.fitness;
|
|
2171
|
-
scratch[0] =
|
|
2172
|
-
typeof reportedFitness === 'number' && Number.isFinite(reportedFitness)
|
|
2173
|
-
? Math.round(reportedFitness * 100)
|
|
2174
|
-
: 0; // fitness * 100 as integer
|
|
2175
|
-
const reportedSteps = Number(currentBestCandidate.result?.steps ?? 0);
|
|
2176
|
-
scratch[1] = Number.isFinite(reportedSteps) ? reportedSteps : 0;
|
|
2177
|
-
const reportedProgress = Number(currentBestCandidate.result?.progress ?? 0);
|
|
2178
|
-
scratch[2] = Number.isFinite(reportedProgress)
|
|
2179
|
-
? Math.round(reportedProgress * 100)
|
|
2180
|
-
: 0; // percent
|
|
2181
|
-
|
|
2182
|
-
// Step 4: Emit a compact framed summary using existing formatting helper to keep consistent style.
|
|
2183
|
-
// We convert scaled integers back into user-friendly strings for display.
|
|
2184
|
-
const formattedFitness = (scratch[0] / 100).toFixed(2);
|
|
2185
|
-
const formattedSteps = `${scratch[1]}`;
|
|
2186
|
-
const formattedProgress = `${scratch[2]}%`;
|
|
2187
|
-
|
|
2188
|
-
// Use the same label width as solved stats for alignment with other dashboard lines.
|
|
2189
|
-
const liveLabelWidth = DashboardManager.#SOLVED_LABEL_WIDTH;
|
|
2190
|
-
const liveStat = (label: string, value: string) =>
|
|
2191
|
-
this.#formatStat(
|
|
2192
|
-
label,
|
|
2193
|
-
value,
|
|
2194
|
-
colors.neonSilver,
|
|
2195
|
-
colors.cyanNeon,
|
|
2196
|
-
liveLabelWidth,
|
|
2197
|
-
);
|
|
2198
|
-
|
|
2199
|
-
this.#logFn(liveStat('Fitness', formattedFitness));
|
|
2200
|
-
this.#logFn(liveStat('Steps', formattedSteps));
|
|
2201
|
-
this.#logFn(liveStat('Progress', formattedProgress));
|
|
2202
|
-
|
|
2203
|
-
// Step 5: Delegate the more detailed maze/stat printing to the existing visualization helper.
|
|
2204
|
-
MazeVisualization.printMazeStats(
|
|
2205
|
-
currentBestCandidate,
|
|
2206
|
-
currentMaze,
|
|
2207
|
-
this.#logFn,
|
|
2208
|
-
);
|
|
2209
|
-
|
|
2210
|
-
// Step 6: Release typed-array scratch buffer back to pool and trailing spacer for readability.
|
|
2211
|
-
releaseInt32(scratch);
|
|
2212
|
-
this.#logBlank();
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
/**
|
|
2216
|
-
* Render the progress bar section for the currently tracked best candidate.
|
|
2217
|
-
*
|
|
2218
|
-
* Steps:
|
|
2219
|
-
* 1. Emit a top spacer framed line to separate the section from above content.
|
|
2220
|
-
* 2. Safely derive the current progress fraction (0..1) from the candidate result.
|
|
2221
|
-
* 3. Build the human-friendly progress bar text via `MazeVisualization.displayProgressBar`.
|
|
2222
|
-
* 4. Frame and emit the progress line using `NetworkVisualization.pad` to maintain consistent width.
|
|
2223
|
-
* 5. Emit a trailing spacer framed line.
|
|
2224
|
-
*
|
|
2225
|
-
* Implementation notes:
|
|
2226
|
-
* - Defensive numeric parsing avoids NaN/Infinity leaking into the display when data is malformed.
|
|
2227
|
-
* - Uses descriptive local names and a small helper `emitFrameBlank` for clarity.
|
|
2228
|
-
*
|
|
2229
|
-
* @example
|
|
2230
|
-
* (dashboard as any)["#printProgressBar"]();
|
|
2231
|
-
*/
|
|
2232
|
-
#printProgressBar(): void {
|
|
2233
|
-
// Helper to emit an empty framed row (kept as a local const for readability)
|
|
2234
|
-
const emitFrameBlank = () =>
|
|
2235
|
-
this.#logFn(
|
|
2236
|
-
`${colors.blueCore}║${NetworkVisualization.pad(
|
|
2237
|
-
' ',
|
|
2238
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
2239
|
-
' ',
|
|
2240
|
-
)}${colors.blueCore}║${colors.reset}`,
|
|
2241
|
-
);
|
|
2242
|
-
|
|
2243
|
-
// Step 1: Top spacer
|
|
2244
|
-
emitFrameBlank();
|
|
2245
|
-
|
|
2246
|
-
// Step 2: Defensive extraction of progress fraction from current best candidate
|
|
2247
|
-
const rawProgressValue = this.#currentBest?.result?.progress ?? 0;
|
|
2248
|
-
const parsedProgressFraction = Number(rawProgressValue);
|
|
2249
|
-
const safeProgressFraction = Number.isFinite(parsedProgressFraction)
|
|
2250
|
-
? parsedProgressFraction
|
|
2251
|
-
: 0;
|
|
2252
|
-
|
|
2253
|
-
// Step 3: Build readable progress bar text
|
|
2254
|
-
const humanReadableBar =
|
|
2255
|
-
MazeVisualization.displayProgressBar(safeProgressFraction);
|
|
2256
|
-
const progressLabel = `Progress to exit: ${humanReadableBar}`;
|
|
2257
|
-
|
|
2258
|
-
// Step 4: Frame and emit the progress label with consistent padding and color accents
|
|
2259
|
-
this.#logFn(
|
|
2260
|
-
`${colors.blueCore}║${NetworkVisualization.pad(
|
|
2261
|
-
' ' + colors.neonSilver + progressLabel + colors.reset,
|
|
2262
|
-
DashboardManager.FRAME_INNER_WIDTH,
|
|
2263
|
-
' ',
|
|
2264
|
-
)}${colors.blueCore}║${colors.reset}`,
|
|
2265
|
-
);
|
|
2266
|
-
|
|
2267
|
-
// Step 5: Trailing spacer
|
|
2268
|
-
emitFrameBlank();
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
reset(): void {
|
|
2272
|
-
this.#solvedMazes = [];
|
|
2273
|
-
this.#solvedMazeKeys.clear();
|
|
2274
|
-
this.#currentBest = null;
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
/**
|
|
2278
|
-
* Produce an immutable tail slice of a bounded numeric history buffer.
|
|
2279
|
-
*
|
|
2280
|
-
* Educational focus:
|
|
2281
|
-
* - Encapsulates export window logic so callers don't duplicate clamp arithmetic.
|
|
2282
|
-
* - Demonstrates a micro-optimized manual copy for partial slices while using
|
|
2283
|
-
* the native fast path (`Array.prototype.slice`) when returning the full buffer.
|
|
2284
|
-
* - Adds defensive guards for null / non-array input (returns empty array) to simplify callers.
|
|
2285
|
-
*
|
|
2286
|
-
* Steps:
|
|
2287
|
-
* 1. Guard: if the provided reference is not a non-empty array, return a new empty array.
|
|
2288
|
-
* 2. Compute the starting index for the export window (clamped to 0).
|
|
2289
|
-
* 3. If the window spans the entire history, return a shallow copy via `.slice()` (fast path).
|
|
2290
|
-
* 4. Allocate an output array sized exactly to the window length.
|
|
2291
|
-
* 5. Manually copy values (forward loop) to avoid creating an intermediate subarray before clone.
|
|
2292
|
-
* 6. Return the populated tail slice (caller receives an independent array).
|
|
2293
|
-
*
|
|
2294
|
-
* Complexity:
|
|
2295
|
-
* - Let N = history length, W = export window size (<= HISTORY_EXPORT_WINDOW).
|
|
2296
|
-
* - Computation: O(min(N, W)) element copies.
|
|
2297
|
-
* - Memory: O(min(N, W)) for the returned array.
|
|
2298
|
-
*
|
|
2299
|
-
* Performance notes:
|
|
2300
|
-
* - Manual copy avoids constructing a temporary array then cloning it; though engines optimize slice well,
|
|
2301
|
-
* the explicit loop keeps intent clear and allows descriptive index naming for style compliance.
|
|
2302
|
-
* - Uses descriptive loop index (`offsetIndex`) instead of a terse variable to satisfy style guidelines.
|
|
2303
|
-
*
|
|
2304
|
-
* Determinism: Pure function of input array contents and static window constant.
|
|
2305
|
-
* Reentrancy: Safe (no shared mutable state). Input array is never mutated.
|
|
2306
|
-
* Edge cases:
|
|
2307
|
-
* - Null / undefined / non-array -> returns [].
|
|
2308
|
-
* - Empty array -> returns [].
|
|
2309
|
-
* - HISTORY_EXPORT_WINDOW >= history length -> returns shallow clone of entire history.
|
|
2310
|
-
*
|
|
2311
|
-
* @param history Source numeric history buffer (may be longer than export window).
|
|
2312
|
-
* @returns New array containing up to `HISTORY_EXPORT_WINDOW` most recent samples (oldest first inside the window).
|
|
2313
|
-
*/
|
|
2314
|
-
#sliceHistoryForExport(history: number[] | undefined | null): number[] {
|
|
2315
|
-
// Step 1: Defensive null / type / emptiness guard
|
|
2316
|
-
if (!Array.isArray(history) || !history.length) return [];
|
|
2317
|
-
|
|
2318
|
-
// Step 2: Compute window start index
|
|
2319
|
-
const startIndex = Math.max(
|
|
2320
|
-
0,
|
|
2321
|
-
history.length - DashboardManager.#HISTORY_EXPORT_WINDOW,
|
|
2322
|
-
);
|
|
2323
|
-
|
|
2324
|
-
// Step 3: Full-buffer fast path (return shallow clone)
|
|
2325
|
-
if (startIndex === 0) return history.slice();
|
|
2326
|
-
|
|
2327
|
-
// Step 4: Allocate result sized to window length
|
|
2328
|
-
const windowLength = history.length - startIndex;
|
|
2329
|
-
const windowSlice = new Array<number>(windowLength);
|
|
2330
|
-
|
|
2331
|
-
// Step 5: Manual forward copy
|
|
2332
|
-
for (let offsetIndex = 0; offsetIndex < windowLength; offsetIndex++) {
|
|
2333
|
-
windowSlice[offsetIndex] = history[startIndex + offsetIndex];
|
|
2334
|
-
}
|
|
2335
8
|
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
9
|
+
export { DashboardManager } from './dashboardManager/dashboardManager';
|
|
10
|
+
export type {
|
|
11
|
+
AsciiMazeDetailedStats,
|
|
12
|
+
AsciiMazeTelemetrySnapshot,
|
|
13
|
+
DashboardPresentationAdapter,
|
|
14
|
+
DashboardTelemetry,
|
|
15
|
+
DashboardTelemetryPayload,
|
|
16
|
+
RuntimeDashboardManager,
|
|
17
|
+
} from './dashboardManager/dashboardManager.types';
|