Jump to content

Thousand Year Old Vampire: Difference between revisions

From Yusupov's House
No edit summary
Line 1: Line 1:
{{Infobox  
{{Infobox software
| 01_name = Thousand Year Old Vampire Web Helper
| name = Thousand Year Old Vampire - Web Helper
| 02_logo =
| developer = Michel Vuijlsteke
| 03_developer = Michel Vuijlsteke
| programming language = Python, TypeScript
| 04_programming language = Python (Django 5), TypeScript (Vue 3)
| operating system = Cross-platform (Web)
| 05_operating system = Cross-platform (Web)
| genre = Solo RPG digital companion
| 06_genre = Solo RPG Digital Companion
| license = Custom
| 07_license = custom
| website = <code>https://tyov.yusupov.cloud</code>
| 08_website = <code>https://tyov.yusupov.cloud</code>
}}
}}


'''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.
'''Thousand Year Old Vampire - Web Helper''' ('''TYOV-Web''') is a browser-based adaptation of the solo role-playing game ''Thousand Year Old Vampire'' by Tim Hutchings. Rather than presenting the game as a digital board game or a rules spreadsheet, it treats a campaign as a long, accumulating chronicle: a vampire begins with a handful of memories, gains new experiences, loses old ones, and leaves behind a life story made of fragments, scars, names, and vanished centuries. The application is built as a '''Django''' backend serving a '''Vue''' single-page frontend, but its real achievement is literary rather than merely technical: it turns bookkeeping-heavy solo play into something that feels legible, mournful, and continuous.


== Overview ==
== Premise ==


''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.
In ''Thousand Year Old Vampire'', the player does not manage hit points, armies, or territory. Instead, the player manages '''memory'''. A vampire may survive for centuries, but cannot perfectly retain those centuries. New experiences force older ones aside. Friends die. Languages decay. Identities are traded, abandoned, or forgotten. The drama comes from watching the self narrow and distort over time.


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.
TYOV-Web preserves that central idea. A player creates a vampire from mortal beginnings, plays through prompts generated by dice rolls, writes narrative responses to those prompts, and organises the resulting fragments into memories. Over time the character collects skills, resources, marks, and known companions, but those are unstable possessions. The most important question is never simply ''what happened?'' It is ''what can still be remembered?''


== Game Rules ==
== How Play Works ==


=== Core Concept ===
The application follows the structure of the tabletop game closely, but presents it as a guided story engine. Character creation unfolds in six stages: mortal life, mortal relationships, skills, resources, a defining early experience, and finally the turning into vampirism. Once play begins, each turn revolves around a prompt. The player reads the current prompt, writes at least one experience in response, manages any consequences on the character sheet, then rolls a ten-sided die and a six-sided die to discover the next destination in the prompt list.


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.
That movement is what gives the game its broken historical rhythm. A positive result on D10 minus D6 pushes the vampire forward; a negative result drags the story backward into earlier themes or alternative branches. Prompts are also divided into variations, so revisiting a number does not necessarily mean revisiting the same moment in the same way.


=== Character Creation ===
The memory system gives the application its emotional pressure. A character can normally hold only '''five active memories''', and each memory can contain no more than '''three experiences'''. When that limit is reached, the player must decide what to lose. Some memories can be moved into a '''diary''', which acts as an external repository, but diaries themselves are only resources, and resources can be lost. The result is a game about narrative curation: the player is constantly deciding which version of the vampire's past deserves to remain visible.


Character creation follows a guided six-step wizard:
Special prompts can alter that rhythm. Some disable experience-writing for a turn. Others allow the vampire's name to change, permit the editing of old experiences, reduce or increase permanent memory capacity, or end the chronicle entirely with an epilogue. TYOV-Web exposes these rules clearly during play, but the rules never overwhelm the fiction. They exist to sharpen it.


{| class="wikitable"
== An Example Unlife: Karmis ==
|-
! 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 most compelling way to understand the application is through one of its stored characters. One campaign in the database follows '''Karmis''', formerly '''Ekurzu''' and before that '''Naram''', across roughly three and a half thousand years.


The memory system is the heart of the game:
He begins in Bronze Age Mari as a temple scribe: precise, observant, and deeply embedded in the religious and political machinery of the city. His mortal life is defined by clay tablets, whispered intelligence, and courtly danger. He falls in love with Hessa, is shaped by the mentorship of the high priest Ibbi-Zamri, and is eventually turned by the ancient vampire Ashurban the Veiled. In one of the campaign's defining cruelties, Ibbi-Zamri becomes the first person Karmis kills after the turning.


* A character may hold a maximum of '''5 active memories''' (adjustable by prompt rules and permanent effects).
The early centuries of his unlife are marked by exile and shame. He flees with Ennatum, struggles to master the hunger, tries to imagine restraint as redemption, and fails. One of the campaign's most effective lines comes from this period: he builds walls and plays flutes in the wilderness, but it is "never penance, only a cage with softer bars." Eventually he devours Ennatum during a sandstorm and carries that loss into the long middle stretch of the story.
* 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 (<code>memory_slot_gain</code>, <code>memory_slot_loss</code>).


=== Dice Rolling and Prompt Navigation ===
From there the campaign moves through one civilisation after another. Karmis passes through Akhenaten's Egypt, the imperial roads of the Achaemenids, the scholarly world of Alexandria, Roman frontiers, monastic Europe, Mediterranean trade routes, and the violence of the early modern Atlantic world. His roles change less than the world around him does. He remains a scribe, witness, forger, archivist, and careful observer of institutions that never last as long as he does.


Each turn the player rolls two dice:
The campaign also shows how the game's systems create character rather than merely recording it. Karmis gains skills such as ''Silent Cartography'', the ability to trace unseen paths of influence, and ''Ash-Tongue'', a talent for insinuation and social survival. But he loses others as time damages his identity. He once knew how to decipher ancient texts; later he no longer can. He once believed he could control the beast within; later even that confidence disappears. The mechanics translate immortality into erosion.


* '''D10''' (1–10) minus '''D6''' (1–6) = result (range: −5 to +9).
One of the campaign's richest relationships is with '''Thooni''', a scholar who recognises the name ''Ekurzu'' not as legend but as continuity. After her mortal death, the relationship persists in altered form: a spectral Thooni begins appearing in the margins of Karmis's diary, leaving fragments in languages he can no longer read. TYOV-Web handles this elegantly because its model of a character is not limited to a living cast list. People can be present, absent, lost, remembered, or transformed into symbols the protagonist can no longer fully interpret.
* 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.
By the late Roman and early medieval portions of the chronicle, the cost of the memory rules becomes brutal. Karmis loses so many memories that even the event of his own turning is no longer available to him. He relies on diaries and external records to stabilise himself, yet those too are vulnerable. A carved chamber called ''The Walls of Forgetting'' and a later monastery diary both disappear. The application does not treat this as simple inventory loss; it feels instead like a second death for pieces of the self.


=== Turn Structure ===
In the final act, Karmis witnesses the Atlantic slave trade and begins keeping a ledger of names drawn from shipping manifests and punishment records. A later response captures the moral exhaustion of the character with unusual sharpness: he no longer imagines himself purified by restraint, only redirected by purpose. He feeds on slavers and calls it justice, yet recognises that the blood tastes the same. This is the kind of line the application is built to preserve: not just event logs, but accumulations of ethical fatigue.


Each game turn follows this sequence:
His last great companion is '''Mirelde of Bracha''', who becomes mirror, partner, and eventually historian. By the time the epilogue arrives, the story is no longer told in Karmis's own voice. Mirelde writes the closing judgment on him: not as a monster laid to rest, but as "just a man who remembered too much, for too long, and chose, in the end, peace." That shift in narrator is exactly the sort of late-game turn TYOV-Web makes easy to sustain, because it keeps the campaign legible even after the protagonist himself has become unreliable.


# '''Prompt Presentation''' – The current prompt text and any special rules are displayed.
== Why the Application Works ==
# '''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 ===
What makes TYOV-Web effective is not that it digitises every rule, though it largely does. Its strength is that it understands which parts of the original game are dramatically important and places those at the centre of the interface.


{| class="wikitable"
The main play screen is organised around the prompt, the character sheet, and the memory display. The prompt is always the immediate literary provocation. The memory display constantly reminds the player of pressure and loss: active memories, diary memories, and forgotten memories sit in visible relation to one another. Skills, resources, marks, and known characters are editable in place, which means the character sheet grows and collapses alongside the fiction instead of feeling like a separate administrative surface.
|-
! 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 ===
The result is a frontend that behaves less like a dashboard and more like a writing desk. Players move between the prompt text, the experience form, and the fragments of the vampire's surviving life. Even when the application is doing something purely mechanical, such as validating whether the player may continue, the effect is narrative: it is asking whether the current moment of the story has been fully accounted for.


Certain prompts carry special mechanics encoded as rules:
== The Frontend as Chronicle ==


{| class="wikitable"
The client side of TYOV-Web is a '''Vue 3''' single-page application written in '''TypeScript'''. In practical terms, this means the entire experience unfolds without page reloads; in literary terms, it allows the game to feel continuous. The player logs in, sees a home screen of active and archived characters, enters a creation wizard for new vampires, and then spends most of the campaign inside a single evolving game view.
|-
! Rule !! Effect
|-
| <code>no_experience</code> || Experience creation is disabled for this turn
|-
| <code>allow_name_change</code> || The player may change the vampire's name
|-
| <code>memory_modification</code> || The player may edit the text of existing experiences
|-
| <code>memory_slot_loss</code> || Permanent reduction of memory capacity by 1
|-
| <code>memory_slot_gain</code> || Permanent increase of memory capacity by 1
|-
| <code>game_over</code> || The game ends; the player writes an epilogue
|}


=== Game Ending ===
That view is supported by a set of focused screens: a login page, a home dashboard, the six-step character creation flow, the main game interface, an ending screen for completed chronicles, an archive view, and a story view that presents the campaign as a timeline. Around those are a collection of smaller windows and dialogs for editing characters, skills, resources, memories, and experiences. These are important not because there are many of them, but because they preserve the sense that a campaign is always open to revision, annotation, and reinterpretation.


The game ends when the player reaches a prompt with the <code>game_over</code> 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.
State is held in Pinia stores for authentication, gameplay, character creation, and theme. That is an implementation detail, but in this case it serves a visible purpose: it keeps the player's character, current prompt, pending changes, and validation state coherent at all times. The interface never feels as if it has forgotten where the player is in the life of the vampire.


== Architecture ==
== The Backend as Memory Keeper ==


=== Technology Stack ===
Behind the interface is a '''Django 5''' application with a REST API built on '''Django REST Framework'''. The backend models the campaign in the same terms the game itself cares about: '''Character''', '''Memory''', '''Experience''', '''Skill''', '''Resource''', '''Mark''', '''GameCharacter''', '''Prompt''', and '''PendingChange'''. This is not especially glamorous architecture, but it is disciplined. The data model is close to the language of play, which makes the whole system easier to reason about.


{| class="wikitable"
The most interesting design choice is the use of '''pending changes'''. TYOV-Web does not immediately finalise every action the player takes. Instead, new experiences and character-sheet adjustments can accumulate as provisional edits until the player chooses to continue the story. Only then are they committed atomically. This makes the system feel less like a CRUD application and more like a drafting process. A turn can be assembled, reconsidered, and only then sealed into the chronicle.
|-
! 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 ===
Authentication is handled by JWT tokens, and character data is scoped to the authenticated user. The backend exposes endpoints for the game state itself, dice rolling, story continuation, and CRUD operations on memories, experiences, skills, resources, marks, and known characters. In the article, however, the important point is not the shape of the URLs. It is that the backend has been designed to support a narrative workflow rather than a generic records system.
 
<pre>
┌─────────────────────────────────────────────────┐
│              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              │
└─────────────────────────────────────────────────┘
</pre>
 
== Backend ==
 
=== Django Apps ===
 
The project is organised into three Django apps plus a settings module:
 
{| class="wikitable"
|-
! App !! Purpose
|-
| <code>vampire</code> || Core domain models: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, PendingChange, GameStateSnapshot
|-
| <code>tyov_api</code> || REST API layer: serialisers, viewsets, URL routing, permissions, middleware
|-
| <code>authentication</code> || JWT-based login and token verification endpoints
|-
| <code>tyov_backend</code> || Django project settings, URL root, WSGI/ASGI configuration
|}
 
=== Data Models ===
 
==== Character ====
 
The central model, owned by a Django <code>User</code> via a ForeignKey:
 
{| class="wikitable"
|-
! Field !! Type !! Description
|-
| <code>name</code> || CharField(200) || Current name of the vampire
|-
| <code>description</code> || TextField || Background/appearance description
|-
| <code>image</code> || ImageField || Optional character portrait
|-
| <code>current_prompt</code> || CharField(10) || ID of the current prompt (e.g. "17a")
|-
| <code>last_dice_roll</code> || JSONField || Stores D10, D6, and result of the last roll
|-
| <code>visited_prompts</code> || JSONField(list) || List of all prompt IDs visited in order
|-
| <code>memory_slots_lost</code> || IntegerField || Permanent memory slot reductions
|-
| <code>memory_slots_gained</code> || IntegerField || Permanent memory slot increases
|-
| <code>creation_step</code> || IntegerField(1–6) || Tracks progress through the creation wizard
|-
| <code>is_creation_complete</code> || BooleanField || True when all six creation steps are finished
|-
| <code>creation_data</code> || JSONField || Temporary storage during creation
|-
| <code>epilogue</code> || TextField || Player's final reflection (set at game end)
|-
| <code>is_completed</code> || BooleanField || Whether the story has ended
|-
| <code>completed_at</code> || DateTimeField || Timestamp of story completion
|}
 
==== Memory ====
 
A memory belongs to a Character and holds up to 3 Experiences:
 
* <code>title</code> – descriptive name (unique per character)
* <code>in_diary</code> – whether the memory is stored in the diary
* <code>is_lost</code> – whether the memory has been forgotten
 
==== Experience ====
 
An individual narrative entry within a Memory:
 
* <code>title</code> – optional short description
* <code>content</code> – the narrative text
* <code>prompt_id</code> – which prompt generated this experience
* <code>date_info</code> – temporal context (e.g. "Winter, 431 CE")
 
==== Skill ====
 
* <code>name</code>, <code>description</code>
* <code>status</code> – one of <code>normal</code>, <code>checked</code>, or <code>lost</code>
 
==== Resource ====
 
* <code>name</code>, <code>description</code>
* <code>resource_type</code> – <code>portable</code> or <code>stationary</code>
* <code>is_diary</code> – flags the special diary resource (constrained to one active diary per character)
* <code>is_lost</code>
 
==== Mark ====
 
* <code>name</code>, <code>description</code>
* <code>is_lost</code>
 
==== GameCharacter ====
 
* <code>name</code>, <code>description</code>
* <code>character_type</code> – <code>mortal</code> or <code>immortal</code>
* <code>relationship</code> – e.g. friend, rival, mentor, love, enemy, neutral
* <code>is_lost</code>
 
==== Prompt ====
 
* <code>prompt_id</code> – e.g. "17a", "32b"
* <code>number</code>, <code>variation</code> – parsed components for sorting
* <code>content</code> – the scenario text
* <code>rules</code> – special mechanics as comma-separated tokens (e.g. <code>no_experience, allow_name_change</code>)
 
==== PendingChange ====
 
Temporary storage for changes before they are atomically committed on "Continue":
 
* <code>change_type</code> – experience, memory, skill, resource, mark, or character
* <code>change_data</code> – JSON payload of the change
 
==== GameStateSnapshot (Audit Trail) ====
 
Automatically created each turn to record the complete game state:
 
* <code>turn_number</code> – sequential, starting from 1
* <code>prompt_id</code> / <code>next_prompt_id</code> – transition
* <code>dice_roll</code> – raw dice data
* <code>game_state</code> – full JSON snapshot of all memories, skills, resources, etc.
* <code>changes_applied</code> – JSON array of pending changes that were committed
* <code>created_at</code> – timestamp
 
=== API Endpoints ===
 
All endpoints require JWT authentication (except login).
 
==== Authentication ====
 
{| class="wikitable"
|-
! Method !! Endpoint !! Description
|-
| POST || <code>/api/auth/login/</code> || Authenticate with username/password; returns JWT token
|-
| POST || <code>/api/auth/verify/</code> || Verify current token and return user info
|}
 
Login is rate-limited to 5 requests per minute per IP.
 
==== Characters ====
 
{| class="wikitable"
|-
! Method !! Endpoint !! Description
|-
| GET || <code>/api/characters/</code> || List all characters for the authenticated user
|-
| POST || <code>/api/characters/</code> || Create a new character
|-
| GET || <code>/api/characters/{id}/</code> || Retrieve full character detail (prefetched relations)
|-
| PUT/PATCH || <code>/api/characters/{id}/</code> || Update character fields
|-
| DELETE || <code>/api/characters/{id}/</code> || Delete a character
|-
| GET || <code>/api/characters/{id}/game_state/</code> || Complete game state including prompt, validation, dice info
|-
| POST || <code>/api/characters/{id}/roll_dice/</code> || Roll D10 − D6 and store the result
|-
| POST || <code>/api/characters/{id}/continue_story/</code> || Validate, commit pending changes, advance to next prompt
|-
| GET || <code>/api/characters/{id}/audit_trail/</code> || View turn-by-turn audit history
|-
| GET || <code>/api/characters/{id}/game-state-changes/</code> || Timeline of state changes between turns
|}
 
==== Nested Character Resources ====
 
For each character, full CRUD is available on:
 
* <code>/api/characters/{id}/memories/</code> (plus <code>move_to_diary</code> and <code>restore_lost_memory</code> actions)
* <code>/api/characters/{id}/experiences/</code>
* <code>/api/characters/{id}/skills/</code>
* <code>/api/characters/{id}/resources/</code>
* <code>/api/characters/{id}/marks/</code>
* <code>/api/characters/{id}/characters/</code> (known NPCs)
* <code>/api/characters/{id}/pending-changes/</code>
 
==== Prompts ====
 
{| class="wikitable"
|-
! Method !! Endpoint !! Description
|-
| GET || <code>/api/prompts/</code> || List all game prompts
|-
| GET || <code>/api/prompts/{prompt_id}/</code> || Retrieve a single prompt by ID
|}
 
=== Permissions and Security ===
 
* All character data is scoped to the authenticated user via <code>IsCharacterOwner</code> permission.
* JWT tokens are sent as <code>Authorization: Bearer {token}</code> 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 <code>django-cors-headers</code>.
 
== Frontend ==
 
=== Views (Pages) ===
 
{| class="wikitable"
|-
! View !! Route !! Description
|-
| '''LoginView''' || <code>/login</code> || Authentication form; redirects authenticated users to home
|-
| '''HomeView''' || <code>/</code> || Dashboard showing all active characters in a card grid
|-
| '''CharacterCreationView''' || <code>/character-creation/:id</code> || Six-step creation wizard
|-
| '''GameView''' || <code>/game/:characterId</code> || Main game loop: prompt display, experience form, dice rolling, entity management, memory display
|-
| '''GameEndedView''' || <code>/game/:characterId/ended</code> || End-of-story screen with statistics and epilogue
|-
| '''RecordsView''' || <code>/records</code> || Archive of current and completed characters
|-
| '''StoryView''' || <code>/story/:characterId</code> || Narrative timeline with experience history, character stats sidebar, and epilogue
|}
 
All routes except <code>/login</code> require authentication. The router guard redirects unauthenticated users.
 
=== Key Components ===
 
{| class="wikitable"
|-
! 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:
 
{| class="wikitable"
|-
! 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 ===
 
* '''<code>api.ts</code>''' – Axios instance with base URL auto-detection (development vs. production), request interceptor for JWT headers, response interceptor for 401/token-expiry handling.
* '''<code>gameService.ts</code>''' – All API call methods: authentication, character CRUD, dice rolling, story continuation, memory/experience/skill/resource/mark/character management.
* '''<code>entityService.ts</code>''' – Generic <code>EntityService</code> class instantiated for skills, resources, marks, and characters for DRY CRUD operations.
 
=== Composables ===
 
Vue composables encapsulate reusable business logic:
 
* '''<code>useGameData()</code>''' – Computed properties derived from the game store (current character, memories, skills, resources, etc.)
* '''<code>useExperienceForm()</code>''' – Form state, validation, memory selection, and reset logic
* '''<code>useEntityManagement()</code>''' – Generic CRUD operations with optional confirmation dialogs
* '''<code>useSkillManagement()</code>''', '''<code>useResourceManagement()</code>''', '''<code>useMarkManagement()</code>''', '''<code>useMemoryManagement()</code>''' – Specialised wrappers
* '''<code>useConfirmationDialog()</code>''' – Promise-based modal confirmation
* '''<code>useMemoryDisplay()</code>''' – Display logic for memories including pending-change awareness
* '''<code>useExperienceDisplay()</code>''' – Markdown rendering for experience content
* '''<code>usePendingChangesDisplay()</code>''' – Badge rendering for undo-able pending changes


== Audit Trail ==
== Audit Trail ==


The application includes an automatic audit trail that captures the complete game state every time the player advances to a new prompt.
One of the application's strongest features is its audit trail. Each time the player advances to a new prompt, TYOV-Web captures a complete snapshot of the game state: prompt transition, dice roll, character sheet, memories, and the set of changes applied on that turn. In effect, the application keeps both the ''current'' vampire and the ''history of how that vampire became current''.
 
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''': <code>GET /api/characters/{id}/audit_trail/</code> with optional <code>?turn=N</code> and <code>?limit=N</code> parameters
* '''Management command''': <code>python manage.py view_audit_trail {id} [--summary] [--turn N] [--export file.json]</code>
* '''Timeline API''': <code>GET /api/characters/{id}/game-state-changes/</code> for a structured timeline of all state changes
 
== Deployment ==
 
=== Local Development ===
 
<pre>
.\startup.ps1
</pre>
 
This script creates a virtual environment, installs dependencies, runs migrations, and starts both the Django backend (<code>http://localhost:8000</code>) and the Vue dev server (<code>http://localhost:5173</code>).
 
=== Production ===
 
* Set <code>DJANGO_ENV=production</code>
* Build the frontend: <code>cd frontend && npm run build</code>
* Serve with Gunicorn behind a reverse proxy
* Static files are collected into the <code>static/</code> directory
* The production frontend points to <code>https://tyov.yusupov.cloud</code>
 
== 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 ====
 
<blockquote>
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''
</blockquote>
 
==== A Life in Summary ====
 
Karmiš's story spans roughly three and a half thousand years. It begins in Bronze Age Mesopotamia, where the mortal scribe Naram serves in the temple of Ishtar at Mari. He writes omens, navigatesg palace intrigue, and fals in love with a woman named Hessa who moves through the court "like smoke." His mentor, the High Priest Ibbi-Zamri, is the first person he kills after being turned by the ancient vampire Ashurban the Veiled.
 
The early centuries are defined by guilt and exile. Fleeing with his mortal companion Ennatum, the newly-made vampire (now calling himself Ekurzu) struggles to control his hunger. He builds walls and plays flutes in the wilderness, but it is "never penance, only a cage with softer bars." Eventually the hunger wins: he devours Ennatum during a sandstorm and spends the following decades alone.
 
The middle game carries the character through the great civilisations of antiquity: Amarna under Akhenaten, the Achaemenid Royal Road, the libraries of Alexandria—always in the role of scribe, forger, or quiet observer. He accumulates skills like ''Silent Cartography'' ("I trace the unseen paths between people, places, and power") and ''Ash-Tongue'' ("I speak in soot and suggestion, in the cracks between laws"), but loses others as the centuries erode his identity: the ability to ''Decipher Ancient Texts'' and even ''I Control the Beast'' both slip away.
 
A pivotal relationship develops with the scholar Thoöni, who recognises the name ''Ekurzu'' not as myth but as continuity. Later, after Thoöni's mortal death, her spectral echo begins appearing in the margins of his diary, scratching fragments in languages he can no longer read.
 
By the late Roman period Karmiš has lost so many memories that he can barely hold a conversation. The game's memory mechanics enforce this viscerally: eight of his sixteen memories were permanently forgotten, including ''The Turning'' itself; the vampire no longer remembers how he was made. He compensates by keeping diaries, but even those are lost: ''The Walls of Forgetting'' (memory carvings in five scripts beneath a shrine near Carchemish) and ''The Monastery Diary'' both vanish, taking their stored memories with them.
 
The final act takes place in early modern Europe. Karmiš witnesses the Atlantic slave trade first-hand and begins compiling ''The Margins of Manifest'', a cipher-folio of names taken from shipping manifests and punishment ledgers. His response to prompt 66a captures the shift:
 
<blockquote>
''It is no longer a question of restraint. That was a younger version of me, one who believed self-control made me different from the others. Now I am different in purpose, not method. I feed from slavers and call it justice, but the blood tastes the same.''
</blockquote>
 
His companion in the final centuries is Mirelde of Bracha. She is an immortal who becomes his mirror, partner, and ultimately his historian. Together they rediscover a clay tablet bearing his original name in a Venetian bookseller's back room (prompt 68a, 1704 CE), and in a ruined estate outside Smyrna Mirelde finds the remains of a life Karmiš can no longer remember.
 
At the story's end Karmiš retains only five active memories, three diary memories, two marks (''The Unblinking Eye'', a scar that burns in the presence of lies and ''The Gesture Forbidden'', an involuntary court gesture mistaken for mockery), and three possessions: his diary ''Kept Against Forgetting'', the clay tablet bearing his name, and the slave-trade folio. Of the eleven characters he encountered, only four remain: Ashurban (his distant maker), Mirelde (his companion), Thoöni's ghost, and a nameless figure known only as ''The One Who Does Not Answer''. Over 34 prompts he accumulated 37 experiences, gained and lost a memory slot each, and burned through three separate diaries.
 
The character's story is narrated in the epilogue not by Karmiš himself but by Mirelde, who insists: "He has not died. [...] Just a man who remembered too much, for too long. And chose, in the end, ''peace''."
 
== Testing ==
 
=== Backend ===
 
Tests are run with '''pytest''' and '''pytest-django''':


<pre>
This matters for more than debugging. In a game built around forgetting, a turn-by-turn archive becomes part of the theme. The player may choose to lose a memory inside the fiction, but the application still preserves the fact that the loss occurred. The vampire forgets; the chronicle does not.
pytest
</pre>


Test files include:
== Development and Deployment ==
* <code>vampire/test_models.py</code> – model unit tests
* <code>tyov_api/test_api.py</code> – API endpoint integration tests
* <code>tyov_api/test_models.py</code> – API model tests
* <code>authentication/test_views.py</code> – authentication tests


=== Frontend ===
TYOV-Web is developed as a two-part web project: Django on the server side, Vue on the client side. In local development the repository can be started with a PowerShell bootstrap script, which creates the Python environment, installs dependencies, runs migrations, and launches both servers. In production, the frontend is built with Vite and the backend is configured through the usual Django environment settings, including a production mode selected with <code>DJANGO_ENV=production</code>.


End-to-end tests use '''Playwright''':
Testing exists on both sides of the stack. The backend is covered with '''pytest''' and '''pytest-django'''; the frontend uses '''Playwright''' for end-to-end coverage of the creation flow and core game loop. Those details are reassuring, but secondary. The more meaningful point is that the project has been built as an actual maintained application rather than a one-off prototype.


<pre>
== Reception Within the Project ==
cd frontend
npx playwright test
</pre>


Test specs include:
As a fan-made implementation, TYOV-Web is best understood as an act of interpretation. It does not try to replace the tabletop experience with spectacle. Instead it asks what a web application can do well for this particular game: preserve long-form writing, reduce clerical overhead, make memory pressure visible, and retain the full history of a life that the protagonist can no longer fully remember. In that sense it is not merely a helper app. It is a machine for turning play into biography.
* <code>e2e/character-creation-workflow.spec.ts</code> – character creation wizard
* <code>e2e/tyov-game.spec.ts</code> – game loop testing
* <code>e2e/vue.spec.ts</code> – general Vue component tests


== See Also ==
== See Also ==


* ''Thousand Year Old Vampire'' by Tim Hutchings – the original tabletop game
* ''Thousand Year Old Vampire'' by Tim Hutchings
* [[Django (web framework)]]
* [[Django (web framework)]]
* [[Vue.js]]
* [[Vue.js]]

Revision as of 11:33, 12 April 2026

Template:Infobox software

Thousand Year Old Vampire - Web Helper (TYOV-Web) is a browser-based adaptation of the solo role-playing game Thousand Year Old Vampire by Tim Hutchings. Rather than presenting the game as a digital board game or a rules spreadsheet, it treats a campaign as a long, accumulating chronicle: a vampire begins with a handful of memories, gains new experiences, loses old ones, and leaves behind a life story made of fragments, scars, names, and vanished centuries. The application is built as a Django backend serving a Vue single-page frontend, but its real achievement is literary rather than merely technical: it turns bookkeeping-heavy solo play into something that feels legible, mournful, and continuous.

Premise

In Thousand Year Old Vampire, the player does not manage hit points, armies, or territory. Instead, the player manages memory. A vampire may survive for centuries, but cannot perfectly retain those centuries. New experiences force older ones aside. Friends die. Languages decay. Identities are traded, abandoned, or forgotten. The drama comes from watching the self narrow and distort over time.

TYOV-Web preserves that central idea. A player creates a vampire from mortal beginnings, plays through prompts generated by dice rolls, writes narrative responses to those prompts, and organises the resulting fragments into memories. Over time the character collects skills, resources, marks, and known companions, but those are unstable possessions. The most important question is never simply what happened? It is what can still be remembered?

How Play Works

The application follows the structure of the tabletop game closely, but presents it as a guided story engine. Character creation unfolds in six stages: mortal life, mortal relationships, skills, resources, a defining early experience, and finally the turning into vampirism. Once play begins, each turn revolves around a prompt. The player reads the current prompt, writes at least one experience in response, manages any consequences on the character sheet, then rolls a ten-sided die and a six-sided die to discover the next destination in the prompt list.

That movement is what gives the game its broken historical rhythm. A positive result on D10 minus D6 pushes the vampire forward; a negative result drags the story backward into earlier themes or alternative branches. Prompts are also divided into variations, so revisiting a number does not necessarily mean revisiting the same moment in the same way.

The memory system gives the application its emotional pressure. A character can normally hold only five active memories, and each memory can contain no more than three experiences. When that limit is reached, the player must decide what to lose. Some memories can be moved into a diary, which acts as an external repository, but diaries themselves are only resources, and resources can be lost. The result is a game about narrative curation: the player is constantly deciding which version of the vampire's past deserves to remain visible.

Special prompts can alter that rhythm. Some disable experience-writing for a turn. Others allow the vampire's name to change, permit the editing of old experiences, reduce or increase permanent memory capacity, or end the chronicle entirely with an epilogue. TYOV-Web exposes these rules clearly during play, but the rules never overwhelm the fiction. They exist to sharpen it.

An Example Unlife: Karmis

The most compelling way to understand the application is through one of its stored characters. One campaign in the database follows Karmis, formerly Ekurzu and before that Naram, across roughly three and a half thousand years.

He begins in Bronze Age Mari as a temple scribe: precise, observant, and deeply embedded in the religious and political machinery of the city. His mortal life is defined by clay tablets, whispered intelligence, and courtly danger. He falls in love with Hessa, is shaped by the mentorship of the high priest Ibbi-Zamri, and is eventually turned by the ancient vampire Ashurban the Veiled. In one of the campaign's defining cruelties, Ibbi-Zamri becomes the first person Karmis kills after the turning.

The early centuries of his unlife are marked by exile and shame. He flees with Ennatum, struggles to master the hunger, tries to imagine restraint as redemption, and fails. One of the campaign's most effective lines comes from this period: he builds walls and plays flutes in the wilderness, but it is "never penance, only a cage with softer bars." Eventually he devours Ennatum during a sandstorm and carries that loss into the long middle stretch of the story.

From there the campaign moves through one civilisation after another. Karmis passes through Akhenaten's Egypt, the imperial roads of the Achaemenids, the scholarly world of Alexandria, Roman frontiers, monastic Europe, Mediterranean trade routes, and the violence of the early modern Atlantic world. His roles change less than the world around him does. He remains a scribe, witness, forger, archivist, and careful observer of institutions that never last as long as he does.

The campaign also shows how the game's systems create character rather than merely recording it. Karmis gains skills such as Silent Cartography, the ability to trace unseen paths of influence, and Ash-Tongue, a talent for insinuation and social survival. But he loses others as time damages his identity. He once knew how to decipher ancient texts; later he no longer can. He once believed he could control the beast within; later even that confidence disappears. The mechanics translate immortality into erosion.

One of the campaign's richest relationships is with Thooni, a scholar who recognises the name Ekurzu not as legend but as continuity. After her mortal death, the relationship persists in altered form: a spectral Thooni begins appearing in the margins of Karmis's diary, leaving fragments in languages he can no longer read. TYOV-Web handles this elegantly because its model of a character is not limited to a living cast list. People can be present, absent, lost, remembered, or transformed into symbols the protagonist can no longer fully interpret.

By the late Roman and early medieval portions of the chronicle, the cost of the memory rules becomes brutal. Karmis loses so many memories that even the event of his own turning is no longer available to him. He relies on diaries and external records to stabilise himself, yet those too are vulnerable. A carved chamber called The Walls of Forgetting and a later monastery diary both disappear. The application does not treat this as simple inventory loss; it feels instead like a second death for pieces of the self.

In the final act, Karmis witnesses the Atlantic slave trade and begins keeping a ledger of names drawn from shipping manifests and punishment records. A later response captures the moral exhaustion of the character with unusual sharpness: he no longer imagines himself purified by restraint, only redirected by purpose. He feeds on slavers and calls it justice, yet recognises that the blood tastes the same. This is the kind of line the application is built to preserve: not just event logs, but accumulations of ethical fatigue.

His last great companion is Mirelde of Bracha, who becomes mirror, partner, and eventually historian. By the time the epilogue arrives, the story is no longer told in Karmis's own voice. Mirelde writes the closing judgment on him: not as a monster laid to rest, but as "just a man who remembered too much, for too long, and chose, in the end, peace." That shift in narrator is exactly the sort of late-game turn TYOV-Web makes easy to sustain, because it keeps the campaign legible even after the protagonist himself has become unreliable.

Why the Application Works

What makes TYOV-Web effective is not that it digitises every rule, though it largely does. Its strength is that it understands which parts of the original game are dramatically important and places those at the centre of the interface.

The main play screen is organised around the prompt, the character sheet, and the memory display. The prompt is always the immediate literary provocation. The memory display constantly reminds the player of pressure and loss: active memories, diary memories, and forgotten memories sit in visible relation to one another. Skills, resources, marks, and known characters are editable in place, which means the character sheet grows and collapses alongside the fiction instead of feeling like a separate administrative surface.

The result is a frontend that behaves less like a dashboard and more like a writing desk. Players move between the prompt text, the experience form, and the fragments of the vampire's surviving life. Even when the application is doing something purely mechanical, such as validating whether the player may continue, the effect is narrative: it is asking whether the current moment of the story has been fully accounted for.

The Frontend as Chronicle

The client side of TYOV-Web is a Vue 3 single-page application written in TypeScript. In practical terms, this means the entire experience unfolds without page reloads; in literary terms, it allows the game to feel continuous. The player logs in, sees a home screen of active and archived characters, enters a creation wizard for new vampires, and then spends most of the campaign inside a single evolving game view.

That view is supported by a set of focused screens: a login page, a home dashboard, the six-step character creation flow, the main game interface, an ending screen for completed chronicles, an archive view, and a story view that presents the campaign as a timeline. Around those are a collection of smaller windows and dialogs for editing characters, skills, resources, memories, and experiences. These are important not because there are many of them, but because they preserve the sense that a campaign is always open to revision, annotation, and reinterpretation.

State is held in Pinia stores for authentication, gameplay, character creation, and theme. That is an implementation detail, but in this case it serves a visible purpose: it keeps the player's character, current prompt, pending changes, and validation state coherent at all times. The interface never feels as if it has forgotten where the player is in the life of the vampire.

The Backend as Memory Keeper

Behind the interface is a Django 5 application with a REST API built on Django REST Framework. The backend models the campaign in the same terms the game itself cares about: Character, Memory, Experience, Skill, Resource, Mark, GameCharacter, Prompt, and PendingChange. This is not especially glamorous architecture, but it is disciplined. The data model is close to the language of play, which makes the whole system easier to reason about.

The most interesting design choice is the use of pending changes. TYOV-Web does not immediately finalise every action the player takes. Instead, new experiences and character-sheet adjustments can accumulate as provisional edits until the player chooses to continue the story. Only then are they committed atomically. This makes the system feel less like a CRUD application and more like a drafting process. A turn can be assembled, reconsidered, and only then sealed into the chronicle.

Authentication is handled by JWT tokens, and character data is scoped to the authenticated user. The backend exposes endpoints for the game state itself, dice rolling, story continuation, and CRUD operations on memories, experiences, skills, resources, marks, and known characters. In the article, however, the important point is not the shape of the URLs. It is that the backend has been designed to support a narrative workflow rather than a generic records system.

Audit Trail

One of the application's strongest features is its audit trail. Each time the player advances to a new prompt, TYOV-Web captures a complete snapshot of the game state: prompt transition, dice roll, character sheet, memories, and the set of changes applied on that turn. In effect, the application keeps both the current vampire and the history of how that vampire became current.

This matters for more than debugging. In a game built around forgetting, a turn-by-turn archive becomes part of the theme. The player may choose to lose a memory inside the fiction, but the application still preserves the fact that the loss occurred. The vampire forgets; the chronicle does not.

Development and Deployment

TYOV-Web is developed as a two-part web project: Django on the server side, Vue on the client side. In local development the repository can be started with a PowerShell bootstrap script, which creates the Python environment, installs dependencies, runs migrations, and launches both servers. In production, the frontend is built with Vite and the backend is configured through the usual Django environment settings, including a production mode selected with DJANGO_ENV=production.

Testing exists on both sides of the stack. The backend is covered with pytest and pytest-django; the frontend uses Playwright for end-to-end coverage of the creation flow and core game loop. Those details are reassuring, but secondary. The more meaningful point is that the project has been built as an actual maintained application rather than a one-off prototype.

Reception Within the Project

As a fan-made implementation, TYOV-Web is best understood as an act of interpretation. It does not try to replace the tabletop experience with spectacle. Instead it asks what a web application can do well for this particular game: preserve long-form writing, reduce clerical overhead, make memory pressure visible, and retain the full history of a life that the protagonist can no longer fully remember. In that sense it is not merely a helper app. It is a machine for turning play into biography.

See Also

References