A life simulation game written in Python. Create a character, watch them grow up, find love, build a career, buy a home, and pass their legacy on to the next generation — play in the terminal or through a modern web UI.
- Getting Started
- Web Version
- How to Play
- Project Structure
- Module Reference
- API Reference
- Frontend Architecture
- Game Systems
- Bug Fixes & Improvements
- Dependencies
| Requirement | Version | Used By |
|---|---|---|
| Python | 3.10+ | Game engine |
| Node.js | 18+ | Web frontend |
| colorama | any | Terminal colors |
# 1. Clone or download the project
cd Life_Sim
# 2. Create a virtual environment (recommended)
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS / Linux
# 3. Install Python dependencies
pip install colorama
# 4. Run the terminal game
python run.pyThe web version wraps the same game engine in a FastAPI backend and presents it through a Nuxt 3 frontend with a dark-themed, responsive dashboard.
# Install all dependencies and start both servers
python start_web.pyThis launches:
- API server →
http://localhost:8000(FastAPI + Uvicorn) - Frontend dev server →
http://localhost:3000(Nuxt 3)
# Backend
pip install fastapi uvicorn[standard] colorama
# (or: pip install -r api/requirements.txt)
# Frontend
cd frontend
npm install
cd ..
# Start API (from project root)
uvicorn api.main:app --host 0.0.0.0 --port 8000
# Start frontend (in a second terminal)
cd frontend
npm run dev| Feature | Description |
|---|---|
| Dashboard layout | 3-column grid: Event Log · Player Cards · NPC list |
| Mobile responsive | Stacked layout with smart column ordering on small screens |
| Event timeline | Scrollable, color-coded, year-grouped event log (max viewport) |
| Auto-scroll | Event log scrolls to the latest year automatically |
| Clickable names | Person names in events are links — click to open their card |
| Category icons | Each event shows an emoji icon matching its category |
| Person modal | Full detail view for any character with family navigation |
| Auto-advance | Toggle to automatically progress years (1.5s interval) |
| Keyboard shortcuts | Press Space or Enter to advance to the next year |
| Tombstones | Deceased important characters displayed with memorial cards |
| Succession | Seamless control transfer when the player character dies |
| Event bus | Structured events with real names — no pronoun ambiguity |
The game is fully automatic — each year advances with a single press of Enter. In the web version you can also press Space to advance, or toggle Auto-advance for hands-free play. You observe your character's life unfold: school, university, career, relationships, home purchases, car ownership, and eventually death.
When the player character dies, control transfers to their eldest living child, preserving the family legacy. If no children survive, the oldest person in the simulation takes over.
Important deceased characters are displayed as tombstones (🪦) so you can track your family history.
| Mechanic | Description |
|---|---|
| Stats | Health ❤️, Happiness 😀, Intelligence 🧠, Mental Health ☮️ |
| Personality | Goodness, Lust, Ambition (0–100 each) |
| Aspirations | Family, Wealth, Knowledge, Romance, Career |
| Education | University enrollment at 18 if smart ≥ 50; 4-year degree |
| Work | Three job tiers with promotion paths |
| Housing | Parents → Apartment → House |
| Cars | Economy / Luxury / Supercar tiers |
| Legacy | Inheritance splits among spouse + children on death |
Life_Sim/
├── run.py # Terminal game entry point
├── start_web.py # Launches API + frontend servers
├── README.md # This file
├── .gitignore
│
├── lifesim/ # Core game engine package
│ ├── __init__.py
│ ├── constants.py # Enumerations & static config
│ ├── utils.py # Shared helpers & person factory
│ ├── assets.py # Car / Manufacturer definitions
│ ├── systems.py # Education, Housing, Work subsystems
│ ├── person.py # Central Person class
│ ├── events.py # Random events, death, inheritance
│ ├── event_bus.py # Structured event emitter (replaces print)
│ ├── display.py # Terminal rendering / UI output
│ └── sim.py # Terminal simulation loop
│
├── api/ # Web backend (FastAPI)
│ ├── main.py # FastAPI app, routes, CORS
│ ├── engine.py # Headless engine wrapper (event bus)
│ ├── requirements.txt # Python dependencies for the API
│ └── test_engine.py # Engine smoke tests
│
└── frontend/ # Web frontend (Nuxt 3)
├── nuxt.config.ts # Nuxt configuration
├── tailwind.config.ts # Tailwind CSS theme & colors
├── package.json
├── tsconfig.json
├── app.vue # Root Vue component
├── pages/
│ └── index.vue # Main dashboard page
├── components/
│ ├── PlayerCard.vue # Player identity & portrait
│ ├── StatsCard.vue # Health, happiness, etc. bars
│ ├── CareerCard.vue # Work, education, housing info
│ ├── FinanceCard.vue # Bank balance, car, assets
│ ├── FamilyCard.vue # Partner & children list
│ ├── EventLog.vue # Scrollable event timeline
│ ├── NPCList.vue # Non-player character sidebar
│ ├── TombstoneList.vue # Deceased character memorials
│ ├── PersonModal.vue # Full person detail overlay
│ └── StatBar.vue # Reusable progress bar
├── composables/
│ └── useGame.ts # Game state management & API calls
└── types/
└── game.ts # TypeScript interfaces
run.py
└── sim.py
├── constants.py
├── utils.py ──────► person.py (lazy import)
├── event_bus.py
├── events.py
│ └── constants.py
├── display.py
│ └── constants.py
└── person.py
├── constants.py
├── event_bus.py
├── systems.py
│ ├── constants.py
│ └── event_bus.py
├── assets.py
├── utils.py
└── events.py (lazy import in get_older)
start_web.py
├── api/main.py
│ └── api/engine.py
│ ├── lifesim/* (same engine, event bus in silent mode)
│ └── event_bus.collect() → structured event dicts
└── frontend/
└── pages/index.vue
├── composables/useGame.ts → HTTP → api/main.py
└── components/*.vue
Circular-import strategy:
construct_person()lives inutils.pyand uses a lazy import ofPersonfromperson.pyinside the function body, breaking the import cycle.
Static configuration — no runtime state.
| Symbol | Type | Description |
|---|---|---|
Gender.MALE |
str |
'M' |
Gender.FEMALE |
str |
'F' |
Gender.label(code) |
static | Returns "boy" / "girl" for display |
RelationshipStatus |
class | SINGLE, IN_RELATIONSHIP, MARRIED, DIVORCED, WIDOWED |
SpecialRole |
class | PLAYER, FATHER, MOTHER, BROTHER, SISTER, SON, DAUGHTER, PARTNER, GIRLFRIEND, BOYFRIEND, WIFE, HUSBAND |
Aspiration |
class | FAMILY, WEALTH, KNOWLEDGE, ROMANCE, CAREER + ALL list |
Shared helpers and the person factory.
| Symbol | Signature | Description |
|---|---|---|
clamp() |
clamp(value, lo=0, hi=100) → int |
Clamp a numeric value into a bounded range |
construct_person() |
construct_person(min_age, max_age, gender, special=None, family_name=None) → Person |
Safe factory with bounds guard |
MALE_NAMES |
list[str] |
26 male first names |
FEMALE_NAMES |
list[str] |
24 female first names |
LAST_NAMES |
list[str] |
24 surnames |
Vehicle definitions and the car-selection factory.
| Symbol | Description |
|---|---|
Manufacturer |
Data holder: name, _cars, _prices, random_car() |
Car |
Owned vehicle: manufacturer, model, price, years_owned |
choose_car() |
choose_car(price_point) → (name, model, price) — picks from economy / luxury / sports catalogues |
Catalogues (module-level, pre-built):
| Catalogue | Brands | Price Range |
|---|---|---|
_ECONOMY |
Toyota, Nissan, Honda, Ford, Chevrolet, Hyundai | $16k – $40k |
_LUXURY |
BMW, Mercedes, Audi, Lexus, Tesla | $40k – $140k |
_SPORTS |
McLaren, Lamborghini, Ferrari, Bugatti, Porsche | $120k – $3M |
Subsystem classes — each holds a back-reference to its owning Person.
| Method | Trigger | Effect |
|---|---|---|
university_check() |
Age == 18, smart ≥ 50 | Enrolls in university, takes $40k loan |
progress() |
Each year | Increments study years; graduates at 4 |
pay_loan() |
Each year | Deducts $3k/yr until loan is $0 |
| Method | Trigger | Effect |
|---|---|---|
check_housing() |
Each year | Parents → Apartment (age 22+) → House |
pay_costs() |
Each year | Rent ($12k/yr) or mortgage ($15k/yr) |
| Tier | Example Roles | Salary Range |
|---|---|---|
| 1 | CEO, CTO, Surgeon, Judge | $150k – $500k |
| 2 | Developer, Engineer, Nurse, Teacher | $45k – $85k |
| 3 | Intern, Janitor, Cashier, Barista | $15k – $30k |
| Method | Description |
|---|---|
payment() |
Adds yearly salary to bank balance |
check_promotion() |
Evaluates tenure + stats + aspiration for tier climb |
Promotion from Tier 3→2 requires 4–10 years; Tier 2→1 requires
10–30 years, influenced by smart, ambition, degree, and the
Career aspiration.
The central Person class.
| Attribute | Type | Description |
|---|---|---|
alive |
bool |
Living/dead flag |
is_controlled |
bool |
Currently the player character |
was_player |
bool |
Was ever player-controlled |
important |
bool |
Preserved in display after death |
parents |
list[Person] |
Father + Mother references |
children |
list[Person] |
All biological children |
partner |
Person | None |
Current romantic partner |
status |
str |
RelationshipStatus value |
aspiration |
str |
Lifetime goal (influences behaviour) |
| Method | Called By | Purpose |
|---|---|---|
get_older(people_list) |
Simulation |
Full yearly lifecycle tick |
clamp_stats() |
Internal | Keeps all stats within 0–100 |
death(death_year) |
death_check() |
Marks person dead, widows partner |
_handle_relationships() |
get_older() |
Partner finding, breakups, marriage, kids |
_try_buy_car() |
get_older() |
Attempt vehicle purchase |
World events that act on a Person from the outside.
| Function | Signature | Description |
|---|---|---|
random_event(person) |
person: Person → None |
1-in-50 chance of a lottery/mugging/etc. |
death_check(person, yr) |
person: Person, current_year: int → bool |
Age + health mortality roll; returns True if died |
distribute_inheritance(deceased, people) |
deceased: Person, people: list → None |
Splits estate among spouse + children |
Lightweight event bus that replaces raw print() calls throughout the
game code. All gameplay events (promotions, deaths, relationships, etc.)
are emitted as structured dicts instead of being printed to stdout.
| Symbol | Signature | Description |
|---|---|---|
emit() |
emit(category, message, person=None) |
Record one event with category + person ref |
collect() |
collect() → list[dict] |
Return all pending events and clear the buffer |
set_mode() |
set_mode("console" | "silent") |
Console mode also prints; silent collects only |
set_context() |
set_context(person) |
Auto-attach person to subsequent emit() calls |
clear() |
clear() |
Discard all pending events |
Modes:
| Mode | Behaviour | Used By |
|---|---|---|
console |
Print with emoji + colour and collect | Terminal game |
silent |
Collect only (no stdout) | Web API engine |
Categories: death, inheritance, love, marriage, breakup,
baby, family, career, education, housing, finance, car,
event, mental, info.
All terminal output formatting.
| Function | Purpose |
|---|---|
render_player_card() |
Full HUD with stats, job, housing, car, bank |
render_npc_card() |
Compact one-liner for non-player characters |
render_tombstone() |
Memorial line for deceased important characters |
render_year_header() |
Year banner with alive population count |
The game engine — owns year and people, orchestrates the loop.
| Method | Description |
|---|---|
start() |
Creates family, enters infinite year loop |
_create_family() |
Generates father, mother, player, optional siblings |
_next_year() |
Ages everyone, checks death, renders, awaits input |
_handle_succession() |
Transfers control to heir after player death |
The FastAPI backend (api/main.py) exposes a JSON API that the
frontend consumes. Game state is held in-memory per session.
| Method | Path | Description |
|---|---|---|
| POST | /api/game/new |
Create a new game — returns initial state |
| POST | /api/game/{id}/advance |
Advance one year — returns updated state |
| GET | /api/game/{id}/state |
Fetch current state without advancing |
| GET | /api/game/{id}/person/{pid} |
Fetch full details for a single person |
Events are emitted by the game engine via the event bus
(lifesim/event_bus.py). Each event is a structured dict with the
person's actual name baked into the message — no pronoun rewriting
or stdout capture required.
{
"category": "career",
"message": "Promotion! Alice got promoted to a Developer!",
"person_id": "a1b2c3d4",
"person_name": "Alice Smith"
}Categories: death, inheritance, love, marriage, breakup,
baby, family, career, education, housing, finance, car,
event, mental, info.
Every event includes the character's full name directly in the message text — no ambiguous "He" / "She" / "I" pronouns.
| Class / Function | Purpose |
|---|---|
GameEngine |
Wraps the lifesim engine for API use |
_bus_events_to_dicts() |
Converts event bus entries → JSON-safe dicts with IDs |
_person_id() |
Returns a stable UUID-based ID for each person |
serialise_person() |
Converts a Person instance to a JSON-safe dict |
Built with Nuxt 3, Vue 3, Tailwind CSS, and TypeScript.
| Component | Purpose |
|---|---|
PlayerCard.vue |
Hero card showing player name, age, gender, aspiration |
StatsCard.vue |
Four stat bars (health, happiness, intelligence, mental) |
CareerCard.vue |
Work role, education status, housing type |
FinanceCard.vue |
Bank balance, car info, financial summary |
FamilyCard.vue |
Partner and children with clickable links |
EventLog.vue |
Scrollable, year-grouped, color-coded event timeline |
NPCList.vue |
Sidebar listing all non-player characters |
TombstoneList.vue |
Memorial cards for deceased important characters |
PersonModal.vue |
Full-detail overlay with family tree navigation |
StatBar.vue |
Reusable animated progress bar with color theming |
Manages all game state and API communication:
| Export | Type | Description |
|---|---|---|
state |
Ref<GameState> |
Reactive game state |
loading |
Ref<boolean> |
Request in-flight flag |
autoAdvance |
Ref<boolean> |
Auto-year toggle |
newGame() |
() → Promise |
Start a fresh game |
advanceYear() |
() → Promise |
Progress one year |
fetchPerson(id) |
(id) → Promise<Person> |
Fetch full person details |
player |
ComputedRef |
Current player character |
npcs |
ComputedRef |
Non-player characters |
tombstones |
ComputedRef |
Deceased important characters |
allEvents |
ComputedRef |
Full event history |
latestEvents |
ComputedRef |
Events from the current year |
| Breakpoint | Layout |
|---|---|
| Mobile | Single column — Player cards → This Year/NPCs → Event Log |
sm (640px) |
Two-column stat/career/finance grids |
lg (1024px) |
Full 3-column dashboard — viewport-height locked, all columns independently scrollable |
| Aspiration | Stat Nudge | Gameplay Effect |
|---|---|---|
| Family | Goodness +10 | Higher child/sibling chance, earlier marriage |
| Wealth | Ambition +20 | Buys cars more often, lower car thresholds |
| Knowledge | Smart +10 | 3× university enrollment chance |
| Romance | Lust +20 | Finds partners faster, more relationship events |
| Career | Ambition +20 | Faster promotions, 2× enrollment chance |
| Condition | Added Death Chance (per 1000) |
|---|---|
| Age > 40 | 10 |
| Age > 65 | 50 |
| Age > 80 | 100 |
| Age > 100 | 500 |
| Age ≥ 115 | Guaranteed |
| Health < 20 | +200 |
| Health < 10 | +300 (cumulative) |
| Health ≤ 0 | +1000 (guaranteed) |
| Freak accident (16+) | 1-in-500 |
- Estate = bank balance + 40% car value + (house value − mortgage)
- If married spouse is alive → included in heir list
- All living children → included in heir list
- Estate split equally among all heirs
The modular rewrite addressed 11 bugs found in the original monolithic codebase:
| # | Bug | Fix |
|---|---|---|
| 1 | Person never initialised self.parents |
Added self.parents = [] in __init__ |
| 2 | death_check() ran on already-dead people |
Early if not person.alive: return False guard |
| 3 | Breaking up removed important partners forever | Guard: skip removal if ex.important or ex.was_player |
| 4 | construct_person crashed on negative age range |
max_age = max(max_age, min_age) + max(16, …) partner guard |
| 5 | Marriage proposal fired repeatedly each year | self.status != MARRIED check before proposing |
| 6 | relationship_status property was unused |
Replaced with self.status, set on partnering + breakup |
| 7 | buy_car() was defined but never called |
Integrated as _try_buy_car() in get_older() cycle |
| 8 | Population count included dead important people | Uses alive_count = sum(… if p.alive) for header |
| 9 | smart stat overflowed above 100 |
clamp_stats() applied after every mutation |
| 10 | Unused imports scattered across files | Clean imports in every new module |
| 11 | Sibling birth printed raw gender code M/F |
Replaced with Gender.label() → "boy" / "girl" |
| + | Partner left as IN_RELATIONSHIP after death |
Now set to WIDOWED in Person.death() |
| + | Events showed "He/She/I" instead of names | Event bus emits person's real name in every message |
| + | Web events had --CATEGORY prefixes |
Replaced stdout capture with structured event bus |
| + | person_id missing on many web events |
Event bus attaches person ref; engine maps to stable ID |
| + | Event log expanded past viewport on desktop | Dashboard locked to viewport height with overflow scroll |
| + | Random events only fired for the player | random_event() now fires for all alive characters |
| Package | Purpose | Install |
|---|---|---|
colorama |
Cross-platform terminal colors | pip install colorama |
fastapi |
Web API framework | pip install fastapi |
uvicorn[standard] |
ASGI server | pip install uvicorn[standard] |
pip install -r api/requirements.txt| Package | Purpose |
|---|---|
nuxt ^3.14 |
Vue 3 meta-framework |
vue ^3.5 |
Reactive UI library |
@nuxtjs/tailwindcss |
Utility-first CSS |
@nuxtjs/google-fonts |
Font loading |
typescript ^5.6 |
Type safety |
cd frontend && npm installBuilt with ❤️ in Python + Vue.