Chapter 6 · Section 2
Whose Stars Are Whose: the Galaxy Index
The BPT law
BPT Part 3.14 says a galaxy is a halo gathered round a well, and it defines which stars are a galaxy's own. In How the Type Is Read: "Each galaxy's stars are the bodies its well pulls hardest, so neighbours' stars are not counted in it." In The Count, and the Wells Unseen: "Most sites are not a galaxy's heart. A galaxy is a halo, and one halo gathers many: the deepest at its centre, the others satellites sinking toward it by dynamical friction (Chandrasekhar, 1943), with their stars, gas and wells, and many small halos never light a galaxy at all (Sawala et al., 2016)." The same part places a dwarf against the galaxies round it, not against the universe: "Galaxy masses follow the Schechter function, φ(M) ∝ (M/M)^α e^(−M/M), many small galaxies and a knee M above which they grow rare ... The game reads its own knee from its galaxies: for α ≈ −1.2 the mass-weighted mean galaxy is (α + 2) M = 0.8 M, so M is 1.25 times that mean, and a galaxy below a fiftieth of M* is a dwarf."
So the law has three pieces: ownership by strongest pull, satellites inside heavier halos, and a knee read from the universe's own galaxies, M* = 1.25 × the mass-weighted mean galaxy. BPT does not state the galaxy's reach (one sixteenth of the field) or the softened pull G·M/(d² + 1); they are game methods.
The established physics
- Galaxy masses follow the Schechter function φ(M) ∝ (M/M)^α·e^(−M/M) (Schechter 1976, An analytic expression for the luminosity function for galaxies); for α ≈ −1.2 the mass-weighted mean is (α + 2)·M*. The knee near 4.6 × 10¹⁰ Suns and dwarfs under about 10⁹ Suns, about a fiftieth of it, are from Baldry et al. 2012 (GAMA, the galaxy stellar mass function at z < 0.06).
- A satellite halo inside a heavier one sinks to its center by dynamical friction (Chandrasekhar 1943, as BPT Part 3.14 cites it); many small halos hold no galaxy (Sawala et al. 2016, The APOSTLE simulations).
- Exactness check: with φ(M) ∝ M^α·e^(−M/M), ∫M²φ dM / ∫Mφ dM = M·Γ(α + 3)/Γ(α + 2) = (α + 2)·M. At α = −1.2 that is 0.8·M, so M* = 1.25 × the mass-weighted mean, as the code uses.
The game's math
galaxyIndex(wells) app.js:2919.
Reach and bins (app.js:2920–2925). The reach of a galaxy is
R = max(3, min(rows, cols) / 16) cells
16 cells on a desktop (256 / 16), 4 on a phone (max(3, 64 / 16)). The bodies are binned in nr × nc bins, nr = max(1, ⌊rows / R⌋), so each bin is at least R wide and a body's 3 × 3 neighboring bins hold every body within R of it, round the torus. Mall is the total drawn mass.
Ownership (app.js:2929–2935). The non-data, non-stellar wells are binned the same way. For each body b, among the wells in its 3 × 3 bins with periodic distance d ≤ R:
pull p = max(1, M_w) / (d² + 1) owner(b) = the well with the greatest p
G is common to every well and left out. The +1 in the denominator softens the pull inside one cell, so a body sitting on a well does not divide by zero.
Halo reach (app.js:2944–2946). For each well, its members are sorted by distance; s = Σm; the halo reach r_h is the distance at which the running sum first reaches 0.9·s (the radius holding nine tenths of its members' mass). The halo mass is M_h = M_w + s.
Hosts and satellites (app.js:2946–2956). Halos are sorted heaviest first. Those with r_h > 0 are binned with bins S = max(1, min(min(rows, cols)/2, max r_h)) wide. For each halo x, the code looks through its 3 × 3 bins for the heaviest halo y with M_y > M_x, y not itself a satellite, and periodic distance from x to y less than r_y; if found, x is y's satellite. Then every body owned by a satellite is handed to its host (app.js:2956). A satellite well therefore owns no bodies.
The knee (app.js:2960). With M_g the summed body mass each owning well now holds:
M* = 1.25 · Σ M_g² / Σ M_g
The index returns Mall, the owner map, the knee, the host map, and a near(r, c) lookup over the body bins.
Example: on a desktop, R = 16 cells; the comment at app.js:2926 records wells "about 14 cells apart", so before ownership a galaxy read from every body within 16 cells took in its neighbors' stars. With ownership a body halfway between two equal wells goes to the nearer; a body 10 cells from a well of mass 100 (p = 100/101 ≈ 0.99) and 4 cells from a well of mass 10 (p = 10/17 ≈ 0.59) goes to the heavier, farther one.
In the game
The index itself draws nothing; what the player sees is its result. Each galaxy's points are laid out round the well that owns its bodies, so two neighboring galaxies are drawn as two, each round its own heart, and a satellite well has no galaxy drawn round it. In the 3D view a satellite is one of the dark wells: "a dark well, a satellite or one with no galaxy ... keep their place (a tap still finds them) but draw no ring" (setWells() comment, scene3d.js:1501–1504). A galaxy's own heart is drawn as a small mark at its center, a third of the usual mark, unless it feeds as a Seyfert or quasar (scene3d.js:1472–1476).
After the universe returns to the Source, the banner reads "Tap a well to dive in". A tap anywhere in a galaxy picks its well: "a tap anywhere in a galaxy picks its well, the heavier the nearer it counts, a galaxy's central well over the small" (pickWell() comment, scene3d.js:1429–1430). Tapping a satellite opens the banner with the line galaxyOf() returns for it (app.js:2979): "a satellite well inside the halo of …, sinking toward its heart · its stars and gas are that galaxy's", the host named by its well label. Tapping a well whose halo has gathered nothing shows "no galaxy gathered here yet" (app.js:2980).
The scale model
Exact: no scale model in the ownership, halo and host tests, which compare drawn masses and drawn distances, and are ratios that do not change when every drawn mass is multiplied by the same starMult. The knee is read in drawn mass and is used only against drawn mass, so it too is scale-free. The reach R is a fixed share of the field, the same at every level and every mass: the largest distance at which any body can belong to any galaxy.
Where the game and the theory differ
- BPT gives each body to the well that pulls it hardest, with no distance stated. The game considers only wells within the reach R = max(3, min(rows, cols)/16), so a body farther than R from every well belongs to no galaxy, and the reach is a share of the field, not a law.
- The pull is softened as G·M/(d² + 1) and a massless well pulls as one of mass 1. BPT states neither; both are numerical methods.
Fixed numbers
| Number | Where | What it is | Kind |
|---|---|---|---|
| 3 | app.js:2920 | least reach of a galaxy, cells | CAP |
| 16 | app.js:2920 | reach = 1/16 of the field's shorter side | CAP |
| 1 | app.js:2920 | least bin count, max(1, ⌊rows/R⌋) | METHOD (at least one bin) |
| −1…1 | app.js:2924, 2953 | the 3 × 3 bin search | METHOD (bins at least R wide hold every neighbor) |
| 1 | app.js:2934 | max(1, M_w): a massless well still pulls | METHOD |
| 1 | app.js:2934 | +1 in d² + 1, softening inside one cell | METHOD (avoids a divide by zero at d = 0) |
| 0.9 | app.js:2945 | halo reach = radius holding nine tenths of members' mass | BPT (Part 3.14, Wells and Galaxies) |
| 1/2 | app.js:2949 | host bins no wider than half the field | METHOD (keeps at least two bins a side) |
| 1 | app.js:2949 | least host bin width, cells | METHOD |
| 1.25 | app.js:2960 | M* = 1.25 × mass-weighted mean galaxy | PHYSICS (Schechter 1976, α ≈ −1.2; BPT Part 3.14) |
Measured
The comment at app.js:2936–2941 (2026-10-28) records: "measured at half a level 25 life: 160 of 314 wells" were satellites. The comment at app.js:2947 (2026-10-29) records that testing every halo against every heavier one "was 10⁸ pairs with 15,000 sites at level 202", which is why the host test is binned.