Thousand Year Old Vampire: Difference between revisions
| Line 703: | Line 703: | ||
<blockquote> | <blockquote> | ||
They arrived in numbers too vast to | 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 | |
|---|---|
| name | Thousand Year Old Vampire – Web Helper |
| developer | Michel Vuijlsteke |
| programming language | Python (Django 5), TypeScript (Vue 3) |
| operating system | Cross-platform (Web) |
| genre | Solo RPG Digital Companion |
| license | custom |
| website | https://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:
- Prompt Presentation – The current prompt text and any special rules are displayed.
- Dice Roll – The player rolls D10 − D6 to determine the next prompt.
- Experience Creation – The player writes at least one narrative experience for the current prompt.
- Character Updates – Skills, resources, marks, and known characters may be added, edited, checked, or removed.
- Memory Management – The player resolves any memory overflow (forget or move to diary).
- Validation – The system verifies all rules are satisfied (at least one experience added, memory limits respected, etc.).
- 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 diaryis_lost– whether the memory has been forgotten
Experience
An individual narrative entry within a Memory:
title– optional short descriptioncontent– the narrative textprompt_id– which prompt generated this experiencedate_info– temporal context (e.g. "Winter, 431 CE")
Skill
name,descriptionstatus– one ofnormal,checked, orlost
Resource
name,descriptionresource_type–portableorstationaryis_diary– flags the special diary resource (constrained to one active diary per character)is_lost
Mark
name,descriptionis_lost
GameCharacter
name,descriptioncharacter_type–mortalorimmortalrelationship– e.g. friend, rival, mentor, love, enemy, neutralis_lost
Prompt
prompt_id– e.g. "17a", "32b"number,variation– parsed components for sortingcontent– the scenario textrules– 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 characterchange_data– JSON payload of the change
GameStateSnapshot (Audit Trail)
Automatically created each turn to record the complete game state:
turn_number– sequential, starting from 1prompt_id/next_prompt_id– transitiondice_roll– raw dice datagame_state– full JSON snapshot of all memories, skills, resources, etc.changes_applied– JSON array of pending changes that were committedcreated_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/(plusmove_to_diaryandrestore_lost_memoryactions)/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
IsCharacterOwnerpermission. - 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– GenericEntityServiceclass 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 logicuseEntityManagement()– Generic CRUD operations with optional confirmation dialogsuseSkillManagement(),useResourceManagement(),useMarkManagement(),useMemoryManagement()– Specialised wrappersuseConfirmationDialog()– Promise-based modal confirmationuseMemoryDisplay()– Display logic for memories including pending-change awarenessuseExperienceDisplay()– Markdown rendering for experience contentusePendingChangesDisplay()– 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=Nand?limit=Nparameters - 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 teststyov_api/test_api.py– API endpoint integration teststyov_api/test_models.py– API model testsauthentication/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 wizarde2e/tyov-game.spec.ts– game loop testinge2e/vue.spec.ts– general Vue component tests
See Also
- Thousand Year Old Vampire by Tim Hutchings – the original tabletop game
- Django (web framework)
- Vue.js
- Single-page application