Make ProtoEvo runnable on modern JDKs + usability and NN fixes#7
Open
ApocalypseGamer1 wants to merge 3 commits into
Open
Make ProtoEvo runnable on modern JDKs + usability and NN fixes#7ApocalypseGamer1 wants to merge 3 commits into
ApocalypseGamer1 wants to merge 3 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: lowersourceCompatibilityfrom 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=permitfromjvmArgs. Both flags were removed in JDK 17+; their presence prevented launch on any modern JVM.int * float(int filter_radius = 8; ... filter_radius * skipX), which strict drivers reject — the game crashed at first sim creation. Promotefilter_radiustofloat:assets/shaders/pause/fragment.glslassets/shaders/chemical/fragment.glslassets/shaders/blur/fragment.glslrun.batso the project can be launched without manual PATH setup.Stability fixes (real bugs)
PlotGridinfinite loop — freezes the entire windowStatsGraphsScreen.refreshPlotcallssetMajorTicks(range/5, ...). On a fresh sim with one or two stat snapshots, range is 0, so the major-tick step is 0. ThendrawAxisdoes:x += 0never advances. The render thread loops forever insideGlyphLayout.calculateWidths, allocating tick-label glyph buffers, and the LWJGL window goes "Not Responding". Confirmed withjhsdb jstack—mainwas stuck inPlotGrid.drawAxisXTickLabel. Fix:<= 0→1.0).Simulation.runbusy-waitwhile (simulate) { if (paused) continue; ... }pinned a full CPU core whenever paused, starving the render thread. Replaced withThread.sleep(20)while paused.REPL.runnull-line spambufferRead.readLine()returnsnullwhen stdin closes (e.g. launched viajavaw). The old code didline.equals("\n")→ NPE → caught generically → tight loop re-printingCannot 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: whentimeDilation > 1, run that many physics substeps per render frame at the original safedt, instead of multiplyingdtdirectly. 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 (steps0.0625, 0.125, ..., 32, 64)[— 2× slower\— reset to1.0×Current speed is printed to console.
Performance toggles
EnvironmentRenderernow exposesrenderChemicals/renderShadowsflags. 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. NewSimulationScreen.toggleLowDetailMode()flips both together.F10— toggle low-detail (both)F11— toggle chemicals onlyhelp_icon.pngas a stand-in — feel free to swap to a better icon).Neural-network mutation fix (high-impact correctness fix)
SynapseGene.cloneWithMutationpreviously did a full re-roll on every weight mutation: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 afternetwork.tick()inGeneExpressionFunction.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 returning0) using the standard NEAT compatibility distance:c1*E/N + c2*D/N + c3*Wbar.New
GeneticClustersScreenis 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 finalconstants 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
PlotGridbug above, but the simpler implementation is also less surprising.Notes for reviewers
🤖 Generated with Claude Code