Jump to content

Thousand Year Old Vampire: Difference between revisions

From Yusupov's House
Line 703: Line 703:


<blockquote>
<blockquote>
They arrived in numbers too vast to count—pressed between barrels, chained to holds slick with piss and seawater, branded, sick, broken. And the sailors were not much better.
They arrived in numbers too vast to count: pressed between barrels, chained to holds slick with piss and seawater, branded, sick, broken. And the sailors were not much better.


The ships would come in [...]
The ships would come in [...]

Revision as of 11:18, 12 April 2026

Infobox
nameThousand Year Old Vampire – Web Helper
developerMichel Vuijlsteke
programming languagePython (Django 5), TypeScript (Vue 3)
operating systemCross-platform (Web)
genreSolo RPG Digital Companion
licensecustom
websitehttps://tyov.yusupov.cloud

Thousand Year Old Vampire – Web Helper (TYOV-Web) is a modern web-based implementation of the solo tabletop role-playing game Thousand Year Old Vampire by Tim Hutchings. The application digitises the game's complex mechanics of memory, experience, and character development, allowing players to create and guide a vampire character across centuries of unlife through an interactive browser interface. The backend is built with Django 5 and Django REST Framework; the frontend is a Vue 3 single-page application.

Overview

Thousand Year Old Vampire is a solo storytelling RPG in which a player creates a vampire character and guides it through an increasingly fragmented existence spanning hundreds or thousands of years. The central tension lies in the vampire's deteriorating memory: as an immortal being it accumulates experiences, but its ancient mind can only retain a limited number of memories at any time. Players must constantly choose what to remember and what to forget.

TYOV-Web fully implements the game's rules—character creation, dice-driven prompt navigation, memory management, diary storage, skills, resources, marks, known characters, and game-ending conditions—in a reactive web interface with persistent server-side storage and a complete audit trail.

Game Rules

Core Concept

The player creates a vampire and progresses through numbered prompts (story scenarios). Each prompt asks the player to narrate what happens to the vampire during a particular era. The game is non-linear: dice rolls determine which prompt to visit next, and most prompts have multiple variations (A, B, C) to prevent repetition.

Character Creation

Character creation follows a guided six-step wizard:

Step Description
1. Mortal Life Establish the vampire's name, background, and mortal existence
2. Mortal Characters Define 2–4 NPCs from the vampire's human life (friends, mentors, rivals, lovers)
3. Skills Choose 2–3 starting skills representing mortal expertise
4. Resources Select initial possessions and locations
5. Combination Experience Write a pivotal mortal-life experience that ties characters, skills, and resources together
6. The Turning Narrate how and why the character was turned into a vampire

Memory System

The memory system is the heart of the game:

  • A character may hold a maximum of 5 active memories (adjustable by prompt rules and permanent effects).
  • Each memory can contain up to 3 experiences (individual narrative entries).
  • When the limit is exceeded, the player must forget (permanently lose) a memory, or move one to a diary (if the character possesses one).
  • A diary is a special resource that stores up to 4 additional memories outside the active limit.
  • If the diary resource is lost, all memories stored in it are also lost.
  • Some prompts grant or remove permanent memory slots (memory_slot_gain, memory_slot_loss).

Dice Rolling and Prompt Navigation

Each turn the player rolls two dice:

  • D10 (1–10) minus D6 (1–6) = result (range: −5 to +9).
  • A positive result moves forward that many prompts.
  • A negative result moves backward.
  • A result of zero stays at the current prompt number but selects the next unused variation.

Variations are visited in order A → B → C. If all variations of a prompt have been used, the system finds the next available variation in prompt order.

Turn Structure

Each game turn follows this sequence:

  1. Prompt Presentation – The current prompt text and any special rules are displayed.
  2. Dice Roll – The player rolls D10 − D6 to determine the next prompt.
  3. Experience Creation – The player writes at least one narrative experience for the current prompt.
  4. Character Updates – Skills, resources, marks, and known characters may be added, edited, checked, or removed.
  5. Memory Management – The player resolves any memory overflow (forget or move to diary).
  6. Validation – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).
  7. Continue – All pending changes are atomically committed and the game advances to the next prompt.

Character Attributes

Attribute Description
Skills Learned abilities; can be normal (latent), checked (used/active), or lost (permanently removed).
Resources Possessions or locations; typed as portable or stationary. One resource may be flagged as a diary.
Marks Physical or psychological scars that accumulate over time.
Known Characters NPCs the vampire has encountered; typed as mortal or immortal; may be lost (dead, vanished, etc.).

Special Prompt Rules

Certain prompts carry special mechanics encoded as rules:

Rule Effect
no_experience Experience creation is disabled for this turn
allow_name_change The player may change the vampire's name
memory_modification The player may edit the text of existing experiences
memory_slot_loss Permanent reduction of memory capacity by 1
memory_slot_gain Permanent increase of memory capacity by 1
game_over The game ends; the player writes an epilogue

Game Ending

The game ends when the player reaches a prompt with the game_over rule (or under other conditions defined by the original game). The player writes an epilogue—a final reflection on the vampire's story—and the character is archived. A statistics summary shows prompt count, memories, experiences, skills, resources, marks, and characters accumulated during the playthrough.

Architecture

Technology Stack

Layer Technology Version
Backend framework Django 5.0.6
REST API Django REST Framework 3.15.1
Authentication Simple JWT (custom) 5.3.0
CORS django-cors-headers 4.3.1
Image handling Pillow 11.3.0
Production server Gunicorn 21.2.0
Frontend framework Vue.js 3.5
State management Pinia 3.0
Routing Vue Router 4.5
HTTP client Axios 1.10
CSS framework Bootstrap 5.3
Build tool Vite 7.0
Language TypeScript 5.8
Testing (backend) pytest + pytest-django 8.0 / 4.8
Testing (frontend) Playwright 1.53

High-Level Diagram

┌─────────────────────────────────────────────────┐
│               Vue 3 SPA (TypeScript)            │
│  ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
│  │  Views   │ │Components│ │  Pinia Stores     ││
│  │ (7 pages)│ │ (14+)    │ │ auth/game/theme/  ││
│  │          │ │          │ │ characterCreation  ││
│  └────┬─────┘ └────┬─────┘ └────────┬─────────┘│
│       └─────────────┴────────────────┘          │
│                     │ Axios                     │
│                     ▼                           │
├─────────────────────────────────────────────────┤
│              Django REST API                    │
│  /api/auth/login    /api/auth/verify            │
│  /api/characters/   (CRUD + game_state,         │
│   roll_dice, continue_story, audit_trail)       │
│  /api/characters/<id>/memories/                 │
│  /api/characters/<id>/experiences/              │
│  /api/characters/<id>/skills/                   │
│  /api/characters/<id>/resources/                │
│  /api/characters/<id>/marks/                    │
│  /api/characters/<id>/characters/               │
│  /api/characters/<id>/pending-changes/          │
│  /api/prompts/                                  │
├─────────────────────────────────────────────────┤
│          Django ORM / SQLite                    │
│  Character · Memory · Experience · Skill        │
│  Resource · Mark · GameCharacter · Prompt        │
│  PendingChange · GameStateSnapshot              │
└─────────────────────────────────────────────────┘

Backend

Django Apps

The project is organised into three Django apps plus a settings module:

App Purpose
vampire Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot
tyov_api REST API layer: serialisers, viewsets, URL routing, permissions, middleware
authentication JWT-based login and token verification endpoints
tyov_backend Django project settings, URL root, WSGI/ASGI configuration

Data Models

Character

The central model, owned by a Django User via a ForeignKey:

Field Type Description
name CharField(200) Current name of the vampire
description TextField Background/appearance description
image ImageField Optional character portrait
current_prompt CharField(10) ID of the current prompt (e.g. "17a")
last_dice_roll JSONField Stores D10, D6, and result of the last roll
visited_prompts JSONField(list) List of all prompt IDs visited in order
memory_slots_lost IntegerField Permanent memory slot reductions
memory_slots_gained IntegerField Permanent memory slot increases
creation_step IntegerField(1–6) Tracks progress through the creation wizard
is_creation_complete BooleanField True when all six creation steps are finished
creation_data JSONField Temporary storage during creation
epilogue TextField Player's final reflection (set at game end)
is_completed BooleanField Whether the story has ended
completed_at DateTimeField Timestamp of story completion

Memory

A memory belongs to a Character and holds up to 3 Experiences:

  • title – descriptive name (unique per character)
  • in_diary – whether the memory is stored in the diary
  • is_lost – whether the memory has been forgotten

Experience

An individual narrative entry within a Memory:

  • title – optional short description
  • content – the narrative text
  • prompt_id – which prompt generated this experience
  • date_info – temporal context (e.g. "Winter, 431 CE")

Skill

  • name, description
  • status – one of normal, checked, or lost

Resource

  • name, description
  • resource_typeportable or stationary
  • is_diary – flags the special diary resource (constrained to one active diary per character)
  • is_lost

Mark

  • name, description
  • is_lost

GameCharacter

  • name, description
  • character_typemortal or immortal
  • relationship – e.g. friend, rival, mentor, love, enemy, neutral
  • is_lost

Prompt

  • prompt_id – e.g. "17a", "32b"
  • number, variation – parsed components for sorting
  • content – the scenario text
  • rules – special mechanics as comma-separated tokens (e.g. no_experience, allow_name_change)

PendingChange

Temporary storage for changes before they are atomically committed on "Continue":

  • change_type – experience, memory, skill, resource, mark, or character
  • change_data – JSON payload of the change

GameStateSnapshot (Audit Trail)

Automatically created each turn to record the complete game state:

  • turn_number – sequential, starting from 1
  • prompt_id / next_prompt_id – transition
  • dice_roll – raw dice data
  • game_state – full JSON snapshot of all memories, skills, resources, etc.
  • changes_applied – JSON array of pending changes that were committed
  • created_at – timestamp

API Endpoints

All endpoints require JWT authentication (except login).

Authentication

Method Endpoint Description
POST /api/auth/login/ Authenticate with username/password; returns JWT token
POST /api/auth/verify/ Verify current token and return user info

Login is rate-limited to 5 requests per minute per IP.

Characters

Method Endpoint Description
GET /api/characters/ List all characters for the authenticated user
POST /api/characters/ Create a new character
GET /api/characters/{id}/ Retrieve full character detail (prefetched relations)
PUT/PATCH /api/characters/{id}/ Update character fields
DELETE /api/characters/{id}/ Delete a character
GET /api/characters/{id}/game_state/ Complete game state including prompt, validation, dice info
POST /api/characters/{id}/roll_dice/ Roll D10 − D6 and store the result
POST /api/characters/{id}/continue_story/ Validate, commit pending changes, advance to next prompt
GET /api/characters/{id}/audit_trail/ View turn-by-turn audit history
GET /api/characters/{id}/game-state-changes/ Timeline of state changes between turns

Nested Character Resources

For each character, full CRUD is available on:

  • /api/characters/{id}/memories/ (plus move_to_diary and restore_lost_memory actions)
  • /api/characters/{id}/experiences/
  • /api/characters/{id}/skills/
  • /api/characters/{id}/resources/
  • /api/characters/{id}/marks/
  • /api/characters/{id}/characters/ (known NPCs)
  • /api/characters/{id}/pending-changes/

Prompts

Method Endpoint Description
GET /api/prompts/ List all game prompts
GET /api/prompts/{prompt_id}/ Retrieve a single prompt by ID

Permissions and Security

  • All character data is scoped to the authenticated user via IsCharacterOwner permission.
  • JWT tokens are sent as Authorization: Bearer {token} headers.
  • The login endpoint is throttled (5/minute) to prevent brute-force attacks.
  • All pending changes are processed inside a database transaction to guarantee atomicity.
  • CORS headers are configured via django-cors-headers.

Frontend

Views (Pages)

View Route Description
LoginView /login Authentication form; redirects authenticated users to home
HomeView / Dashboard showing all active characters in a card grid
CharacterCreationView /character-creation/:id Six-step creation wizard
GameView /game/:characterId Main game loop: prompt display, experience form, dice rolling, entity management, memory display
GameEndedView /game/:characterId/ended End-of-story screen with statistics and epilogue
RecordsView /records Archive of current and completed characters
StoryView /story/:characterId Narrative timeline with experience history, character stats sidebar, and epilogue

All routes except /login require authentication. The router guard redirects unauthenticated users.

Key Components

Component Purpose
PromptSection Displays the current prompt text, special rules, and dice roll results
ExperienceForm Form for writing new experiences with title, date, content, and memory selection
MemoriesDisplay Shows active, diary, and lost memories with experience counts; includes move-to-diary, delete, and restore actions
GameDataSection Reusable list for skills, resources, or marks with add/edit/delete controls
CharactersSection Grid of known NPCs with type badges and edit/delete
EntityCreationWindow Draggable modal for creating/editing skills, resources, marks
CharacterCreationWindow Draggable modal for creating/editing NPC characters
ExperienceEditWindow Window for editing existing experience text
DiaryCreationModal Modal for creating a diary resource and moving a memory into it
ConfirmationDialog Generic confirmation dialog for destructive actions
ValidationWarning Displays validation errors when game rules are not satisfied

State Management (Pinia)

Four Pinia stores manage application state:

Store Responsibility
auth JWT token, user object, login/logout, token expiry handling
game Characters list, current character, game state, all CRUD actions for entities, dice rolling, story continuation
characterCreation Multi-step wizard data, initial character creation
theme Dark/light mode toggle with localStorage persistence

API Service Layer

  • api.ts – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.
  • gameService.ts – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.
  • entityService.ts – Generic EntityService class instantiated for skills, resources, marks, and characters for DRY CRUD operations.

Composables

Vue composables encapsulate reusable business logic:

  • useGameData() – Computed properties derived from the game store (current character, memories, skills, resources, etc.)
  • useExperienceForm() – Form state, validation, memory selection, and reset logic
  • useEntityManagement() – Generic CRUD operations with optional confirmation dialogs
  • useSkillManagement(), useResourceManagement(), useMarkManagement(), useMemoryManagement() – Specialised wrappers
  • useConfirmationDialog() – Promise-based modal confirmation
  • useMemoryDisplay() – Display logic for memories including pending-change awareness
  • useExperienceDisplay() – Markdown rendering for experience content
  • usePendingChangesDisplay() – Badge rendering for undo-able pending changes

Audit Trail

The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.

Each GameStateSnapshot records:

  • The turn number (sequential from 1)
  • The prompt transition (e.g. 36a → 34a)
  • The dice roll
  • A complete JSON snapshot of all memories, skills, resources, marks, and known characters
  • All pending changes that were committed

The audit trail is accessible via:

  • API: GET /api/characters/{id}/audit_trail/ with optional ?turn=N and ?limit=N parameters
  • Management command: python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]
  • Timeline API: GET /api/characters/{id}/game-state-changes/ for a structured timeline of all state changes

Deployment

Local Development

.\startup.ps1

This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (http://localhost:8000) and the Vue dev server (http://localhost:5173).

Production

  • Set DJANGO_ENV=production
  • Build the frontend: cd frontend && npm run build
  • Serve with Gunicorn behind a reverse proxy
  • Static files are collected into the static/ directory
  • The production frontend points to https://tyov.yusupov.cloud

Example: A Finished Character

The following is an example of a character from an actual playthrough stored in the application database. It demonstrates how the game's mechanics play out over an extended narrative.

Karmiš (formerly Ekurzu / Naram)

Description: Naram was a temple scribe in the ancient city of Mari: meticulous, observant, and deeply entangled in the political and spiritual life of the city. His life was shaped by clay tablets, omens, and whispered secrets carried through palace corridors and temple courtyards. Behind his calm demeanor lies a mind constantly at work—recording, interpreting, and surviving.

Prompts visited: 34 prompts across variations, including: 1a, 4a, 11a–c, 12a, 17a, 20a, 24a, 25a, 32a–b, 34a–36a, 37a–41c, 43a–c, 47a, 52a, 57a, 64a, 66a, 68a, 71a, 73a.

Final prompt: 73a

Memory slots lost: 1 · Memory slots gained: 1 (net effect: standard 5-memory limit)

Epilogue

He has not died. Let me say that plainly. There will be no grave. He is alive, but quietly, deliberately, and utterly outside your reach.

When I first met him, I did not understand what he was. He wore his years like dust on old vellum—thin, barely visible, but ever present. [...] Over time, I became his mirror, then his partner, and finally his historian.

But someone has to begin the telling. Someone has to place the first stone. So this book begins in a house of quiet gardens and low ceilings, where he writes in the mornings and feeds only when he must. [...]

Just a man who remembered too much, for too long. And chose, in the end, peace.

Mirelde

Statistics

Metric Count
Total memories 16
Active memories 5
Diary memories 3
Lost (forgotten) memories 8
Total experiences 37
Skills 11 (3 lost during play)
Resources 10 (6 lost during play)
Marks 2
Known characters 11 (6 lost during play)

Active Memories (at end of game)

Memory Title Location Experiences
Memory 1 Active "Mortal life" (Initial), "The Name Given in Ash" (24a)
The Thorn and the Quill Active 1 experience (35a)
The Light That Does Not Burn Active "The Dawn at Mirelde's Fire" (52a), "The Dust in the Foundation" (57a)
We Were Not Counted Among the Cargo Active "The Bellies of Ships and Men" (64a), "Ink Without Meaning" (66a)
Echoes Before the Self Was Named Active "The Weight in the Smoke" (71a), "The Clay That Knew My Name" (68a)

Diary Memories

Memory Title Experiences
The Hill That Waits "The Hollow Beneath the Cairn" (40a), "The Ring Beneath the Clay" (43c), "The Face Returned Through Time" (44a)
Ash of the Ledger "The Last Ledger Vanishes" (38a), "The Fog Beneath Names" (39b), "The Weight of Unspoken Words" (34a)
The Fever-Ledger "The Illness in Karkheda" (36a), "The Name That Returns in Ash" (41c), "The Echo That Cannot Translate" (47a)

Lost Memories

Eight memories were forgotten over the course of the game, including "The Forged Omen" (Memory 2), "Hessa's Last Message" (Memory 3), "Duel of Stars and Signs" (Memory 4), "The Turning", "The Name That Cannot Burn", "The Blood Between Accusations", "Trade Beneath Empire", and "What Was Meant to Last". These losses illustrate the game's core mechanic: as new experiences accumulate, the vampire is forced to sacrifice older memories, creating a poignant sense of identity erosion across the centuries.

Skills

Skill Status Description (excerpt)
Ash-Tongue Template:Checked "I speak in soot and suggestion, in the cracks between laws."
Bloodthirsty Template:Lost "The scent of mortal blood calls to me like a hymn in the dark."
Ceremonial Composure Template:Checked "In the presence of gods or kings, my face is unreadable."
Decipher Ancient Texts Template:Lost "I can read languages long dead and spot forgeries."
I Control the Beast Template:Lost "When the thirst rises, I do not flinch. I meet the monster's gaze."
Ledger Without End Normal "Born from the quiet rituals of inventory and account."
Physick of the Pale Vein Normal "I feed gently, beneath the guise of care."
Remain in Unknowing Template:Checked "I do not flee the edges of memory."
Silent Cartography Template:Checked "I trace the unseen paths between people, places, and power."
Snare and Stillness Template:Checked "I move with silence that makes mortals forget they heard me."
Tongue of the Unnamed Normal "I no longer understand the languages of my past, but I wear the sounds of others like masks."

Resources

Resource Type Status Notes
Kept Against Forgetting Portable / Diary Active The character's diary; written with Mirelde
Tablet Bearing My Name Portable Active A clay tablet from the ancient past, rediscovered
The Margins of Manifest Portable Active A cipher-folio cataloguing names of the enslaved
Burrowed Sanctum Portable Lost An earthen chamber beneath a forgotten altar
Chamber of Whispers Stationary Lost A hidden storeroom beneath the temple of Ishtar
Clay Tablets and Reed Stylus Portable Lost Writing implements used by instinct, not understanding
Hidden Archive Tablet Portable Lost A tablet inscribed with dynasty-breaking truths
The Monastery Diary Portable / Diary Lost A former diary, lost when the monastery fell
The Silent Authority Portable Lost A carved seal whose symbols can no longer be read
The Walls of Forgetting Stationary / Diary Lost Memory carvings in five scripts beneath a forgotten shrine

Known Characters

Name Type Relationship Status
Ashurban the Veiled Immortal Neutral Active
Mirelde of Bracha Immortal Friend Active
The One Who Does Not Answer Mortal Neutral Active
Thoöni (spectral) Immortal Neutral Active
Belatu Mortal Rival Lost
Ennatum Mortal Friend Lost
Hessa Mortal Love Lost
Ibbi-Zamri Mortal Mentor Lost
Mekha Mortal Enemy Lost
Pelagon Mortal Enemy Lost
Thoöni (scholar) Mortal Friend Lost

Marks

Mark Description
The Unblinking Eye An ancient eye-like scar below the collarbone; burns faintly in the presence of lies
The Gesture Forbidden An involuntary three-fingered gesture of silence, often mistaken for mockery

Example Experience: "The Bellies of Ships and Men" (Prompt 64a)

They arrived in numbers too vast to count: pressed between barrels, chained to holds slick with piss and seawater, branded, sick, broken. And the sailors were not much better.

The ships would come in [...]

Date: around 1510 CE

This experience is part of the memory "We Were Not Counted Among the Cargo", illustrating the vampire's witness to the Atlantic slave trade—one of many historical eras the character passes through during a millennium-spanning story.

Testing

Backend

Tests are run with pytest and pytest-django:

pytest

Test files include:

  • vampire/test_models.py – model unit tests
  • tyov_api/test_api.py – API endpoint integration tests
  • tyov_api/test_models.py – API model tests
  • authentication/test_views.py – authentication tests

Frontend

End-to-end tests use Playwright:

cd frontend
npx playwright test

Test specs include:

  • e2e/character-creation-workflow.spec.ts – character creation wizard
  • e2e/tyov-game.spec.ts – game loop testing
  • e2e/vue.spec.ts – general Vue component tests

See Also

References