Add complete game content: Vogon ship, Heart of Gold, off-Earth, dream system

Engine refactoring:
- Replace hardcoded state attributes with generic state.flags dict
- Add death system (jigs_up/finish) with dream-restore callbacks
- Add inventory extras hook system (removes hardcoded headache/tea lines)
- Add 16 new verb handlers (consult, say, carve, plug, repair, kick, etc.)
- Add verb synonyms to parser

New content modules (25 rooms, 107 objects total):
- vogon.py: Hold, Captain's Quarters, Airlock; babel fish puzzle, poetry
  scene, airlock ejection sequence with timed events
- heart.py: 10 Heart of Gold rooms; Marvin tool quest, tea/no-tea paradox,
  Nutrimat overload, Infinite Improbability Drive puzzle, victory sequence
- unearth.py: Traal (Beast lair with towel/name/carve puzzle), War Chamber,
  Inside Whale, Maze with random exits and particle
- dark.py: Dream dispatch room with probabilistic destination selection,
  sensory discovery mechanic, dream-restore callback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 21:52:57 +02:00
parent a6fe624c5e
commit a1bb4cbf02
11 changed files with 3901 additions and 63 deletions
+42
View File
@@ -0,0 +1,42 @@
{
"cSpell.words": [
"ACTORBIT",
"CONTBIT",
"DARKBIT",
"DOORBIT",
"DRINKBIT",
"fclear",
"fdesc",
"Infocom",
"INTEGRALBIT",
"ldesc",
"LIGHTBIT",
"MUNGEDBIT",
"NARTICLEBIT",
"NDESCBIT",
"ONBIT",
"OPENBIT",
"OUTSIDEBIT",
"prosser",
"prsa",
"prsi",
"prso",
"READBIT",
"REVISITBIT",
"RLANDBIT",
"SEARCHBIT",
"superbrief",
"SURFACEBIT",
"TAKEBIT",
"TOOLBIT",
"TOUCHBIT",
"traal",
"TRANSBIT",
"TRYTAKEBIT",
"VEHBIT",
"vogon",
"VOWELBIT",
"WEARBIT",
"WORNBIT"
]
}
+240
View File
@@ -0,0 +1,240 @@
"""DARK room — sensory-deprivation dream dispatch chamber."""
import random
from h2g2.engine.game_object import (
GameObject, Room, Flag, Direction,
)
from h2g2.engine.world import World
from h2g2.engine.state import GameState
# ---- Flavor texts ----
_ENTRY_TEXTS = [
(
"You are floating in a dark, formless void. You can see nothing. "
"You can hear nothing. You can smell nothing. You are not even sure "
"who you are."
),
(
"A cold grey fog rolls in, enveloping your senses. Your limbs are "
"numb. Your mind is numb. Everything is numb."
),
(
"Mist swirls around you. Your thoughts dissolve into the gloom. "
"You are nowhere."
),
]
# ---- Sensory discovery helpers ----
_SENSE_HINTS = {
"listen": "You strain your ears. From somewhere far away, a faint hum reaches you.",
"smell": "You inhale deeply. There is a faint chemical tang in the air.",
"touch": "You reach out tentatively. Your fingers brush something cold and smooth.",
"feel": "You reach out tentatively. Your fingers brush something cold and smooth.",
"look": "You peer into the darkness. A faint glimmer pulses somewhere ahead.",
"examine": "You peer into the darkness. A faint glimmer pulses somewhere ahead.",
}
# ---- Timed events ----
def _i_dark_hint(state: GameState) -> bool:
"""After 3 turns in DARK, hint at using senses."""
out = state.output
if state.here is None or state.here.id != "DARK":
return False
counter = state.flags.get("dark_counter", 0) + 1
state.flags["dark_counter"] = counter
if counter == 3:
out.tell(
"\nYou sense something in the darkness. Try using your senses.\n"
)
state.flags["dark_hint_given"] = True
return True
return False
def _i_dark_dispatch(state: GameState) -> bool:
"""After senses used or enough turns, dispatch to dream destination."""
out = state.output
if state.here is None or state.here.id != "DARK":
return False
if not state.flags.get("dark_exit_ready"):
return False
# Build weighted destination list
destinations = []
weights = []
heart_prob = state.flags.get("heart_prob", 0)
vogon_prob = state.flags.get("vogon_prob", 100)
traal_prob = state.flags.get("traal_prob", 60)
fleet_prob = state.flags.get("fleet_prob", 0)
if heart_prob > 0 and state.world.rooms.get("ENTRY-BAY"):
destinations.append("ENTRY-BAY")
weights.append(heart_prob)
if vogon_prob > 0 and state.world.rooms.get("HOLD"):
destinations.append("HOLD")
weights.append(vogon_prob)
if traal_prob > 0 and state.world.rooms.get("LAIR"):
destinations.append("LAIR")
weights.append(traal_prob)
if fleet_prob > 0 and state.world.rooms.get("WAR-CHAMBER"):
destinations.append("WAR-CHAMBER")
weights.append(fleet_prob)
if not destinations:
out.tell("\nThe darkness persists. Nothing happens.\n")
return True
chosen = random.choices(destinations, weights=weights, k=1)[0]
dest = state.world.get_room(chosen)
out.tell(
"\nThe darkness shifts. Shapes coalesce around you. The world "
"reassembles itself...\n\n"
)
# Move player to destination
state.here = dest
state.protagonist.move_to(dest)
state.dreaming = True
# Reset dark state
state.flags["dark_counter"] = 0
state.flags["dark_hint_given"] = False
state.flags["dark_exit_ready"] = False
state.flags["dark_senses_used"] = 0
# Disable dispatch timer
for entry in state.clock.entries:
if entry.routine is _i_dark_dispatch:
entry.enabled = False
for entry in state.clock.entries:
if entry.routine is _i_dark_hint:
entry.enabled = False
# Trigger destination room's M-ENTER via its action
if dest.action:
dest.action(state, "M-ENTER")
return True
# ---- Room action handler ----
def _dark_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-ENTER":
# Clear player inventory (ROB)
if state.protagonist:
for item in list(state.protagonist.children):
item.move_to(state.world.local_globals)
# Set dreaming
state.dreaming = True
# Display random entry text
out.tell(random.choice(_ENTRY_TEXTS) + "\n")
# Reset dark room state
state.flags["dark_counter"] = 0
state.flags["dark_hint_given"] = False
state.flags["dark_exit_ready"] = False
state.flags["dark_senses_used"] = 0
# Queue hint timer (fires every turn, counts to 3)
state.clock.queue(_i_dark_hint, -1, name="I-DARK-HINT")
# Queue dispatch timer (fires every turn, waits for exit_ready)
state.clock.queue(_i_dark_dispatch, -1, name="I-DARK-DISPATCH")
return True
if rarg == "M-LOOK":
out.tell("It is pitch dark. You can see nothing.\n")
return True
if rarg == "M-END":
# Handle sense verbs
verb = state.prsa
if verb in _SENSE_HINTS:
out.tell(_SENSE_HINTS[verb] + "\n")
senses_used = state.flags.get("dark_senses_used", 0) + 1
state.flags["dark_senses_used"] = senses_used
if senses_used >= 2 and state.flags.get("dark_hint_given"):
out.tell(
"\nYou begin to get your bearings. You could try "
"walking now.\n"
)
state.flags["dark_exit_ready"] = True
return True
if verb in ("walk", "go") and state.flags.get("dark_exit_ready"):
# Trigger dispatch immediately
state.flags["dark_exit_ready"] = True
_i_dark_dispatch(state)
return True
if verb in ("walk", "go"):
out.tell("You stumble blindly but get nowhere.\n")
return True
return False
return False
# ---- Dream restore callback ----
def _dream_restore(state: GameState) -> None:
"""Called when the player dies in a dream — return to DARK."""
dark = state.world.rooms.get("DARK")
if dark:
state.here = dark
state.protagonist.move_to(dark)
if dark.action:
dark.action(state, "M-ENTER")
# ---- Registration ----
def register(world: World, state: GameState) -> None:
"""Create the DARK dream dispatch room."""
# ---- Initialize state flags ----
state.flags.setdefault("dark_counter", 0)
state.flags.setdefault("dark_hint_given", False)
state.flags.setdefault("dark_exit_ready", False)
state.flags.setdefault("dark_senses_used", 0)
# Probability weights for dream dispatch
state.flags.setdefault("heart_prob", 0)
state.flags.setdefault("vogon_prob", 100)
state.flags.setdefault("traal_prob", 60)
state.flags.setdefault("fleet_prob", 0)
# ---- DARK room ----
world.register(Room(
"DARK", desc="Dark",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_dark_action,
exits={}, # No exits; player leaves via dispatch mechanic
))
# Register dream restore callback
state._dream_restore_callback = _dream_restore
+24 -21
View File
@@ -33,7 +33,7 @@ def bed_action(state: GameState) -> bool:
protagonist = state.protagonist
if state.prsa == "get out":
if state.headache:
if state.flags.get("headache"):
protagonist.move_to(state.here)
state.lying_down = False
out.tell(
@@ -67,7 +67,7 @@ def gown_action(state: GameState) -> bool:
gown = state.world.get("GOWN")
if state.prsa == "take" and state.prso is gown:
if state.headache:
if state.flags.get("headache"):
gown.fclear(Flag.TRYTAKEBIT)
gown.fclear(Flag.NDESCBIT)
gown.move_to(state.protagonist)
@@ -141,7 +141,7 @@ def tablet_action(state: GameState) -> bool:
if state.prsa in ("eat", "take", "drink", "swallow"):
tablet.move_to(state.world.local_globals) # consumed
state.headache = False
state.flags["headache"] = False
state.score += 10
out.tell(
"You swallow the tablet. After a few seconds the room begins "
@@ -196,7 +196,7 @@ def curtains_action(state: GameState) -> bool:
def screwdriver_action(state: GameState) -> bool:
out = state.output
if state.prsa == "take" and state.headache:
if state.prsa == "take" and state.flags.get("headache"):
out.tell("It dances by you like a thing possessed.\n")
return True
if state.prsa == "examine":
@@ -207,7 +207,7 @@ def screwdriver_action(state: GameState) -> bool:
def toothbrush_action(state: GameState) -> bool:
out = state.output
if state.prsa == "take" and state.headache:
if state.prsa == "take" and state.flags.get("headache"):
out.tell(
"You lunge for it, but the room spins nauseatingly away. "
"The floor gives you a light tap on the forehead.\n"
@@ -238,7 +238,7 @@ def bulldozer_action(state: GameState) -> bool:
def prosser_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
if state.prosser_in_mud:
if state.flags.get("prosser_in_mud"):
out.tell(
"Mr. Prosser is lying in the mud in front of the "
"bulldozer.\n"
@@ -249,7 +249,7 @@ def prosser_action(state: GameState) -> bool:
)
return True
if state.prsa in ("tell", "ask", "talk"):
if state.in_front_of_bulldozer:
if state.flags.get("in_front_of_bulldozer"):
out.tell(
'"Look, Mr. Dent, the plans have been available in the '
"planning office for the last nine months!\"\n"
@@ -283,9 +283,10 @@ def beer_action(state: GameState) -> bool:
out = state.output
beer = state.world.get("BEER")
if state.prsa in ("drink", "eat"):
state.beer_counter += 1
state.flags["beer_counter"] = state.flags.get("beer_counter", 0) + 1
state.score += 5
if state.beer_counter >= 3:
beer_count = state.flags.get("beer_counter", 0)
if beer_count >= 3:
beer.move_to(state.world.local_globals)
out.tell(
"You finish the last of your beer. Ford then buys some "
@@ -293,15 +294,17 @@ def beer_action(state: GameState) -> bool:
"wriggly and shoves it in your ear.\n"
)
else:
remaining = 3 - beer_count
out.tell(
f"You drink {'some' if state.beer_counter == 1 else 'more'} "
f"beer. {3 - state.beer_counter} pint"
f"{'s' if 3 - state.beer_counter != 1 else ''} left.\n"
f"You drink {'some' if beer_count == 1 else 'more'} "
f"beer. {remaining} pint"
f"{'s' if remaining != 1 else ''} left.\n"
)
return True
if state.prsa == "examine":
out.tell(f"There {'are' if state.beer_counter < 3 else 'is no'} beer"
f"{'s' if state.beer_counter == 0 else ''} here.\n")
beer_count = state.flags.get("beer_counter", 0)
out.tell(f"There {'are' if beer_count < 3 else 'is no'} beer"
f"{'s' if beer_count == 0 else ''} here.\n")
return True
return False
@@ -337,7 +340,7 @@ def bedroom_action(state: GameState, rarg: str) -> bool:
# Can't reach things from bed
if state.prso.parent is not bed and not state.prso.is_held_by(protagonist):
out.tell("You can't reach it from the bed.")
if state.headache:
if state.flags.get("headache"):
out.tell(" The effort almost kills you.")
out.tell("\n")
return True
@@ -361,7 +364,7 @@ def bedroom_exit(state: GameState) -> "Room | None":
out.tell("The door is closed.\n")
return None
if state.headache:
if state.flags.get("headache"):
out.tell(
"You miss the doorway by a good eighteen inches. The wall "
"jostles you rather rudely.\n"
@@ -403,7 +406,7 @@ def clothes_exit(state: GameState) -> "Room | None":
def front_of_house_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-LOOK":
if state.house_demolished:
if state.flags.get("house_demolished"):
out.tell(
"Where your home used to be there is now a pile of "
"rubble, and through it runs a shiny new bypass.\n"
@@ -413,7 +416,7 @@ def front_of_house_action(state: GameState, rarg: str) -> bool:
"You can see your house from here. A large yellow "
"bulldozer is approaching it.\n"
)
if state.in_front_of_bulldozer:
if state.flags.get("in_front_of_bulldozer"):
out.tell("You are lying in the path of the bulldozer.\n")
return True
@@ -425,7 +428,7 @@ def front_of_house_action(state: GameState, rarg: str) -> bool:
def house_enter(state: GameState) -> "Room | None":
if state.house_demolished:
if state.flags.get("house_demolished"):
state.output.tell("Your home is now a pile of rubble.\n")
return None
return state.world.get_room("FRONT-PORCH")
@@ -454,8 +457,8 @@ def pub_action(state: GameState, rarg: str) -> bool:
if rarg == "M-ENTER":
ford = state.world.get("FORD")
beer = state.world.get("BEER")
if not state.ford_arrived:
state.ford_arrived = True
if not state.flags.get("ford_arrived"):
state.flags["ford_arrived"] = True
ford.move_to(state.here)
beer.move_to(state.here)
out.tell(
+3 -3
View File
@@ -8,7 +8,7 @@ from h2g2.engine.state import GameState
def hangover_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine" or state.prsa == "diagnose":
if state.headache:
if state.flags.get("headache"):
out.tell("You have a big blinding throbber.\n")
else:
out.tell("You don't have a headache.\n")
@@ -51,7 +51,7 @@ def walls_action(state: GameState) -> bool:
def me_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
if state.headache:
if state.flags.get("headache"):
out.tell("You look about as ill as you feel.\n")
else:
out.tell("You look pretty normal.\n")
@@ -64,7 +64,7 @@ def ground_action(state: GameState) -> bool:
if state.prsa == "lie down":
state.lying_down = True
if state.here and state.here.id == "FRONT-OF-HOUSE":
state.in_front_of_bulldozer = True
state.flags["in_front_of_bulldozer"] = True
out.tell("You lie down in the path of the advancing bulldozer.\n")
else:
out.tell("You lie down.\n")
File diff suppressed because it is too large Load Diff
+690
View File
@@ -0,0 +1,690 @@
"""Off-Earth locations — Beast's Lair, War Chamber, Whale, Maze."""
import random
from h2g2.engine.game_object import (
GameObject, Room, Flag, Direction,
DirectExit, ConditionalExit, BlockedExit,
)
from h2g2.engine.world import World
from h2g2.engine.state import GameState
# ---- Memorial names ----
MEMORIAL_NAMES = (
"Gleb Snardfitz, Bibs Trench, Zeke Fitzberry, Elmo Smith, "
"Arg Vooloo, Boz Scrimble, Nug Trellis, Pib Frumkin"
)
# ---- War dialogue ----
VLHURG_DIALOGUE = [
(
'"We have been at war with the G\'Gugvuntt for ten thousand years!" '
"bellows the Vlhurg leader."
),
(
'"It was a perfectly innocent remark!" the Vlhurg leader continues. '
'"Someone said something careless at a diplomatic reception, and the '
'next thing you know — WAR!"'
),
(
"The Vlhurg leader pounds the table. \"They said our leader's mother "
'was a — well, I can\'t repeat it in polite company."'
),
]
GGUGVUNT_DIALOGUE = [
(
"The G'Gugvuntt leader sneers. \"They started it. Ten thousand years "
'ago. And we intend to finish it."'
),
(
'"Their entire civilization is based on a misunderstanding of a '
'casual remark," the G\'Gugvuntt leader hisses.'
),
(
'"We shall destroy their home planet," says the G\'Gugvuntt leader, '
'"as soon as we work out where it is."'
),
]
# ---- Object action handlers ----
def _beast_action(state: GameState) -> bool:
out = state.output
beast = state.world.get("BEAST")
if state.prsa == "examine":
if beast.fset_q(Flag.MUNGEDBIT):
out.tell(
"The Ravenous Bugblatter Beast of Traal is fast asleep, "
"snoring loudly.\n"
)
else:
out.tell(
"The Ravenous Bugblatter Beast of Traal is a "
"mind-bogglingly stupid animal. It assumes that if you "
"can't see it, it can't see you. It is currently very much "
"awake and very much hungry.\n"
)
return True
if state.prsa in ("tell", "ask", "talk", "say"):
if beast.fset_q(Flag.MUNGEDBIT):
out.tell("The Beast is asleep and doesn't hear you.\n")
return True
towel = state.world.objects.get("TOWEL")
if towel and towel.fset_q(Flag.WORNBIT):
state.flags["name_told"] = True
out.tell(
"With the towel wrapped around your head, you boldly tell "
"the Beast your name. The Beast, being mind-bogglingly "
"stupid, carefully notes this down.\n"
)
else:
out.tell(
"The Beast lunges at you! Perhaps talking to it while it "
"can see you isn't the wisest strategy.\n"
)
return True
if state.prsa in ("attack", "fight", "hit", "kill"):
if beast.fset_q(Flag.MUNGEDBIT):
out.tell("It's asleep. Leave it be.\n")
else:
state.jigs_up(
"You charge at the Ravenous Bugblatter Beast of Traal. "
"This was not one of your better ideas. The Beast eats you "
"in a single gulp."
)
return True
return False
def _memorial_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
if state.flags.get("beast_defeated"):
out.tell(
"A large sandstone memorial. Carved upon it are the names "
f"of those who have attempted to slay the Beast:\n"
f"{MEMORIAL_NAMES}, Arthur Dent.\n"
)
else:
out.tell(
"A large sandstone memorial. Carved upon it are the names "
f"of those who have attempted to slay the Beast:\n"
f"{MEMORIAL_NAMES}.\n"
)
return True
if state.prsa in ("carve", "write", "engrave", "scratch"):
stone = state.world.objects.get("STONE")
if state.prsi is not stone and not (
stone and stone.is_held_by(state.protagonist)
):
out.tell("You have nothing to carve with.\n")
return True
if not state.flags.get("name_told"):
out.tell(
"You carve something on the memorial, but the Beast doesn't "
"seem to care. Perhaps you need to tell it your name first.\n"
)
return True
# Success — defeat the Beast
beast = state.world.get("BEAST")
beast.fset(Flag.MUNGEDBIT)
state.flags["beast_defeated"] = True
state.score += 25
out.tell(
"You carve your name into the sandstone memorial. The Beast, "
"seeing your name, realises that since it knows who you are, "
"it doesn't need to eat you to find out. Satisfied, it curls "
"up and falls into a deep sleep.\n"
)
return True
return False
def _stone_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
out.tell(
"A sharp, flat stone. It looks suitable for carving.\n"
)
return True
return False
def _skeleton_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
out.tell(
"The skeleton of an unfortunate beasthunter. In its bony grasp "
"is a small device.\n"
)
return True
if state.prsa in ("search", "look in"):
nut_com = state.world.get("NUT-COM-INTERFACE")
if nut_com.parent and nut_com.parent.id == "SKELETON":
out.tell(
"You find a Nutrimatic Interface Sub-Processor wedged in "
"the skeleton's fingers.\n"
)
else:
out.tell("There is nothing else of interest on the skeleton.\n")
return True
return False
def _nut_com_interface_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
out.tell(
"A small circuit board labeled 'Nutrimatic Interface "
"Sub-Processor'. It looks like it could be connected to "
"something.\n"
)
return True
if state.prsa == "take":
nut_com = state.world.get("NUT-COM-INTERFACE")
if not state.flags.get("nut_com_scored"):
state.flags["nut_com_scored"] = True
state.score += 25
nut_com.move_to(state.protagonist)
out.tell("Taken.\n")
return True
return False
def _particle_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
out.tell(
"A tiny black particle. Printed on it in impossibly small "
"letters are the words: \"Common sense, Dent, Arthur "
"(for replacement, order part #31-541).\"\n"
)
return True
if state.prsa == "take":
if not state.flags.get("particle_scored"):
state.flags["particle_scored"] = True
state.score += 25
state.jigs_up(
"As you pick up the particle, a massive jolt of electrical "
"impulses surges through your body. The particle was, it "
"seems, an integral part of your brain. Removing it was "
"inadvisable."
)
return True
return False
def _vlhurg_leader_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
out.tell(
"The Vlhurg leader is a tall, spiny creature with an "
"expression of permanent outrage.\n"
)
return True
if state.prsa in ("listen", "talk", "ask", "tell"):
idx = state.flags.get("vlhurg_dialogue_idx", 0)
if idx < len(VLHURG_DIALOGUE):
out.tell(VLHURG_DIALOGUE[idx] + "\n")
state.flags["vlhurg_dialogue_idx"] = idx + 1
else:
out.tell(
"The Vlhurg leader repeats himself angrily, as war "
"leaders tend to do.\n"
)
return True
return False
def _ggugvunt_leader_action(state: GameState) -> bool:
out = state.output
if state.prsa == "examine":
out.tell(
"The G'Gugvuntt leader is a squat, reptilian creature with "
"beady eyes and a malevolent grin.\n"
)
return True
if state.prsa in ("listen", "talk", "ask", "tell"):
idx = state.flags.get("ggugvunt_dialogue_idx", 0)
if idx < len(GGUGVUNT_DIALOGUE):
out.tell(GGUGVUNT_DIALOGUE[idx] + "\n")
state.flags["ggugvunt_dialogue_idx"] = idx + 1
else:
out.tell(
"The G'Gugvuntt leader just glares at you and mutters "
"something about revenge.\n"
)
return True
return False
def _conversation_action(state: GameState) -> bool:
out = state.output
if state.prsa in ("listen", "examine"):
out.tell(
"The two leaders are arguing heatedly about a war that has "
"raged for ten thousand years, apparently triggered by a "
"careless remark at a diplomatic reception.\n"
)
return True
return False
# ---- Timed event handlers ----
def _i_beast(state: GameState) -> bool:
"""Escalating danger in the Beast's lair."""
out = state.output
if state.here is None or state.here.id != "LAIR":
return False
beast = state.world.get("BEAST")
if beast.fset_q(Flag.MUNGEDBIT):
return False # Beast is asleep
counter = state.flags.get("beast_counter", 0) + 1
state.flags["beast_counter"] = counter
towel = state.world.objects.get("TOWEL")
wearing_towel = towel and towel.fset_q(Flag.WORNBIT)
if wearing_towel:
if counter == 1:
out.tell(
"\nThe Beast sniffs the air, confused. It can't seem to "
"see you with the towel over your head.\n"
)
return True
if counter == 2:
out.tell(
"\nThe Beast stomps around, bewildered. It knows you're "
"here somewhere but can't work out where.\n"
)
return True
if counter >= 3:
out.tell(
"\nThe Beast growls in frustration and wanders off to "
"look for easier prey.\n"
)
return True
else:
if counter == 1:
out.tell(
"\nThe Ravenous Bugblatter Beast of Traal eyes you "
"hungrily.\n"
)
return True
if counter == 2:
out.tell(
"\nThe Beast takes a menacing step toward you, drool "
"dripping from its fangs.\n"
)
return True
if counter >= 3:
state.jigs_up(
"The Ravenous Bugblatter Beast of Traal lunges forward "
"and devours you in a single, terrible gulp."
)
return True
return False
def _i_whale(state: GameState) -> bool:
"""Countdown to whale impact."""
out = state.output
if state.here is None or state.here.id != "INSIDE-WHALE":
return False
counter = state.flags.get("whale_counter", 0) + 1
state.flags["whale_counter"] = counter
if counter == 3:
out.tell(
"\nThe rushing sound is getting louder. The whale seems to "
"be thinking about something.\n"
)
return True
if counter == 6:
out.tell(
"\n\"Oh no, not again,\" the whale thinks to itself.\n"
)
return True
if counter == 9:
out.tell(
"\nThe rushing sound is now deafening. The walls are "
"shaking violently.\n"
)
return True
if counter >= 11:
state.jigs_up(
"SPLAT! The whale hits the ground at terminal velocity. "
"You, being inside the whale at the time, also hit the "
"ground at terminal velocity. This is not survivable."
)
return True
return False
# ---- Room action handlers ----
def _lair_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-ENTER":
state.flags["beast_counter"] = 0
state.clock.queue(_i_beast, 4, name="I-BEAST")
return True
if rarg == "M-LOOK":
beast = state.world.get("BEAST")
if beast.fset_q(Flag.MUNGEDBIT):
out.tell(
"You are in the outer lair of the Ravenous Bugblatter "
"Beast of Traal. The Beast lies curled up, fast asleep. "
"Exits lead east and southwest.\n"
)
else:
out.tell(
"You are in the outer lair of the Ravenous Bugblatter "
"Beast of Traal. The Beast is here, looking hungry. "
"An exit leads east.\n"
)
return True
return False
def _lair_sw_exit(state: GameState) -> "Room | None":
"""SW exit from LAIR to INNER-LAIR — requires Beast defeated."""
out = state.output
beast = state.world.get("BEAST")
if not beast.fset_q(Flag.MUNGEDBIT):
out.tell(
"The Beast blocks your path to the southwest. You'd have "
"to get past it first.\n"
)
return None
return state.world.get_room("INNER-LAIR")
def _outer_lair_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-LOOK":
out.tell(
"A large walled courtyard. Strewn about are a profusion of "
"gnawed bones bleaching in the sun.\n"
)
return True
return False
def _inner_lair_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-LOOK":
out.tell(
"This is the heart of the Beast's lair. The only exit "
"leads northeast.\n"
)
return True
return False
def _war_chamber_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-LOOK":
out.tell(
"You are in the War Chamber of a star battle cruiser. "
"Two alien leaders sit across from each other at a large "
"table, arguing furiously.\n"
)
return True
if rarg == "M-ENTER":
state.flags["vlhurg_dialogue_idx"] = 0
state.flags["ggugvunt_dialogue_idx"] = 0
return True
return False
def _inside_whale_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-ENTER":
state.flags["whale_counter"] = 0
state.clock.queue(_i_whale, -1, name="I-WHALE")
return True
if rarg == "M-LOOK":
out.tell(
"You are in the stomach of a sperm whale. There is a "
"distant sound of rushing wind.\n"
)
return True
return False
def _maze_action(state: GameState, rarg: str) -> bool:
out = state.output
if rarg == "M-LOOK":
out.tell(
"A spongy gray maze of twisty little synapses, all alike.\n"
)
return True
if rarg == "M-ENTER":
state.flags["maze_counter"] = 0
# Randomly show/hide the particle
particle = state.world.objects.get("PARTICLE")
maze = state.world.get_room("MAZE")
if particle:
if random.random() < 0.5:
particle.move_to(maze)
particle.fclear(Flag.INVISIBLE)
else:
particle.move_to(state.world.local_globals)
return True
return False
def _maze_exit(state: GameState) -> "Room | None":
"""Maze exits randomly work or fail (40% fail rate)."""
out = state.output
if random.random() < 0.4:
out.tell("You wander around but end up back where you started.\n")
return None
return state.world.get_room("MAZE")
# ---- Registration ----
def register(world: World, state: GameState) -> None:
"""Create all off-Earth rooms and objects."""
# ---- Initialize state flags ----
state.flags.setdefault("beast_counter", 0)
state.flags.setdefault("name_told", False)
state.flags.setdefault("beast_defeated", False)
state.flags.setdefault("bearings_lost", False)
state.flags.setdefault("maze_counter", 0)
state.flags.setdefault("whale_counter", 0)
state.flags.setdefault("vlhurg_dialogue_idx", 0)
state.flags.setdefault("ggugvunt_dialogue_idx", 0)
state.flags.setdefault("nut_com_scored", False)
state.flags.setdefault("particle_scored", False)
# ---- Objects ----
beast = world.register(GameObject(
"BEAST", desc="Ravenous Bugblatter Beast of Traal",
synonyms=["beast", "bugblatter", "animal"],
adjectives=["ravenous", "bugblatter"],
flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT},
action=_beast_action,
))
memorial = world.register(GameObject(
"MEMORIAL", desc="sandstone memorial",
synonyms=["memorial", "monument", "sandstone"],
adjectives=["sandstone", "large"],
flags={Flag.NDESCBIT},
action=_memorial_action,
))
stone = world.register(GameObject(
"STONE", desc="sharp stone",
synonyms=["stone", "rock"],
adjectives=["sharp", "flat"],
flags={Flag.TAKEBIT},
size=3,
action=_stone_action,
))
skeleton = world.register(GameObject(
"SKELETON", desc="skeleton",
synonyms=["skeleton", "bones", "remains"],
adjectives=["dead", "beasthunter"],
flags={Flag.NDESCBIT, Flag.CONTBIT, Flag.SEARCHBIT},
action=_skeleton_action,
))
nut_com_interface = world.register(GameObject(
"NUT-COM-INTERFACE", desc="Nutrimatic Interface Sub-Processor",
synonyms=["interface", "processor", "circuit", "board"],
adjectives=["nutrimatic", "sub"],
flags={Flag.TAKEBIT},
size=2,
action=_nut_com_interface_action,
))
particle = world.register(GameObject(
"PARTICLE", desc="black particle",
synonyms=["particle", "speck"],
adjectives=["black", "tiny"],
flags={Flag.TAKEBIT, Flag.INVISIBLE},
size=1,
action=_particle_action,
))
vlhurg_leader = world.register(GameObject(
"VLHURG-LEADER", desc="Vlhurg leader",
synonyms=["vlhurg", "leader"],
adjectives=["vlhurg", "tall", "spiny"],
flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT},
action=_vlhurg_leader_action,
))
ggugvunt_leader = world.register(GameObject(
"GGUGVUNT-LEADER", desc="G'Gugvuntt leader",
synonyms=["ggugvunt", "g'gugvuntt", "leader"],
adjectives=["ggugvunt", "squat", "reptilian"],
flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT},
action=_ggugvunt_leader_action,
))
conversation = world.register(GameObject(
"CONVERSATION", desc="conversation",
synonyms=["conversation", "argument", "discussion"],
flags={Flag.NDESCBIT, Flag.INVISIBLE},
action=_conversation_action,
))
# ---- Rooms ----
lair = world.register(Room(
"LAIR", desc="Lair",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_lair_action,
))
beast.move_to(lair)
outer_lair = world.register(Room(
"OUTER-LAIR", desc="Outer Lair",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_outer_lair_action,
))
memorial.move_to(outer_lair)
stone.move_to(outer_lair)
inner_lair = world.register(Room(
"INNER-LAIR", desc="Inner Lair",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_inner_lair_action,
))
skeleton.move_to(inner_lair)
nut_com_interface.move_to(skeleton)
war_chamber = world.register(Room(
"WAR-CHAMBER", desc="War Chamber",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_war_chamber_action,
))
vlhurg_leader.move_to(war_chamber)
ggugvunt_leader.move_to(war_chamber)
conversation.move_to(war_chamber)
inside_whale = world.register(Room(
"INSIDE-WHALE", desc="Inside a Sperm Whale",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_inside_whale_action,
exits={}, # No exits
))
maze = world.register(Room(
"MAZE", desc="Maze",
flags={Flag.RLANDBIT, Flag.ONBIT},
action=_maze_action,
))
# ---- Connect rooms ----
lair.exits = {
Direction.EAST: DirectExit(outer_lair),
Direction.SW: ConditionalExit(_lair_sw_exit),
}
outer_lair.exits = {
Direction.WEST: DirectExit(lair),
}
inner_lair.exits = {
Direction.NE: DirectExit(lair),
}
# War chamber has no standard exits (arrived via dream dispatch)
war_chamber.exits = {}
# Maze exits all go through the random fail check
maze.exits = {
Direction.NORTH: ConditionalExit(_maze_exit),
Direction.SOUTH: ConditionalExit(_maze_exit),
Direction.EAST: ConditionalExit(_maze_exit),
Direction.WEST: ConditionalExit(_maze_exit),
}
File diff suppressed because it is too large Load Diff
+21 -1
View File
@@ -96,6 +96,22 @@ VERB_SYNONYMS: dict[str, str] = {
"pray": "pray",
"brush": "brush",
"swallow": "swallow",
"consult": "consult",
"say": "say",
"carve": "carve", "engrave": "carve", "write": "carve", "inscribe": "carve",
"plug": "plug",
"unplug": "unplug", "disconnect": "unplug",
"connect": "plug",
"block": "block",
"rub": "rub",
"repair": "repair", "fix": "repair", "mend": "repair",
"follow": "follow",
"kick": "kick",
"knock": "knock",
"type": "type",
"panic": "panic",
"relax": "relax",
"hang": "hang",
}
@@ -122,7 +138,7 @@ class Parser:
self.last_result: ParseResult | None = None
def parse(self, raw: str, state: "GameState") -> ParseResult | None:
"""Parse a raw input string into a ParseResult, or None if unparseable."""
"""Parse a raw input string into a ParseResult, or None if unparsable."""
raw = raw.strip()
if not raw:
return None
@@ -217,6 +233,10 @@ class Parser:
elif verb_word == "hang" and tokens and tokens[0] == "up":
verb = "hang up"
tokens.pop(0)
elif verb_word == "consult":
verb = "consult"
# "consult X about Y" -> verb=consult, direct_obj=X, prep=about, indirect_obj=Y
# The about preposition is already in PREPOSITIONS so it will split naturally
else:
verb = VERB_SYNONYMS.get(verb_word, verb_word)
+34 -29
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Any, Callable, TYPE_CHECKING
if TYPE_CHECKING:
from h2g2.engine.game_object import GameObject, Room
@@ -44,39 +44,19 @@ class GameState:
self.l_prso: GameObject | None = None
self.l_prsi: GameObject | None = None
# Condition flags
# Engine-level condition flags
self.lying_down: bool = False
self.headache: bool = True
self.groggy: bool = False
self.groggy_counter: int = 0
self.verbosity: int = 1 # 0=superbrief, 1=brief, 2=verbose
# Earth progression
self.house_demolished: bool = False
self.earth_demolished: bool = False
self.in_front_of_bulldozer: bool = False
self.ford_arrived: bool = False
self.ford_has_satchel: bool = True
self.prosser_in_mud: bool = False
self.beer_counter: int = 0
# Vogon
self.babel_fish_in_ear: bool = False
self.poem_enjoyed: bool = False
# Heart of Gold
self.holding_no_tea: bool = True
self.dreaming: bool = False
# Probability system (dream dispatch weights)
self.vogon_prob: int = 100
self.heart_prob: int = 0
self.traal_prob: int = 60
self.fleet_prob: int = 0
self.whale_prob: int = 0
# Generic game state flags (content-specific)
self.flags: dict[str, Any] = {}
# Deaths
self.dead_counter: int = 0
# Hook system for inventory extras
self.inventory_extras: list[Callable] = []
# Death system
self._dream_restore_callback: Callable | None = None
def update_lit(self) -> None:
"""Recalculate whether the current location is lit."""
@@ -95,3 +75,28 @@ class GameState:
self.lit = True
return
self.lit = False
def jigs_up(self, message: str) -> None:
"""Handle player death."""
self.output.tell(message + "\n")
if self.dreaming:
# Dream death: restore state and return to DARK room
self.output.tell("\nEverything becomes...\n\n")
# Content modules register dream-restore callbacks
if self._dream_restore_callback:
self._dream_restore_callback(self)
dark = self.world.rooms.get("DARK")
if dark:
self.here = dark
# Will be handled by dark room's M-ENTER
else:
self.output.tell("\n **** You have died ****\n\n")
self.finish()
def finish(self) -> None:
"""End the game with score display and restart prompt."""
self.output.tell(
f"Your score is {self.score} of a possible 400, "
f"in {self.moves} turns.\n"
)
self.running = False
+167 -7
View File
@@ -82,10 +82,10 @@ def v_inventory(state: GameState, prso: GameObject | None, prsi: GameObject | No
out.tell("You are empty-handed.\n")
return True
out.tell("You have:\n")
if state.headache:
out.tell(" a splitting headache\n")
if state.holding_no_tea:
out.tell(" no tea\n")
for extra_fn in state.inventory_extras:
line = extra_fn(state)
if line:
out.tell(f" {line}\n")
for obj in state.protagonist.children:
desc = obj.a_desc()
if obj.fset_q(Flag.WORNBIT):
@@ -326,9 +326,9 @@ def v_score(state: GameState, prso: GameObject | None, prsi: GameObject | None)
@verb_handler("diagnose")
def v_diagnose(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
out = state.output
if state.headache:
if state.flags.get("headache"):
out.tell("You have a big blinding throbber.\n")
elif state.groggy:
elif state.flags.get("groggy"):
out.tell("You feel weak.\n")
else:
out.tell("You are in good health.\n")
@@ -437,7 +437,7 @@ def v_pray(state: GameState, prso: GameObject | None, prsi: GameObject | None) -
@verb_handler("attack")
def v_attack(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso:
state.output.tell(f"Violence isn't the answer to this one.\n")
state.output.tell("Violence isn't the answer to this one.\n")
else:
state.output.tell("What do you want to attack?\n")
return True
@@ -445,16 +445,44 @@ def v_attack(state: GameState, prso: GameObject | None, prsi: GameObject | None)
@verb_handler("give")
def v_give(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prsi and prsi.action:
result = prsi.action(state)
if result:
return True
state.output.tell("No one is interested.\n")
return True
@verb_handler("show")
def v_show(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prsi and prsi.action:
result = prsi.action(state)
if result:
return True
state.output.tell("No one is interested.\n")
return True
@verb_handler("tell")
def v_tell(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("No one is listening.\n")
return True
@verb_handler("ask")
def v_ask(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("No one answers.\n")
return True
@verb_handler("hang up")
def v_hang_up(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("You can't hang that up.\n")
@@ -471,3 +499,135 @@ def v_answer(state: GameState, prso: GameObject | None, prsi: GameObject | None)
def v_call(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("There's no one to call.\n")
return True
@verb_handler("consult")
def v_consult(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("There's nothing useful to consult.\n")
return True
@verb_handler("say")
def v_say(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("Talking to yourself is a sign of impending mental collapse.\n")
return True
@verb_handler("carve")
def v_carve(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("Bizarre.\n")
return True
@verb_handler("plug")
def v_plug(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("You can't plug that in.\n")
return True
@verb_handler("unplug")
def v_unplug(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("It's not plugged in.\n")
return True
@verb_handler("block")
def v_block(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("You can't block that.\n")
return True
@verb_handler("rub")
def v_rub(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso:
state.output.tell(f"Rubbing {prso.the_desc()} has no effect.\n")
else:
state.output.tell("What do you want to rub?\n")
return True
@verb_handler("push")
def v_push(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("Nothing happens.\n")
return True
@verb_handler("hang")
def v_hang(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("You can't hang that up.\n")
return True
@verb_handler("repair")
def v_repair(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("You can't fix that.\n")
return True
@verb_handler("follow")
def v_follow(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("You can't follow that.\n")
return True
@verb_handler("kick")
def v_kick(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso:
state.output.tell(f"Kicking {prso.the_desc()} has no effect.\n")
else:
state.output.tell("What do you want to kick?\n")
return True
@verb_handler("knock")
def v_knock(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("Nobody's home.\n")
return True
@verb_handler("type")
def v_type(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
if prso and prso.action:
result = prso.action(state)
if result:
return True
state.output.tell("There's nothing to type on.\n")
return True
@verb_handler("enjoy")
def v_enjoy(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("I wouldn't dream of it.\n")
return True
@verb_handler("panic")
def v_panic(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("DON'T PANIC!\n")
return True
@verb_handler("relax")
def v_relax(state: GameState, prso: GameObject | None, prsi: GameObject | None) -> bool:
state.output.tell("You feel calmer.\n")
return True
+36 -2
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
"""The Hitchhiker's Guide to the Galaxy — Python text adventure engine."""
from h2g2.engine.game_object import Flag
from h2g2.engine.world import World
from h2g2.engine.state import GameState
from h2g2.engine.output import Output
@@ -13,7 +12,7 @@ from h2g2.engine.loop import GameLoop
import h2g2.engine.verbs # noqa: F401
# Import content modules
from h2g2.content import globals_content, earth
from h2g2.content import globals_content, earth, vogon, heart, unearth, dark
def main() -> None:
@@ -33,6 +32,41 @@ def main() -> None:
state.winner = world.protagonist
state.lying_down = True # start in bed
# Initialize content-specific flags
state.flags["headache"] = True
state.flags["groggy"] = False
state.flags["groggy_counter"] = 0
state.flags["house_demolished"] = False
state.flags["earth_demolished"] = False
state.flags["in_front_of_bulldozer"] = False
state.flags["ford_arrived"] = False
state.flags["ford_has_satchel"] = True
state.flags["prosser_in_mud"] = False
state.flags["beer_counter"] = 0
state.flags["babel_fish_in_ear"] = False
state.flags["poem_enjoyed"] = False
state.flags["holding_no_tea"] = True
state.flags["dead_counter"] = 0
state.flags["vogon_prob"] = 100
state.flags["heart_prob"] = 0
state.flags["traal_prob"] = 60
state.flags["fleet_prob"] = 0
state.flags["whale_prob"] = 0
# Register content that needs state access
vogon.register(world, state)
heart.register(world, state)
unearth.register(world, state)
dark.register(world, state)
# Register inventory extras
state.inventory_extras.append(
lambda s: "a splitting headache" if s.flags.get("headache") else None
)
state.inventory_extras.append(
lambda s: "no tea" if s.flags.get("holding_no_tea") else None
)
# Banner
output.tell(
"\n *** THE HITCHHIKER'S GUIDE TO THE GALAXY: "