Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .cursor/rules/02-scientific-honesty.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ in its doc comment is rejected. Use the original paper where possible:

## 4. Constants are single-sourced and cited

Physical constants live once, as `pub const` in `accretion-core`, each tagged
with its source (CODATA 2018 / IAU). Do not redefine `G`, `c`, `M_sun`, etc.
anywhere else (not in `godot-ext`, not in a shader, not in GDScript).
Physical constants live once, in `accretion-core`, each tagged with its source.
Fundamentals (`G`, `c`, `M_sun`, `m_p`, `m_e`, `h`, `k_B`, `alpha`, ...) are
generated from astropy into `constants.rs` (CODATA / IAU). Composite constants
that are exact functions of fundamentals (`sigma_sb`, `sigma_T`) are DERIVED in
`derived.rs` with their formula + citation and pinned by a golden test — never
tabulated as a second independent copy (see rule 11). Do not redefine any of
them anywhere else (not in `godot-ext`, not in a shader, not in GDScript).

## 5. Hardcoded literature numbers are tech debt outside two places

Expand Down
28 changes: 25 additions & 3 deletions .cursor/rules/11-constants-provenance.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,34 @@ alwaysApply: true

# Constants provenance (invariant)

Two layers, with different rules:

## Fundamental constants (`constants.rs`) — generated, never derived

- `crates/accretion-core/src/constants.rs` is GENERATED by `scripts/gen_constants.py`
from `astropy.constants` (CGS). It is never edited by hand.
- It holds ONLY fundamentals (e.g. `G`, `C_LIGHT`, `M_SUN`, `M_P`, `M_E`, `H_PLANCK`,
`K_BOLTZMANN`, `ALPHA`). A constant that is an exact mathematical function of these
does NOT belong here — it is derived (below).
- Re-running the generator MUST produce a byte-identical file
(`git diff --exit-code` clean). A non-empty diff is a build failure.
- No physical-constant literal may appear in any `src/**/*.rs` outside `constants.rs`.
- The choice of any fundamental's value is not a decision; it is whatever astropy ships
at the pinned version. Do not substitute "textbook" values.

## Derived constants (`derived.rs`) — computed from fundamentals, with a golden test

- Composite constants (e.g. Stefan-Boltzmann `SIGMA_SB`, Thomson `SIGMA_T`) are
COMPUTED in `crates/accretion-core/src/derived.rs` from `constants.rs`, NOT tabulated.
Tabulating a second copy of a derivable quantity is forbidden — derive it.
- Each derived constant carries its closed-form formula and a primary-source citation
in its doc comment, and is pinned by a golden test in `tests/golden.rs` against the
astropy oracle (`scripts/gen_golden.py`), at the stated tolerance.
- ✅ `crate::derived::SIGMA_SB` (derived) ✅ `crate::constants::M_SUN` (fundamental)
❌ a hand-typed `5.67e-5` for sigma_sb anywhere.

## Both layers

- No physical-constant *literal* may appear in any `src/**/*.rs` outside `constants.rs`.
Derived values are built from named fundamentals and dimensionless math literals only.
Check: `! grep -rEn '[0-9]+\.?[0-9]*[eE][+-]?[0-9]+' crates/accretion-core/src --include='*.rs' | grep -v constants.rs`
- ✅ `crate::constants::M_SUN` ❌ `1.98892e33` (anywhere but constants.rs)
- The choice of any constant's value is not a decision; it is whatever astropy ships
at the pinned version. Do not substitute "textbook" values.
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,21 @@ jobs:

- uses: Swatinem/rust-cache@v2

- name: Install Godot 4.7 (headless smoke test)
run: |
sudo apt-get update
sudo apt-get install -y libfontconfig1 libglu1-mesa libx11-6 libxcursor1 libxinerama1 libxi6 libxrandr2
curl -L -o godot.zip \
"https://github.com/godotengine/godot/releases/download/4.7-stable/Godot_v4.7-stable_linux.x86_64.zip"
unzip -q godot.zip
chmod +x Godot_v4.7-stable_linux.x86_64
echo "GODOT_BIN=${{ github.workspace }}/Godot_v4.7-stable_linux.x86_64" >> "$GITHUB_ENV"

- name: Verify gdext artifact after build
run: |
make build
ls -la bin/
ldd bin/libgodot_ext.so

- name: Full gate
run: make check
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@

# Rust / Cargo build artifacts
/target/
rust_out

# bin/ holds Godot-loaded native libs (must NOT be gitignored — Godot skips
# ignored paths when resolving res://). Do not commit the binaries; make build
# creates them locally.
14 changes: 11 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
> automatically.

`accretion` is an accretion-disk survival/management game built like scientific
software: the physics is honest, every formula cites a primary source, and the
software: the physics is rigorous, every formula cites a primary source, and the
core is independently testable. It is the game-side companion to
[`ysSemanticSystems/BlackHoleResearch`](https://github.com/ysSemanticSystems/BlackHoleResearch),
and inherits its discipline — relocated from Python/Streamlit to Rust/Godot.
Expand Down Expand Up @@ -52,6 +52,14 @@ accretion/ # repo root (Godot 4.7 project lives here)
├── Cargo.toml # [workspace]
├── crates/
│ ├── accretion-core/ # pure physics lib (CGS), cargo-testable
│ │ └── src/
│ │ ├── constants.rs # fundamentals (generated)
│ │ ├── derived.rs # SIGMA_SB, SIGMA_T from fundamentals
│ │ ├── eddington.rs # L_Edd, lambda, mdot↔L
│ │ ├── kerr.rs # ISCO, efficiency, spin geometry
│ │ ├── disk.rs # Shakura–Sunyaev T(r)
│ │ ├── evolution.rs # mass/spin evolution, integrity
│ │ └── colorimetry.rs # Planck → CIE → sRGB
│ └── godot-ext/ # cdylib gdext binding — presentation only
├── accretion.gdextension # native lib wiring (api-4-6, loads under 4.7)
├── project.godot, icon.svg # Godot project
Expand Down Expand Up @@ -85,7 +93,7 @@ accretion/ # repo root (Godot 4.7 project lives here)

When publishing the repository, set the **About** description to:

> Accretion-disk survival game — honest Rust physics, Godot 4.7 HDR lensing. Companion to BlackHoleResearch.
> Accretion-disk survival game — first-principles Rust physics, Godot 4.7 HDR lensing. Companion to BlackHoleResearch.

Suggested **Topics:** `black-hole`, `accretion-disk`, `godot`, `rust`, `astrophysics`,
`shakura-sunyaev`, `gravitational-lensing`, `game`, `scientific-software`.
Expand All @@ -94,7 +102,7 @@ Enable **Issues** and **Discussions** if you want community feedback. CI badge i
README assumes the default branch is `main` and workflow file is
`.github/workflows/ci.yml`.

## Known deferred physics (be honest about it)
## Known deferred physics (stated plainly)

- `disk_temperature` uses the **bare** Shakura-Sunyaev form (`T ∝ r^(-3/4)`,
no inner-boundary factor), so there is no temperature peak / dark inner gap
Expand Down
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Run lifecycle and replay value.** Each run starts at Cyg X-1 scale with a
clear win (reach M87*) and lose (three super-Eddington disk disruptions).
Live score, persisted high score (`user://accretion_best.run`), end-of-run
overlay, and `R`/Enter restart. Challenge presets 1/2/3 start at different
mass/feed/spin. λ-safe Ṁ ceiling shown in the HUD.
- **Kerr spin-up from accretion.** `advance_spin` and ISCO specific angular
momentum (Bardeen-Press-Teukolsky 1972; King & Raine 2002); spin now evolves
during play, raising η and tightening the ISCO.
- **QPO hotspot animation.** Shader `qpo_phase_rate` driven from the Rust ISCO
orbital frequency so the inner disk flickers at a physically scaled rate.
- **Survival/progression loop.** Mass now evolves in (compressed) time via
`advance_mass` (`dM/dt = (1 - eta) Mdot`, Frank/King/Raine 2002); a disk-integrity
meter drains when super-Eddington and rebuilds when sub-Eddington, and reaching
zero triggers a disruption that blows the disk apart and resets the feed. Growth
reclassifies the hole (stellar → IMBH → SMBH) with milestone banners and a
next-goal progress bar.
- **New physics in `accretion-core`** (each cited + golden/identity tested):
`salpeter_time_s` (Salpeter 1964), `integrity_rate` (Eddington 1926 effective
gravity `1 - lambda`), `isco_specific_energy` / `efficiency_from_spin` (Bardeen-
Press-Teukolsky 1972; Thorne 1974), `orbital_frequency_hz` (BPT 1972 QPO scale).
- **Derived constants module** `derived.rs`: `SIGMA_SB = 2 pi^5 k_B^4 / (15 c^2 h^3)`
(exact under 2019 SI) and `SIGMA_T = (8 pi / 3)(alpha hbar / m_e c)^2`, computed
from fundamentals instead of tabulated, pinned by astropy-oracle golden tests.
- Cinematic camera: damped orbit/zoom, idle auto-orbit, intro dolly.
- Graphics driven from Rust: radial blackbody gradient (inner + outer color),
bounded HDR bloom tone-mapped from the inner-edge temperature, and spin-driven
inner-edge / event-horizon tightening; disk grows with mass class.
- **README.md** — public landing page with architecture, controls, quick start, CI badge.
- **LICENSE** (MPL-2.0), **CONTRIBUTING.md**, GitHub CI workflow, PR and issue templates.
- Astropy-oracle pipeline: `scripts/gen_constants.py` → `constants.rs`,
Expand All @@ -21,6 +48,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Modular `accretion-core` architecture.** Split the monolithic `lib.rs` into
focused modules (`eddington`, `kerr`, `disk`, `evolution`, `colorimetry`)
with crate-root re-exports preserving the public API. Added extensive unit tests
in each module, shared integration-test helpers, and a cross-module
`tests/integration.rs` suite.
- `constants.rs` now holds only FUNDAMENTAL constants (added `M_E`, `ALPHA`;
dropped tabulated `SIGMA_SB`/`SIGMA_T`); composites are derived in `derived.rs`.
Rule 11 / rule 02 updated for the two-layer (fundamental vs derived) provenance.
- Fixed the disk shader sampling an unassigned `disc_texture` (now a procedural
`NoiseTexture2D`) so the accretion disk renders.
- Reworded "honest" marketing phrasing to "rigorous / first-principles /
verifiable" in user-facing docs (the scientific-honesty rule and prime directive
are unchanged).
- Physical constants are generated from astropy (no hand-written M_sun).
- Golden tests use astropy oracle fixtures, not hand-pinned magnitudes.
- `compatibility_minimum = 4.6` matches `api-4-6` gdext feature.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to accretion

Thank you for helping build an honest accretion-disk game. The full operational
Thank you for helping build a rigorous accretion-disk game. The full operational
guide is **[AGENTS.md](AGENTS.md)** — read it before your first PR.

## Quick checklist
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ version = "0.1.0"
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/ysSemanticSystems/accretion"
description = "Accretion-disk survival game — honest Rust physics, Godot 4.7 HDR lensing"
description = "Accretion-disk survival game — first-principles Rust physics, Godot 4.7 HDR lensing"

[profile.dev]
opt-level = 1
Expand Down
36 changes: 27 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
.PHONY: check gen build test clippy fmt hooks
.PHONY: check gen build test clippy fmt hooks godot-smoke

GODOT_BIN ?= godot
# Godot loads res://target/debug/libgodot_ext.dylib — always build there.
CARGO_TARGET_DIR := $(CURDIR)/target
export CARGO_TARGET_DIR

# macOS default; override with GODOT_BIN=... or put `godot` on PATH.
GODOT_BIN ?= /Applications/Godot.app/Contents/MacOS/Godot
GODOT_FALLBACK := godot

hooks:
./scripts/setup-hooks.sh
Expand All @@ -10,7 +16,7 @@ gen:
python3 scripts/gen_golden.py

build:
cargo build
./scripts/build_godot_ext.sh

test:
cargo test --workspace
Expand All @@ -21,15 +27,27 @@ clippy:
fmt:
cargo fmt --check

godot-smoke: build
@set -e; \
GODOT=""; \
if [ -x "$(GODOT_BIN)" ]; then GODOT="$(GODOT_BIN)"; \
elif command -v $(GODOT_FALLBACK) >/dev/null 2>&1; then GODOT="$(GODOT_FALLBACK)"; \
else echo "ERROR: Godot not found. Install to /Applications/Godot.app or set GODOT_BIN."; exit 1; fi; \
case "$(uname -s)" in \
Darwin) test -f bin/libgodot_ext.dylib || { echo "ERROR: bin/libgodot_ext.dylib missing"; exit 1; } ;; \
Linux) test -f bin/libgodot_ext.so || { echo "ERROR: bin/libgodot_ext.so missing"; exit 1; } ;; \
esac; \
echo "Godot smoke test ($$GODOT)…"; \
if [ ! -f .godot/extension_list.cfg ]; then \
echo "Bootstrapping .godot/ (fresh clone — verifying GDExtensions)…"; \
"$$GODOT" --headless --path . -e --quit-after 1 >/dev/null 2>&1 || true; \
fi; \
"$$GODOT" --headless --path . res://scenes/GodotSmoke.tscn

check: gen hooks
git diff --exit-code crates/accretion-core/src/constants.rs
git diff --exit-code crates/accretion-core/tests/fixtures/golden.json
sh scripts/check_invariants.sh
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo build
@if command -v $(GODOT_BIN) >/dev/null 2>&1; then \
$(GODOT_BIN) --headless --quit --path . ; \
else \
echo "WARN: $(GODOT_BIN) not on PATH; skipping Godot headless load test"; \
fi
$(MAKE) godot-smoke
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,19 @@ cd accretion
./scripts/setup-hooks.sh # once per clone — AI-attribution strip hooks
pip install -r scripts/requirements.txt

make check # generators, invariants, test, clippy, build
make check # generators, invariants, test, clippy, build, Godot smoke
# Open the project in Godot 4.7 and run scenes/Main.tscn (F5)
```

If `make check` passes but Godot is not on your `PATH`, the headless load step
is skipped with a warning — run the scene manually in the editor.
**Before opening Godot**, run `make build` (or `make godot-smoke`). The native
library is copied to `bin/libgodot_ext.dylib` (macOS) or `bin/libgodot_ext.so`
(Linux) — that is what `accretion.gdextension` loads. Do not point Godot at a
stale `target/debug/` artifact from an old build.

`make godot-smoke` headlessly instantiates `BlackHole` and calls every Rust API
used by the game (`salpeter_time_s`, `advance_mass`, …). On macOS the Makefile
defaults to `/Applications/Godot.app/Contents/MacOS/Godot`; override with
`GODOT_BIN=...` if needed.

---

Expand All @@ -85,7 +92,7 @@ pressure blowout that will become the game's loss condition.

---

## Physics (honest scope)
## Physics (verifiable scope)

Every public function in `accretion-core` documents its primary source in the
doc comment. Current Slice 0 coverage:
Expand Down
24 changes: 11 additions & 13 deletions accretion.gdextension
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
; GDExtension manifest for the accretion game.
; The native library is built by `cargo build` (debug) / `cargo build --release`
; from the workspace root into `target/{debug,release}/`.
; Built by `make build` → scripts/build_godot_ext.sh copies the cdylib into bin/.
;
; entry_symbol is gdext's default init function.
; Built against the prebuilt api-4-6 bindings; loads under the Godot 4.7
Expand All @@ -9,17 +8,16 @@
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.6
reloadable = true
reloadable = false

[libraries]
; Primary target: macOS (Apple Silicon / arm64).
macos.debug = "res://target/debug/libgodot_ext.dylib"
macos.release = "res://target/release/libgodot_ext.dylib"
macos.debug.arm64 = "res://target/debug/libgodot_ext.dylib"
macos.release.arm64 = "res://target/release/libgodot_ext.dylib"
macos.debug = "res://bin/libgodot_ext.dylib"
macos.release = "res://bin/libgodot_ext.dylib"
macos.debug.arm64 = "res://bin/libgodot_ext.dylib"
macos.release.arm64 = "res://bin/libgodot_ext.dylib"

; Convenience entries for other hosts (build the matching target first).
linux.debug.x86_64 = "res://target/debug/libgodot_ext.so"
linux.release.x86_64 = "res://target/release/libgodot_ext.so"
windows.debug.x86_64 = "res://target/debug/godot_ext.dll"
windows.release.x86_64 = "res://target/release/godot_ext.dll"
linux.debug.x86_64 = "res://bin/libgodot_ext.so"
linux.release.x86_64 = "res://bin/libgodot_ext.so"

windows.debug.x86_64 = "res://bin/godot_ext.dll"
windows.release.x86_64 = "res://bin/godot_ext.dll"
Empty file added bin/.gitkeep
Empty file.
Loading
Loading