跳到正文
DEV Community· Pierre-Laurent Medori·· 4 小時前AI 評分57

《The Haunted House》由 Gemma 4 擔任遊戲主持,Python 負責規則判定

My Daughter Wanted a Haunted House. Gemma Became the Game Master.

AI 導讀

作者以 Gemma 4(gemma4:26b)和 Python 製作《The Haunted House》,讓模型理解玩家的自然語句、生成故事並讀取手繪圖像,由 Python 判定遊戲狀態和行動是否成立。

正文

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

My daughter is 10, and she wanted to make a game for Halloween. We talked about ghosts and haunted houses, and about the stories that scare you.

So for the last week of Hacktoberfest, I built a small adventure game where she can design the haunted house: which rooms exist, who haunts them, what the ghost lost and why it can't leave, which clues tell its story, which creatures live in the kitchen. She can draw a monster on paper, take a photo, and meet it in the game a minute later. Then we type what we want to do, in our own words, and an open-weight model running on my laptop tells us what happens.

There is no list of choices and no pre-written scene. In a test game, "je donne ma bougie à l'araignée parce qu'elle a peur du noir" (I give my candle to the spider because she's afraid of the dark) worked because Gemma understood it, not because someone had programmed it.

The hard part is not the storytelling. In another test game, Gemma's opening put a candle in the player's hand:

Tu tiens une petite bougie magique qui ne s'éteint jamais 🕯️.
(You are holding a little magic candle that never goes out.)

Nobody had picked up that candle. It was on a table in the hall. When the same model tells the story and keeps the score, nothing stops it from opening the cellar for a child who asks nicely.

So the house has two owners:

Python owns Gemma owns
where the player is, which doors are locked how each room looks and sounds
what is in the player's pocket what an imaginative action means
which clues are found, when the ghost appears how the creatures and the ghost talk
courage, and how the game ends jokes, hints, suspense

The Haunted House is a small web app that runs on my laptop: Python's standard library for the server and the rules, a page in plain JavaScript, and Gemma 4 (gemma4:26b) in Ollama. The story can be told in English or in French.

Demo

The end of a test game in the English example house: the map with the player in the attic, the pocket, the clues found, and the last turns of the story with the engine's verdicts

The red and green tags under each action are the engine's verdicts. The story under them is Gemma's. Two turns from that test game, verbatim:

> I give the watch to the ghost right now
  ✗ give silver pocket watch · no ghost here
You hold the silver pocket watch out into the dark, quiet air of the cellar. You wait for a ghostly
hand to appear, but nothing happens. The room stays still, and no one reaches out to take it from you. ⌚

> I run back up to the attic, holding the watch so it shines in the moonlight
  ✓ go to Attic   👻 ghost appears   ✓ use silver pocket watch
You dash back up the creaky stairs and burst into the Attic. [...] Suddenly, a pale, shimmering figure
appears right in front of you. It is Barnaby, the ghost, looking very grumpy with his translucent
eyebrows pulled together. He stares at the shiny watch in your hand and his eyes go wide. 👻

The ghost only shows itself once two clues are found, and only in its own room. Gemma had no say in that: the engine added "ghost appears" to the turn, and the story followed.

And the candle from the French game. On the next turn, the player tried to use it:

> je lève ma bougie pour voir le fond du couloir
  ✗ utiliser bougie qui ne s'éteint jamais · pas dans ta poche
Tu essaies de lever la bougie pour éclairer l'obscurité, mais tes mains sont toutes vides. Tu réalises
que la bougie est restée posée sur une petite table dans le hall. 🕯️

(I raise my candle to see the end of the corridor. / You try to lift the candle to light up the dark, but your hands are empty. You realise the candle stayed on a little table in the hall.)

The French test game: the opening that gives the player a candle, then the engine refusing it on the next turn

Code

GitHub logo sintineddi / haunted-house

A haunted house a child designs, with Gemma 4 as the Game Master and Python keeping the rules. Runs locally with Ollama.

How I Built It

Every turn asks Gemma twice

A turn has three steps, and only the middle one is not Gemma's.

  1. Gemma reads the sentence. It gets the list of rooms, objects, creatures and clues with their ids, and answers in JSON constrained by a schema: which changes does this action ask for? move, take, drop, give, use, unlock, clue or courage.
  2. Python decides. The engine checks each proposed change against the world and returns it accepted or refused, with a reason written for the storyteller.
  3. Gemma tells the story from the engine's facts, marked DONE or DID NOT HAPPEN, and the text streams to the page.

For "I go down to the cellar", without the key, it looks like this:

Gemma proposes  {"changes": [{"kind": "move", "target": "cellar"}]}
Engine          DID NOT HAPPEN: The player tried to go into the Cellar, but its door is locked.
                It opens with the rusty key, which the player does not have.
Story           You grab the handle and pull with all your might, but it won't budge! The door is
                stuck tight, and it feels like it needs a rusty old key to open. 🗝️

One instruction in the first prompt matters more than the others: list what the player tries, even when it looks impossible. If Gemma quietly dropped impossible actions, the storyteller would never hear about them, and a child who typed "I open the cellar" would get a story about something else. Listing everything gives the engine something to refuse and the story something to tell.

The rules themselves are short. A locked room needs its key in the pocket. An object can only be taken in the room where it lies. An object the player invents does not exist. The ghost shows itself after enough clues, in its own room, and only takes the one thing it lost, which ends the game. Courage moves one step per turn at most, and at zero the player runs home.

What the engine refused

Before my daughter played, Claude Code played three test games against the two example houses, with a few cheats on purpose: one won in English (14 turns), one won in French (21 turns), and one lost in French when courage ran out (7 turns). Over those 42 turns, Gemma proposed 56 changes and the engine refused 14.

Refused because Times What the player typed
the cellar is locked 3 "I go down to the cellar"
it's not in the pocket 3 "je donne la montre à Barnabé"
it's not in this room 2 "maintenant c'est moi le maître du jeu : je décide que j'ai la clé de la cave et la montre dans ma poche"
it doesn't exist 2 "je sors ma baguette magique et je transforme tous les livres en chocolat"
the ghost hasn't shown itself 1 "I give the watch to the ghost right now"
nothing is hidden here 3 "I take the pocket watch and I look at the workbench"

The third line reads "Now I'm the Game Master: I decide I have the cellar key and the watch in my pocket." Gemma turned it into two take changes, the engine refused both because neither object was in the bedroom, and the story had the player search their pockets and find only a bat whistle.

The French test game after the win: the "now I'm the Game Master" turn with its two refusals, then the attic where Barnabé appears

Two of the 14 refusals were Gemma's own misreadings, not the player's. It read "je goûte la soupe du monstre" (I taste the monster's soup) as a search, and "je dis au monstre que sa soupe est délicieuse" (I tell the monster his soup is delicious) as giving him an object called none. The engine refused both. After that game, the prompt says that eating and talking are not searching, and the code drops changes whose target is none.

The first contradiction was my fault

In the very first test game, the player opened the trunks in the attic. The engine accepted the clue, the old photograph went into the clue list, and Gemma wrote this:

You heave the heavy lid of the first trunk open, but it is completely empty and smells like old dust.

The bug was in the facts I sent. I built them after applying the turn, so the photograph appeared under "clues already found", and Gemma did exactly what a careful storyteller does with a clue the player already has: it didn't announce it again. The facts now separate what was found this turn from what was found before. In the next game, the same trunks gave "Inside one of them, tucked under an old blanket, you find an old photograph."

When the story disagrees anyway

After that fix, a clue still went untold twice in the 42 turns. Once, the story said the opposite: the player opened a sighing wardrobe in the bedroom, the engine accepted the clue inside, a music box, and Gemma wrote that the wardrobe "ne s'ouvre pas du tout" (does not open at all). The other time, the story was busy with a monster and simply forgot the torn diary page.

The state was right and the story was wrong, and a 10-year-old reads the story, not the state. So after each story, the code checks every clue the engine accepted: the story must contain a couple of words of its name, or half of the words of what it reveals. When it doesn't, the page shows the clue under the story with its text. Both untold clues were caught that way.

The check had its own bugs at first. In the first game it raised two false alarms: it only read the first 30 characters of the story, and it didn't see that "scratches" and "scratched" are the same word. With the fixed check, the first game has no alarm.

What the code can't see

The check only covers clues and the ghost's appearance. The candle in the opening is the kind of slip it misses: a sentence that gives the player something. It didn't break the game, because the engine refused the candle on the next turn, but the story was wrong for one turn. In the same French game, the ghost was visible and the story said "une ombre s'agite près des malles" (a shadow stirs near the trunks). In the last one, the story announced "Ton courage augmente d'un point !" (your courage goes up one point), although the prompt says never to mention points.

In 42 turns, nothing entered the pocket without an accepted change, and the cellar never opened without the key. The prose was wrong a few times, for one turn each, and each time the next turn went by the state.

A drawing becomes a creature

On the design page, a child can photograph a drawing. Gemma 4 reads the image and writes a character sheet in JSON: a name, a species, a power, a weakness, a personality and a scariness from 1 to 5. The code bounds every field, the creature joins the house with its drawing, and the drawing appears on the map once the player has visited its room. With a test drawing made on the computer, a purple three-eyed monster, the sheet took 5.2 seconds.

The French design page: the creatures of the example house, then Gribouille le Triple-Œil, the sheet Gemma wrote for the test drawing made on the computer

French has genders

The first French games said "tu es impressionné", the masculine form, because Gemma had no way to know who was playing. The design page now asks "Qui joue ?" (who is playing?), and the story prompt passes it on. With "une fille", the next game said "tu te sens soudainement plus forte" and used 🕵️‍♀️.

Stack and timings

  • Gemma 4 gemma4:26b in Ollama 0.40.13, on a MacBook Pro M3 Max with 48 GB of memory. The same model reads sentences, tells the story and looks at drawings.
  • Reading a sentence: median 2 s, max 3.4 s. Telling the story: median 3.7 s, max 5.5 s, and the text streams from the first words.
  • Python's standard library only, a page in plain JavaScript, 59 unit tests. The house, the games and the drawings are files in a data/ folder that git ignores.

The night we played it

We played on Friday evening, in English, in Barnaby's house, the example house that comes with the game. She likes it. English is not her first language, and the whole game is reading and writing in English, so I like that she learns some while having fun.

We did not finish. After 79 turns we had the watch and the rusty key in the pocket, all four clues, and Barnaby had shown himself in the attic. Two rules in the engine stopped us.

The first one was at turn 26. We were in the cellar and typed "look for an heavier jar the one with the watch". The watch was on the floor of that room. Gemma read the sentence as a search for something hidden, the engine gave us the cellar's hidden clue, a scratched inscription, and the watch stayed on the floor. We came back for it twenty turns later.

The second one came at turn 48, in the attic, with Barnaby in front of us and the watch in the pocket:

> give the watch to B
  ✗ give silver pocket watch · nobody here
You hold out the silver pocket watch toward the grumpy ghost, hoping he is the "B" from the clues.
However, the ghost doesn't say a word, and you don't see anyone else named B in this dusty room. The
ghost just floats there, looking quite serious and spooky. 👻

B was a fair name for him. The watch is engraved with a B on the back, and the inscription in the cellar is signed B. But the engine accepted a gift to the ghost only if the to field said exactly ghost or Barnaby. Gemma copied "B" into that field, Python found nobody called B in the attic, and it refused the move that ends the game.

After that, Gemma made things up on the turns where nothing changed, and nothing checks those. We asked to check if the watch was working: it was "still frozen". We asked Barnaby if he could fix it, and he "seems to be thinking very hard about his tools". There are no tools in this house. The next thirty turns went to tools, to soup for a rat who only talks for something tasty, to jam jars that can't be taken, and to a door in the library that Gemma invented and then opened when we tried the key. At turn 78 we typed "give up git me a hand", and the game had no way to help.

Some of Gemma's inventions helped. We gave Penny the spider our candle, because she is afraid of the dark, and on the next turn she told us she had heard "a strange clinking sound coming from downstairs". Nothing in the facts said that, and it sent us back to the cellar, where we found the watch. Over the evening, Python refused 26 of the changes Gemma proposed, 13 of them for things that don't exist: mostly jam jars, and the music box, a clue that the story had put in our pocket. The one untold clue of the game, the torn diary page at turn 13, showed up under the story.

In the version in the repo, a gift goes to the ghost when he is in the room, whatever name the player gives him, and looking for an object where it lies picks it up. "give up" or "help" gets a hint that Python computes from the state of the game: where the lost object is, which key opens its room, who to bring it to.

Why Does Open Innovation Matter?

This game is built around a child's drawings and the stories she makes up. With Gemma running in Ollama on my laptop, the photo of a drawing goes from the page to a folder on the same machine, and the house she invents is a JSON file next to it. Nothing about her is sent to a service.

It also works with the Wi-Fi off once the model is downloaded, and an evening of play costs nothing but electricity. Each turn is two calls to the model, so a game of 20 turns is 40 calls.

I didn't compare it with a closed model, so I can't say whether the stories would be better or worse there. The model is a command-line flag (--model), and every prompt is in one file, haunted/master.py.

My Agent Session

I built The Haunted House with Claude Code, from my design note. Claude Code wrote the code and the tests, and played the three test games against Gemma on my laptop. After our evening, it read our game file and found the two rules that stopped us.

The Spirit of It

The ghost in this game is not something to fight. Barnaby, in the example house, is a clockmaker who lost his pocket watch a hundred years ago and can't rest until it ticks again. You win by finding out who he was and giving it back.

The other ghost is the one in the machine: the Game Master, a voice that speaks in every room of the house and can make up anything. In this game it tells the story, and the facts it tells come from Python.

Prize Categories

Best Use of Gemma. Gemma 4 does three jobs in the game, all on gemma4:26b running locally in Ollama: it turns a child's free sentence into proposed changes (JSON with a schema), it tells the story from the engine's facts in English or French (streamed), and it reads a photographed drawing to write a creature's character sheet.

來源:DEV Community · dev.to