A Capstone by Bradley Charles

BARD

Batched Asynchronous Response Delivery

A Godot 4 action RPG where the NPCs remember what you did yesterday. Every night a local LLM rewrites their dialogue, and by morning the game is back to reading plain JSON.

▶ PRESS START

Achievement unlocked

1st Place · Hands-on Demo

Capstone demo day, North Seattle College. The winner was voted for by the audience.

TALK TO THE INNKEEPER

Everything Amara says below was written by the pipeline the night before Day 1, taken as-is from amara_svor_day1.json. The one option taggedGAME is hardcoded. That's the rule that makes the whole thing safe: the model writes the conversation, the game owns anything that matters.

Amara Svor Innkeeper · Thornwall · Day 1

Welcome to Thornwall. You look like you've seen the dust of the Ashfield today; it settles in the very bones, doesn't it? Come in, come in. You must be tired from your first foray out.

↑↓ Navigate · [Enter] Select · click to skip text

QUEST LOG

Make NPCs react to what the player actually did.

The usual approach calls a model live, mid-conversation. That inflicts the same four status effects every time:

  • LAGDialogue stalls while the model generates. Players feel every second.
  • CONFUSEThe NPC invents quests, items or lore that were never in the game.
  • POISONCloud APIs cost money per line and need a connection to work at all.
  • CURSEOutput at the moment of play is unreviewed and can never be reproduced.

BARD's cure is to move generation out of the real-time loop. The game writes down what happened. When the day ends, a batch job turns that record into a full dialogue tree for every NPC, checks it and saves it to disk. During play the game only reads files. The LLM is a content generator, never a runtime dependency, so the game stays fast, offline and deterministic.

Solo project for the BASc in Application Development at North Seattle College. I did the design, game code, AI pipeline, tooling, docs and the poster.

ONE DAY IN TOWN

Godot owns numbers, flags and rules. Python owns prose. They only talk through files: JSON in, JSON out, plus little flag files that sayconnected, ready, failed or crashed.

  1. DAY

    1. Hunt the field

      Kills, bounty progress and story flags are tracked as plain numbers and booleans.

    2. Sleep at the inn

      end_day() snapshots the state and advances the calendar.

  2. DUSK

    1. Write game_state.json

      Written before Python starts, so the pipeline always sees a finished day.

    2. Launch the pipeline

      Godot starts a Python subprocess, shows a loading overlay and polls flag files every 3 s. It never blocks.

  3. NIGHT

    1. Write each NPC

      Build the prompt, call Gemma 4 E4B through Ollama, validate, repair or fall back, then save dialogue/{npc}_day{N}.json.

    2. Remember

      A second small call asks the NPC for one subjective sentence about the hunter. It goes into memory.

    3. Small talk

      A pool of two-line exchanges for the villagers wandering the square.

  4. DAWN

    1. pipeline_ready.flag

      Godot sees the flag, reloads every NPC's dialogue and fades back into town.

A progress file drives the loading bar NPC by NPC, and a watchdog treats a pipeline that never connects as crashed.

WHAT GODOT WRITES

"meta": { "day": 2 },
"world_state": {
  "monsters_killed_today": {
    "slime1": 3
  }
},
"flags": {
  "met_amara_svor": true
}

WHAT THE MODEL READS

“Light slime activity today -- only a handful encountered.”

The model never sees a raw number. nl_descriptors.py turns counts into tiered phrases, each with several wordings picked at random so NPCs don't repeat themselves. Bounty quantities are left out entirely, so there's nothing to misquote.

ITEM RARITY: HOW MUCH TO TRUST IT

Every bit of context in the prompt is labelled by how sure the NPC should be of it. A fact's weight (core, recent, stale) is worked out from its age when the prompt is built, not stored and edited.

  • VERIFIEDFacts from the game. Treated as ground truth.
  • RECOLLECTIONThe NPC's own memory. Subjective, possibly incomplete.
  • BACKGROUNDWorld and town lore. Possibly out of date.
  • RUMOURSomething heard in town. The NPC cannot confirm it.

AMARA'S JOURNAL

After writing each NPC's dialogue, a second call asks for one sentence about the hunter, from that NPC's point of view. These are real entries from her memory:

Day 1The hunter seemed more like a wanderer today than a warrior, moving through the Ashfield with an unnerving stillness.

Day 2The quiet that settled around the hunter today felt less like peace and more like the careful holding of a breath, waiting for a single moment to break.

Same town, different voice. Demetrius Volkov, the guild commander, opened Day 2 with:

Another day logged. The records suggest a remarkable lack of incident. State your operational summary, and keep it concise.

Once a week, chronicle.py sums up your deeds and turns them into rumours. Some name you, some don't. They age out over time and reach later prompts as things the NPC has heard but can't confirm.

CONTINUE?

A bad model run should make the game a bit duller, never break it. Every stage has a fallback, and each NPC's root menu (sleep, shop, bounty board, turn-in) is hardcoded, so even total failure leaves a working shopkeeper.

  1. Ollama isn't runningCheck the connection, then try to start it automatically
  2. Network errorThree attempts with a delay
  3. Unparseable JSONAsk again
  4. Fails the schemaOne repair call to the model, then validate again
  5. Repair failsReuse the most recent previous day's dialogue
  6. Missing variant, lore or namesHand-written fallback files or hardcoded defaults
  7. One NPC throwsSkip that NPC and mark the run partial
  8. Anything unhandledTraceback to a crash flag; Godot shows an error overlay

The validator checks that every node exists, greeting and farewell are present, every reply points somewhere real, and every path can reach an exit.

BESTIARY

The enemies share one base class for health, damage and signals. Each one gets its own personality on top:

  • SLIME I

    Pack hunter. Runs when alone, chases when friends are near, and spreads aggro through the group.

  • SLIME II

    Minds its own business until you hit it. Then it calls in every slime nearby.

  • SLIME III

    Doesn't need help. Comes straight at you.

Orcs, plants and vampires each come in three tiers. Bosses spawn once you've hit a kill threshold, with a telegraphed area attack (an expanding red circle), a cooldown and a boss health bar.

BOSS FIGHTS

The bugs that took the longest, and how each one went down.

  • THE SHOVE

    DEFEATED

    Mobs pushed the player around the map.

    Put mobs and blocking NPCs on their own collision layer with a mask of 0 so they apply no force, removed that layer from the player's mask, and did blocking in script by cancelling any velocity pointed into a nearby mob.

  • PHANTOM KNOCKBACK

    DEFEATED

    Hits landed, but nothing flew backwards.

    The chase AI was overwriting velocity on the very next physics tick. A hurt-state early return in the AI loop let the impulse play out.

  • THE GREAT FREEZE

    DEFEATED

    One mob landed a hit and every mob on screen stopped.

    Contact logic checked the player's invincibility, which is global. Contact stopping now uses each mob's own distance.

  • THE UNTOUCHABLE

    DEFEATED

    Mobs chased you forever and never did damage.

    Separation code in _integrate_forces held them just outside hurt range. Removing it restored contact damage.

EQUIPMENT

ENGINE
Godot 4.6 · GDScript
ORACLE
Gemma 4 E4B (Q4), swappable by env var
SUMMONER
Ollama, local GPU inference
SPELLBOOK
Python 3 + requests
MESSENGER
JSON + sentinel flag files
ARTISAN
Aseprite · AsepriteWizard
SCRIBE
Almendra / Almendra SC
FORGE
RTX 4060 class, 8 GB VRAM

GDScript over C# on purpose: the bottleneck is LLM inference, not the game runtime. Progression is gear-only with no levels or stat trees, which keeps the focus on the pipeline.

SCREENSHOTS

The poster I designed for demo day.
  • Elara Vance, an innkeeper, greeting the hunter on Day 2 with dialogue written overnight.
  • The Bounty Board: contracts across four zones. Quantities stay hidden from the player and the model.
  • In the field with the axe, a plant creature mid-swing and the minimap in the corner.
  • Lysandra Grove: the guild hall, the inn and the bounty board.

ASSET CREDITS

BARD is built on original code and design, but leans on free third-party art, music and sound packs. Full credit to the artists below.

Art & tilesets — craftpix.net

Music

Sound effects

Fonts & tools

  • Xolonium — Severin Meyer, SIL Open Font License
  • Almendra — Ana Sanfelippo, SIL Open Font License
  • AsepriteWizard — Vinicius Gerevini, MIT License

BARD · Academic Capstone 2026