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
135 changes: 135 additions & 0 deletions docs/game-design/RESEARCHER_QUIRKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Researcher Quirks

*The hidden-but-true rider layer on researchers. Replaces the retired placeholder
"trait / perk" system.*

Status: implemented on `feat/hiring-phase-b-pipeline` (PR #664). Data:
`godot/data/researchers/quirks.json`. Loader: `godot/scripts/core/quirk_catalogue.gd`.
Model: `godot/scripts/core/researcher.gd` (`quirk`, `quirk_known`, `quirk_effect()`).

## What a quirk is

A **quirk** is a rare, thematically-grounded lab archetype riding on a researcher: a
philosophical stance, a working style, a political tic. Each carries a **small, legible
mechanical effect** and is **hidden but TRUE** -- it is real from the moment the researcher
is created; play only *reveals* it. The sim never lies (BUILD_BRIEF_HIRING_PIPELINE.md:
"Hidden info is TRUE-but-incomplete -- interviewing reveals, it does not fabricate"). A
leaker leaks from day one; you just do not yet know *who* or *why*.

Quirks sit alongside, but are distinct from, the five **appetites**
(compute / prestige / mentees / money / mission_purity, ADR-0011 sec.8). An appetite is a
0..1 negotiation hunger revealed by *interviewing*; a quirk is a discrete rider revealed by
an *exposure* (an incident, or simply time on the team) -- **never** by the interview ladder
(A2). A fully-interviewed hire can still be hiding a quirk.

Register is held to Papers-Please deadpan: **"bother," not HR gravity** (WORLD_AND_LORE.md).
Quirks are archetype-flavours, never portraits of real people (the event-horizon guardrail).

## The catalogue (14 quirks)

Valence key: `+` positive, `-` negative, `+/-` double-edged (the best ones cut both ways).

| # | id | name | v | one-line flavour | hidden effect (legible once revealed) | appetite | reveal |
|---|----|------|---|------------------|----------------------------------------|----------|--------|
| 1 | `secret_successionist` | Secret Successionist | - | Believes the machines should inherit the earth -- and is quietly at peace with it. | ships fast (+12% own productivity) but quietly discounts safety (+0.04 doom) | mission_purity | incident (an old commit's half-joke manifesto) |
| 2 | `doom_absolutist` | Doom Absolutist | +/- | P(doom) is 0.99 and they will cc the whole company to say so. | their diligence lowers doom (-0.06) but the moralizing drags the room (-3% team) | mission_purity | tenure (~8 turns) |
| 3 | `e_acc_sympathizer` | e/acc Sympathizer | - | Wants to feel the AGI. "Also helps safety, really." | burns compute, ships (+15% own) but pushes danger (+0.05 doom) | compute | incident (unsanctioned scaling run) |
| 4 | `open_science_zealot` | Open-Science Zealot | +/- | Information wants to be free; the NDA was a suggestion. | spreads knowledge (+2% team) but leaks on principle (3%/turn) | prestige | leak (a preprint legal never saw) |
| 5 | `secrecy_maximalist` | Secrecy Maximalist | +/- | Compartmentalizes everything, including from you. | heads-down focus (+5% own) but silos the team (-4% team) | mission_purity | tenure (~6 turns) |
| 6 | `empire_builder` | Empire Builder | +/- | Every problem is solved by hiring three more reports. | grows the team (+4% team) but drives everyone hard (+0.3 burnout/turn) | prestige | tenure (~7 turns) |
| 7 | `runs_hot` | Runs Hot | +/- | Ships twice the work and burns at twice the rate. | +20% own productivity, +2.0 burnout/turn | -- | tenure (~6 turns) |
| 8 | `lab_parent` | Lab Parent | + | The one who actually onboards the juniors nobody else will. | +6% productivity to the whole team | mentees | tenure (~5 turns) |
| 9 | `loose_lips` | Loose Lips | - | A delightful conversationalist with a poor sense of what was confidential. | 5%/turn chance to leak research | prestige | leak (a detail in a rival deck) |
| 10 | `sponge` | Sponge | + | Absorbs a whole subfield over a long weekend. | 2.5x skill-growth rate | mentees | tenure (~10 turns) |
| 11 | `cat_whisperer` | Cat Whisperer | + | The office cat is calmest at their desk -- a small mercy against the doom. | +3% team, -0.5 burnout/turn (cancels the base) | -- | tenure (~4 turns) |
| 12 | `glory_hound` | Glory Hound | - | First author or no author; co-authors are load-bearing furniture. | +10% own but demoralizes co-authors (-4% team) | prestige | incident (authorship dispute) |
| 13 | `quiet_quitter` | Quiet Quitter | - | Present, paid, and quietly checked out. | -15% own productivity, -1 loyalty/turn (flight risk) | money | tenure (~6 turns) |
| 14 | `true_believer` | True Believer | + | Would do this for free, and nearly does; recruiters bounce off. | -0.04 doom, +1 loyalty/turn (poach-resistant) | mission_purity | tenure (~8 turns) |

Deliberate spread: 4 positive, 5 negative, 5 double-edged. The two secrecy poles
(`open_science_zealot` vs `secrecy_maximalist`) and the three mission-purity registers
(`true_believer` / `doom_absolutist` / `secret_successionist`) map the doomer-vs-accelerationist
axis onto real hiring dilemmas.

## Effect channels

A quirk's effect is a small fixed set of keys the sim reads through
`Researcher.quirk_effect(key, default)`. No stat soup -- each quirk touches one or two:

| channel | default | where it hooks | sign meaning |
|---------|---------|----------------|--------------|
| `self_productivity_mult` | 1.0 | `get_effective_productivity()` | >1 faster, <1 slower |
| `burnout_per_turn_add` | 0.0 | `process_turn()` (on top of base 0.5) | + burns, - relieves |
| `doom_mod_add` | 0.0 | `get_doom_modifier()` | + raises doom, - lowers |
| `leak_chance` | 0.0 | `turn_manager` roster loop | per-turn leak probability |
| `team_productivity_add` | 0.0 | `turn_manager` team bonus | + lifts team, - drags it |
| `skill_growth_mult` | 1.0 | `process_turn()` skill roll | >1 grows faster |
| `loyalty_per_turn_add` | 0 | `process_turn()` loyalty drift | + retains, - flight-risk |

These are the **exact hook points the retired traits used**, so wiring quirks in was a swap,
not new sim surface. The effect is live even while the quirk is hidden -- the player observes
the *output* (a leak, a productivity dip) before learning the *cause*.

## Reveal mechanics

Quirks flip `quirk_known` (never fabricated -- the value pre-exists) via:

1. **Tenure fallback (deterministic, always present).** Every quirk carries
`reveal.after_turns`; once `turns_employed` reaches it, `process_turn()` surfaces the
quirk (`maybe_reveal_quirk_by_tenure()`). This guarantees no rider stays invisible
forever, and is fully seed-reproducible (ADR-0006).
2. **Leak incident.** When a leak-risk quirk (`leak_chance > 0`) actually fires a leak,
`turn_manager` calls `expose_quirk()` that turn -- the incident names the culprit.
3. **Exposure events.** `reveal.via = "incident"` quirks are tagged for the existing
ADR-0003 exposure-event genre (`expose_quirk()`); the tenure fallback still guarantees
eventual reveal until a bespoke event exists.

Interviewing (the reveal ladder) deliberately does **not** surface quirks (A2 contract,
`test_quirk_hidden_even_when_fully_interviewed`).

## Starters (the "starter pokemon")

The four turn-0 founders each get a **guaranteed hidden rider** (`game_state.gd
_assign_guaranteed_rider`): a coin-flip between a strong appetite and a **catalogue quirk**
(`QuirkCatalogue.pick_id`). Riders start hidden; the depth is latent for late-game drama.

## Determinism (ADR-0006)

- Assignment draws only from the seeded RNG. `QuirkCatalogue.pick_id()` indexes a **sorted**
id list, so the pick is independent of JSON/Dictionary iteration order.
- In `generate_random`, the quirk draw reuses the same two hidden-RNG draws (chance +
index) the old pool used, so the RNG stream is byte-identical downstream -- only the
resulting id changes.
- `quirk` / `quirk_known` serialize in `to_dict`/`from_dict` and round-trip through JSON.
- Replay verification (`test_replay_verification`) is run-vs-replay, and both tiers pass.

## Legacy traits: retired vs kept-reframed

The old `POSITIVE_TRAITS` / `NEGATIVE_TRAITS` dicts, the `traits` array, `add_trait` /
`has_trait` / `get_trait_description`, and the dead `_assign_random_traits` assigner are
**removed**. Their effect hooks were repointed at the quirk channels above. Pip: "happy to
move away from the perk thinking ... very placeholdery and shallowly designed."

**Kept by reframing into a quirk** (the ones worth keeping):

| legacy trait | -> quirk | why kept |
|--------------|----------|----------|
| `workaholic` (+prod, +burnout) | `runs_hot` | the cleanest double-edged; classic |
| `team_player` (+team prod) | `lab_parent` | team-wide lift, now with a mentees tie-in |
| `leak_prone` (leaks) | `loose_lips` | anchors the secrecy/leaks theme |
| `fast_learner` (skill growth) | `sponge` | keeps the growth-over-time hook |
| `safety_conscious` (-doom) | folded into `true_believer` | doom-diligence, now with loyalty |

**Retired outright** (shallow / redundant / tied to other systems):
`prima_donna` (salary-threshold micro-penalty), `burnout_prone` (subsumed by `runs_hot` /
per-quirk burnout), `pessimist` (flat morale hit, no depth), `media_savvy` (paper-quality /
reputation bonus -- a press-facing quirk can re-add it later via a dedicated channel),
`road_warrior` (jet-lag-specific; the jet-lag system stands on its own).

## Follow-ups (not in this pass)

- Bespoke exposure *events* per `incident` quirk (currently tenure-fallback only).
- The placeholder staff-perks panels (`staff_perks_panel.gd`, `staff_perks_compact.gd`)
are decoupled from the removed `traits` field and now show empty slots; a future pass can
rebuild them on the quirk layer (or delete them).
- A press/reputation channel to re-home the retired `media_savvy` effect.
18 changes: 18 additions & 0 deletions godot/autoload/event_service.gd
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,15 @@ func _transform_event(raw: Dictionary) -> Dictionary:
"cooldown_turns": rarity_settings.get("cooldown_turns", 20)
}

# P0 feed-flooding fix (playtest 2026-07-17): the entire arxiv/technical-research-breakthrough
# deck (200+ entries) fires as flavour and drowns real, actionable events in the feed. Demote
# that stream to its own low-severity "flavour" channel on the FEED tier so it never demands a
# decision and can be filtered out of the default feed view. Conservative match: only arxiv_*
# ids and the technical_research_breakthrough category (genuine policy/incident events untouched).
if _is_flavour_event(raw, category):
game_event["delivery_tier"] = "feed"
game_event["channel"] = "flavour"

# Add reactions if present (for UI display)
if raw.has("safety_researcher_reaction"):
game_event["safety_researcher_reaction"] = raw["safety_researcher_reaction"]
Expand All @@ -476,6 +485,15 @@ func _transform_event(raw: Dictionary) -> Dictionary:
return game_event


func _is_flavour_event(raw: Dictionary, category: String) -> bool:
"""True for the low-stakes arxiv/research-breakthrough flavour stream that floods the feed.
Kept deliberately narrow so real events (policy, incidents, org founding) are never demoted."""
if category.to_lower() == "technical_research_breakthrough":
return true
var id_str := str(raw.get("id", ""))
return id_str.begins_with("arxiv")


func _calculate_significance(raw: Dictionary) -> int:
"""Calculate significance score from impacts array"""
if not raw.has("impacts") or not raw["impacts"] is Array:
Expand Down
35 changes: 35 additions & 0 deletions godot/data/actions/core.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,41 @@
"costs": {},
"category": "influence",
"is_submenu": true
},
{
"id": "advertise",
"name": "Advertise a Role",
"description": "SOURCE: spend money + Attention to run a hiring campaign. Candidates trickle into the pool over the coming months (also raises your visibility).",
"costs": {},
"category": "hiring"
},
{
"id": "use_connections",
"name": "Work Your Connections",
"description": "SOURCE: spend a favor (reputation) + Attention for one fast, pre-vetted lead. Success scales with your standing; the favor is spent whether or not it lands.",
"costs": {},
"category": "hiring"
},
{
"id": "interview_next",
"name": "Interview a Candidate",
"description": "INTERVIEW: spend Attention to interview the least-known pool candidate (triage). Reveals more of their card after a few days. Hiring blind is legal but leaves more hidden.",
"costs": {},
"category": "hiring"
},
{
"id": "hire_best",
"name": "Make an Offer",
"description": "OFFER: offer at expectation to the strongest available candidate. They decide after a couple of days; if in range they accept, then onboard before becoming productive.",
"costs": {},
"category": "hiring"
},
{
"id": "onboard_next",
"name": "Onboard New Hires",
"description": "ONBOARD: spend Attention (+ money) on the checklist (laptop, visa, mentoring). Un-onboarded hires are barely productive; skipping mentoring risks early attrition.",
"costs": {},
"category": "hiring"
}
]
}
18 changes: 17 additions & 1 deletion godot/data/balance/defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,23 @@
"desperation_payroll": { "severity_base": 1200.0, "severity_spread": 800.0, "fuse_turns": 55, "interest_rate": 0.008 },
"staff_rider": { "principal": 1200.0, "fuse_turns": 88, "interest_rate": 0.001 },
"blackmail": { "principal_multiplier": 1.5, "fuse_turns": 22, "interest_rate": 0.02 },
"expose": { "rep_per_1000": 2.5, "governance_multiplier": 0.5 }
"expose": { "rep_per_1000": 2.5, "governance_multiplier": 0.5 },
"promise": {
"_description": "fix/promise-currency (Option B): appetite promises cost a FUTURE OBLIGATION IN THEIR OWN DOMAIN, never raw reputation. The old reputation-currency principal vs a starting rep of 50 (death at rep<=0) was an instant-death trap. Magnitudes are small/survivable; interest 0.0 (obligations, not compounding mortality levers). first_authorship->papers, compute_budget->compute, mission/mentorship->governance.",
"first_authorship": { "principal": 1.0, "fuse_turns": 10, "interest_rate": 0.0 },
"compute_budget": { "principal": 20.0, "fuse_turns": 8, "interest_rate": 0.0 },
"mission_charter": { "principal": 6.0, "fuse_turns": 12, "interest_rate": 0.0 },
"mentorship": { "principal": 4.0, "fuse_turns": 10, "interest_rate": 0.0 }
},
"resentment_debt": { "principal": 1500.0, "fuse_turns": 6, "interest_rate": 0.02 }
},
"hiring": {
"_description": "Hiring pipeline (BUILD_BRIEF_HIRING_PIPELINE Phase B): source -> interview -> offer -> onboard. Every stage is Attention-gated and duration-bearing (ADR-0009). advertise = money + slow multi-month trickle; connections = a reputation favor for one fast pre-vetted lead (success scales with standing). offer.* is the hidden self-worth range; onboarding.* gates a new hire's productivity.",
"advertise": { "cost_money": 8000.0, "cost_attention": 3, "campaign_months": 3, "per_month_min": 0, "per_month_max": 2, "reputation_gain": 1.0 },
"connections": { "cost_reputation": 6.0, "cost_attention": 2, "duration_ticks": 2, "rep_for_certainty": 80.0, "min_success": 0.05, "max_success": 0.95, "prevet_skill_min": 5, "prevet_skill_max": 8 },
"interview": { "cost_attention": 2, "duration_ticks": 3, "reveal_step": 1 },
"offer": { "cost_attention": 1, "duration_ticks": 2, "base_tolerance": 0.15, "money_appetite_weight": 0.5, "promise_discount": 0.20, "reject_scale": 2.0, "resentment_loyalty_penalty": 15, "read_spread": 0.06, "recruiter_min_skill": 4 },
"onboarding": { "foreign_modulo": 4, "laptop_money": 3000.0, "laptop_attention": 1, "visa_money": 5000.0, "visa_attention": 2, "mentoring_money": 0.0, "mentoring_attention": 2, "unproductive_multiplier": 0.4, "skimped_multiplier": 0.85, "attrition_risk": 0.15 }
},
"doom": {
"_description": "Doom momentum + per-source coefficients (doom_system.gd) and player-facing status cutoffs. Momentum is a low-commitment SWITCH (#638 ruling): momentum_enabled 0/1 kills the contribution, momentum_weight scales it; internal accumulators keep ticking while disabled so mid-run toggles behave.",
Expand Down
Loading
Loading