尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A local, character-driven story sim in the spirit of Pax Historia, but at personal scale: you play one person in a group, and every other member is their own character with wants, needs, secrets and feelings. Runs entirely in the browser against KoboldCpp.
start.bat (Windows) or run ./start.sh. This serves the folder at http://localhost:8000 (using Node if installed, otherwise Python) and opens your browser. With Node you can also run npm run serve.No Python? Open index.html (or new.html, or library.html) directly and use Open folder to pick the scenarios folder (or a single world folder). Worlds you open are kept in the browser, so you do this once: they are there after a reload and on every page. In Firefox, use start.bat or start.sh instead: opened straight from a file, it keeps each page's saves and settings apart, and the page tells you so.
Each backend has its own context size: Test reads it from KoboldCpp, or from the provider's model list when it has one (OpenRouter does). Settings also has the memory cap, scene length, temperature and the most turns a chapter runs.
Library (library.html, in the top bar) shows every world as a card, with its genre tags, who made it and a picture, filtered by tag or by where it came from. A card opens the world's details: its starts, each with Play, "✦ Make my own" where the world allows it, and its lore packs. Open folder and Open zip add worlds and lore packs; they are kept in this browser (once each: opening the same world again changes nothing, and a changed copy replaces the old one), and Remove from this browser frees the space. Your saves of a removed world keep playing, since a save carries its whole scenario; only its pictures fall back to monograms.
Lists of worlds to download. Someone can share a list of worlds as a link. On the library page, paste it under Lists and press Add list: its worlds show as cards marked "From ", and Download on a world's details page checks it like a world you open from a folder and keeps it in this browser, where it plays with no network. Lists are fetched when you open the library and when you press Refresh, never while you play; Ensemble comes with none. When a list has a newer version of a world you downloaded, its card says Update available. Removing a list keeps the worlds you downloaded from it. Downloads are at most 50 MB (and 200 MB unpacked), and a list or download that fails says why and changes nothing.
Hosting a list. A list is a JSON file anyone can fetch, beside the zips of the worlds (each zip one world folder, as the Creator's Export zip makes it; ordinary compressed zips work too):
{ "format": "ensemble-list", "version": 1, "title": "Community worlds",
"worlds": [{ "id": "harbor-watch", "title": "Harbor Watch", "summary": "A fog-bound port town.", "tags": ["mystery"],
"author": "you", "credit": "Written by you", "license": "CC BY 4.0", "url": "harbor-watch.zip", "size": 2400000, "version": "1.0" }] }
id becomes the world's folder name (lowercase letters, digits, - or _); title, credit, license and url are required; url may be relative to the list; bump version when you change a world. The files must allow other sites to read them (CORS): GitHub's raw files (raw.githubusercontent.com) and GitHub Pages do, and a page opened straight from a file can read nothing else. A world with a problem in the list is skipped, and the library says which and why.
Settings is its own page (settings.html, the link at the top right of every page). Changes are kept as you make them, and Back to the game returns to the game you were playing with them applied. Its Saved games section lists every game kept in this browser: Export all saves downloads them as one file (games only, never settings or API keys), Import saves adds them back (a game that's here already keeps whichever copy was played last), and Delete all saves names every game it will delete and asks first.
Settings → Appearance sets the theme (Dark, Light, High visibility for low vision, or the same as your device), text size (90–150%), story font (serif or sans), line spacing, reduce motion, map detail (detailed floor plans, or simple boxes for older phones), map motion (people walk through the rooms and doors on the beat they move, only where you saw them go; a region that changes hands takes the new holder's colour from the side facing a neighbour it holds, on the beat the story tells it, and the world shifts before the people move; on a phone the map pops up while they do; Reduced fades instead, Off just updates, and Reduce motion keeps it to fades) and names in the story. Changes show as you pick them, on the Settings page and in the game when you go back, and are kept in this browser. With a screen reader, each new beat and note is read out as it appears.
Names in the story. People, places and lore terms in a scene are underlined: hover (or Tab to one) for a short note, click or tap for the whole entry, and a person opens their card. Only what you could know is linked: regions you know of, rooms you have seen, people in the story, and lore, never secrets. Settings → Appearance turns it off. The journal shows the latest chapter and folds the earlier ones to their titles.
index.html): Continue your last game in one click, your other recent games, and the way to everything else. The first-run setup shows here.new.html): pick a world, where you start and who you play (or make your own character, or have a start built for you), lore packs, then Start. Load opens a saved game from a file.play.html): the story, your action, the people, the map, the journal and Debug. Home in its top bar goes back to the landing page; the game is saved after every turn, so leaving or refreshing is safe (in the middle of a turn, it goes back to that turn's start).library.html), Creator (creator.html) and Settings (settings.html), as described below.Every page opens straight from a file as well as from the local server. A page's address carries only ids (which save, which world), never the game itself.
Where your games live. In this browser (its IndexedDB storage), never on a server. Clearing the site's data in the browser deletes them, and a different browser or computer has its own. Settings → Saved games → Export all saves downloads every game as one file; Import saves brings them back, here or on another computer. Save in the game downloads just that game. API keys stay in Settings and are never written into a save or an export.
Settings has two backends, each with presets:
http://localhost:5001)vendor/model-name).Under Which model does what, each step (characters & events, narration, memory updates, chapter summaries) can use either backend. Hybrid sends characters, events and narration to the external model and keeps bookkeeping local. Use Test on each backend; errors say what's wrong (bad key, unknown model, out of credits, can't reach).
API keys are stored only in your browser and are never written into save files.
Constrain JSON (on by default for KoboldCpp) gives the model a grammar that allows only the reply each step expects, with this game's ids and nothing around it. Small local models then stop wrapping replies in code fences or lists and stop inventing ids, which used to cost a retry. On OpenRouter the same box turns on the API's JSON mode. If a provider refuses it, the call is sent again without it.
Smaller calls (on by default for KoboldCpp and for any model at 16K context or less) splits the two big bookkeeping steps into short requests that each ask for one thing:
Turns take a little longer, but small models get them right far more often. A part that fails costs only that part, and Debug groups each step's calls together.
Ensemble is plain HTML and JavaScript, so it can be hosted as-is on any static host (GitHub Pages, itch.io as an HTML game, Netlify). Open the link on a phone; the crew and situation panel slides up from the 👥 button.
To hand it to someone without a GPU: send them the link, have them open Settings → External model → OpenRouter, paste their key and a model name, then press All external.
Note: a page hosted on https:// usually can't reach http://localhost, so on a hosted copy use the remote KoboldCpp URL (which is https://) rather than localhost.
eventRates, with cooldowns, so they stay rare; bigger events carry concrete effects on world status, group stances, pressure clocks and regions (who holds one, its tags and status line), and can add a new pressure. Known regional and world events appear under News.[+0m], [+40m]…): one for a minute or a few minutes, up to four overnight. A minute is for dense conversations: each exchange moves the clock one minute, so a long talk doesn't eat the evening. The page shows one beat at a time, with Step in here and Next ▸ under the newest one. Next settles that beat and shows the following one. Stepping in (or simply acting before you reach the last beat) cuts the turn at that point: later beats never happen and are never shown, time only advances to that moment, and your next action starts there. "About an hour" and "A few hours" are approximate: the clock follows where the scene actually ends, and a scene that stops before half the step is asked to continue once.If a call fails, the turn is rolled back. If only the update step fails, the scene is kept and a basic fact is recorded instead.
💡 Suggest (optional, 1 call) asks for three or four ideas for your next action. Clicking one puts it in the action box, where you can edit it; nothing happens until you press Act. The model sees only what you know and the scene as far as you've read it (up to your step-in point), and the call goes to the backend routed for characters & events.
⏭ Until something major happens fast-forwards through downtime (days or weeks) and stops at whichever comes first: a pressure clock running out, a random major event (on average every majorEventDays), or the horizon (majorHorizonDays, default 30). During downtime people eat and sleep, so hunger and fatigue settle instead of climbing. The scene comes back as a short montage of day-stamped beats followed by the interrupting event as a live scene, with everyone wherever they were when it hit. You can type a plan for the downtime ("keep the arrays clean and avoid Teo") or leave it empty, and you can still step in after any montage beat.
Clocks can have follow-ups (then): when one fires, new pressures start. In Ares Deep, the storm arriving starts "Storm clears" and "Battery reserve depleted"; whichever runs out first is what the next big skip stops at, unless something else happens sooner.
Prompts are ordered for KoboldCpp's prompt cache: a fixed header (rules, setting, places, cast), then chapter summaries and facts the player knows, then everything that changes per call. The header is identical for all calls in a turn and only grows at its end between turns, so most of it is reused. Context is the smallest among the backends in use (the shared header goes to all of them), capped by the memory cap (default 32K) so a huge hosted context doesn't inflate every prompt. It sets a tier: compact (up to 10K), standard (up to 24K) or roomy. A compact context gets a shorter outline of the map, fewer nearby regions and only the groups that are around, and its memory is fitted into what's left after the reply, the fixed header and the turn's own message, so a game forty turns in still fits 8K; standard and roomy keep facts at about 20% of context and chapter summaries at about 12%. A call that would still overflow drops its lore, then the start of the last scene, and says so in Debug. npm run context prints what every part of every prompt costs at each size.
Every fact records who knows it (known_by). The narrator only gets facts the player knows; each character's intent only gets their own private knowledge.
A world is everything that's true wherever you start: the setting, rules, skills, groups, event pacing and a pool of characters. A start is one way into it: when, where, who you play, who's around you, the local pressures and the opening scene. One world can have several starts; Ares Deep has two (Kit in the commons, Priya in the garage).
Copy a folder in scenarios/ and edit it:
scenarios/my-world/
world.json
starts/<id>.json one file per id listed in world.json "starts" (the first is the default)
characters/<id>.json one file per id listed in world.json "characters"
Then add the folder to scenarios/index.json (only needed when running with the server; Open folder finds worlds on its own). Worlds are validated on load, with readable errors for unknown locations, skills, relations, cast members and so on. With Node installed, npm run check validates every world and every start, and warns about field names it doesn't know (a typo like oppening).
The smallest world is a world.json with only title and setting: no starts, characters, skills or regions. The picker then offers ✦ Make my own start, and everything else below is optional.
world.json fields: format (2, may be left out), id, title, date (optional {day, hour}: when a generated start begins, and an authored start without start; default day 1, 9:00), calendar (optional {"day1": "1939-09-01"}: the real date of day 1, so days read as dates, "Thursday 16 April 1942", in the game and the prompts; without it they read "Day 12"), summary (one or two lines for the picker), author, credit, license and cover (optional, for the library card: who made it, credits, the license, and an image in the folder; without a cover the card shows the world map's picture), tags (genre tags, most important first: survival, slice-of-life, romance, intrigue, war, horror, mystery, comedy, cozy, grim, adventure, political, or your own words; up to 8), toneNote (a sentence on the tone you want), setting, rules (list), timeLabel ("Sol", "Day"), wakeHour, skills (list), needs ({id, label, perHour}; leave it out, or use [], for a world without needs: no bars, no penalties; an optional limit says what happens to a player who skips ahead without seeing to it: {relief, missing, text, worst, grace, after}, where relief is the words in a skip plan that cover it (like ["sleep", "rest"]), missing names what they went without, text is what happens ("collapses from exhaustion"), worst is the worst health it can leave them in (ok, hurt, badly_hurt, dead), grace is the hours a body lasts at the limit, and after is where the need drops back to; fatigue, hunger, thirst, stress and fear have built-in limits, and any other need gets a plain one, so each field is optional), skillPenalties (optional: {health: {hurt: -2, badly_hurt: -4}, needs: [{need: "fatigue", at: 0.85, penalty: -2}]}, today's values shown; a need the world doesn't have never applies), revealPressure (optional: which needs count as pressure for cracking secrets, default ), (key → short description), (groups such as an organization, gang or government: ; their stance can shift as events ripple out; optional and , lists of group ids), (the player's standing with each group at the start, , −100 to 100; starts and player slots may change it), (: average in-game hours between regional and world events; 0 turns a tier off), (: idea lists that steer what those events are about; an idea can be for something that happens in one region), (see below), , and (for the major-event skip), (optional: , see cast tiers below), and (minor people, see below), (public facts true everywhere), (the pool: ids; optional), (ids; optional: none means generated starts only). can go here too when every start shares the same rooms, as in Ares Deep.
starts/<id>.json fields: id, title, summary (who you are and what's going on), start {day, hour}, player (a cast id), region (where the player starts, when the world has regions), cast (who's in this start), castLabel (sidebar heading), locations ({id, name, desc, connects, region}; region defaults to the start's), clocks ({id, name, value, max, perHour, fireText, fireWorld}, with then for follow-up clocks started when a clock fires; fireHarm ("all" or "outside") makes everyone, or everyone outside, one step more hurt when it fires, and repeat: true starts it again after it fires, so it keeps hurting until someone deals with it; fixed: true means only time moves it (no repair or event buys time), and stopIf (conditions, as for timeline events) stops it for good once they hold, such as a reactor shut down or a fire put out; "ending": true makes it end the story when it comes to a head), facts (public starting facts), opening (first scene), playerSlot (see below). A start can also override world fields: a different world status, a group's stance, extra rules, and so on. Lists of rules and facts add to the world's; rooms, clocks and groups with the same id change the world's version field by field.
Making your own character. Give a start a playerSlot and the picker offers "Play as " or "Make my own" (leave out player and everyone makes their own): "playerSlot": {"location": "commons", "summary": "You make your own crew member...", "roles": [{"id": "systems", "label": "Systems engineer", "desc": "Keeps the air and water running.", "skills": {"tech": 3}, "suggestedTraits": ["practical"]}], "peoples": [...], "freeform": false, "skillPoints": 10, "maxSkill": 3, "relationsDefault": 10, "castFeelings": 10}. The form asks for a name, pronouns, age, a role (and people, if listed; "freeform": true lets players write their own), who they are, appearance, traits (with ideas from the role), voice, what they want, an optional secret, skills (point-buy: skillPoints to spend, up to maxSkill each, which is also as far as anyone's skills grow with use: three successes at normal difficulty or harder raise a skill by one, at most once a week, and your card notes it; a role's skills are filled in first), a portrait (from portraitSet, the stand-in, or initials only) and starting feelings toward each person (relationsDefault, a number or {id: n}). castFeelings is how they feel about the newcomer, knows and needs are the made character's private starting facts and needs, and summary replaces the start's summary in the picker. The authored player leaves the cast unless "keepDefault": true. 💡 Suggest next to traits, voice and description asks the model for an idea (routed with World building); nothing changes until you pick it. Save this character (in the form, or on your card's Details) downloads it as a character file, and Load character fills the form from one. Both bundled worlds' default starts have a slot.
Making your own start. A world with no starts always offers it; a world with starts opts in with "generatedStarts": {"roles": [...], "skillPoints": 10, "maxSkill": 3, "castSize": [3, 5]} in world.json (every field optional; those are the defaults besides roles) and mark the regions players may start in with "startable": true (it takes in everything inside a region; false shuts a branch out). In a big world the start picker searches by name, shows a tree or the map, or picks one for you (🎲 Surprise me), and you can say where exactly you start ("a moisture farm outside the city"). The start picker then offers ✦ Make my own start: pick where, make your character (or describe one in a line) and say what kind of story you'd like, and the story engine builds the rooms, three to five people with their own wants and secrets, how they feel about each other, one or two pressures (each may start a follow-up when it comes to a head), a few ideas for later events around the place, minor people and groups, an opening, and a few notes of local lore (the diner, the street's market) that keep those details the same from turn to turn, one small checked step at a time (routed with World building). A step the model gets wrong is retried with the problem named, and any step can be redone; progress is kept in the browser, so a closed tab picks up where it stopped. With no starts at all, it's the only choice. With "customSetting": true the player also describes the world itself ("a fishing village on a Scottish island in the 1950s") and the first step turns that into the story's own setting and genre tags, which the review lets you edit; Your own story is a bundled world that does only this, for a fully custom story. Harmon's has it; Ares Deep, a closed five-person station, doesn't. When it's done you get a review: the opening, the places (with a map), the people and the pressures, every field editable and checked as you type, with ↻ Reroll for one room, one person, the pressures, the minor people, the opening or the local lore (the rest stays; a lore note can also be taken out with ✕). Secrets, private facts and story-engine notes are hidden until you tick Show spoilers. Save as start downloads a zip (the start, a character file per new person, and world.json with both added to its lists) to unzip over the world folder, where it loads like any authored start; Play starts it now. Neither works while the review lists a problem (a field that doesn't validate, a secret in the opening, a pressure that gives one away).
New regions. With "generatedRegions": {"maxRegions": 6, "keepDays": 7, "routeKind": "road"} as well, the story can also head somewhere bigger that isn't on the map, another town or a planet: the engine adds it as a region with a short description and a route from where you are, builds a place to arrive at, and lets it fade back to a name if nothing happens there. New places. Add "generatedSites": {"maxSites": 8, "keepDays": 3} to world.json and the story can go somewhere the map doesn't have: "I head to the pharmacy on Main Street" builds the pharmacy (a few rooms, who's there, what there is to find), joins it to where you are, and you arrive at its door. Only your own action takes you there, only where a route goes (a far place is a long trip), and not through a locked door. Events can name new places too, built when you first go. A place is kept if something notable happens there or you go back; otherwise it fades a few days after your visit and is rebuilt (remembering your visit) if you return. Put hand-drawn layouts in sites/<id>.json and list them in "sites" (the creator's Sites tab draws them on a battlemap or floor plan): a new place that fits one by type or tag uses it, drawn on its picture, and the model only adds the people and details. With "library": ["modern"] in generatedSites, the shared maps in library/ for that setting join in (Harmon's gets the roadside diner). Harmon's builds places; Ares Deep doesn't. Debug › "Places made during the story" shows what was built and why. Map focus. "mapFocus": "local" keeps the tactical floor plan for every place (doors, sight lines, patrols: tense stories); "region" makes the city the lived space: the map opens at your district with the places you know as pins, and new places are quick flavour scenes (one to three rooms, built in one call) that grow a floor plan if danger arrives. Left out, a lethal or hostile world is local and an everyday one region. A library map or start can say "tactical": true. Both bundled worlds are local.
Regions. The map above the rooms: "regions": [{"id": "junction", "name": "Highway junction", "controller": "convoy", "tags": ["blocked"], "status": "abandoned cars across two lanes", "neighbours": ["highway-strip"], "lore": "Where the highway meets the interstate..."}]. controller is a group id or null (no one), tags are up to 6 short words, status is a line up to 120 characters, lore a summary up to 400 (no secrets). Neighbour links work both ways, so listing one side is enough. Every room belongs to a region (the start's unless it says otherwise). Regional and world events can hand a region to another group, add or remove its tags and change its status, as rarely as the event rates allow. The sidebar shows your region; the story engine sees the regions' names, neighbours and lore (on a big map, the ones near the start in full and the rest as names), but who holds what only for your region and what's near it. A start can change a region by id. Regions inside regions: give a region a parent (another region's id) to put it inside that one, up to 6 levels deep where your story needs detail (a country, its zones, a city, its wards) while the rest stays broad; label names what it is ("zone", "ward", "planet"), and regionLabels in world.json gives the word for each level so you don't repeat it. A region with no controller of its own shows who holds its parts. The story engine sees the chain you're in (ward › city › country) and what's near, so a deep world costs no more per turn than a flat one. Harmon's is the example: the highway strip, Main Street and the houses are parts of the town of Harmon. Presence and hidden places: "presence": {"convoy": {"junction": 0.4}} says how strongly a group is in a region besides who holds it (several can be; close ones make it contested), and events can shift it; "known": false on a region keeps it, and everything inside it, off the map and out of the story until something reveals it (an event, a fact the player learns, or the player getting there). Routes and travel: "routes": [{"to": "ridge-town", "kind": "road", "time": "45m"}] on a region says how people get somewhere: a kind (border, road, rail, sea, air, checkpoint, hyperlane, portal, or your own word), a time, and optionally requires (a condition in the timeline's form, such as {"world": "papers", "is": "yes"}), risky (the player can try anyway, as a roll) and . Neighbours are free borders, so add routes only where travel matters. A closed way stops a move before the story engine is asked; a trip longer than your time step becomes the turn, a journey that pressures and events can interrupt (you then wait "on the way" and can go on). The story engine sees the routes out of your region with their times, and why a closed one is closed. Details: › Routes and travel.
Lore. Background knowledge about the world, chosen per turn so a big world still fits a small model: "lore": [{"id": "rim-work", "title": "Working on the rim", "level": "region", "ref": "crater-rim", "keywords": ["EVA", "dust devil"], "priority": 3, "text": "Every trip outside needs two people..."}], or file ids such as "lore": ["town"] for lore/town.json (one entry or a list). level is world, region, location, character or topic; ref names the region, room or character (not for world and topic entries). An entry comes up when the player is in its room or region, when its character is present, or when one of its keywords (whole words, any case) is in the player's action or the last scene, best first by priority (1–5, default 3) within about a tenth of the context. topics (up to 4: school, work, business, crime, military, politics, religion, nightlife, family, health, travel, tech, science, sport, arts, law, or the world's own from "loreTopics": {"scavenging": ["supply run", "looting"]}) bring an entry in for a player character whose role, traits or tags fit (a character file may add topics of its own), or when the scene turns to it, so a student and a manager in the same city get different lore. Where nothing has lore of its own, the world entries come in last. alwaysOn: true world entries are always included; keep them short. text is up to 800 characters (40–80 words on one topic works best), and sources ([{title, url}]) are for you, never sent to the model. Lore is public: secrets go in a character's secret, and the game refuses an entry that gives one away. Starts add or replace entries by id. Debug lists which entries each call included and why. Details: docs/WORLDS.md › Lore.
Endings. How the story can end besides your death: "endings": [{"id": "got-out", "title": "Out of Harmon", "kind": "win", "when": {"judged": "the player gets out of Harmon for good"}}, {"id": "a-month-on", "kind": "bittersweet", "when": {"after": {"day": 53, "hour": 9}}}]. A timeline event or a pressure can also just say "ending": true (or {"title", "kind", "epilogue"}) to end the story when it happens. when is a timeline event, a time (after), a pressure coming to a head (clock), the player getting somewhere (reach: a room, or anywhere inside a region), or judged: something the scene has to show, which the story engine rules on while it records the scene (it has to quote it). if adds conditions in the timeline's form, and kind (win, loss, bittersweet, neutral) and epilogue set the closing's tone. The scene that reaches an ending closes the story, then an end card offers Keep playing. Details: docs/WORLDS.md › Endings.
Lore packs. Add-on lore for a world, made by anyone without changing the world: a folder packs/<id>/ with pack.json ({"id", "title", "world", "version", "credit", "license", "lore": [...], "timeline": [...], "loreTopics": {...}}), listed in packs/index.json. Pick a world, then Lore packs to tick the ones you want; they're added at New game and your saves keep them. A pack only adds entries (a world may let them replace its own with "lorePacks": {"replace": true}). The Creator's Lore tab makes, imports, edits and exports them. packs/harmon-county is an example for the holdout. Details: docs/WORLDS.md › Lore packs.
Canon timeline. Dated events: "timeline": [{"id": "convoy-checkpoint", "when": {"day": 26, "hour": 9}, "scope": "regional", "region": "junction", "text": "The rescue convoy reaches the junction and sets up a checkpoint.", "effects": {...}, "preconditions": [{"controls": {"group": null, "region": "junction"}}], "onBroken": "diverge"}]. Events before the start are the past: they become facts and a "Before the story" chapter. Future ones happen on their date as news (with their effects), outside the usual event pacing, if their preconditions still hold: alive, controls, clock with atLeast, world with is, feature with is (a tracked feature's state, "room|name"), known (an event id, or {"words": [...]}, optionally by someone), reached (a region the player has been in), built ({"place": "poormans-gulch", "atLeast": 4}: lots built on in a place) and not. An event can also happen on arrival instead of on a date: "when": {"reach": "fort-laramie"} happens the first time the player reaches that region, and a journey stops there for it (the stages of a trail); pair it with a dated event that has {"not": {"reached": "fort-laramie"}} for what happens to those who are late. If one broke, onBroken decides: drop (the default) or diverge (the model writes what happens instead). visibility is heard (default), seen or hidden (only who learn of it). "moves": [{"id": "reiko", "to": "tokyo-campus"}] sends people to a room when the event happens as written (the third-years off to university at graduation), and people whose joins names the event come into the story with it. "Until something major happens" stops at the next one that is world-wide or near the player; regional news from far away is heard late, at the next turn. When nothing is planned for the next few days, the model plans a few likely events (an exam, a holiday, a delivery) so the story keeps its pace; "projectTimeline": false turns that off and {"days": 3, "max": 3} tunes it. Never put future events in lore or descriptions. Details: docs/WORLDS.md › Canon timeline.
Research helper. npm run research -- plan <world> lists what to research from the world itself (the world, its regions, groups and characters) in research.json; trim it ("skip": true), or point a topic at a wiki page, a url or pasted text. npm run research -- run <world> reads each source (Wikipedia by default; --wiki takes any MediaWiki wiki, such as a Fandom one) a part at a time and has your model draft short lore entries (each with the topics it proposes from the world's list) and, with --date YYYY-MM-DD (the start's real date), dated timeline events. It uses KoboldCpp on this computer, or an API with --backend external --url … --model … and the key in ENSEMBLE_API_KEY (never saved). The engine refuses copied text, oversized entries, entries about nothing in your world and anything that gives away a secret; secrets and undated later events are printed as suggestions. Everything lands as drafts the game ignores until you accept them in the creator's Lore and Timeline tabs. It runs only while you build a world, never during play. Worlds made from franchise wikis are for your own play and mods.
cast entries are a character id, or an object that sets where that person is and how they feel in this start: { "id": "bauer", "status": { "location": "hydro" }, "needs": { "stress": 0.7 }, "knows": ["..."] }. An entry can override status, needs, relations, knows, want, secret, guard and tags, and can set tier ("local" or "secondary"), pinned (true keeps that person local) and agenda (see below).
The map. Next to the story there's a map of the rooms you know, with everyone where you last saw them (people out of sight are faded; the cards say "last seen in…" too). Switch between Text, Split and Map above the story; on a phone, 🗺 swaps the story for the map. Clicking a room or a person fills in the action box. The game plays by it: you move only through linked rooms, a locked door stops everyone but whoever holds its key (picking a lock takes a roll), shut doors block your view, and cameras or watched entrances on your way make sneaking harder. Use something the map doesn't list ("I climb out the back window") and it's added if nothing contradicts it. A world can shape it: rooms take size (small, medium, large), level (1 for upstairs or a roof), outside, features ("forklift", "the pickup"), seen: false for a room you don't know yet, and links ([{"to": "roof", "kind": "hatch"}]; kinds open, door, locked door, vent, hatch, window, stairs; a locked door can name "key": ["ferris"], and any link a "time": "10m" when the usual minute is wrong, like an airlock or a ladder). How far things are: the game works out the way and how long it takes from link times, route times and a default for each kind of link and region (a neighbourhood's border is 15 minutes on foot; "travelTimes" changes the defaults). A short trip arrives partway through your time step and the scene goes on there; a long one stops where the time runs out, the game tells you how far is left under the scene, and "keep going" or ➜ Travel there goes on. Features can carry a state ({"name": "camera", "state": "on"}), the states it may take (["on", "off"]), and what a state needs first: "requires": {"shut down": {"if": [conditions], "otherwise": "the rods won't drop"}} (until then the scene is told the otherwise line and the change is refused). The feature condition ({"feature": "reactor|reactor", "is": "shut down"}) tests one, in endings, timeline events and routes. And entrances ([{"room": "dock", "kind": "roller door", "watched": true}]) mark ways in from outside. "site": {"type": "hall"} or "corridor" picks a layout style, and localMap ({image, width, height, rooms: {id: {x, y, w, h}}}) uses your own floor-plan picture instead.
The creator. Creator in the top bar opens creator.html, where you can open a world (from the server list or a folder) and edit it with live validation; your edits are kept in the browser until you export. The Map tab draws regions on a map image (click corners, drag to adjust, or place a point), sets who holds each region, its tags, status, lore and borders (it suggests regions whose outlines touch), places rooms on the map, and sets each start's region and each group's colour. Regions form a tree: + Inside and + Next to it add regions, and dragging one onto another puts it inside. Each region can have routes (a kind, a time, one way, risky, and what it needs) and Attach a map… for a picture of the regions inside it; the Route kinds table sets how each way of travelling works for the whole world. The Lore tab edits lore entries by what they are about, with keyword chips, topic tags (and topics of the world's own), priority and sources, and a test box that shows which entries a room and an action would bring in, and their token cost. The Timeline tab lists events before and after the start, and edits each one's date, place, text, who hears of it, its conditions (picked from the world's real people, groups, regions, pressures and events), what happens if one breaks, and its effects; it also turns planned events for quiet stretches on or off. The World tab edits the setting, rules, genre tags (with what each one means for the story), time, skills, needs (or none), pacing, event ideas, world status and public facts. The Groups tab edits each group's name, description, stance and colour. The Starts tab adds, duplicates and removes starts and edits each one: its date and region, who you play (or a slot where players make their own), the cast with where each person starts and what they feel and know there, the rooms and the ways between them (with a preview of the local map), pressures and what follows them, minor people, groups of people, facts and the opening. The Characters tab edits the world's people in the same form players use to make their own character, plus what only an author sets: the secret, what they know, an agenda, whether they start in the foreground, tags, starting needs, feelings toward everyone, and a portrait from your own image files. Import a card brings in a SillyTavern/Tavern character card (PNG or JSON, card versions 1–3): one call to your model (routed with World building) turns it into a person who lives in this world, keeping the card's personality and leaving out its own scenario, first message and system prompt; the engine also cuts any line written to the player or taken from the card's scenario, and tells you. You review the sheet before it is added, and the card's image becomes the portrait only if you tick that you have the right to use it. The Portraits tab manages the stand-ins (portraitSet) and shows who gets which. The Sites tab turns battlemaps and floor plans into site maps (see › Site maps). starts from a blank world that already loads, or from a template (One country with provinces, One city, Planets and travel) with regions, labels and route kinds set; opens an exported world; opens the game in a new tab with the world as it is now, without exporting (it never touches your own saved game). downloads the whole world folder, images included; unzip it into or open it with .
Real places. New world in the creator can start on the shared topographic world map (from Natural Earth, public domain): pick an area and the map knows where it is on the globe, so the Map tab shows latitude and longitude, and places (capitals, cities) go where they really are. Regions can have several pieces (islands). npm run basemap -- fetch once, then npm run basemap -- regions <world> --within --places capitals adds today's countries and capitals to a world (or --pick "France, Italy"), with neighbours from their borders and their Wikipedia pages for the research helper; crop cuts a sharper picture for a small area. It also builds a tree: --from states puts provinces under their country, crop <world> --region france gives a country its own map, and cities go in their province as pins or (--as regions) as regions. A big map keeps the prompt small: the start's surroundings in full, the rest as names. The world today is bundled as the base for modern-day worlds: every country and every city of a million or more, each with its Wikipedia page for the research helper and no lore yet; play it as it is (make your own start anywhere) or copy it. Occupied France, 1942–1944 is a historical world built the same way: France on its own map with the zones and départements of 1942, the demarcation line as a border with risky crossings, Paris, Lyon and Marseille as region maps (arrondissements from IGN's open data, Licence Ouverte 2.0, credited in each map), a real calendar and a canon timeline to the liberation. Seiryo High is a slice-of-life world on Japan's map: the prefectures from Natural Earth, Tokyo's wards and Ibaraki's towns from MLIT's municipal outlines (credited in each map), close-ups of Mito and of Daigo's hot-spring valley, the school, the dorm and the family's house as floor plans of their own (a room's site makes a separate place, joined to the others by trip links), and a school calendar from the entrance ceremony in April 2026 to graduation in March 2029, with a cast of thirty-four (some join in the second term and the second year). The Frontier, 1866–1869 is America a year after the Civil War: the states and territories of 1866, the lands of the Native nations held by their peoples, the trails, stage lines, rivers and railroads with the travel times of the time (rails, passes and fords opening and closing with the timeline, to the Golden Spike), Boston and San Francisco as street maps of 1866, and four towns drawn as plans (a railroad town, a cattle town, Julesburg's stage station and a hidden gold camp); five In the West starts (Salt Lick, Haddon, Julesburg, a Shoshone camp at Wind River, an Oglala camp in the Powder River country), each with a cast you can play any of, or make your own start anywhere; in a town you start in its streets. is the same country for one story: a wagon company going west from Council Bluffs, as its captain, its hired guide or an emigrant of your own, with the trail's stages happening as the camp reaches them, deadlines that punish the late, and a hidden gold camp at the end to turn into a town. Details: › Base maps and real places.
The world map. In a world with regions the map zooms through them: a breadcrumb runs from the world down to the room you're in ("Holdout at Harmon's › Harmon › The highway strip › Sales Floor"), with 📍 Where I am and 🌍 Whole world to jump. Each level shows the regions in the colour of the group that holds them (a group's color, 1–8, picks the colour), with their tags, and your region and the regions it's inside marked; click a region with regions inside it to zoom in, or any other to see who holds it, its status and its lore. Ways to other places under the local map lists the routes out of your region ("🚗 Ridge · 45m"); clicking one fills the action box. Any region can have a map of its own (the same format), showing the regions inside it; a level without one zooms the nearest picture above it, and with no picture anywhere the game draws the regions as a node map with their routes. Pictures are always underneath: decoration, never data. Add "map": {"image": "maps/town.svg", "width": 800, "height": 560, "regions": {"junction": {"polygon": [[300, 150], [620, 150], [640, 290]]}}, "locations": {"floor": [515, 322]}} to world.json to draw it on your own picture (the holdout's maps/town.svg is an example); without one, the game lays out the regions from their neighbours. A region's outline can change in the story: its entry may list outlines, stages drawn while their conditions hold, and "border": true draws a zone as a line over the regions (Occupied France's demarcation line moves to the Rhône in November 1942; the Oregon Trail's gold camp grows as it is built). See docs/WORLDS.md › Outlines that move.
Cast tiers. Big casts stay cheap because only local people (with you, or important right now) get the full simulation. Secondary people are in the background: no turn-by-turn intent, one short line (role, where they are, what they want) for the story's events, never their secret, and their needs settle as if they're keeping to their routine. They're listed under Elsewhere in the sidebar. By default the people in your starting room are local and the rest secondary. Someone becomes local when they enter your scene, when your action names them, or when they're in an event you see or hear. They drop back to secondary after demoteAfter turns (default 4) without being seen, and if more than maxLocal (default 6) local people are away from you, the ones you haven't seen for longest go first. pinned people never drop; you can pin or unpin someone yourself from their card (📌 Pin under Details, up to maxLocal people). Every change shows in Debug.
Agendas. A character file (or a cast entry, per start) can give someone a goal they work on while you're elsewhere: "agenda": {"goal": "prove the deep-ice cultures are alive", "hours": 14, "steps": ["runs a second culture in the unlogged incubator", "checks the results against the controls", {"text": "comes to find Kit, wanting a second opinion", "contact": true}]}. Steps are short actions without the name, hours is how long each takes (1–240, default 12), and visibility is "hidden" (default: the player never sees the step happen) or "heard". A contact step brings them to you, and a long skip can stop there. People in the background make progress; people with you act turn by turn instead. Use "agenda": null (or {"goal": "keep routine"}) for no agenda; leave the field out and the model proposes one when they first end up in the background. Agendas never show on cards; Debug lists them.
Relationship web. Web in the people panel shows who feels what about whom: your feelings, how each person treats you, and, between other people, only what you have seen or been told (dashed lines, with the fact that showed it). Words only; exact numbers are in Debug. A toggle adds the people in the background.
Extras. Minor people who don't need a full sheet: "extras": [{"id": "albee", "name": "Mrs. Albee", "pronouns": "she/her", "role": "elderly neighbour", "location": "dock", "note": "keeps a shotgun she says is not loaded"}], in world.json or a start. "location": null means nowhere in particular. Crowds, patrols and hordes are standees (below). Extras have no turn of their own, needs, feelings or card: they show in the scene bar when they're with you, can be part of events and facts, and can move. The note is for the story engine and never shown. Name someone twice (on different turns), or have them in an important fact, and one World building call (routable in Settings) writes them a full character sheet; they join the cast from then on, keeping everything known about them. With "inventExtras": true the model may add new minor people (or a group, as a standee) as scenes need them (one a turn); leave it off for closed settings like a five-person Mars station.
Standees. Unnamed groups on the map, such as guards, a gang or a few of the dead: "standees": [{"id": "stragglers", "label": "stragglers", "count": 5, "location": "lot", "state": "calm", "note": "they bunch up at the dock door when they hear noise"}]. state is calm, alert, hostile or down; faction (a group id) says who they belong to; post (a room they hold) or patrol (a loop of linked rooms, one room an hour) make guards. A group acts as one, moves only to a linked room, gets harder to fight or sneak past the more of them there are and the warier they are, and notices you when a sneak fails. Name one in your action ("I grab the nearest straggler") and they become a minor person. A group that is wiped out or flees leaves the map. Groups exist only in rooms; a horde far away is a pressure or a faction. The old "group": true extras still load, as standees when they have a room.
Play anyone. A start's "playable": true (or a list of cast ids) lets the player pick anyone in its cast to be, beside its own player and Make my own.
Places that travel and lots to build on. A place (rooms with the same site) can travel with its people: "siteInfo": {"camp": {"name": "The company camp", "travel": {"group": "company", "kinds": ["wagon", "ferry"], "toward": "oregon-city"}}}. When the player's words set out for a region for the whole party ("we roll on to Fort Laramie"), the place moves there as one, its rooms and everyone in them, by routes of those kinds only; only the group's leader (or a favored member) can take it off the way it is heading. A room with "lot": true in a place is a lot: an event's build effect ({"room": "poorman-lot-1", "name": "Assay office", "desc": "...", "as": "tent"}) puts up a tent or a building there once someone holds the region, and the built condition counts them. The Oregon Trail's wagon camp and its gulch are the examples.
Routines. People can move around the map as the hours pass, with no model call: "routines": {"student": {"dayTypes": {"school": {"days": ["Mon", "Tue", "Wed", "Thu", "Fri"]}, "free": {"default": true}}, "slots": {"school": [{"from": 9, "to": 15, "at": ["class"]}, {"from": 22, "to": 7, "at": ["home"]}], "free": [{"from": 22, "to": 8, "at": ["home"]}]}}} in world.json or a start, and "routine": "student", "places": {"class": "classroom-1c", "home": null} on a character (null: away, off the map; own slots optional; a playerSlot may have routine and places too). The story wins until the next slot, and routines stop for the badly hurt and for emergencies. A timeline event's "routines": [{"id": "rin", "places": {"class": "classrooms-2"}}, {"id": "reiko", "routine": null}] changes someone's day from then on. Cards show a person's usual day, without slots marked "private": true, and the creator has a Routines tab. Built-in routines (worker, homebody, shopkeeper, farmhand, student, night-shift, wander) work in any world; anyone without a routine follows a worker's day until the model gives them one; "routine": "fixed" keeps someone put, and "routines": "fixed" keeps a whole world put. See Routines.
Character files hold who the person is: id, name, pronouns, role, age, traits, want, secret, guard (low, medium or high: how hard the secret is to crack), voice, skills (0–4), relations (id → −100..100, how they feel about that person), and optionally portrait (an image in the world folder, like "portraits/okafor.png"; png, jpg, webp, gif or svg), description (who they are, up to 600 characters) and appearance (up to 400; keep the first sentence a good one-line look, since the narrator sees that sentence for the people in each scene), and people (a people or origin, up to 60 characters, shown with the role). "portrait": false means initials only, even when the world has a portraitSet. A character can belong to groups: groups is a list of group ids, or {"id": "resistance", "role": "favored"} with a role (member, favored or leader); every group regards the player somewhere from −100 to 100, and the player's own role decides whether they can ask (favored) or order (leader) a group to act. A character can go without some of the world's needs: needsOff is a list of need ids (["hunger", "fatigue"] for a droid) or true for none at all; those needs never grow, give out, show on the card or reach the prompts, and a start's cast entry or a role in a playerSlot can set it too. Someone who comes into the story later (a transfer student, next year's first-years, a scout with the convoy) has joins: the id of a timeline event after the start; until it happens they are in no prompt and nobody's feelings, and then they arrive where their status.location puts them (a start's cast entry can set "joins": null to have them there from the beginning). They may also hold needs (0–1), status {health, location}, knows (private starting facts) and tags (short visible states such as "bandaged left forearm", 32 characters each; they last until the story removes them) as defaults for every start. status.conditions from older files is read as tags. items is what they visibly carry, up to 10: [{"name": "multitool"}, {"name": "rations", "amount": "some"}], the amount a word ("a little", "some", "a lot"), never a number. A world uses items once anyone in it has an list (even ): scenes can then add, change, hand over and use up what the people there carry, the intent and narrator see it, and your card shows yours and what you've seen others carry. Anything they hide belongs in their secret, not in .
During play, each card shows a mood line (how that person comes across right now) and tags: ones the story adds (each lasts a few turns, at most 4) and ones from the numbers, such as "on edge" at stress 0.8 or "hurt". A world can change those thresholds with tagThresholds: {"stress": {"at": 0.7, "tag": "frayed"}}. Needs bars are under Details on each card, with the description, appearance and what you know about that person (facts you have seen or been told). Details also has your feelings toward that person, with − and + (5 a step) and an optional "why": use it when the story misread your tone (sarcasm taken as hostility). Only your own feelings change, the next turn uses the new value, and Undo also undoes corrections made since your last action. A card can be minimized to one row, or all of them with Collapse all; the game remembers this per world. People without a portrait get a stand-in from the world's portraitSet (a list of image paths, picked by id so it never changes) or, without one, their initials. Saves keep only the image paths.
The full spec, including the merge rules, is docs/WORLDS.md. The game lists every start under the world picker; a start with mistakes in it is marked "(has errors)" and shows why.
Older scenario folders (one scenario.json + characters/, no world.json) still load, as a world with a single start. scenario.json has the world and start fields together, with characters as the cast.
Health is one of ok, hurt, badly_hurt, dead. Death is permanent; if the player dies, the story ends.
Progress autosaves to the browser after every turn, and each game keeps its own save. In a private window the page says that nothing will be kept. Save downloads a JSON file that includes the scenario, so saves work even if the scenario folder changes later.
js/engine.js: state, resolution, applying validated updates, the turn loopjs/memory.js: facts, known_by filtering, chapter rollups, budgetsjs/genres.js: genre tags: narrator guidance and default event pacingjs/prompts.js: the four prompt buildersjs/llm.js: KoboldCpp client (OpenAI-compatible endpoint), tolerant JSON parsing with one retryjs/scenario.js: loading and validating scenario foldersjs/charmaker.js: the character creator (player slots, point-buy, building the character, Suggest)js/startgen.js: generated starts (the step pipeline, each step's checks, the finished start)js/sites.js: generated sites (deciding, library maps, building, trip links, keeping and fading)js/ui/: the game page, in modules, and the header every page sharesSee ROADMAP.md: worlds with multiple starts, character creation, cast tiers, world/regional/local maps, lore and a canon timeline, and a creator suite.
CLAUDE.md: architecture, the rules that keep the game consistent, and the playtesting workflow (read by Claude Code at the start of every session).docs/DESIGN.md: how the engine works and why, including a log of what each playtest fixed.ROADMAP.md: what's next.npm install # dev dependency for UI tests only
npm test # engine tests, no browser or model needed
npm run soak -- all # hundreds of mock turns on every world, reports what looks wrong (several minutes)
npm run test:ui # browser smoke tests against fake model servers (npx playwright install chromium once)
npm run check # syntax + scenario validation
npm run mocks # fake KoboldCpp on :5001 and fake API on :5002 for manual testing
npm run fixture:mock # re-record the sample replay fixture (tests/fixtures/README.md)
Ensemble is free to play, study, modify and share for noncommercial purposes under the PolyForm Noncommercial License 1.0.0. Selling it, selling modified versions, or running it as a paid service isn't allowed without permission.
Scenarios and characters you create are yours; see MODDING.md. Contributions are welcome under the terms in CONTRIBUTING.md.
Copyright 2026 Umedyn.
["stress", "fear"]worldfactions{id, name, desc, stance}alliesenemiesreputation{"convoy": 20}eventRates{regional, world}eventSeeds{regional: [...], world: [...]}{"text": "...", "region": "region_id"}regionsmajorEventDaysmajorHorizonDaysmajorSeedstiers{demoteAfter: 4, maxLocal: 6}extrasinventExtrasfactscharactersstartslocationsoneWaydocs/WORLDS.mdMODDING.mdscenarios/docs/WORLDS.mditems[]items