Skip to content

Make ProtoEvo runnable on modern JDKs + usability and NN fixes#7

Open
ApocalypseGamer1 wants to merge 3 commits into
DylanCope:mainfrom
ApocalypseGamer1:main
Open

Make ProtoEvo runnable on modern JDKs + usability and NN fixes#7
ApocalypseGamer1 wants to merge 3 commits into
DylanCope:mainfrom
ApocalypseGamer1:main

Conversation

@ApocalypseGamer1
Copy link
Copy Markdown

Summary

This PR contains a batch of fixes I made to get the project building and running on a fresh modern setup, plus a few quality-of-life and correctness fixes I hit along the way. All changes preserve existing behavior at default settings; new toggles default off and existing UI is unchanged unless you press a new hotkey or click the new button.

Build / launch fixes (without these, the project does not run at all on a fresh modern JDK)

  • core/build.gradle, desktop/build.gradle: lower sourceCompatibility from 23 to 21. JDK 21 is the latest installable LTS via winget; few users have JDK 23 lying around.
  • desktop/build.gradle: remove --add-modules jdk.incubator.foreign --illegal-access=permit from jvmArgs. Both flags were removed in JDK 17+; their presence prevented launch on any modern JVM.
  • Three GLSL fragment shaders mixed int * float (int filter_radius = 8; ... filter_radius * skipX), which strict drivers reject — the game crashed at first sim creation. Promote filter_radius to float:
    • assets/shaders/pause/fragment.glsl
    • assets/shaders/chemical/fragment.glsl
    • assets/shaders/blur/fragment.glsl
  • Added a run.bat so the project can be launched without manual PATH setup.

Stability fixes (real bugs)

PlotGrid infinite loop — freezes the entire window

StatsGraphsScreen.refreshPlot calls setMajorTicks(range/5, ...). On a fresh sim with one or two stat snapshots, range is 0, so the major-tick step is 0. Then drawAxis does:

for (float x = 0; x <= xMax; x += xMajorTick) { ... }

x += 0 never advances. The render thread loops forever inside GlyphLayout.calculateWidths, allocating tick-label glyph buffers, and the LWJGL window goes "Not Responding". Confirmed with jhsdb jstackmain was stuck in PlotGrid.drawAxisXTickLabel. Fix:

  • Sanitize tick steps at the setters (non-finite or <= 01.0).
  • Hard cap of 200 ticks per axis loop as a second line of defense.

Simulation.run busy-wait

while (simulate) { if (paused) continue; ... } pinned a full CPU core whenever paused, starving the render thread. Replaced with Thread.sleep(20) while paused.

REPL.run null-line spam

bufferRead.readLine() returns null when stdin closes (e.g. launched via javaw). The old code did line.equals("\n") → NPE → caught generically → tight loop re-printing Cannot invoke "String.equals(Object)" because "line" is null. Now it cleanly exits the REPL on EOF and skips empty lines.

Time controls — variable speed without breaking physics

Simulation.update: when timeDilation > 1, run that many physics substeps per render frame at the original safe dt, instead of multiplying dt directly. Lets the user run the sim at e.g. 16× without protozoa tunneling through rocks. Capped at 64 substeps/frame to prevent the thread locking up; fractional values handled with a partial last step.

New hotkeys (SimulationKeyboardControls):

  • ] — 2× faster (steps 0.0625, 0.125, ..., 32, 64)
  • [ — 2× slower
  • \ — reset to 1.0×

Current speed is printed to console.

Performance toggles

EnvironmentRenderer now exposes renderChemicals / renderShadows flags. The chemical-field overlay (ChemicalsRenderer.render) and the baked shadow texture (LightRenderer.render) are gated on them — both are screen-filling fragment passes that dominate GPU cost on weak hardware. New SimulationScreen.toggleLowDetailMode() flips both together.

  • F10 — toggle low-detail (both)
  • F11 — toggle chemicals only
  • New top-bar button (left side, help_icon.png as a stand-in — feel free to swap to a better icon).

Neural-network mutation fix (high-impact correctness fix)

SynapseGene.cloneWithMutation previously did a full re-roll on every weight mutation:

newGene.weight = randomInitialWeight();   // uniform [-1, 1]

This makes fine-tuning impossible — every mutation is a discontinuous jump, so a population can never converge on good weights. Standard NEAT explicitly recommends the opposite: 90% small-Gaussian perturbation, ~10% full re-roll. Implemented exactly that, with weights clamped to [-4, 4] to prevent runaway random walk.

Also removed a redundant second setGRNInputs() call after network.tick() in GeneExpressionFunction.tick — the second call was wasted work and got overwritten on the next tick.

Genetic clusters viewer (new feature)

Implemented NetworkGenome.distance() (was a stub returning 0) using the standard NEAT compatibility distance: c1*E/N + c2*D/N + c3*Wbar.

New GeneticClustersScreen is a snapshot view of living protozoa, grouped by greedy speciation against the distance threshold (1.5). Each cluster row shows: color swatch (mean cell color), population, avg generation, avg energy, avg health, avg age. Bounded: 250 cells sampled, 40 clusters tracked, 30 displayed.

Reachable from the simulation top-bar (right side, multicell icon). Clusters and tick threshold are both private static final constants near the top of the file — easy to tune.

The screen is intentionally minimal (no FBO, no own SpriteBatch, no own ShaderProgram, no worker thread) after an earlier fancier version sometimes hung — that turned out to be the unrelated PlotGrid bug above, but the simpler implementation is also less surprising.

Notes for reviewers

  • Saves directory layout, save format, and existing settings YAML are unchanged.
  • All new UI is opt-in via hotkey or button; no existing controls are remapped.
  • I deliberately did not touch CUDA / JCuda paths. Project still builds + runs without CUDA installed.
  • Plant-evolution path still appears to crash separately (mentioned by the user mid-development); I did not investigate further. Not addressed here.

🤖 Generated with Claude Code

ApocalypseGamer1 and others added 3 commits May 7, 2026 23:02
Build / run
- Lower sourceCompatibility from 23 to 21 (matches widely-installable LTS).
- Strip JVM args removed in JDK 17+ (--add-modules jdk.incubator.foreign,
  --illegal-access=permit) which prevented launch on any modern JVM.
- Fix three GLSL fragment shaders that mixed int * float (rejected by strict
  drivers, crashing on first sim creation): shaders/pause, shaders/chemical,
  shaders/blur. Promote `int filter_radius` to float.
- Add run.bat convenience script that pins JDK 21 + Gradle on PATH.

Stability
- PlotGrid: setMajorTicks / setMinorTicks were called with `range/5`, which
  is 0 when only one stat snapshot exists. The drawAxis loops then ran
  `for (x = 0; x <= xMax; x += 0)` — an infinite loop spinning inside
  GlyphLayout.calculateWidths and freezing the entire window. Sanitize
  non-finite/<=0 ticks at the setter and add a 200-iteration safety cap
  in the draw loops.
- Simulation.run: replace `if (paused) continue;` busy-wait with a 20ms
  sleep. Pinned a CPU core whenever paused, starving the render thread
  enough that opening a heavy modal screen could mark the LWJGL window
  "not responding".
- REPL.run: handle null line from BufferedReader (EOF / no console). The
  old code NPE'd on line.equals("\n") and tight-looped printing the same
  exception forever when launched without a console.

Time controls
- Simulation.update: when timeDilation > 1, run that many physics substeps
  per render frame at the original safe dt instead of multiplying dt
  directly. Lets the sim run N× faster without breaking physics; capped
  at 64 substeps/frame as a safety. Fractional values use a partial last
  step.
- New hotkeys in SimulationKeyboardControls:
    `]`  -> 2× faster (steps 0.0625 ... 64)
    `[`  -> 2× slower
    `\`  -> reset to 1.0×
  Current speed printed to console.

Performance toggles
- EnvironmentRenderer: add `renderChemicals` and `renderShadows` flags
  with public getters/setters; gate the chemical-field overlay and the
  baked-shadow LightRenderer pass on them. Both are screen-filling
  fragment passes and dominate GPU time.
- New SimulationScreen.toggleLowDetailMode() flips both flags together.
- Hotkeys: F10 toggles low-detail (both), F11 toggles chemicals only.
- New top-bar button (left side) to toggle low-detail mode.

Neural-network mutation fix (high-impact, real bug)
- SynapseGene.cloneWithMutation: previous behavior fully re-rolled weights
  to uniform [-1, 1] on every mutation, making fine-tuning impossible —
  every mutation was a discontinuous jump, so a population could never
  converge on good weights. Switch to standard NEAT-style 90% small
  Gaussian perturb (sigma=0.15) + 10% full re-roll, with weights bounded
  to [-4, 4]. Lets evolved networks actually improve over generations.
- GeneExpressionFunction.tick: remove redundant second setGRNInputs() call
  after the network tick — it was wasted work and immediately overwritten
  on the next tick anyway.

Genetic clusters viewer
- Implement NetworkGenome.distance() (was a stub returning 0). Standard
  NEAT compatibility distance: c1*E/N + c2*D/N + c3*Wbar from excess /
  disjoint genes and matching-gene weight diff.
- New GeneticClustersScreen: snapshot of living protozoa grouped by
  greedy speciation against the distance threshold. Per cluster shows
  population, avg generation, avg energy, avg health, avg age, and a
  color swatch (mean cell color). Capped at 250 cells / 40 clusters
  / 30 displayed for bounded work. Reachable via a top-bar button on
  the simulation screen (right side, multicell icon).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Energy / feeding (real bugs found by playthrough)
- Food.addEnergy: was `mass += energy` (typo). Combined with the other
  bugs below, this added the energy parameter to the wrong field and
  let `getEnergy(m)` re-release `density × m` every digestion tick.
- Food.getEnergy: returned `energy + density*m` on every call without
  ever decrementing the stored field, so the same stored energy was
  released forever. Now releases proportional to mass extracted and
  consumes the field.
- Cell.eat: fresh Food chunks had `mass = extractedMass` from the
  constructor and then `addSimpleMass(extractedMass)` was called
  unconditionally — doubling mass on every fresh-chunk eat. Also,
  the engulfed cell's stored energy was *copied* to the food chunk
  without being depleted from the victim. Energy is now transferred
  rather than duplicated.
- Combined effect of the three bugs was the "infinite energy from
  eating own meat" loop the user observed: a 6-figure-multiplier
  positive feedback at meat density (3e5 J/kg).
- PhagocyticReceptor: engulf condition was `input > 0`, so cells
  with random/conservative weights had a working receptor and still
  starved next to plants. Switched to `input > -0.25` so a cell with
  a receptor defaults to engulfing; the GRN must actively suppress
  to inhibit.

Settings rebalance after fixing the bugs above
- plantEnergyDensity: 1e5 → 2e5 (compensates the doubled-mass bug
  removal that effectively halved real food yield).
- meatEnergyDensity: 3e5 → 6e5 (same 2× compensation, preserves
  ratio).
- cell.energyDecayRate: 0.05 → 0.025 (doubles starve half-life so
  cells survive long enough for foraging to evolve).
- cell.startingAvailableCellEnergy: 1 → 50 (cap is 500; old default
  spawned cells at 0.2% of capacity, effectively starving on
  spawn).
- chemicalExtractionPlantConversion: 1e-3 → 5e-3, meat 1e-4 → 5e-4
  (newborns are too small to engulf and need bigger drip-absorption
  yield to bootstrap to engulf size).

NN evolution improvements
- FloatTrait.cloneWithMutation: full re-roll on every mutation made
  fine-tuning impossible, same problem as the synapse-weight fix.
  Now 90% Gaussian perturbation (sigma = 10% of trait range), 10%
  full re-roll, clamped to [min, max]. Fixes "eyes never form" and
  similar rare-trait emergence issues.
- NetworkGenome.distance: was a stub returning 0. Implemented
  standard NEAT compatibility distance (c1·E/N + c2·D/N + c3·Wbar)
  used by the genetic-clusters viewer.
- GRNFactory.createNetworkGenome: hand-wires basic foraging reflex
  into newly created protozoan genomes:
    Plant Density Local → Cilia Thrust  (+2.0)
    Plant Gradient      → Cilia Turn    (-1.0)
  Without this, populations couldn't bootstrap a foraging policy
  before starving. The seed only modifies existing synapses
  (never appends) and is wrapped in try/catch so non-protozoan
  genomes silently no-op.
- New `Plant Density Local` regulator: counts plant *entities* in
  the cell's spatial-hash chunk. The pre-existing `Plant Density`
  signal sampled the chemical field at the cell's own position,
  but the cell consumes that chemical itself, reading ~0 even when
  surrounded by plants. The entity-count signal is ground truth.
- `Plant Gradient` rewritten: was sampled at 1.1×radius (inside the
  cell's own extraction footprint) and returned raw `ahead-behind`
  (saturated to 0 in dense clusters). Now samples at 4×radius and
  returns `(ahead-behind)/(ahead+behind)` so direction stays
  meaningful even when both samples are dense.

Homeostatic difficulty controller
- Simulation.homeostasisTick (every 5 sim seconds, default ON):
  computes `scale = sqrt(clamp(pop/target, 0.2, 2.5))` and adjusts
  energyDecayRate, plant/meat energy density, and starting energy
  by `× scale` or `÷ scale`. Tightens when pop is above target
  (preserves selection pressure), relaxes when below (avoids
  extinction before evolution gets a foothold).
- F7 toggles the controller; turning it off restores static
  defaults so disable actually disables.
- Logs `[homeostat] pop=X target=500 scale=Y decay=Z plantED=W`
  whenever scale moves ≥0.15 from last log.

Turbo mode
- F8 (or new top-bar button) toggles black-screen fast-forward.
  render() draws a clear, then runs simulation.update() in a tight
  12ms-budgeted loop and renders a small live line chart of
  protozoa + plant population sampled once per sim second.
- Console logs `[TURBO] sim=X protozoa=Y plants=Z` every 3 minutes
  OR when protozoa count changes ≥25%, so an overnight run leaves
  a record.

Genetic clusters viewer (already in PR DylanCope#7, refined)
- New top-bar button on the simulation screen opens a snapshot of
  living protozoa grouped by NEAT compatibility distance (greedy
  speciation against threshold 1.5). Each row shows population,
  avg generation, avg energy, avg health, avg age, and a color
  swatch (mean cell color).
- Stripped to a minimal screen (no FBO, no own SpriteBatch, no own
  ShaderProgram, no worker thread) after the fancier version
  trigged a window hang — root cause turned out to be the
  PlotGrid bug below, not this screen, but the simpler
  implementation is also less surprising.

Stability fixes
- PlotGrid: setMajorTicks/setMinorTicks were called with `range/5`,
  which is 0 when only one stat snapshot exists. The drawAxis
  loops then ran `for (x = 0; x <= xMax; x += 0)` — infinite loop
  spinning inside GlyphLayout.calculateWidths and freezing the
  whole window. Sanitize non-finite/<=0 ticks at the setter and
  hard-cap loop iterations at 200.
- Simulation.run: replaced `if (paused) continue;` busy-wait with
  a 20ms sleep. Pinned a CPU core whenever paused, starving the
  render thread enough that opening a heavy modal screen could
  mark the LWJGL window "not responding".

run.bat: tee output to run.log so a crash trace survives even if
the console window auto-closes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sim model
- Plants now actively rotate via a Rotate ControlVariable + low angular
  damping, so they can aim spike/photoreceptor at attackers.
- Per-cell phylogeny tracking (lineageId, parentId, LineageRecord on
  Environment); F4 toggles a real phylogenetic tree restricted to
  ancestors of currently-living protozoa with linear chains collapsed.
- Multi-layered Protozoan memory: 6 latches across fast/medium/slow
  α-blend layers, each visible in the NN viz.
- Per-lineage evolvable mutation rate multiplier on NetworkGenome,
  drifts log-Gaussian with bounded clamp, exposed in cell stats.
- Currents: divergence-free curl-of-potential flow field pushes cells
  around the world, exposed as env.currentStrength/scale/timeRate.

Homeostat
- Two PIDs running in parallel: protozoa target (default 250) and plant
  target (default 1000). Both use leaky integrals + raised
  MAX_LOG_DEVIATION + bumped Kd so they fine-tune via derivative
  rather than windup-clamp-and-overshoot.
- New consumption-side levers: chemicalExtractionFactor (cuts the
  plant-chemical drip food the old PID couldn't reach), per-plant
  splitRate (probabilistic so plants don't all burst on the same tick).
- Removed GAIN_CONTACT_DEATH lever (it ironically created more meat for
  the protozoa being starved). Plant population is now solely the
  plant PID's responsibility.
- New universal starvation damage on Cell: below 5% of energy cap, take
  health damage proportional to deficit. Without this, the energyDecay
  lever had no death pathway and the controller couldn't actually
  reduce population.
- REPL: new homeostat command (status / on|off / target / plants).

Performance
- HashSet-backed SpatialHash chunks (was ConcurrentSkipListSet) with
  O(1) size and a remove() so deaths are tracked incrementally;
  full chunks rebuild throttled to once every 0.5s.
- Cached Kryo instance.
- Default Kryo serializer switched to CompatibleFieldSerializer so
  future field additions don't break saves. (Old saves from before
  this change won't load.)
- SimulationHistory in-memory map capped at 500 snapshots.
- Chemical screenshot raster pack via setRGB instead of 1M
  fillRect(1,1) calls.
- Pixmap callback removed from ChemicalSolution; ChemicalsRenderer
  now bulk-copies the colour array into the pixmap on a dirty-bit
  path -- ~1M JNI calls/frame -> 0 on idle frames.
- Time-dilation step model rewritten: physics steps with stability
  ceiling that scales with td, chemicals batched once per render,
  plant/meat updates batched once per render at high td (linear-
  in-delta integration, accumulator on Environment).
- Box2DParticle sleep fast-path; cached fixture radius/damping writes;
  fixed endContact arg bug; removed pixmap-channel callback.

Plant + cell biology
- Plants tick surface-node attachments and report interaction range,
  enabling spike/photoreceptor to actually function on plants.
- PlantCell + EnvolvableCell get organelles + surface nodes via the
  Evolvable framework with plant evolution enabled by default.
- Protozoa Mate/Split desire are continuous probabilistic per-frame
  signals, not the old binary >0.5 thresholds (preserves the
  selection gradient).
- Spike + PhagocyticReceptor skip adhered partners so multicell
  cooperation is reachable evolutionarily.
- Box2DParticle SUFFOCATION death scoped to different-type and
  significantly larger -- previously, plant burst children
  suffocated each other on spawn (thousands of false deaths/sec).
- Plant default photosynthesis interval bumped 20x (was the dominant
  cost at high td; plants don't need fast cognition).

UI
- F4 lineage phylogeny overlay (replaces earlier bar-chart sketch).
- Removed the "terminal" top-bar button -- it called Gdx.app.exit()
  while sim continued, which crashed on the next chemical-diffusion
  step (no GL context). REPL is still on the launching terminal.
- New REPL DumpGenome command -- writes one or all protozoan genomes
  to a JSON next to the save folder for offline analysis.
- Speed key cap raised 64x -> 256x.

Save/load
- Diagnostic timing prints in Serialization.reloadEnvironment so
  any future load hang has an obvious culprit.
- Defensive null-checks in Environment.rebuildCurrentField for
  partially-loaded settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant