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:
Vendored
+42
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: "
|
||||
|
||||
Reference in New Issue
Block a user