Start here

The genetics model

Alleles are objects, not letters in a string. A horse carries one AllelePair per registered Gene, plus an epigenome — a priority and a set of literal numbers riding on each allele copy. Everything below lives in common/genetics/ and imports nothing from Minecraft.

The pieces

TypeWhat it is
GeneAn interface: the alleles a locus defines, how they segregate, and how the locus changes the coat. One singleton instance per gene.
AlleleOne allele of one gene. Identified by <modauthor>.<gene>.<token>, e.g. horsegenetics.splash.Spl.
AllelePairThe two copies a horse carries at one gene — an unordered combination, put in a canonical slot order on construction so a pair equals its reverse.
ExpressionOne outcome a gene can produce: a name, a human-readable description, whether it is a wild type, whether it masks, and the paint function. Replaces DominancePattern entirely.
FounderTableHow common each allele combination is in the wild population, as percentages. Replaces the old per-allele “1 in N” frequency.
GenotypeOne AllelePair per registered gene. Round-trips through a code string.
AlleleEpigenetics(int priority, EpiValues values) — the heritable payload on one copy of one allele.
EpiSchema / EpiValueWhat a gene declares it writes on a copy (Gene.epiSchema()): each value’s name, kind, design range and founder distribution.
EpiValuesThe literal numbers themselves, read by name. EpiRoll rolls a founder’s; EpiDrift nudges them at each breeding; EpiCodec writes and reads them as text.
EpigenomeOne AlleleEpigenetics per copy, aligned slot-for-slot with the genotype’s pairs.
GenomeGenotype + Epigenome. The thing a foal actually inherits. breedWith takes an optional per-parent GameteBias — what a breeding carrot does.
GameteBias / CarrotEffectA per-parent modifier on the breeding draw (re-roll epigenetics / prefer the dominant or recessive copy / substitute a pair for one gene), and the sealed set of carrot effects that fold into one. NONE is bit-for-bit the plain draw.
GeneRaritySix tiers. Gene.rarity() (default UNCOMMON), hasGeneCarrot() and spliceTable() are the §19 metadata the carrot economy and the gene database read.
GenesThe registry. Three orderings: codeOrder(), naturalOrder(), magicalOrder().
GenotypeCatalogEvery visually distinct genotype the registry can produce, in a stable order.
GeneSpec / SpecGeneA gene described as a JSON file rather than a class, and the Gene that answers everything from it. Indistinguishable from a hand-written gene everywhere below this table.

The genotype code string

A Genotype is held internally as a map keyed by gene, and serialises to one gene-keyed segment per gene, in Genes.codeOrder(). Each segment is <geneKey>=<a>/<b> — the full Gene.key(), then the two alleles in the gene’s own declaration order; segments joined by -, alleles by /:

a wild-type-ish horse — the first few of the 17 gene segments
horsegenetics.extension=E/e-horsegenetics.agouti=A/a-horsegenetics.silver=z/z-horsegenetics.matp=Cr/N- …

Allele tokens can be any run of charactersSpl, Cr, prl, Mzeb, Blupnk — not single letters. The =, / and - separators (and the gene key, which contains none of them) keep that unambiguous. The forward-looking rule for third-party genes is stricter: alphanumeric tokens, 1–128 characters, wild type named n.

Parsing is tolerant — a gene is added or removed, nothing else

Genotype.parse looks each segment's gene up by key, not by position. A registered gene with no segment reads as its wild type; a segment naming a gene that is not registered is dropped; the empty string is the all-wild-type genotype. A bad allele token on a known gene is still a hard error. So adding or removing a gene is just a coat regeneration — no padding, no version field, no positional / legacy fallback. What happens when a player installs a gene mod is still an open question.

Gene priority and the derived orderings

Every Gene declares an int priority() — a fixed constant of the gene, no default. Genes keeps a single order: every registered gene, built-in and data-driven alike, sorted on (priority, key). The three public views are a result of that one sort, never a source: codeOrder() / all() is the whole list, naturalOrder() is it filtered to isNatural(), magicalOrder() to the magical genes. Ties break alphabetically by key. A data-driven natural gene at priority 45 lands between the built-in MATP (40) and champagne (50) — loaded genes are not appended.

Bands are a convention: 0–99 natural, 100+ magical — the phase (isNatural()), not the number, splits the two coat passes, and Genes.register only warns when a gene sits outside its band. Within the natural band, low numbers set pigment absolutely, higher numbers dilute (agouti's absolute points must precede PigmentField.dilute). Built-in priorities were chosen to reproduce the pre-rewrite hand-written order, so no coat changed. This is not AlleleEpigenetics.priority, which selects a seed and never an order.

Alleles, combinations and expressions

A gene has any number of alleles. A horse carries two, and order does not matter, so a gene with n alleles has n(n+1)/2 combinations — three for two alleles, six for three, 465 for a thirty-allele KIT. Every combination produces some result, and the gene says which by declaring its distinct results as Expressions and mapping any pair to one of them with expressionOf(AllelePair).

There is no dominance property anywhere in this model

“Dominant” and “recessive” are shorthand for which combinations happen to share a result. They only describe a two-allele locus, and they cannot say what a third allele does without inventing a fourth word. The combination table says all of it directly, and says it for any number of alleles:

  • several combinations landing on one outcome is what “dominant” meant — E/E and E/e both wild type;
  • only the double-variant landing on a non-wild-type outcome is what “recessive” meant — Hlr/n is a carrier because it maps to a wild type, not because of a label;
  • two variant alleles each with their own outcome and a third for the pair of them is codominance — MATP is exactly that.

None of them needs a name, and none of them needs a special case.

What an expression carries

FieldWhat it means
idThe gene’s slug for this outcome, unique within the gene and stable. Two expressions are equal when their ids match, and the catalogue dedups allele pairs by it.
nameShort display name — “Sabino-white”.
descriptionOne human-readable sentence: what this combination does to the horse. What the gene dictionary, the tooltips and these wiki pages show.
wildTypeThis combination changes nothing. No painter, skipped by the composer, excluded from the texture key, reads as “absent” in the genome display. A property of the combination, not of an allele.
masksWhile this shows, no other gene is visible — dominant white, frame lethal white. Also per-combination: w/w does not mask, W/w does.
deterministicEvery horse with this combination is painted byte-for-byte identically. A false anywhere forces per-horse texture generation.
the painterEither a phase-1 restrict (a natural gene) or a phase-3 tint (a magical one) — never both, because a gene is one or the other. Both are pure: handed read-only views, they return a contribution.
Sabino, whose three combinations land on three different outcomes
private final Expression WILD = Expression.wildType("No white markings.");

private final Expression SABINO = Expression.of("sabino1", "Sabino 1")
        .describe("Tall jagged stockings, a splash of white up the belly, ...")
        .varies()
        .restrict((ctx, coat) -> paintSabino(ctx, coat, 1));

private final Expression SABINO_WHITE = Expression.of("sabino-white", "Sabino-white")
        .describe("Ninety per cent white or more, with a few coloured flecks ...")
        .varies()
        .restrict((ctx, coat) -> paintSabino(ctx, coat, 2));

@Override
public Expression expressionOf(AllelePair pair) {
    return switch (pair.count(SB1)) {
        case 2  -> SABINO_WHITE;
        case 1  -> SABINO;
        default -> WILD;
    };
}

A gene may declare more than one wild type when different silent combinations deserve different wording — MATP’s prl/N is a “pearl carrier”, distinct in the gene dictionary from the plain N/N even though the two horses look identical. The catalogue collapses every wild type into one entry, because “changes nothing” is one look.

The genotype-aware question

A handful of genes depend on another gene: agouti paints black points, so it does nothing at all on a chestnut horse. Those override expressionIn(pair, genotype), which defaults to expressionOf(pair) and is what the coat pipeline actually calls. An override may only return one of the gene’s own declared expressions — typically a wild type, meaning “suppressed here”.

A data-driven gene can do this too now. An expression in a gene file may carry a needs block naming another locus and a copy count, and SpecGene implements expressionIn and coatDependsOn off it. That is the whole of the addition: no new concept, just a way to reach the hook the leopard complex has used since long before the file format existed. Accretion is the gene built on it — one locus decides whether there is a pale field and what colour it is, and Accretion Field, which paints nothing at all and which every horse carries, decides whether it takes the topline or the underside.

Founder frequency, per combination

Gene.founderTable(FounderContext) returns a FounderTable: a weight per allele combination, as percentages that should sum to 100. One nextFloat() per gene per founder picks a bucket, drawn in codeOrder(), so a founder’s genotype is reproducible from the RNG stream. Founders only — breeding never consults it.

This replaces the “1 in N per allele” frequency each gene used to hand-roll, for two reasons. It has no meaning past two alleles — “1 in 45 per allele” says nothing about a locus with thirty of them. And it hid the number the author cares about: a per-allele chance only implies a homozygote rate, where a per-combination table lets you set it directly. The two splash loci are the clearest case — a majority of MITF founders carrying one SW1 and none at all homozygous is a shape no allele frequency can produce, because Hardy-Weinberg’s heterozygote share peaks at 50% and always brings homozygotes with it.

The table is sparse: a combination left out simply never turns up in the wild. That is what lets a thirty-allele locus declare the dozen combinations that actually occur instead of all 465. Weights that miss 100 are normalised proportionally with a warning, not an error. FounderTable.hardyWeinberg(variant, baseline, p) is a convenience that computes the three numbers a two-allele author would otherwise work out by hand — it is what the old “1 in N” meant, not a second model.

The multi-allele overload is hardyWeinberg(Map<Allele, Double>, Predicate<AllelePair>), and KIT is why it exists: thirty-six combinations, four of them ruled out, is not a table anyone can hand-write without a typo nobody would ever see. Excluded combinations are dropped and the rest rescaled, which is also the biology — a lethal is absent from the adult population you are observing.

FounderContext carries the part of the genotype already rolled, so a gene’s frequency may depend on what the horse already is (“champagne twice as likely on a chestnut”). Genes roll in codeOrder(), so a gene can only read genes with a lower priority; asking about a later one throws rather than quietly handing back a wild type that would make the bug invisible.

The forty-eight registered genes

The table is in codeOrder() — the one (priority, key) sort every ordering is derived from — so it doubles as the list of the numbers the built-ins occupy. Low numbers set pigment absolutely, higher ones dilute; 80–99 is the non-coat sub-band and 100+ is magical.

Six of them are white-pattern loci, and they are six because real white patterning is six genes. Only alleles at exactly the same locus share a gene here: sabino and the numbered W series are all KIT variants, so they are one gene; splash lives on MITF and PAX3, so it is two; and tobiano (an inversion near KIT, not a KIT variant), roan (region-mapped, causal change unresolved) and frame (EDNRB) each keep their own. That is what lets a horse be tobiano and W20 and splash at two loci — and what stops it being sabino and dominant white at once.

Pri.GeneAllelesOutcomesIn the wildPhase
1sexX / Ymare, stallion — both wild types50 / 50, no Y/Yneither (paints nothing)
10extensionE / ewild, chestnut25 / 50 / 25natural
20agoutiA / awild, bay25 / 50 / 25natural
30silverZ / zwild, silver1/60 per allelenatural
32mushroomMu / muwild, mushroom-carrier, mushroom1/34 per allelenatural
34dunD / d1 / d2wild, primitive-marks, dun§1/24 D, 1/10 d1natural
36brindleBrn / n / Ywild, brindle-carrier, brindle2% per X — 2% of stallions, 0.04% of maresnatural
40MATPCr / prl / Nwild, pearl-carrier, single-cream, classic-pearl, double-dilute1/30 Cr, 1/22 prlnatural
50champagneCh / cwild, champagne1/40 per allelenatural
55greyG / gwild, grey1/16 per allelenatural
60tiger eyeTE1 / TE2 / Nwild, tiger-eye-carrier, tiger-eye-amber, tiger-eye-yellow1/70 per allele — but 26% of Puerto Rican Paso Finosnatural (eyes only)
68natural zebraZeb / nwild, shadow-striped, zebra-striped1/60 per allelenatural (codominant)
70roanRn / rnwild, roan1/30 per allelenatural
72tobianoTo / towild, tobiano1/50 per allelenatural
73leopard complexLP / lpwild, mottled, varnish-roan, leopard, fewspot, blanket, snowcap, semi-leopard¶¶1/40 per allelenatural
74EDNRB — frameO / Nwild, frame, lethal-white (masks)1/55 per allele; no O/O foundernatural
76KIT — white spottingW22 / W13 / W10 / W5 / W23 / SB1 / W20 / Nwild, minimal-white, modest-white, sabino, broad-white, extensive-white, near-white, dominant-white (masks)*W20 6%, SB1 2.2%, the rest under 1% each; four homozygotes cannot occurnatural
78MITF — splashSW3 / SW1 / SW5 / Nwild, splash, splash-bold, splash-extensiveSW1 4%, SW5 0.6%, SW3 0.4%; no SW3/SW3natural
79PAX3 — splashSW2 / SW4 / Nwild, splash, splash-bold90% SW2/N, 1% SW4/N; heterozygotes onlynatural
80–92the thirteen non-coat genes — see the table below
93PATN1PATN1 / nwild only — a silent modifier¶¶0 unless the founder rolled LPneither (paints nothing)
94PATN2PATN2 / nwild only — a silent modifier¶¶0 unless the founder rolled LPneither (paints nothing)
112mane colourMnsld / Mnstrp / nwild, solid, striped, solid-striped¶2.0% / 1.5% per allelemagical
114tail colourTlsld / Tlstrp / nwild, solid, striped, solid-striped¶2.0% / 1.5% per allelemagical
116healerHlr / nwild, healer-carrier, healer9% per allelemagical
120magic zebraMzeb / nwild, zebra1/100 per allelemagical
130milkWatr / Lava / nmares-milk, milk-carrier, water-milk, lava-milk, milk-lethal — all wild types8% / 6% per allele; no Watr/Lavaneither (paints nothing)
140magic body sizeBig / Small / nsix combinations, six outcomes — codominant, and all wild types‡80% carry one copy; no homozygote is ever wildneither (paints nothing)
141magic speedSwift / Sluggish / nsix combinations, six outcomes — codominant, all wild types‡80% carry one copy; no homozygote is ever wildneither (paints nothing)
142magic healthHardy / Frail / nsix combinations, six outcomes — codominant, all wild types‡80% carry one copy; no homozygote is ever wildneither (paints nothing)
143magic jumpSpringy / Leaden / nsix combinations, six outcomes — codominant, all wild types‡80% carry one copy; no homozygote is ever wildneither (paints nothing)
150particle40 variants / nwild + 40 single + 46 codominant double — all wild types0.1% per variant alleleneither (paints nothing)
160lightLthf / Ltmn / Lteye / nwild + six regions, codominant‡‡0.5% per variant allelemagical
180verdantmush / moss / grass / nwild, verdant-carrier, mycelium, moss, grass — all wild types6% / 7% / 8% per alleleneither (paints nothing)
190LUTn / Blupnk / Grnpnkwild, a carrier wild type per palette, bluepink, greenpink~1 in 60 carry each copy; no homozygote is ever a founder, and a mixed pair is the wild typemagical — swaps the phase-2 gradient (no phase-1 or phase-3 paint)
196cutie markCutmrk / nwild, cutie-mark-carrier, cutie-markall wild types6% per allele (recessive)neither — a client render layer draws a 1-3 item emblem on both flanks over everything
690extreme white dominantEWD / nwild, white-dominant1/320 per alleleneither — it paints nothing and changes the fold instead
This table is the built-ins; the whole order is a page

The hundred-odd data-driven genes take their slots by the same rule and are not listed here, because a hundred rows of numbers typed into a wiki page is exactly the thing that goes stale. The paint order draws every gene that paints, highest first, from the registry at run time. It is the place to look for what covers what.

Brindle is the model’s only sex-linked gene. Its Y is not an allele a horse can express — it is the reserved placeholder a hemizygous stallion’s spare slot holds, meaning “this locus is not on the Y”. That is what lets a genotype stay one two-slotted AllelePair per gene with no structural change, and it is why a stallion carrying brindle displays as X-Brn rather than beside a fake second copy. See its page. ¶ Mane and tail colour are the model’s first genes whose heterozygote paints with two allele copies at once, one colour each, which is why they need CoatBuildContext.epigeneticsForCopy rather than "the copy that expresses". ‡‡ Light is the clearest codominance in the model: three variants, each dominant to the wild type and to none of each other, so a horse shows everything it carries — ten combinations, seven outcomes, and no ordering of three alleles that could describe it. * KIT is the locus that broke the last of the old model. Eight alleles, thirty-six combinations, and four of them homozygotes that are thought nonviable — so canOccur rules them out and the founder table is the survivors, renormalised. It also settles a genotype the old model allowed and biology does not: a horse can be homozygous sabino or dominant white, never both, because they are alleles of one gene competing for two slots. See its page. † O/O is one of the model’s two kinds of lethal. An embryonic lethal is a horse that never existed (canOccur false, no pen, no founder) — KIT’s four W homozygotes, and MET’s met/met. Lethal white is the other kind: the foal is born, so it occurs, it has its own masking all-white outcome and it gets a pen — it is simply never a founder, because a founder is an adult that survived. The death is modelled; see the horse’s body and its page. ¶¶ The leopard complex was the model’s first cross-locus gene, and accretion is the second — the first to do it from a file rather than from Java. LP reads PATN1 and PATN2 in expressionIn to pick one of eight outcomes (leopard, fewspot, spotted blanket, snowcap, semi-leopard, or the plain LP looks). The modifiers paint nothing on their own, so they carry Gene.affectsCoat() false and would be left out of the texture key — LP.coatDependsOn() names them and coatCode() folds their alleles in, but only for a horse that actually has LP. A founder draws a variant PATN copy only if it already rolled LP. See its page. § Dun is the locus with two dominance orders. Dilution reads D > d1 = d2 but marking reads D = d1 > d2, so d1 draws the dorsal stripe without diluting anything — a horse with primitive markings that is not a dun. No single dominance label covers both orders, which is the clearest case yet for the table. See its page. ‡ Sex was the first gene that never paints, and is no longer the only one. Both its outcomes are wild types, so it costs the catalogue nothing and is left out of the texture key; it is registered at priority 1 so a future sex-linked gene can read a sex that is already resolved, and so a foal’s sex is inherited rather than rolled. See its page.

The thirteen that never paint

Every outcome of every gene below is a wild type, which here means “changes nothing about the coat”. So affectsCoat() is false for all of them, they are out of the texture key, and the catalogue collapses each locus to a single entry — thirteen genes for zero catalogue growth, which is the sex locus’s trick reused. What they do instead goes through common/trait/: the machinery is on the horse’s body, and each gene has its own page. Their order among themselves is arbitrary, because none of them can reach another through the coat.

Pri.GeneAllelesIn the wildWhat it moves
80MSTNC / Tp(C) = 0.35codominant: each C is +0.020 speed and −2 health
81PDK4A / Gp(A) = 0.25+0.018 speed per copy
82CKMT / Cp(T) = 0.20+0.015 speed per copy
83RYR2J / np(J) = 0.20+0.09 jump strength per copy
84LCORLL / np(L) = 0.30height: +0.05 scale, +0.010 speed, +0.02 jump per copy
85HMGA2p / Np(p) = 0.25pony: −0.06 scale, −0.008 speed, −0.02 jump, +2 health per copy
86ACAND1 / D2 / D3 / D4 / N0.4% per variant; affected combinations excludeddwarfism. Five alleles, fifteen combinations; affected is “no working copy left”, so D1/D4 counts. D1/D1 is lethal at birth, every other D/D is scale ×0.70 and −6 health
87B4GALT7d / N4.0% carriersFriesian dwarfism — scale ×0.75, −5 health. The one disorder a horse lives with
88PLOD1ffs / N2.6% carriersfragile foal syndrome — lethal at birth
89RAPGEF5efih / N1.4% carriersEFIH — lethal at birth; the most severe and the rarest
90ST14nfs / N1.8% carriersnaked foal syndrome — lethal at birth. The bare coat is not drawn
91SHOXsa / N2.0% carriersskeletal atavism — lethal at birth. Pseudoautosomal, so it segregates like an autosome
92METmet / N3.0% carrierslethal at conceptioncanOccur false, and the pairing produces no foal at all
93SCN4AH / N1.2% affected — there is no carrierHYPP. Dominant, and the first locus that is both a survivable disorder and a lethal: H/N is −6 health and rideable, H/H is lethal at birth. Founders can be born H/N, because a dominant that never reached a founder could never exist
94GYS1P / N2.5% affected — there is no carrierPSSM1, tying-up. Dominant and neither combination is lethal, which makes it the mildest locus here and by far the one a player is most likely to meet
95PPIBherda / N3.4% carriersHERDA, fragile skin — −7 health, survivable. The highest carrier rate of any recessive here, because the real thing hides in the working lines a breeder would choose
96PRKDCscid / N2.8% carriersSCID — lethal at birth. No immune system at all
97MYO5Alfs / N2.2% carrierslavender foal syndrome — lethal at birth. The only lethal whose real presentation includes a colour, and it is deliberately not drawn
98TOE1ca / N2.4% carrierscerebellar abiotrophy — survivable, and the heaviest jump penalty in the mod. Progressive in reality; flat here, because there is no age model
99CVMcvm / N1.6% carrierscervical vertebral malformation — lethal at birth. No confirmed causal variant; one locus is the mod's simplification, and the page says so
100GBE1gbed / N2.0% carriersGBED — lethal at birth, and the largest heart reduction in the mod
101Megaesophagusmeg / N1.7% carrierslethal at birth, and the gentlest of them on the numbers — the failure is mechanical, not systemic. Also has no confirmed causal variant

No founder is ever affected. Every health founder table lists only the clear horse and the carrier, because a wild-caught horse is an adult that survived — so the only way to meet a disorder is to breed two carriers. Four colour genes carry one as well: silver Z/Z is MCOA, EDNRB O/O is lethal white, and a homozygote at either splash locus (MITF / PAX3) is deaf — one condition with two causes, de-duplicated by id.

Seal brown has no allele of its own. Neither has blood bay nor liver bay: all four bays are bands of one shade score, which the shade locus, MC1R dosage, ASIP dosage and a per-horse roll add up between them. Agouti declares the four as outcomes and picks between them by reading the rest of the genotype. Cream and pearl are one gene, and so are dominant white and sabino: they are allelic in real horses (MATP and KIT respectively) and are now one locus each, which kills the impossible Cr/Cr-and-prl/prl genotype the old two-gene model allowed. See its page. Dun is the other three-allele locus, and the one that shows why a dominance label could never have worked: see its page.

Sex-linked inheritance

Every gene in the model is autosomal except one. Gene.inheritance() declares AUTOSOMAL (the default), X_LINKED or Y_LINKED, and brindle is the gene that made it worth building: a stallion has one X, so he carries one copy and can never be a carrier, while a mare needs two. That asymmetry produces pedigree shapes no autosomal gene can.

The reserved slot

A genotype stays one AllelePair per gene, two slots, always. A hemizygous horse fills the slot it does not really have with a reserved placeholder allele the gene declares:

ModeMare X/XStallion X/Y
AUTOSOMALtwo real allelestwo real alleles
X_LINKEDtwo real allelesone real allele + reserved Y
Y_LINKEDtwo reserved Xreserved X + one real allele

That is what keeps the change small. Genotype is untouched, epigenome alignment is untouched (the reserved slot simply carries epigenetics nothing reads), the code string keeps its shape, and parsing needs no special case because the placeholder is a declared allele like any other.

The placeholder must be declared at the end of alleles() that its slot sorts to — Y last on an X-linked gene, X first on a Y-linked one — because AllelePair canonicalises on declaration order. Genes.register refuses a gene that gets this wrong, since the failure otherwise is not an exception but a gene that quietly misbehaves for exactly one sex.

What changes in breeding

On screen a hemizygous copy carries an X- / Y- prefix (X-Brn), unambiguous on sight because - is the gene separator in a code string and can never appear inside an allele token. The code string itself is unaffected.

Epigenetics ride on the allele, not on the horse

A genotype says which alleles a horse carries. It does not say what those particular copies are carrying, and every copy carries two things:

So a horse carrying A/a has one set of numbers on its A and another on its a, and a foal that inherits the A inherits that copy’s numbers. Bay point heights run in families rather than being reinvented every generation.

These used to be one opaque seed

A copy carried a single long, and every gene recovered its numbers by replaying a PRNG off it in a documented draw order. That was compact and completely unreadable: “how much jump does this copy add” could only be answered by running the gene, and the position of a draw was the meaning of each number — so inserting one while tuning a gene silently rewrote every horse in the save.

Values are stored literally and read by name now. It costs a great deal more space (an epigenome code went from tens of characters per gene to a couple of hundred, and is only written for the genes that actually vary), and it buys a genotype a player can read, edit and select on. That was the owner’s call and the reason for the whole rewrite.

Three kinds of value, because drift cannot treat them alike

KindWhat it isHow drift treats it
SCALARA magnitude — how much white, how much bigger, how wide.Nudged continuously. Nudging it a hair makes a horse a hair different.
SEEDThe long behind a noise field — the shape of a splash, a streak, a set of spots.Never nudged. There is no “slightly different” splash, so it is replaced whole or not at all — and almost never.
CATEGORYAn index into a list the gene owns — a particle body site, an iris wedge.Never nudged, for the same reason: a boundary nudge would silently move a horse’s particles from its mane to its tail.

Colours are three SCALAR channels (<name>_r / _g / _b) rather than one packed int, so a line’s colour drifts gradually instead of jumping. A founder’s is still rolled bright — hue over the whole circle at a held-up saturation and value — because a magical mane that rolled a muddy olive reads as a bug.

Founder distributions are declared, not flattened

A value names the distribution its founders are rolled from: uniform, a bounded Gaussian, or a power curve. That matters because the genes were deliberately skewed — ednrb’s cover is a power curve so most splash horses carry a little white and a few carry a lot; the magical stat deltas are Gaussian so a single copy is usually subtle. Rolling everything uniformly would have been simpler and would have quietly changed the character of the wild population on every one of them.

Which copy expresses

CaseExpressed copy
Two different allelesThe canonically first copy — the earlier-declared allele. An arbitrary but fixed choice, not a claim about which allele “wins”: what the combination does is its expression, which reads both.
HomozygoteBoth copies express, so the tie goes to the higher priority.

That is what Epigenome.expressed(gene, genotype) answers, and its values are what CoatBuildContext.epigeneticsFor(geneKey) returns.

Priority selects a seed, never an order

Epigenetic priority (AlleleEpigenetics.priority) picks which copy’s values express. It can never move a gene in the processing queue, and by design it never will — if it could, two horses with the same genotype and the same values could diverge on priority alone and silently share a coat cache entry. It also never drifts: it is an ordinal deciding a tie, not a magnitude, and a horse whose expressed copy silently swapped between generations would be inexplicable. Gene processing order is a separate per-gene constant, Gene.priority() (Genes sorts on (priority, key)). See Philosophy §2 and § Gene priority above.

The epigenome code string

One <geneKey>=<copy>/<copy> segment per gene that declares a schema, joined by ;. Each copy is p:<priority> followed by that gene’s named values: a plain number, # for a hex noise seed, [a|b|c|d] for a per-leg value.

horsegenetics.body_size=p:1734,delta:0.34215/p:9871,delta:0.31049;horsegenetics.ednrb=p:44,seed:#8f3a21c4b70e1d55,face:0.61,cover:0.4113,…

Two things changed shape here. Genes join with ; rather than -, because values are signed now and a hyphen can no longer double as a delimiter. And the majority of genes write nothing at all — a gene whose behaviour is fixed by its alleles declares an empty schema, which is what keeps the code readable now that each stored gene carries a couple of hundred characters instead of a sixteen-digit seed.

Parsing is tolerant the same way the genotype code is: an unregistered gene’s segment is dropped, and anything the schema declares but the text omits — a gene, or one value inside a gene — is rolled deterministically from the gene key and the value’s name, so every horse in the world agrees on it.

Every value is quantised to what the text can write the moment it is created, so a horse’s numbers in memory are exactly what its code says. That is not tidiness: a value that lost a digit on the way to disk would give a horse a subtly different coat after a reload than before it, and the two would fork the texture cache without ever looking different enough to notice.

Epigenome.fromSeed(long) replays a whole epigenome from a single long — a reproducible test and tooling horse, used by the golden-coat suite, the coat sample sheet and the designer preview. It is not a stand-in for a horse whose epigenome was not stored; FamilyTreeScreen used it that way and drew ancestors that never existed, and that fallback is gone.

Founders roll; foals inherit

If both copies arrive on the same priority, one extra nextBoolean() bumps the second ±1 (AlleleEpigenetics.deconflict), clamped to [1, MAX] — so a horse never carries a tie. Full inheritance detail is on the breeding page.

What the texture key captures

CoatData.textureKey() is the genotype’s coat code, plus @<fingerprint hex> only when the horse is non-deterministic. Epigenome.visibleFingerprint(genotype) digests only the expressed values of genes that are both visible and non-deterministic, so epigenetics a horse cannot show do not fork the cache. It folds in every stored number rather than a single seed — getting that wrong does not throw, it silently forks the cache or collides two horses onto one texture.

Genotype.coatCode() is the full code minus every gene that can never paint anything — Gene.affectsCoat(), derived as “is any of my outcomes not a wild type?”. Today the sex locus is the only such gene, and leaving it out is what stops a mare and a stallion of the same colour baking two identical textures. It is not a persistence format: it is lossy on purpose and nothing parses it back.

common/genetics/Genotype.java — the two questions the cache asks
/** Is every gene's contribution identical on every horse with this genotype? */
public boolean isDeterministic();

/** Does any *visible* gene need per-horse randomness? -> bake per horse. */
public boolean hasVisibleNonDeterministic();

Gene.isVisible(pair, genotype) and Gene.isDeterministic(pair, genotype) both see the whole genotype — both derive from expressionIn(pair, genotype), which is how agouti knows it is invisible on a chestnut.

Declare determinism honestly

Claiming determinism you do not have poisons the coat cache for every horse sharing your genotype: the first horse to bake wins, and the others render its coat. This is the single most damaging mistake a third-party gene can make.

The genotype catalogue

GenotypeCatalog enumerates every visually distinct genotype the registry can produce. Nothing is hard-coded — it falls out of codeOrder(), each gene’s allele list and each gene’s expression table, so registering a gene widens it on its own — the catalogue is built lazily and thrown away whenever the registry changes, which is what makes a drop-in gene show up in the catalogue without anything else being told about it.

allPairsOf(gene)Every unordered pair a horse can carry — all n(n+1)/2 of them (ee, Ee, EE), minus any the gene rules out with canOccur. Only the sex locus does: there is no Y/Y horse.
distinctPairsOf(gene)One representative per distinct Expression — the homozygous pair where the group has one. Every wild type is one group, because “changes nothing” is one look.
totalGenotypes()The raw product — every distinct heritable genotype.
size()The reduced catalogue — how many of them are distinct to look at.
get(i) / entries()Read an entry. Nothing is materialisedsize() is arithmetic and get(i) reads a mixed-radix odometer, because a two-million-entry List<Genotype> is hundreds of megabytes. entries() is a lazy view: streaming it still walks all of them, so sample it or ask the arithmetic.

Two reductions, both read straight off the expression table with no dominance metadata in the middle: pairs landing on the same expression collapse to one, and a gene on a masks expression keeps exactly one entry — that combination with every other gene at a wild type, which is why a masking combination keeps just one pen instead of hundreds of thousands. Ordering is a mixed-radix odometer whose first gene is the fastest-varying digit, so the catalogue reads

Nothing in the game reads it any more. It used to drive the horse dimension, one pen per entry; that corridor went back to random pens on 2026-09-04, because at two million entries the corridor was longer than the world. The catalogue stays because the tests read it and a punnett display will — the reduction is a statement about the genes, not about the dimension.

Phenotype, coarsely

Genotype.phenotype() gives a coarse CoatPhenotype (CHESTNUT / BLACK / BAY / WHITE; everything else ignored). It is only a family-tree fallback now — foals and adults alike are fully generated through the coat pipeline.

Source: common/src/main/java/com/example/horsegenetics/common/genetics/

Genes that never paint

Twenty-four of the registry’s forty-eight built-in genes change nothing about a horse’s colour: the sex locus, the six performance and size loci, the seven health loci, the two leopard-complex modifiers (PATN1, PATN2 — read by LP, but never painting themselves), and seven magical genes — milk, verdant, particle, and the four magical body-stat genes (size, speed, health and jump). Every combination they can produce is an Expression marked wildType.

That reads oddly for a gene that can kill a foal, and it is exactly right: wildType means “changes nothing about the coat. It is the property the coat pipeline asks about, and nothing else. What these genes do instead travels on the trait system — speed, max health, jump strength, body scale, and the disorders a horse expresses.

Three consequences follow, and together they are why the non-painting genes are nearly free:

The magical utility and body-stat genes stretched the phrase further than the health loci did, and it still holds. A lava-bearing horse is emphatically not “the same as an ordinary horse”, and a ten-times giant is the most visible thing on the field — but neither changes a coat pixel, and the coat pipeline is the only thing that asks. The cost is real and is logged as a gap: milk and verdant are invisible until you put a bucket under the horse or watch the floor, where healer deliberately draws a stripe so you can see what it does.

An expression still carries a name and a description when it is a wild type, which is what lets a carrier outcome say something useful (MATP’s pearl-carrier, every health locus’s …-carrier) without pretending to change the coat.