# Implementation Notes Snippets and implementation details for logic that may need tuning or adjustment later. Not the full codebase — only isolated pieces worth documenting separately. Values marked "tunable" are starting points, expected to change during playtesting. ## Event Weights & Stage Filters ### Hard Locks — events impossible at certain stages or genders Checked before any weight calculation. Events not listed here are always allowed regardless of stage. ```python STAGE_LOCKS: dict[Event, callable] = { # Offspring — not for CHILD/TEEN, not for SENIOR Event.CHILD_BORN: lambda s, p: s >= StageType.YOUNG_ADULT and s < StageType.SENIOR and p.gender != Gender.NONBINARY, Event.CHILD_TWINS: lambda s, p: s >= StageType.YOUNG_ADULT and s < StageType.SENIOR and p.gender != Gender.NONBINARY, Event.CHILD_TRIPLETS: lambda s, p: s >= StageType.YOUNG_ADULT and s < StageType.SENIOR and p.gender != Gender.NONBINARY, Event.CHILD_ADOPT: lambda s, p: s >= StageType.YOUNG_ADULT, Event.DEATH_CHILDBIRTH: lambda s, p: s >= StageType.YOUNG_ADULT and p.gender == Gender.FEMALE, # Death Event.DEATH_COMBAT: lambda s, p: s >= StageType.TEEN, Event.DEATH_OLD_AGE: lambda s, p: s == StageType.SENIOR, # Partnership Event.PARTNER_CHILDHOOD_PROMISE:lambda s, p: s <= StageType.TEEN, Event.PARTNER_MARRIAGE: lambda s, p: s >= StageType.TEEN, Event.PARTNER_AFFAIR: lambda s, p: s >= StageType.TEEN, Event.PARTNER_ENGAGEMENT: lambda s, p: s >= StageType.TEEN, Event.PARTNER_POLITICAL: lambda s, p: s >= StageType.TEEN, # Travel Event.TRAVEL_FAR: lambda s, p: s >= StageType.TEEN, Event.TRAVEL_PILGRIMAGE: lambda s, p: s >= StageType.YOUNG_ADULT, # Learning Event.LEARN_APPRENTICESHIP: lambda s, p: s >= StageType.TEEN, Event.LEARN_MENTOR: lambda s, p: s <= StageType.ADULT, # Conflict Event.CONFLICT_FEUD: lambda s, p: s >= StageType.YOUNG_ADULT, # Daily family — also requires living family members (see get_living_family) Event.DAILY_FAMILY: lambda s, p: True, # stage always ok, checked separately } ``` ### Same-sex Marriage Weight Reduction ```python SAME_SEX_MARRIAGE_MULTIPLIER = 0.3 # tunable def is_same_sex(person: Person, partner: Person) -> bool: # NONBINARY never counts as same-sex if person.gender == Gender.NONBINARY or partner.gender == Gender.NONBINARY: return False return person.gender == partner.gender ``` ### Follow Bonus Mechanism Follow bonuses are additive on top of the base event chance. ```python def get_effective_chance( event_type: Event, base_chance: float, active_follow_bonuses: dict[Event, dict[Event, float]] ) -> float: """ active_follow_bonuses: { trigger_event: { bonus_event: bonus_value } } Example: { Event.INJURY_COMBAT: { Event.ILLNESS_SEVERE: 0.10 } } """ bonus = sum( bonuses[event_type] for bonuses in active_follow_bonuses.values() if event_type in bonuses ) return base_chance + bonus ``` ## Living Family Lookup Used to check whether `DAILY_FAMILY` is valid, and to provide participant candidates for that event. Family = parents, siblings, own children, aunts/uncles (parent's siblings), cousins (children of aunts/uncles). ```python def get_living_family(person: Person, tree: FamilyTree, current_year: int) -> list[str]: candidates = set() # 1. Parents if person.parent_id: parent = tree.persons.get(person.parent_id) if parent: candidates.add(parent.id) # other parent = partner of parent from whom person descends for pe in parent.partners: if person.id in pe.children_ids: candidates.add(pe.person_id) # 2. Siblings = other children of same parents for pid in candidates.copy(): p = tree.persons.get(pid) if p: for pe in p.partners: candidates.update(pe.children_ids) candidates.discard(person.id) # 3. Own children for pe in person.partners: candidates.update(pe.children_ids) # 4. Aunts/Uncles (parent's siblings) + Cousins (their children) grandparent_ids = set() if person.parent_id: parent = tree.persons.get(person.parent_id) if parent and parent.parent_id: grandparent = tree.persons.get(parent.parent_id) if grandparent: grandparent_ids.add(grandparent.id) for pe in grandparent.partners: if parent.id in pe.children_ids: grandparent_ids.add(pe.person_id) for gid in grandparent_ids: grandparent = tree.persons.get(gid) if grandparent: for pe in grandparent.partners: for child_id in pe.children_ids: if child_id != person.parent_id: candidates.add(child_id) # aunt/uncle aunt_uncle = tree.persons.get(child_id) if aunt_uncle: for ape in aunt_uncle.partners: candidates.update(ape.children_ids) # cousins # filter: alive and already born return [ pid for pid in candidates if pid in tree.persons and tree.persons[pid].alive and tree.persons[pid].birth_year <= current_year ] ``` ## Location Generator Two functions: `generate_location(type)` when a specific type is needed, `generate_random_location()` when any type is fine. Returns a dict with `name` and `type` — used for `person.home` and event locations. ```python from enum import IntEnum import random class LocationType(IntEnum): VILLAGE = 0 TOWN = 1 CITY = 2 RIVER = 3 LAKE = 4 MOUNTAIN = 5 FOREST = 6 LANDMARK = 7 PREFIXES = { LocationType.VILLAGE: ["Stock", "Birch", "Ash", "Elm", "Green", "Black", "Cold", "Old"], LocationType.TOWN: ["New", "Old", "Chester", "Alden", "Iron", "Stone", "Crow"], LocationType.CITY: ["Lim", "Dur", "Solm", "Alten", "Harken", "Veld", "Orm"], LocationType.RIVER: ["Willow", "Silver", "Black", "Swift", "Cold", "Amber", "Dark"], LocationType.LAKE: ["Hark", "Mirror", "Grey", "Still", "Deep", "Dusk"], LocationType.MOUNTAIN: ["Feld", "Grey", "Iron", "Storm", "Frost", "Ash", "Grim"], LocationType.FOREST: ["Dark", "Elder", "Moss", "Thorn", "Whisper", "Hollow"], LocationType.LANDMARK: ["Grand", "Ancient", "Broken", "Lost", "Black", "Hollow"], } SUFFIXES = { LocationType.VILLAGE: ["heim", "dorf", "wick", "ford", "ton", "stead"], LocationType.TOWN: ["shire", "ham", "burg", "haven", "gate", "cross"], LocationType.CITY: ["burg", "mark", "hold", "spire", "gate", "wall"], LocationType.RIVER: ["creek", "brook", "run", "water", "stream", "beck"], LocationType.LAKE: ["lake", "mere", "pool", "water", "tarn"], LocationType.MOUNTAIN: ["fell", "peak", "stone", "berg", "crag", "tor"], LocationType.FOREST: ["wood", "forest", "grove", "thicket", "weald"], LocationType.LANDMARK: ["stone", "rock", "spire", "arch", "ruin", "mound"], } def generate_location(location_type: LocationType) -> dict: prefix = random.choice(PREFIXES[location_type]) suffix = random.choice(SUFFIXES[location_type]) return { "name": f"{prefix}{suffix}", "type": location_type } def generate_random_location() -> dict: location_type = random.choice(list(LocationType)) return generate_location(location_type) # Examples: # generate_location(LocationType.VILLAGE) -> {"name": "Ashwick", "type": LocationType.VILLAGE} # generate_location(LocationType.RIVER) -> {"name": "Willowcreek","type": LocationType.RIVER} # generate_location(LocationType.MOUNTAIN) -> {"name": "Frostcrag", "type": LocationType.MOUNTAIN} ``` **Tuning knobs:** - Expand `PREFIXES` and `SUFFIXES` lists per type for more variety. - `person.home` uses this dict directly: `{"name": "Ashwick", "type": LocationType.VILLAGE}`. ## Name Generator Called once when a `Person` object is created. Returns a full name string. No gender filtering needed — all titles use "the" and are gender-neutral in English. ```python import random FIRST_NAMES = [ "Julius", "Olaf", "Maria", "Edric", "Mira", "Bram", "Signe", "Aldric", "Freya", "Cassius", "Isolde", "Roran", "Thyra", "Leif", "Seren", "Eadric", "Wulfric", "Astrid", "Bjorn", "Ingrid", "Ragnar", "Elara", "Cedric", "Maren", "Aldis", "Torben", "Sigrid", "Halvard", "Liora" ] LAST_NAMES = [ "Voss", "Eisfeld", "Brunnwald", "Alliatus", "Andrine", "Kaltmar", "Steinholz", "Ashvale", "Dornwald", "Frey", "Ironwood", "Blackthorn", "Greymoor", "Coldwater", "Ashford", "Dunmore", "Ravenscar" ] TITLES = [ "the Butcher", "the Greedy", "the Bold", "the Wise", "the Unyielding", "the Gentle", "the Wanderer", "the Red", "the Pale", "the Scarred", "the Old", "the Young", "the Swift", "the Lame", "the Blind", "the Cruel", "the Just", "the Meek", "the Loud", "the Silent" ] LAST_NAME_CHANCE = 0.75 # tunable TITLE_CHANCE = 0.20 # tunable def generate_name() -> str: first = random.choice(FIRST_NAMES) last = random.choice(LAST_NAMES) if random.random() < LAST_NAME_CHANCE else None title = random.choice(TITLES) if random.random() < TITLE_CHANCE else None parts = [first] if last: parts.append(last) if title: parts.append(title) return " ".join(parts) # Possible outputs: # "Julius" # "Julius Voss" # "Julius Voss the Butcher" # "Olaf the Bold" # "Maria Andrine the Greedy" ``` **Tuning knobs:** - `LAST_NAME_CHANCE` — probability of having a last name. - `TITLE_CHANCE` — probability of having a title. - Expand `FIRST_NAMES`, `LAST_NAMES`, `TITLES` lists freely. ## Appearance Generator Called once when a `Person` object is created. Returns a dict stored in `person.appearance`. ```python import random HAIR_COLORS = [ "black", "dark brown", "brown", "auburn", "blonde", "grey", "white", "red" ] EYE_COLORS = [ "brown", "grey", "green", "blue", "hazel", "amber" ] BUILDS = [ "lean", "wiry", "stocky", "broad-shouldered", "slender", "heavyset", "average" ] FEATURES = [ "scar on left cheek", "crooked nose", "missing finger", "birthmark on neck", "unusually pale skin", "deep-set eyes", "prominent jaw", "freckles", "calloused hands", "walks with a slight limp", "unusually tall", "unusually short", ] def generate_appearance() -> dict: # weights: higher chance for fewer features # [0, 1, 2, 3, 4] -> [30%, 35%, 20%, 10%, 5%] k = random.choices([0, 1, 2, 3, 4], weights=[30, 35, 20, 10, 5])[0] features = random.sample(FEATURES, k=k) return { "hair": random.choice(HAIR_COLORS), "eyes": random.choice(EYE_COLORS), "build": random.choice(BUILDS), "feature": ", ".join(features) if features else None } ``` **Tuning knobs:** - `weights` list controls feature count distribution. - Add entries to `HAIR_COLORS`, `EYE_COLORS`, `BUILDS`, `FEATURES` to expand variety. - `feature` is `None` if no features rolled — LLM prompt should handle this gracefully.