# Life Backfill for Age-N Characters — Design

- **Date:** 2026-05-27
- **Status:** Approved (design)
- **Component:** `server/` (TypeScript backend)

## Problem

When a character is created — or an NPC is spawned — at an older starting age
(18, 25, 40, …) they begin with a blank life history. Today:

- `setEducation` sets only a *current* enrollment string; there is no record of
  completed prior schooling (no transcript/milestones).
- There is no job/career history — jobs are forward-only and an adult character
  can start unemployed.
- Money and stats start at flat baselines, so an established adult is broke and
  undeveloped.

(Concretely: a freshly created 18-year-old "Sam" started with `education: ""`
and, before the recent fix, a 40% chance of no school at all.)

Production games start characters young and age them in, so this mainly affects
(a) test characters created at an older age and (b) **NPCs spawned at adult
ages**, which need a plausible life state. The two should share one generator.

## Goals

Given a character with `ageYears` set, populate a *coherent current state plus
lightweight milestone history* across four dimensions:

1. **Education history** — completed level/degree for the age + a milestone
   record per completed school.
2. **Career + light job history** — a current job fitting age & education, with
   salary, plus 0–2 prior jobs.
3. **Money & assets** — scaled to age and career.
4. **Developed stats** — intelligence/social/creativity nudged above baseline.

Balance fidelity with ease of building: no full year-by-year timeline, no
fast-forward simulation.

## Non-Goals (v1)

- **Family generation** (marriage / child Person objects) — left to existing
  parents/partner logic; clean follow-up.
- Full generated life timeline (per-year events).
- Fast-forward simulation through the real game loop.
- Wiring every NPC spawn site (coworkers, friends) — follow-up.
- Education *variety* among adults (HS-only adults). v1 assumes adults completed
  college, consistent with current `setEducation` behavior; variety is a future
  tweak.

## Decisions

| Decision | Choice |
|---|---|
| Depth | Coherent current state + lightweight milestone history |
| Dimensions | Education, Career (+light history), Money, Stats |
| Architecture | One shared `backfillLifeForAge` generator |
| Family | Deferred to existing logic |
| Wiring (v1) | Player `characterSetup` + parents/partner NPC spawns |

## Architecture

New module `server/src/services/character/life_backfill.ts`:

```
backfillLifeForAge(player, person): void
  -> backfillEducation(player, person)   // education + completed milestones
  -> backfillCareer(player, person)      // current job + light history
  -> backfillMoney(person)               // money scaled to age/career
  -> backfillStats(person)               // stats nudged above baseline
```

- **Order matters:** education first (gates career), then career (drives money),
  then money, then stats (uses age + education).
- **Fill-blanks-only / idempotent:** never overwrites a job, money, or stat the
  character already has meaningfully. Safe to call on NPCs already touched by
  `setValues`, and consistent with the idempotency built into `setEducation`.
- **Exported** so NPC spawn paths reuse the same generator.

### Call sites (v1)

- Player: `characterSetup()` in `services/character/character_manager.ts` —
  replaces the bare `setEducation` call with `backfillLifeForAge`.
- NPCs: `addParents` and the partner spawn (highest-impact adult NPCs).
- Coworkers/friends/boss adopt it in a follow-up.

## Behavioral refinement: age bands

This corrects the blanket "18+ always college" currently in `setEducation`:

| Age | Current enrollment | Completed milestones | Occupation |
|---|---|---|---|
| 5–13 | elementary (student) | — | student |
| 14–17 | high school (student) | elementary | student |
| 18–~22 | college (student) | elementary, high school | student |
| ~23+ | none (graduated) | elementary, high school, college (degree) | **working** |

Approximate graduation ages: elementary ~14, high school ~18, college ~22.

## Per-dimension rules

### Education (`backfillEducation`)

- Compute completed levels from age. For each completed level, add **one
  activity + one `EducationRecord`** (school chosen from the cached
  `getElementarySchools` / `getHighSchools` / `getColleges` lists), with:
  - `completed: true`
  - `date` = graduation date (now − (age − gradAge) years)
  - `gpa`, `focus` (sane defaults / light randomization)
- For currently-enrolled ages (5–22): call `setEducation` for the current
  enrollment (sets `current_education`, `occupation: 'student'`).
- For 23+: synthesize completed college (degree tier scales with age — bachelor's
  typical, master's/doctorate more likely older), `current_education = null`,
  occupation handled by career backfill.
- `person.education` = highest level string, matching job gating
  (`"high_school"`, `"bachelors_degree"`, `"doctorate_degree"`).
- **One activity + record per school *type*** → survives `dedupeSchoolEnrollments`
  (which keeps one-per-type and one record per kept school).

### Career (`backfillCareer`)

- Only for graduated adults (~23+, not current students).
- `yearsWorked = max(0, age − collegeGradAge)`.
- Pick a current job from `job_manager` whose `checkEducationRequirement` is
  satisfied by `person.education`; bias tier by `yearsWorked`.
- Set `person.job`, `person.occupation`, `person.salary` **directly** (not via
  `applyForJob`, to avoid spawning coworkers during backfill).
- `jobHistory`: synthesize 0–2 prior entries (lower-tier job(s) with from/to
  dates), scaled by `yearsWorked`.

### Money (`backfillMoney`)

- Simple formula: `money = clamp(round(yearsWorked * salary * savingsRate), min, max)`
  (`savingsRate` ≈ 0.1–0.2). Students get a small student-level amount. No ledger.

### Stats (`backfillStats`)

- For intelligence / social / creativity (and similar): `clamp(50 +
  educationBonus + ageBonus + smallRandom, 0, 100)` (e.g. +5 per completed level,
  small age term). Bounded and cheap.

## Data-model additions (minimal, optional)

- `EducationRecord.completed?: boolean` (in `services/education/education_manager.ts`)
- `Person.jobHistory?: Array<{ title: string; salary?: number; from?: string; to?: string }>`
  (in `models/Person.ts`, plus passthrough in `toJSON`)

## Interactions & edge cases

- **`dedupeSchoolEnrollments`:** completed milestone activities/records are
  one-per-type, so dedup keeps them; the `completed` flag is preserved (dedup
  doesn't strip fields). Covered by a dedicated test.
- **`current_education`:** null for graduated adults (23+); dedup only repoints it
  when dangling, so null is left alone.
- **Reconciliation with "always college 18+":** `backfillEducation` owns the
  age-band logic — it calls `setEducation` for current enrollment only at ages
  5–22, and synthesizes completed records for 23+ (so older adults are graduates
  with a career, not perpetual students).
- **Idempotency:** education recomputed on a blank character; career set only if
  no current job; money set only if at/below baseline; stats nudged from baseline.

## Testing

- Unit tests for `backfillLifeForAge` at representative ages:
  - **16** → in high school, completed elementary, still student.
  - **20** → in college, completed elementary + high school, student.
  - **30** → degree completed, has a current job meeting its education
    requirement, money > baseline, stats > 50, not a student.
  - **45** → higher degree tier + senior-tier job + more `jobHistory`/money.
- Interaction test: after backfill, `dedupeSchoolEnrollments` keeps the completed
  milestone records (one per type).
- Determinism via `Math.random` mock where needed.
- Full-suite regression (`npx tsc --noEmit` + `npx vitest run`).

## Follow-ups

- Family backfill (marriage + children).
- Wire remaining NPC spawns (coworkers, boss, friends).
- Education variety among adults (not everyone has a degree).
