Skip to content

Commit 42e98c2

Browse files
committed
feat(mars-genesis): add deterministic between-turn progression (aging, births, deaths, careers)
1 parent eee6e8c commit 42e98c2

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { Colonist, ColonySystems, TurnEvent, SimulationState } from './state.js';
2+
import { SeededRng } from './rng.js';
3+
4+
const MARS_RADIATION_MSV_PER_YEAR = 0.67 * 365; // ~244.55 mSv/year
5+
6+
/**
7+
* Run all between-turn progression: aging, mortality, births, careers,
8+
* health degradation, resource production. All deterministic from seed.
9+
*/
10+
export function progressBetweenTurns(
11+
state: SimulationState,
12+
yearDelta: number,
13+
turnRng: SeededRng,
14+
): { state: SimulationState; events: TurnEvent[] } {
15+
const events: TurnEvent[] = [];
16+
const year = state.metadata.currentYear;
17+
const turn = state.metadata.currentTurn;
18+
let colonists = state.colonists.map(c => structuredClone(c));
19+
let colony = structuredClone(state.colony);
20+
21+
// 1. Age all colonists and accumulate radiation
22+
for (const c of colonists) {
23+
if (!c.health.alive) continue;
24+
c.career.yearsExperience += yearDelta;
25+
c.health.cumulativeRadiationMsv += MARS_RADIATION_MSV_PER_YEAR * yearDelta;
26+
27+
// Bone density loss (stabilizes after ~20 years on Mars)
28+
const lossRate = c.core.marsborn ? 0.003 : 0.005;
29+
const yearsOnMars = year - (c.core.marsborn ? c.core.birthYear : 2035);
30+
const decayFactor = Math.max(0.5, 1 - lossRate * Math.min(yearsOnMars, 20));
31+
c.health.boneDensityPct = Math.max(50, c.health.boneDensityPct * decayFactor);
32+
33+
// Earth contacts decay
34+
if (c.social.earthContacts > 0 && turnRng.chance(0.15 * yearDelta)) {
35+
c.social.earthContacts = Math.max(0, c.social.earthContacts - 1);
36+
}
37+
}
38+
39+
// 2. Natural mortality
40+
for (const c of colonists) {
41+
if (!c.health.alive) continue;
42+
const age = year - c.core.birthYear;
43+
if (age < 60) continue;
44+
45+
let mortalityProb = 0;
46+
if (age >= 60) mortalityProb = 0.01 * yearDelta;
47+
if (age >= 70) mortalityProb = 0.03 * yearDelta;
48+
if (age >= 80) mortalityProb = 0.08 * yearDelta;
49+
if (age >= 90) mortalityProb = 0.20 * yearDelta;
50+
51+
if (c.health.cumulativeRadiationMsv > 1000) mortalityProb += 0.02 * yearDelta;
52+
if (c.health.cumulativeRadiationMsv > 2000) mortalityProb += 0.05 * yearDelta;
53+
54+
if (turnRng.chance(Math.min(mortalityProb, 0.5))) {
55+
c.health.alive = false;
56+
c.health.deathYear = year;
57+
c.health.deathCause = age >= 80 ? 'natural causes' : 'age-related complications';
58+
c.narrative.lifeEvents.push({ year, event: `Died at age ${age} (${c.health.deathCause})`, source: 'kernel' });
59+
events.push({ turn, year, type: 'death', description: `${c.core.name} died at age ${age}`, colonistId: c.core.id });
60+
}
61+
}
62+
63+
// 3. Births
64+
const aliveAdults = colonists.filter(c => c.health.alive && (year - c.core.birthYear) >= 20 && (year - c.core.birthYear) <= 42);
65+
const birthProb = colony.morale > 0.4 && colony.foodMonthsReserve > 6 ? 0.08 * yearDelta : 0.02 * yearDelta;
66+
const potentialParents = aliveAdults.filter(c => c.social.childrenIds.length < 3);
67+
68+
for (let i = 0; i < potentialParents.length - 1; i += 2) {
69+
if (turnRng.chance(birthProb)) {
70+
const p1 = potentialParents[i];
71+
const p2 = potentialParents[i + 1];
72+
const childName = `${turnRng.pick(['Nova', 'Kai', 'Sol', 'Tera', 'Eos', 'Zan', 'Lyra', 'Orion', 'Vega', 'Juno', 'Atlas', 'Iris', 'Clio', 'Pax', 'Io', 'Thea'])} ${p1.core.name.split(' ').pop()}`;
73+
const childId = `col-mars-${year}-${turnRng.int(1000, 9999)}`;
74+
const child: Colonist = {
75+
core: { id: childId, name: childName, birthYear: year, marsborn: true, department: 'science', role: 'Child' },
76+
health: { alive: true, boneDensityPct: 88, cumulativeRadiationMsv: 0, psychScore: 0.9, conditions: [] },
77+
career: { specialization: 'Undetermined', yearsExperience: 0, rank: 'junior', achievements: ['Born on Mars'] },
78+
social: { childrenIds: [], friendIds: [], earthContacts: 0 },
79+
narrative: { lifeEvents: [{ year, event: `Born on Mars to ${p1.core.name} and ${p2.core.name}`, source: 'kernel' }], featured: false },
80+
};
81+
p1.social.childrenIds.push(childId);
82+
p2.social.childrenIds.push(childId);
83+
p1.narrative.lifeEvents.push({ year, event: `Child born: ${childName}`, source: 'kernel' });
84+
p2.narrative.lifeEvents.push({ year, event: `Child born: ${childName}`, source: 'kernel' });
85+
colonists.push(child);
86+
events.push({ turn, year, type: 'birth', description: `${childName} born to ${p1.core.name} and ${p2.core.name}`, colonistId: childId });
87+
}
88+
}
89+
90+
// 4. Career progression
91+
for (const c of colonists) {
92+
if (!c.health.alive) continue;
93+
const age = year - c.core.birthYear;
94+
95+
// Mars-born children enter workforce at 18
96+
if (c.core.role === 'Child' && age >= 18) {
97+
c.core.department = turnRng.pick(['medical', 'engineering', 'agriculture', 'science'] as const);
98+
c.career.specialization = turnRng.pick(['General', 'Support', 'Research']);
99+
c.core.role = `Junior ${c.career.specialization} Specialist`;
100+
c.career.rank = 'junior';
101+
c.narrative.lifeEvents.push({ year, event: `Began career in ${c.core.department}`, source: 'kernel' });
102+
}
103+
104+
if (age < 18) continue;
105+
106+
if (c.career.rank === 'junior' && c.career.yearsExperience >= 5 && turnRng.chance(0.15 * yearDelta)) {
107+
c.career.rank = 'senior';
108+
c.narrative.lifeEvents.push({ year, event: `Promoted to Senior ${c.career.specialization}`, source: 'kernel' });
109+
events.push({ turn, year, type: 'promotion', description: `${c.core.name} promoted to senior`, colonistId: c.core.id });
110+
}
111+
if (c.career.rank === 'senior' && c.career.yearsExperience >= 12 && turnRng.chance(0.08 * yearDelta)) {
112+
c.career.rank = 'lead';
113+
c.narrative.lifeEvents.push({ year, event: `Promoted to Lead ${c.career.specialization}`, source: 'kernel' });
114+
events.push({ turn, year, type: 'promotion', description: `${c.core.name} promoted to lead`, colonistId: c.core.id });
115+
}
116+
}
117+
118+
// 5. Morale drift
119+
const foodPressure = colony.foodMonthsReserve < 6 ? -0.05 : 0;
120+
const popPressure = colonists.filter(c => c.health.alive).length > colony.lifeSupportCapacity ? -0.08 : 0;
121+
colony.morale = Math.max(0, Math.min(1, colony.morale + (0.6 - colony.morale) * 0.1 + foodPressure + popPressure));
122+
123+
// 6. Update population count
124+
colony.population = colonists.filter(c => c.health.alive).length;
125+
126+
// 7. Resource production
127+
colony.foodMonthsReserve = Math.max(0, colony.foodMonthsReserve - (yearDelta * 0.5) + (colony.infrastructureModules * 0.3 * yearDelta));
128+
colony.scienceOutput += yearDelta;
129+
130+
return {
131+
state: { ...state, colonists, colony, eventLog: [...state.eventLog, ...events] },
132+
events,
133+
};
134+
}

0 commit comments

Comments
 (0)