sagaphone/spec/data-model.md
Tim 06df9df339 Initial spec for Sagaphone
Pulled from Notion project docs: overview, architecture, data model,
event system, line logic, and implementation notes for the family-tree
generator + TTS narrator.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 22:04:33 +02:00

4.4 KiB
Raw Blame History

Data Model

All identifiers and code are in English.

Enums

class Gender(IntEnum):
    MALE = 0
    FEMALE = 1
    NONBINARY = 2

class PartnerType(IntEnum):
    MARRIAGE = 0
    AFFAIR = 1
    POLITICAL = 2
    ILLEGITIMATE = 3

class StageType(IntEnum):
    CHILD = 0
    TEEN = 1
    YOUNG_ADULT = 2
    ADULT = 3
    SENIOR = 4

class Event(IntEnum):
    # Category 1 — Death
    DEATH_ILLNESS = 10
    DEATH_ACCIDENT = 11
    DEATH_COMBAT = 12
    DEATH_OLD_AGE = 13
    DEATH_CHILDBIRTH = 14       # mother dies during birth

    # Category 2 — Partnership
    PARTNER_MARRIAGE = 20
    PARTNER_AFFAIR = 21
    PARTNER_ENGAGEMENT = 22
    PARTNER_POLITICAL = 23
    PARTNER_CHILDHOOD_PROMISE = 24

    # Category 3 — Offspring
    CHILD_BORN = 30
    CHILD_TWINS = 31
    CHILD_TRIPLETS = 32
    CHILD_STILLBORN = 33
    CHILD_ADOPT = 34         # only path to offspring for NONBINARY; from YOUNG_ADULT onwards

    # Category 4 — Daily Life
    DAILY_FRIENDSHIP = 40
    DAILY_FAMILY = 41
    DAILY_TEMP_CHAR = 42

    # Category 5 — Travel
    TRAVEL_NEAR = 50
    TRAVEL_FAR = 51
    TRAVEL_PILGRIMAGE = 52

    # Category 6 — Learning
    LEARN_APPRENTICESHIP = 60
    LEARN_MENTOR = 61
    LEARN_SELF = 62

    # Category 7 — Conflict (personal)
    CONFLICT_PERSONAL = 70
    CONFLICT_FIGHT = 71
    CONFLICT_FEUD = 72

    # Category 8 — Discovery
    DISCOVER_OBJECT = 80
    DISCOVER_SPELL = 81
    DISCOVER_WONDER = 82
    DISCOVER_KNOWLEDGE = 83

    # Category 9 — Local Conflict
    LOCAL_REBELLION = 90
    LOCAL_PLAGUE = 91
    LOCAL_DISASTER = 92
    LOCAL_CRISIS = 93

    # Category 10 — Intrigue
    INTRIGUE_POLITICAL = 100
    INTRIGUE_EXILE = 101
    INTRIGUE_CONSPIRACY = 102

    # Category 11 — Illness / Injury
    ILLNESS_WEAK = 110
    ILLNESS_SEVERE = 111
    ILLNESS_CHRONIC = 112
    INJURY_ACCIDENT = 113
    INJURY_COMBAT = 114

event_type // 10 → category, event_type % 10 → subtype within category.

Person

Person {
  id: str
  name: str
  gender: Gender
  birth_year: int            # absolute, set when person is created via parent event
  death_year: int | None     # birth_year + age_at_death
  alive: bool
  appearance: dict
  profession: str | None     # set by a LEARN event
  home: str
  is_ancestor: bool          # True only for Person 1
  parent_id: str | None      # previous active node; None = joined from outside
  generation: int
  partners: PartnerEntry[]
  events: str[]              # event IDs in chronological order
  acquaintances: str[]       # IDs from person_pool only (not family tree)
  stages: Stage[]
}

PartnerEntry {
  person_id: str
  type: PartnerType
  children_ids: str[]        # all children from this union
}

Stage

Stage {
  type: StageType
  age: { from: int, to: int }  # relative to person
  events: str[]                # event IDs
}

Stage Age Ranges

Stage Age
Child 012
Teen 1216
Young Adult 1632
Adult 3250
Senior 50X (LLM decides)

EventEntry

EventEntry {
  id: str
  type: Event                  # e.g. Event.ILLNESS_SEVERE
  age: int                     # age of person at time of event
  # absolute_year: @property -> person.birth_year + self.age
  location: str | None         # None = takes place at person.home
  location_is_temp: bool       # True = travel destination, False = known location
  participants: str[]          # person_ids or pool_ids
  result: dict                 # predetermined simulation outcome
  context: dict                # extra info passed to LLM prompt
  follow_bonus: Event[]        # event types made more likely after this event
                               # e.g. after TRAVEL_FAR -> [DISCOVER_OBJECT, DISCOVER_WONDER]
}

FamilyTree & person_pool

FamilyTree {
  persons: dict[str, Person]       # all family tree persons
  person_pool: dict[str, Person]   # temporary chars with no tree membership
                                   # e.g. random villager
                                   # distant cousin -> lives in persons, not here
  world: World
  current_generation: int
  active_node: str                 # person_id of currently narrated person
}

World {
  start_year: int            # randomly generated, max 4 digits, no calendar system stated
  name: str
  epoch_flavor: str          # setting keywords passed to LLM
}