diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a850ddb --- /dev/null +++ b/.vscode/settings.json @@ -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" + ] +} \ No newline at end of file diff --git a/h2g2/content/dark.py b/h2g2/content/dark.py new file mode 100644 index 0000000..a839f6c --- /dev/null +++ b/h2g2/content/dark.py @@ -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 diff --git a/h2g2/content/earth.py b/h2g2/content/earth.py index 95a5279..abda995 100644 --- a/h2g2/content/earth.py +++ b/h2g2/content/earth.py @@ -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( diff --git a/h2g2/content/globals_content.py b/h2g2/content/globals_content.py index 68eabcc..8f91e02 100644 --- a/h2g2/content/globals_content.py +++ b/h2g2/content/globals_content.py @@ -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") diff --git a/h2g2/content/heart.py b/h2g2/content/heart.py new file mode 100644 index 0000000..843a317 --- /dev/null +++ b/h2g2/content/heart.py @@ -0,0 +1,1491 @@ +"""Heart of Gold locations -- Entry Bay, Corridors, Bridge, Galley, Engine Room, +Pantry, Hatchway, Access Space, Ramp.""" + +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 + + +# ---- Tool list for Marvin's random request ---- + +_MARVIN_TOOLS = [ + "screwdriver", "toothbrush", "wrench", "pliers", "rasp", "tweezers", +] + + +# ---- Object action handlers ---- + +def _sales_brochure_action(state: GameState) -> bool: + out = state.output + if state.prsa in ("read", "examine"): + out.tell( + "The brochure reads:\n\n" + '"The Heart of Gold -- the sleekest, most advanced, most ' + "hip spaceship in the Galaxy. Boasting the revolutionary " + "Infinite Improbability Drive, the Heart of Gold can take " + "you anywhere in the Universe almost before you've decided " + "where you want to go. Seats 300. Complimentary peanuts. " + 'Not liable for loss of limbs, sanity, or probability.\"\n' + ) + return True + return False + + +def _eddie_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "Eddie is the shipboard computer. His cheery manner is " + "almost unbearably irritating.\n" + ) + return True + if state.prsa in ("tell", "ask", "talk"): + out.tell( + '"Hi there!" says Eddie brightly. "I\'m Eddie, your ' + "shipboard computer, and I'm feeling just great, guys, " + "and I know I'm just going to get a bundle of joy out of " + 'whatever you do next!"\n' + ) + return True + return False + + +def _zaphod_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "Zaphod Beeblebrox, adventurer, ex-hippie, good-timer, " + "(fraud? quite possibly), manic self-publicist, terribly " + "bad at personal relationships, often thought to be " + "completely out to lunch. President of the Galaxy.\n" + ) + return True + if state.prsa in ("tell", "ask", "talk"): + out.tell('"Shut up, Earthman," says Zaphod.\n') + return True + return False + + +def _trillian_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "Trillian is a dark-haired woman. She is slim and " + "attractive, and seems to be the only person on this ship " + "with any sense.\n" + ) + return True + if state.prsa in ("tell", "ask", "talk"): + out.tell( + '"Hello, Arthur," says Trillian with a sympathetic smile.\n' + ) + return True + return False + + +def _handbag_action(state: GameState) -> bool: + out = state.output + handbag = state.world.get("HANDBAG") + + if state.prsa == "examine": + out.tell("It's Trillian's handbag.\n") + return True + + if state.prsa in ("open", "look in"): + visible = handbag.contents_string() + if visible: + descs = [obj.a_desc() for obj in visible] + out.tell("Inside the handbag you find " + ", ".join(descs) + ".\n") + else: + out.tell("The handbag is empty.\n") + return True + + if state.prsa == "take": + trillian = state.world.get("TRILLIAN") + if handbag.parent is trillian: + out.tell( + "Trillian gives you a look that suggests you should keep " + "your hands off her handbag.\n" + ) + return True + return False + + return False + + +def _tweezers_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell("A small pair of tweezers.\n") + return True + return False + + +def _marvin_action(state: GameState) -> bool: + out = state.output + marvin = state.world.get("MARVIN") + + if state.prsa == "examine": + out.tell( + "Marvin the Paranoid Android stands here, looking profoundly " + "depressed. His metal body is the size and shape of a short, " + "stocky human, and his head hangs at a disconsolate angle.\n" + ) + return True + + if state.prsa in ("tell", "ask", "talk"): + mc = state.flags.get("marvin_counter", 0) + if mc == 0: + out.tell( + '"Life," says Marvin. "Don\'t talk to me about life."\n' + ) + elif mc == 1: + out.tell( + '"I could fix the hatch mechanism," Marvin sighs, "but ' + "nobody ever asks me to do anything interesting. Go on " + 'then, I suppose you want me to fix it."\n' + ) + state.flags["marvin_counter"] = 2 + # Queue Marvin's work timer + state.clock.queue(_i_marvin, 12, name="I-MARVIN") + elif mc == 2: + out.tell( + '"I\'m working on it," says Marvin gloomily. "Not that ' + 'anyone cares."\n' + ) + elif mc == 3: + tool = state.flags.get("tool_required", "screwdriver") + out.tell( + f'"I need a {tool} to finish the job," says Marvin. ' + '"Not that it matters. Nothing matters."\n' + ) + elif mc >= 4: + out.tell( + '"I\'ve fixed the hatch," says Marvin. "I won\'t pretend ' + 'it gives me any satisfaction, because it doesn\'t."\n' + ) + return True + + if state.prsa in ("give", "show"): + if state.flags.get("marvin_counter", 0) == 3: + tool_needed = state.flags.get("tool_required", "screwdriver") + if state.prso and tool_needed in state.prso.desc.lower(): + state.prso.move_to(marvin) + state.flags["marvin_counter"] = 4 + state.score += 25 + # Fix the hatch + hatch = state.world.get("HATCH") + hatch.fset(Flag.OPENBIT) + out.tell( + "Marvin takes the tool. With agonising slowness and an " + "air of infinite depression, he fixes the hatch mechanism.\n\n" + '"There," he says. "I\'ve done it. I won\'t pretend it ' + "gives me any satisfaction, because it doesn't. The hatch " + 'is now open."\n' + ) + return True + else: + out.tell( + f'"That\'s not a {tool_needed}," says Marvin. "Here I am, ' + "brain the size of a planet, and they can't even bring me " + 'the right tool."\n' + ) + return True + return False + + return False + + +def _nutrimat_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "The Nutrimatic Drinks Synthesizer is a squat, ugly device " + "with a single button pad and a slot from which drinks emerge. " + 'A sign reads: "Almost, but not quite, entirely unlike tea."\n' + ) + return True + return False + + +def _pad_action(state: GameState) -> bool: + out = state.output + if state.prsa in ("push", "press"): + if state.flags.get("tea_counter", 0) > 0: + out.tell( + "The Nutrimat is already processing your request.\n" + ) + return True + state.flags["tea_counter"] = 1 + state.clock.queue(_i_tea, -1, name="I-TEA") + out.tell( + 'You press the pad. The Nutrimat goes "beeble" and starts ' + "to think about tea.\n" + ) + return True + if state.prsa == "examine": + out.tell("A button pad on the Nutrimatic Drinks Synthesizer.\n") + return True + return False + + +def _slot_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell("A slot from which drinks emerge.\n") + return True + return False + + +def _tea_action(state: GameState) -> bool: + out = state.output + tea = state.world.get("TEA") + + if state.prsa in ("drink", "eat"): + tea.move_to(state.world.local_globals) + state.score += 100 + out.tell( + "You drink the tea. It is the most perfectly normal cup of " + "tea you have ever tasted. It is exactly what tea should be. " + "You feel wonderfully refreshed.\n" + ) + return True + + if state.prsa == "examine": + out.tell( + "It is a cup of proper, real tea -- rich, brown, and " + "reviving.\n" + ) + return True + + if state.prsa in ("show", "give"): + door = state.world.get("SCREENING-DOOR") + if state.prsi is door or (state.here and state.here.id == "AFT-CORRIDOR"): + state.flags["tea_shown"] = True + if state.flags.get("no_tea_shown"): + _open_screening_door(state) + else: + out.tell( + "The screening door scans the tea but does not open. " + "Perhaps it requires something more.\n" + ) + return True + return False + + return False + + +def _substitute_action(state: GameState) -> bool: + out = state.output + sub = state.world.get("SUBSTITUTE") + + if state.prsa in ("drink", "eat"): + sub.move_to(state.world.local_globals) + state.flags["substitute_drunk"] = True + state.score -= 30 + out.tell( + "You drink the Advanced Tea Substitute. It tastes almost, " + "but not quite, entirely unlike tea. You feel vaguely " + "dissatisfied.\n" + ) + return True + + if state.prsa == "examine": + out.tell( + "A plastic cup containing a liquid that is almost, but not " + "quite, entirely unlike tea.\n" + ) + return True + + return False + + +def _no_tea_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + out.tell( + "You are holding no tea. It's the absence of tea -- a " + "concept given form by your complete lack of tea.\n" + ) + return True + + if state.prsa in ("show", "give"): + door = state.world.get("SCREENING-DOOR") + if state.prsi is door or (state.here and state.here.id == "AFT-CORRIDOR"): + state.flags["no_tea_shown"] = True + if state.flags.get("tea_shown"): + _open_screening_door(state) + else: + out.tell( + "The screening door scans your absence of tea but does " + "not open. Perhaps it requires something more.\n" + ) + return True + return False + + if state.prsa == "drop": + out.tell("How do you drop something you don't have?\n") + return True + + return False + + +def _open_screening_door(state: GameState) -> None: + """Open the screening door when shown both tea and no-tea.""" + out = state.output + state.flags["screening_door_open"] = True + door = state.world.get("SCREENING-DOOR") + door.fset(Flag.OPENBIT) + state.score += 25 + out.tell( + "The screening door flickers with confusion as it tries to " + "reconcile the simultaneous existence of tea and no tea. " + "Circuits spark. Logic banks overflow. With a defeated whirr, " + "the door slides open.\n" + ) + + +def _screening_door_action(state: GameState) -> bool: + out = state.output + door = state.world.get("SCREENING-DOOR") + + if state.prsa == "examine": + if door.fset_q(Flag.OPENBIT): + out.tell("The screening door is open.\n") + else: + out.tell( + "A gleaming electronic screening door blocks the way " + "west to the pantry. It appears to require some form of " + "proof of identity or paradox.\n" + ) + return True + + if state.prsa == "open": + if door.fset_q(Flag.OPENBIT): + out.tell("It's already open.\n") + else: + out.tell( + "The door refuses to open. It seems to want you to " + "show it something.\n" + ) + return True + + return False + + +def _spare_drive_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + out.tell( + "It's a portable Infinite Improbability Generator. It has " + "a large plug at one end, a small plug at the other, and a " + "switch on the side.\n" + ) + return True + + return False + + +def _large_plug_action(state: GameState) -> bool: + out = state.output + + if state.prsa in ("put", "connect", "plug", "attach"): + console = state.world.get("CONTROL-CONSOLE") + if state.prsi is console or ( + state.prsi and state.prsi.id == "CONTROL-CONSOLE" + ): + state.flags["drive_to_controls"] = True + out.tell( + "You connect the large plug to the control console.\n" + ) + return True + return False + + if state.prsa == "examine": + out.tell("A large plug at one end of the spare drive.\n") + return True + + return False + + +def _small_plug_action(state: GameState) -> bool: + out = state.output + + if state.prsa in ("put", "connect", "plug", "attach"): + plotter = state.world.get("PLOTTER") + receptacle = state.world.get("SMALL-RECEPTACLE") + if state.prsi is plotter or state.prsi is receptacle: + state.flags["drive_to_plotter"] = True + out.tell( + "You connect the small plug to the atomic vector plotter.\n" + ) + return True + return False + + if state.prsa == "examine": + out.tell("A small plug at one end of the spare drive.\n") + return True + + return False + + +def _drive_switch_action(state: GameState) -> bool: + out = state.output + + if state.prsa in ("push", "press", "flip", "turn on", "turn off", "activate"): + d2c = state.flags.get("drive_to_controls") + d2p = state.flags.get("drive_to_plotter") + bsrc = state.flags.get("brownian_source") + + if not d2c or not d2p: + out.tell( + "Click. Nothing happens. The drive doesn't seem to be " + "connected to anything useful.\n" + ) + return True + + if not bsrc: + out.tell( + "The drive hums momentarily then stops. It seems to need " + "a source of Brownian motion to function.\n" + ) + return True + + # Success! Trigger improbability event + out.tell( + "You flip the switch. The Infinite Improbability Drive " + "shudders into life. The universe twists. Reality buckles. " + "For a brief moment, every particle in existence passes " + "through every other point in the Universe.\n\n" + "When things settle down, the Heart of Gold is in orbit " + "around Magrathea.\n" + ) + state.score += 50 + state.flags["landing_countdown"] = True + state.clock.queue(_i_landing, 24, name="I-LANDING") + return True + + if state.prsa == "examine": + out.tell("A switch on the side of the spare drive.\n") + return True + + return False + + +def _plotter_heart_action(state: GameState) -> bool: + out = state.output + plotter = state.world.get("PLOTTER") + + if state.prsa == "examine": + out.tell( + "It's an atomic vector plotter -- a small, sleek device " + "of obviously advanced technology. It has a small receptacle " + "and a dangly bit.\n" + ) + return True + + return False + + +def _small_receptacle_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + out.tell("A small receptacle on the atomic vector plotter.\n") + return True + + return False + + +def _dangly_bit_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + out.tell( + "A small dangly bit on the atomic vector plotter. It looks " + "like it should be suspended in some kind of liquid.\n" + ) + return True + + if state.prsa in ("put", "dip", "suspend", "submerge"): + # Check if prsi is tea or substitute (as Brownian motion source) + if state.prsi and state.prsi.id in ("TEA", "SUBSTITUTE"): + state.flags["brownian_source"] = state.prsi + out.tell( + f"You suspend the dangly bit in {state.prsi.the_desc()}. " + "Brownian motion of the molecules provides the necessary " + "random input.\n" + ) + return True + return False + + return False + + +def _control_console_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + out.tell( + "The main control console of the Heart of Gold. It has an " + "impressive array of buttons, switches, and screens, most of " + "which seem to be controlled by Eddie.\n" + ) + return True + + return False + + +def _hatch_action(state: GameState) -> bool: + out = state.output + hatch = state.world.get("HATCH") + + if state.prsa == "examine": + if hatch.fset_q(Flag.OPENBIT): + out.tell("The hatch is open. A ramp leads down.\n") + else: + out.tell( + "The hatch mechanism is jammed. It looks like it needs " + "to be repaired.\n" + ) + return True + + if state.prsa == "open": + if hatch.fset_q(Flag.OPENBIT): + out.tell("The hatch is already open.\n") + else: + out.tell( + "The hatch mechanism is jammed solid. You can't open it " + "by hand.\n" + ) + return True + + return False + + +def _wrench_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell("A heavy adjustable wrench.\n") + return True + return False + + +def _pliers_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell("A sturdy pair of pliers.\n") + return True + return False + + +def _rasp_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell("A coarse metal rasp.\n") + return True + return False + + +# ---- Timed event handlers ---- + +def _i_tea(state: GameState) -> bool: + """Nutrimat tea processing and missile attack countdown.""" + out = state.output + counter = state.flags.get("tea_counter", 0) + + if counter <= 0: + return False + + counter += 1 + state.flags["tea_counter"] = counter + + if counter == 2: + out.tell( + "\nThe Nutrimat hums to itself. It is trying to work out " + "what tea is.\n" + ) + return True + if counter == 3: + out.tell( + "\nThe Nutrimat is still thinking. It is exploring the " + "concept of dried leaves in boiling water.\n" + ) + return True + if counter == 4: + out.tell( + "\nThe Nutrimat makes some encouraging gurgling noises.\n" + ) + return True + if counter == 5: + out.tell( + "\nThe Nutrimat whirrs. It seems to be making progress.\n" + ) + return True + if counter == 6: + out.tell( + "\nThe Nutrimat dings. A cup of something emerges from the " + "slot. Unfortunately it is a cup of Advanced Tea Substitute.\n" + ) + sub = state.world.get("SUBSTITUTE") + galley = state.world.get_room("GALLEY") + sub.move_to(galley) + return True + if counter == 7: + out.tell( + "\nThe Nutrimat has become obsessed with the question of " + "tea. It has connected itself to the main computer to think " + "about it more deeply.\n" + ) + return True + if counter == 8: + out.tell( + '\nEddie announces: "Guys! We have a small problem! Two ' + "Magrathean missiles have just locked on to us! I think " + "they might be planning to destroy us! Isn't that just " + 'wild?"\n' + ) + return True + if counter == 9: + out.tell( + "\nThe ship shudders. Warning klaxons sound throughout the " + "corridors.\n" + ) + return True + if counter == 10: + out.tell( + '\n"The missiles are getting closer!" Eddie reports ' + "cheerfully.\n" + ) + return True + if counter == 11: + out.tell( + "\nAnother shudder rocks the ship. Something explodes " + "somewhere aft.\n" + ) + return True + if counter == 12: + out.tell( + '\n"Impact in about thirty seconds!" says Eddie. "Have a ' + 'nice day!"\n' + ) + return True + if counter == 13: + out.tell( + "\nThe ship lurches violently. Panels spark and shatter.\n" + ) + return True + if counter == 14: + out.tell( + '\n"Ten seconds to impact! It\'s been great knowing you ' + 'guys!" says Eddie.\n' + ) + return True + if counter >= 15: + # Disable this timer + for entry in state.clock.entries: + if entry.routine is _i_tea: + entry.enabled = False + state.jigs_up( + "The missiles strike the Heart of Gold amidships. The ship " + "is blown into a billion tiny pieces, most of which land on " + "the entirely blameless planet of Bartledan where they cause " + "untold havoc among the native population who spend the next " + "three generations wondering what hit them." + ) + return True + + return False + + +def _i_marvin(state: GameState) -> bool: + """After 12 turns, Marvin moves to access space and asks for a tool.""" + out = state.output + + if state.flags.get("marvin_counter", 0) != 2: + return False + + marvin = state.world.get("MARVIN") + access = state.world.get_room("ACCESS-SPACE") + + # Select a random tool + tool = random.choice(_MARVIN_TOOLS) + state.flags["tool_required"] = tool + state.flags["marvin_counter"] = 3 + + # Move Marvin to the access space + marvin.move_to(access) + + out.tell( + "\nMarvin's voice echoes up from somewhere below. \"I've had a " + "look at the hatch mechanism. It's typical. Shoddy workmanship. " + f'I need a {tool} to fix it. Not that I expect anyone to ' + 'bother bringing me one.\"\n' + ) + return True + + +def _i_landing(state: GameState) -> bool: + """Landing countdown on Magrathea.""" + out = state.output + + state.flags["landed"] = True + + # Disable this timer + for entry in state.clock.entries: + if entry.routine is _i_landing: + entry.enabled = False + + out.tell( + "\nThe Heart of Gold descends through the atmosphere of " + "Magrathea. The legendary planet of the planet-builders fills " + "the viewscreen. The ship settles gently onto a vast grey " + "plain.\n\n" + '"We have landed," announces Eddie, completely unnecessarily.\n' + ) + + # Open the ramp path + hatchway = state.world.get_room("HATCHWAY") + ramp = state.world.get_room("RAMP") + hatchway.exits[Direction.DOWN] = ConditionalExit( + lambda s: _ramp_exit(s) + ) + + return True + + +# ---- Room action handlers ---- + +def _entry_bay_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is Entry Bay Number Two of the starship Heart of " + "Gold. It is a wide, gleaming white chamber. The main " + "corridor of the ship lies to the south.\n" + ) + return True + + if rarg == "M-ENTER": + entry_bay = state.world.get_room("ENTRY-BAY") + if not entry_bay.fset_q(Flag.TOUCHBIT): + entry_bay.fset(Flag.TOUCHBIT) + state.score += 10 + out.tell( + "\nYou materialise in a wide, white chamber. You look " + "around, slightly dazed.\n\n" + "Ford Prefect stumbles in behind you. \"Incredible!\" he " + "says. \"We've been picked up by the Heart of Gold! The " + "ship that was stolen by Zaphod Beeblebrox! The most " + "improbable ship in the whole of the galaxy!\"\n\n" + "Ford wanders off down the corridor muttering excitedly " + "to himself.\n" + ) + + return False + + +def _fore_corridor_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "You are in a bright corridor near the fore end of the " + "Heart of Gold. The entry bay lies to the north, the " + "bridge is up, the galley is to the west, and the corridor " + "continues aft to the south.\n" + ) + return True + + return False + + +def _aft_corridor_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + door = state.world.get("SCREENING-DOOR") + out.tell( + "You are in the aft section of the corridor. The corridor " + "continues north. The engine room lies to the south. " + "A hatchway leads down.\n" + ) + if door.fset_q(Flag.OPENBIT): + out.tell("An open doorway leads west to the pantry.\n") + else: + out.tell( + "A gleaming screening door blocks the way west.\n" + ) + return True + + return False + + +def _aft_south_exit(state: GameState) -> "Room | None": + """Exit south from aft corridor to engine room -- requires arguing.""" + out = state.output + counter = state.flags.get("argument_counter", 0) + + if counter < 3: + state.flags["argument_counter"] = counter + 1 + responses = [ + "A sign reads: \"Engine Room -- Authorised Personnel Only.\" " + "You aren't authorised.\n", + "You still aren't authorised. The sign is quite firm about " + "this.\n", + "Oh, all right. Since you're so insistent, the sign gives " + "up and lets you through.\n", + ] + out.tell(responses[counter]) + if counter == 2: + return state.world.get_room("ENGINE-ROOM") + return None + + return state.world.get_room("ENGINE-ROOM") + + +def _aft_west_exit(state: GameState) -> "Room | None": + """Exit west from aft corridor to pantry -- requires screening door open.""" + out = state.output + door = state.world.get("SCREENING-DOOR") + if not door.fset_q(Flag.OPENBIT): + out.tell("The screening door blocks your way.\n") + return None + return state.world.get_room("PANTRY") + + +def _bridge_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is the bridge of the Heart of Gold. Huge " + "viewscreens display the stars outside. The main control " + "console dominates the center of the room. A companionway " + "leads down.\n" + ) + return True + + return False + + +def _galley_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is the ship's galley. Dominating the room is the " + "Nutrimatic Drinks Synthesizer, a machine designed to " + "produce the widest possible range of drinks personally " + "matched to your taste and metabolism. It invariably " + "produces a liquid almost, but not quite, entirely unlike " + "tea. The corridor lies to the east.\n" + ) + return True + + return False + + +def _engine_room_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + look_count = state.flags.get("look_counter", 0) + look_count += 1 + state.flags["look_counter"] = look_count + + out.tell( + "This is the engine room of the Heart of Gold. The " + "Infinite Improbability Drive sits in the middle of the " + "room, humming gently to itself.\n" + ) + + if look_count == 1: + out.tell( + "The room is cluttered with equipment. You can't make " + "out much detail.\n" + ) + elif look_count == 2: + out.tell( + "Looking more carefully, you notice some equipment " + "partially hidden behind panels.\n" + ) + elif look_count >= 3: + if not state.flags.get("engine_revealed"): + state.flags["engine_revealed"] = True + state.score += 25 + # Reveal hidden objects + spare = state.world.get("SPARE-DRIVE") + spare.fclear(Flag.INVISIBLE) + wrench = state.world.get("WRENCH") + wrench.fclear(Flag.INVISIBLE) + pliers = state.world.get("PLIERS") + pliers.fclear(Flag.INVISIBLE) + rasp = state.world.get("RASP") + rasp.fclear(Flag.INVISIBLE) + out.tell( + "At last! Careful examination reveals a portable " + "Infinite Improbability Generator, a wrench, a pair " + "of pliers, and a rasp tucked away behind the " + "machinery.\n" + ) + else: + out.tell( + "You can see the spare drive and various tools.\n" + ) + + return True + + return False + + +def _pantry_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is a small pantry off the main corridor. It smells " + "faintly of synthetic food and despair.\n" + ) + return True + + if rarg == "M-ENTER": + pantry = state.world.get_room("PANTRY") + if not pantry.fset_q(Flag.TOUCHBIT): + pantry.fset(Flag.TOUCHBIT) + marvin = state.world.get("MARVIN") + if marvin.parent is pantry: + state.flags["marvin_counter"] = max( + state.flags.get("marvin_counter", 0), 1 + ) + + return False + + +def _hatchway_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + hatch = state.world.get("HATCH") + out.tell( + "You are in a narrow hatchway. The corridor is above you.\n" + ) + if hatch.fset_q(Flag.OPENBIT): + out.tell("The hatch is open. A ramp leads down.\n") + else: + out.tell("A hatch below you is jammed shut.\n") + out.tell("A narrow gap leads east into a cramped access space.\n") + return True + + return False + + +def _hatchway_down_exit(state: GameState) -> "Room | None": + """Exit down from hatchway -- requires hatch open and landed.""" + out = state.output + hatch = state.world.get("HATCH") + if not hatch.fset_q(Flag.OPENBIT): + out.tell("The hatch is jammed shut.\n") + return None + if not state.flags.get("landed"): + out.tell( + "The hatch is open, but the ship hasn't landed anywhere " + "yet. Below is the cold vacuum of space.\n" + ) + return None + return state.world.get_room("RAMP") + + +def _access_space_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is a tiny, cramped access space barely large enough " + "to crouch in. Cables and pipes run along every surface. " + "The hatchway is to the west.\n" + ) + return True + + if rarg == "M-BEG": + # Can only carry 1 item in the access space + protagonist = state.protagonist + if state.prsa == "take" and state.prso: + held = [c for c in protagonist.children + if Flag.INVISIBLE not in c.flags] + if len(held) >= 1: + out.tell( + "There's no room to carry anything more in this " + "cramped space. You'll have to drop something first.\n" + ) + return True + return False + + return False + + +def _ramp_exit(state: GameState) -> "Room | None": + """Conditional ramp access from hatchway.""" + hatch = state.world.get("HATCH") + if not hatch.fset_q(Flag.OPENBIT): + state.output.tell("The hatch is jammed shut.\n") + return None + if not state.flags.get("landed"): + state.output.tell("The ship hasn't landed yet.\n") + return None + return state.world.get_room("RAMP") + + +def _ramp_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "You are standing on the landing ramp of the Heart of Gold. " + "The vast grey surface of Magrathea stretches away in every " + "direction beneath a dark sky. You have arrived at the " + "legendary planet.\n" + ) + return True + + if rarg == "M-ENTER": + ramp = state.world.get_room("RAMP") + if not ramp.fset_q(Flag.TOUCHBIT): + ramp.fset(Flag.TOUCHBIT) + state.score += 50 + out.tell( + "\nYou step out onto the surface of Magrathea. The " + "air is cold and thin, and the silence is immense.\n\n" + "You have reached the legendary lost planet of " + "Magrathea, home of the custom-made luxury planet " + "industry. This is a great achievement.\n\n" + "Congratulations.\n" + ) + + return False + + +def _no_tea_inventory(state: GameState) -> str | None: + """Inventory extra: show 'no tea' if player has it.""" + no_tea = state.world.get("NO-TEA") + if no_tea.is_held_by(state.protagonist): + return " no tea (the concept)\n" + return None + + +# ---- Registration ---- + +def register(world: World, state: GameState) -> None: + """Create all Heart of Gold rooms and objects.""" + + # ---- Initialize state flags ---- + + state.flags.setdefault("look_counter", 0) + state.flags.setdefault("marvin_counter", 0) + state.flags.setdefault("tool_required", None) + state.flags.setdefault("tea_counter", 0) + state.flags.setdefault("tea_shown", False) + state.flags.setdefault("no_tea_shown", False) + state.flags.setdefault("substitute_drunk", False) + state.flags.setdefault("drive_to_controls", False) + state.flags.setdefault("drive_to_plotter", False) + state.flags.setdefault("brownian_source", None) + state.flags.setdefault("landed", False) + state.flags.setdefault("argument_counter", 0) + state.flags.setdefault("screening_door_open", False) + state.flags.setdefault("engine_revealed", False) + state.flags.setdefault("landing_countdown", False) + + # ---- Global objects for the ship ---- + + screening_door = world.register(GameObject( + "SCREENING-DOOR", desc="screening door", + synonyms=["door", "screening"], + adjectives=["screening", "electronic"], + flags={Flag.DOORBIT, Flag.NDESCBIT}, + action=_screening_door_action, + )) + screening_door.move_to(world.local_globals) + + hatch = world.register(GameObject( + "HATCH", desc="hatch", + synonyms=["hatch", "hatchway"], + adjectives=["jammed"], + flags={Flag.DOORBIT, Flag.NDESCBIT}, + action=_hatch_action, + )) + hatch.move_to(world.local_globals) + + # ---- ENTRY BAY ---- + + entry_bay = world.register(Room( + "ENTRY-BAY", desc="Entry Bay Number Two", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_entry_bay_action, + )) + + sales_brochure = world.register(GameObject( + "SALES-BROCHURE", desc="Heart of Gold sales brochure", + synonyms=["brochure", "pamphlet", "leaflet"], + adjectives=["sales", "heart", "gold"], + flags={Flag.TAKEBIT, Flag.READBIT}, + size=2, + action=_sales_brochure_action, + )) + sales_brochure.move_to(entry_bay) + + # ---- FORE CORRIDOR ---- + + fore_corridor = world.register(Room( + "FORE-CORRIDOR", desc="Corridor, Fore End", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_fore_corridor_action, + )) + + # ---- AFT CORRIDOR ---- + + aft_corridor = world.register(Room( + "AFT-CORRIDOR", desc="Corridor, Aft End", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_aft_corridor_action, + global_objects=[screening_door], + )) + + # ---- BRIDGE ---- + + bridge = world.register(Room( + "BRIDGE", desc="Bridge", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_bridge_action, + )) + + eddie = world.register(GameObject( + "EDDIE", desc="Eddie", + synonyms=["eddie", "computer"], + adjectives=["shipboard"], + flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT}, + action=_eddie_action, + )) + eddie.move_to(bridge) + + zaphod = world.register(GameObject( + "ZAPHOD", desc="Zaphod Beeblebrox", + synonyms=["zaphod", "beeblebrox", "president"], + adjectives=["zaphod"], + flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT}, + action=_zaphod_action, + )) + zaphod.move_to(bridge) + + trillian = world.register(GameObject( + "TRILLIAN", desc="Trillian", + synonyms=["trillian", "woman"], + adjectives=["dark-haired"], + flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT}, + action=_trillian_action, + )) + trillian.move_to(bridge) + + handbag = world.register(GameObject( + "HANDBAG", desc="handbag", + synonyms=["handbag", "bag", "purse"], + adjectives=["trillian's"], + flags={Flag.CONTBIT, Flag.SEARCHBIT, Flag.OPENBIT, Flag.NDESCBIT}, + capacity=10, + action=_handbag_action, + )) + handbag.move_to(trillian) + + tweezers = world.register(GameObject( + "TWEEZERS", desc="tweezers", + synonyms=["tweezers"], + adjectives=["small"], + flags={Flag.TAKEBIT, Flag.TOOLBIT}, + size=1, + action=_tweezers_action, + )) + tweezers.move_to(handbag) + + control_console = world.register(GameObject( + "CONTROL-CONSOLE", desc="control console", + synonyms=["console", "controls"], + adjectives=["main", "control"], + flags={Flag.NDESCBIT}, + action=_control_console_action, + )) + control_console.move_to(bridge) + + # ---- GALLEY ---- + + galley = world.register(Room( + "GALLEY", desc="Galley", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_galley_action, + )) + + nutrimat = world.register(GameObject( + "NUTRIMAT", desc="Nutrimatic Drinks Synthesizer", + synonyms=["nutrimat", "nutrimatic", "synthesizer", "machine"], + adjectives=["nutrimatic", "drinks"], + flags={Flag.NARTICLEBIT, Flag.NDESCBIT}, + action=_nutrimat_action, + )) + nutrimat.move_to(galley) + + pad = world.register(GameObject( + "PAD", desc="button pad", + synonyms=["pad", "button"], + adjectives=["button"], + flags={Flag.NDESCBIT}, + action=_pad_action, + )) + pad.move_to(galley) + + slot = world.register(GameObject( + "SLOT", desc="slot", + synonyms=["slot"], + adjectives=["drink", "dispensing"], + flags={Flag.NDESCBIT}, + action=_slot_action, + )) + slot.move_to(galley) + + substitute = world.register(GameObject( + "SUBSTITUTE", desc="Advanced Tea Substitute", + synonyms=["substitute", "liquid", "cup"], + adjectives=["advanced", "tea"], + flags={Flag.TAKEBIT, Flag.DRINKBIT, Flag.NARTICLEBIT, Flag.INVISIBLE}, + size=3, + action=_substitute_action, + )) + + tea = world.register(GameObject( + "TEA", desc="cup of tea", + synonyms=["tea", "cup"], + adjectives=["real", "proper"], + flags={Flag.TAKEBIT, Flag.DRINKBIT, Flag.INVISIBLE}, + size=3, + action=_tea_action, + )) + + no_tea = world.register(GameObject( + "NO-TEA", desc="no tea", + synonyms=["no-tea", "absence"], + adjectives=["no"], + flags={Flag.TAKEBIT, Flag.NARTICLEBIT}, + size=0, + action=_no_tea_action, + )) + no_tea.move_to(state.protagonist) + + # Register inventory extra for no-tea display + state.inventory_extras.append(_no_tea_inventory) + + # ---- ENGINE ROOM ---- + + engine_room = world.register(Room( + "ENGINE-ROOM", desc="Engine Room", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_engine_room_action, + )) + + spare_drive = world.register(GameObject( + "SPARE-DRIVE", desc="portable Infinite Improbability Generator", + synonyms=["drive", "generator"], + adjectives=["spare", "portable", "improbability", "infinite"], + flags={Flag.TAKEBIT, Flag.INVISIBLE}, + size=8, + action=_spare_drive_action, + )) + spare_drive.move_to(engine_room) + + large_plug = world.register(GameObject( + "LARGE-PLUG", desc="large plug", + synonyms=["plug"], + adjectives=["large"], + flags={Flag.NDESCBIT, Flag.INTEGRALBIT}, + action=_large_plug_action, + )) + large_plug.move_to(spare_drive) + + small_plug = world.register(GameObject( + "SMALL-PLUG", desc="small plug", + synonyms=["plug"], + adjectives=["small"], + flags={Flag.NDESCBIT, Flag.INTEGRALBIT}, + action=_small_plug_action, + )) + small_plug.move_to(spare_drive) + + drive_switch = world.register(GameObject( + "DRIVE-SWITCH", desc="drive switch", + synonyms=["switch"], + adjectives=["drive"], + flags={Flag.NDESCBIT, Flag.INTEGRALBIT}, + action=_drive_switch_action, + )) + drive_switch.move_to(spare_drive) + + wrench = world.register(GameObject( + "WRENCH", desc="wrench", + synonyms=["wrench", "spanner"], + adjectives=["adjustable", "heavy"], + flags={Flag.TAKEBIT, Flag.TOOLBIT, Flag.INVISIBLE}, + size=4, + action=_wrench_action, + )) + wrench.move_to(engine_room) + + pliers = world.register(GameObject( + "PLIERS", desc="pliers", + synonyms=["pliers"], + adjectives=["sturdy"], + flags={Flag.TAKEBIT, Flag.TOOLBIT, Flag.INVISIBLE}, + size=3, + action=_pliers_action, + )) + pliers.move_to(engine_room) + + rasp = world.register(GameObject( + "RASP", desc="rasp", + synonyms=["rasp", "file"], + adjectives=["coarse", "metal"], + flags={Flag.TAKEBIT, Flag.TOOLBIT, Flag.INVISIBLE}, + size=3, + action=_rasp_action, + )) + rasp.move_to(engine_room) + + # ---- PANTRY ---- + + pantry = world.register(Room( + "PANTRY", desc="Pantry", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_pantry_action, + )) + + marvin = world.register(GameObject( + "MARVIN", desc="Marvin", + synonyms=["marvin", "android", "robot"], + adjectives=["paranoid"], + flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT}, + action=_marvin_action, + )) + marvin.move_to(pantry) + + # ---- HATCHWAY ---- + + hatchway = world.register(Room( + "HATCHWAY", desc="Hatchway", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_hatchway_action, + global_objects=[hatch], + )) + + # ---- ACCESS SPACE ---- + + access_space = world.register(Room( + "ACCESS-SPACE", desc="Access Space", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_access_space_action, + )) + + # ---- RAMP ---- + + ramp = world.register(Room( + "RAMP", desc="Ramp", + flags={Flag.RLANDBIT, Flag.ONBIT, Flag.OUTSIDEBIT}, + action=_ramp_action, + )) + + # Plotter (Heart of Gold version -- may also exist on Vogon ship) + plotter = world.register(GameObject( + "PLOTTER-HOG", desc="atomic vector plotter", + synonyms=["plotter", "device"], + adjectives=["atomic", "vector"], + flags={Flag.TAKEBIT}, + size=4, + action=_plotter_heart_action, + )) + + small_receptacle = world.register(GameObject( + "SMALL-RECEPTACLE", desc="small receptacle", + synonyms=["receptacle"], + adjectives=["small"], + flags={Flag.NDESCBIT, Flag.INTEGRALBIT}, + action=_small_receptacle_action, + )) + small_receptacle.move_to(plotter) + + dangly_bit = world.register(GameObject( + "DANGLY-BIT", desc="dangly bit", + synonyms=["bit", "dangly"], + adjectives=["dangly", "small"], + flags={Flag.NDESCBIT, Flag.INTEGRALBIT}, + action=_dangly_bit_action, + )) + dangly_bit.move_to(plotter) + + # ---- Connect rooms ---- + + entry_bay.exits = { + Direction.SOUTH: DirectExit(fore_corridor), + Direction.OUT: DirectExit(fore_corridor), + } + + fore_corridor.exits = { + Direction.NORTH: DirectExit(entry_bay), + Direction.UP: DirectExit(bridge), + Direction.WEST: DirectExit(galley), + Direction.SOUTH: DirectExit(aft_corridor), + } + + aft_corridor.exits = { + Direction.NORTH: DirectExit(fore_corridor), + Direction.SOUTH: ConditionalExit(_aft_south_exit), + Direction.WEST: ConditionalExit(_aft_west_exit), + Direction.DOWN: DirectExit(hatchway), + } + + bridge.exits = { + Direction.DOWN: DirectExit(fore_corridor), + } + + galley.exits = { + Direction.EAST: DirectExit(fore_corridor), + Direction.OUT: DirectExit(fore_corridor), + } + + engine_room.exits = { + Direction.NORTH: DirectExit(aft_corridor), + Direction.OUT: DirectExit(aft_corridor), + } + + pantry.exits = { + Direction.EAST: DirectExit(aft_corridor), + Direction.OUT: DirectExit(aft_corridor), + } + + hatchway.exits = { + Direction.UP: DirectExit(aft_corridor), + Direction.DOWN: ConditionalExit(_hatchway_down_exit), + Direction.EAST: DirectExit(access_space), + } + + access_space.exits = { + Direction.WEST: DirectExit(hatchway), + Direction.OUT: DirectExit(hatchway), + } + + ramp.exits = { + Direction.UP: DirectExit(hatchway), + Direction.IN: DirectExit(hatchway), + } diff --git a/h2g2/content/unearth.py b/h2g2/content/unearth.py new file mode 100644 index 0000000..92d1e52 --- /dev/null +++ b/h2g2/content/unearth.py @@ -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), + } diff --git a/h2g2/content/vogon.py b/h2g2/content/vogon.py new file mode 100644 index 0000000..3fd7f79 --- /dev/null +++ b/h2g2/content/vogon.py @@ -0,0 +1,1153 @@ +"""Vogon ship locations -- Hold, Captain's Quarters, Airlock.""" + +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 + + +# ---- Gibberish generator ---- + +_VOGON_SYLLABLES = [ + "blurp", "grun", "flib", "morgh", "splut", "vrunt", "glub", "snarg", + "plod", "quork", "bleg", "frot", "nargh", "sklurb", "thrip", "zunt", + "drob", "munt", "flob", "grak", "prunt", "vlib", "skrog", "blort", +] + +_VOGON_SUFFIXES = [ + "ulous", "ting", "ment", "ish", "wards", "oid", "esque", "ble", + "ness", "ling", "osity", "ated", "ingly", "ation", +] + + +def _gibberish_word() -> str: + """Generate a random Vogon-sounding gibberish word.""" + base = random.choice(_VOGON_SYLLABLES) + if random.random() > 0.5: + base += random.choice(_VOGON_SYLLABLES)[:3] + base += random.choice(_VOGON_SUFFIXES) + return base + + +def _gibberish_line() -> str: + """Generate a full line of Vogon gibberish.""" + words = [_gibberish_word() for _ in range(random.randint(4, 8))] + line = " ".join(words) + return f'"{line.capitalize()}!"' + + +# ---- Poetry text ---- + +POETRY_LINES = [ + '"Oh freddled gruntbuggly, thy nacturations are to me!"', + '"As plurdled gabbleblotchits on a lurgid bee."', + '"Groop I implore thee, my foonting turlingdromes."', + '"And hooptiously drangle me with crinkly bindlewurdles,"', + '"or I will rend thee in the gobberwarts with my blurglecruncheon, see if I don\'t!"', +] + +POETRY_VERSE_2 = [ + '"Fripping lyshus wimbgunts, awhilst moongrovenly kormzibs."', + '"Gashee morphousite, thou expungiest quoopisk!"', + '"Bleem miserable venchit! Bleem forever mestinglish asunder frapt."', +] + + +# ---- Object action handlers ---- + +def _inner_door_action(state: GameState) -> bool: + out = state.output + door = state.world.get("INNER-DOOR") + + if state.prsa == "open": + if door.fset_q(Flag.OPENBIT): + out.tell("The inner airlock door is already open.\n") + else: + door.fset(Flag.OPENBIT) + out.tell("You open the inner airlock door.\n") + return True + + if state.prsa == "close": + if not door.fset_q(Flag.OPENBIT): + out.tell("It's already closed.\n") + else: + door.fclear(Flag.OPENBIT) + out.tell("You close the inner airlock door.\n") + return True + + if state.prsa == "examine": + status = "open" if door.fset_q(Flag.OPENBIT) else "closed" + out.tell(f"A heavy inner airlock door. It is {status}.\n") + return True + + return False + + +def _airlock_obj_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "The airlock is a standard Vogon design: built to eject " + "unwanted hitchhikers into the vacuum of space.\n" + ) + return True + return False + + +def _dispenser_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "The dispenser has a button, and a small sign that reads " + '"Babel Fish." A small hole at the bottom dispenses the fish.\n' + ) + return True + return False + + +def _dispenser_button_action(state: GameState) -> bool: + out = state.output + + if state.prsa not in ("push", "press"): + return False + + fish_counter = state.flags.get("fish_counter", 5) + + if state.flags.get("babel_fish_in_ear"): + out.tell("You already have a babel fish in your ear.\n") + return True + + if fish_counter <= 0: + out.tell( + "The dispenser makes a grinding noise, but nothing comes " + "out. It appears to be empty.\n" + ) + return True + + state.flags["fish_counter"] = fish_counter - 1 + + # Check puzzle conditions + gown_hung = state.flags.get("gown_hung", False) + towel_on_drain = state.flags.get("towel_on_drain", False) + panel_blocker = state.flags.get("panel_blocker") + item_on_satchel = state.flags.get("item_on_satchel") + + if not gown_hung: + out.tell( + "A small babel fish shoots out of the dispenser, arcs through " + "the air, and vanishes down a drain in the floor.\n" + ) + return True + + if not towel_on_drain: + out.tell( + "A small babel fish shoots out of the dispenser, bounces off " + "the dressing gown hanging on the hook, and vanishes down a " + "drain in the floor.\n" + ) + return True + + if panel_blocker is None: + out.tell( + "A small babel fish shoots out of the dispenser, bounces off " + "the dressing gown, slides across the towel covering the " + "drain, and is scooped up by a small cleaning robot that " + "emerges from a panel in the wall. The robot disappears back " + "into the wall.\n" + ) + return True + + if item_on_satchel is None: + out.tell( + "A small babel fish shoots out of the dispenser, bounces off " + "the dressing gown, slides across the towel, hits the satchel " + "blocking the robot panel, and flies upward -- straight into " + "the upper reaches of the room and out of sight.\n" + ) + return True + + # Success! + out.tell( + "A small babel fish shoots out of the dispenser, bounces off " + "the dressing gown, slides across the towel, hits the satchel " + "blocking the robot panel, flies upward, bounces off the junk " + "mail balanced on the satchel, and lands neatly in your ear.\n\n" + "You can suddenly understand everything around you. The babel " + "fish is now happily nestled in your ear.\n" + ) + state.flags["babel_fish_in_ear"] = True + state.score += 12 + return True + + +def _hook_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + if state.flags.get("gown_hung"): + out.tell("Your gown is hanging on the hook.\n") + else: + out.tell("A small hook on the wall opposite the dispenser.\n") + return True + + if state.prsa in ("hang", "put"): + gown = state.world.objects.get("GOWN") + if state.prso is gown: + if not gown.is_held_by(state.protagonist): + out.tell("You're not carrying the gown.\n") + return True + state.flags["gown_hung"] = True + gown.move_to(state.world.local_globals) + out.tell("You hang the gown on the hook.\n") + return True + + return False + + +def _drain_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + if state.flags.get("towel_on_drain"): + out.tell("The drain is covered by a towel.\n") + else: + out.tell("A small drain grate in the floor.\n") + return True + + if state.prsa in ("cover", "put", "block"): + towel = state.world.objects.get("TOWEL") + if state.prso is towel: + if not towel.is_held_by(state.protagonist): + out.tell("You're not carrying the towel.\n") + return True + state.flags["towel_on_drain"] = True + towel.move_to(state.world.local_globals) + out.tell("You cover the drain with the towel.\n") + return True + + return False + + +def _robot_panel_action(state: GameState) -> bool: + out = state.output + + if state.prsa == "examine": + blocker = state.flags.get("panel_blocker") + if blocker: + out.tell(f"The robot panel is blocked by {blocker.the_desc()}.\n") + else: + out.tell("A small panel in the wall from which a cleaning robot emerges.\n") + return True + + if state.prsa in ("put", "block", "cover"): + satchel = state.world.objects.get("SATCHEL") + if state.prso is satchel: + if not satchel.is_held_by(state.protagonist): + out.tell("You're not carrying the satchel.\n") + return True + state.flags["panel_blocker"] = satchel + satchel.move_to(state.world.local_globals) + out.tell("You put the satchel in front of the robot panel.\n") + return True + + return False + + +def _babel_fish_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + if state.flags.get("babel_fish_in_ear"): + out.tell( + "The babel fish is snugly lodged in your ear, translating " + "all alien languages into English for you.\n" + ) + else: + out.tell("A small, yellow, leech-like fish.\n") + return True + if state.prsa == "take": + if state.flags.get("babel_fish_in_ear"): + out.tell("It's far too comfortable in your ear to remove.\n") + return True + return False + + +def _vogon_captain_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "The Vogon Captain is a large, slug-like creature with a " + "foul temper and a passion for bureaucratic paperwork. And " + "poetry. Especially poetry.\n" + ) + return True + if state.prsa in ("tell", "ask", "talk"): + out.tell( + 'The Captain grunts. "Resistance is useless!" he bellows.\n' + ) + return True + return False + + +def _poetry_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + if state.flags.get("babel_fish_in_ear"): + out.tell( + "It's Vogon poetry. The third worst in the known galaxy.\n" + ) + else: + out.tell( + "It sounds like someone gargling with a throat full of " + "live weasels.\n" + ) + return True + + if state.prsa in ("enjoy", "appreciate", "like", "listen"): + if not state.flags.get("babel_fish_in_ear"): + out.tell( + "You can't even understand it. How could you enjoy it?\n" + ) + return True + if state.flags.get("poem_enjoyed"): + out.tell("You've already expressed your enjoyment.\n") + return True + state.flags["poem_enjoyed"] = True + state.score += 15 + out.tell( + 'You nod your head approvingly. "Oh, very good," you say.\n' + "The Captain beams with delight and continues reading.\n" + ) + return True + + return False + + +def _chair_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "The Poetry Appreciation Chair is a fiendish Vogon device " + "that clamps you in place and forces you to listen. Steel " + "bands hold your arms, legs, and head firmly in position.\n" + ) + return True + if state.prsa in ("get out", "leave", "escape", "stand"): + out.tell( + "The steel bands hold you firmly in the chair. You're not " + "going anywhere until the Captain decides otherwise.\n" + ) + return True + return False + + +def _glass_case_action(state: GameState) -> bool: + out = state.output + case = state.world.get("GLASS-CASE") + + if state.prsa == "examine": + if case.fset_q(Flag.OPENBIT): + out.tell("The glass case is open.\n") + else: + out.tell( + "A sealed glass case on the wall. Inside you can see a " + "device of some sort. There is a keyboard and a small " + "switch on the front.\n" + ) + return True + + if state.prsa == "open": + if case.fset_q(Flag.OPENBIT): + out.tell("It's already open.\n") + else: + out.tell("It won't open by force. Try the keyboard.\n") + return True + + return False + + +def _keyboard_action(state: GameState) -> bool: + out = state.output + + if state.prsa in ("type", "use", "push"): + case = state.world.get("GLASS-CASE") + if case.fset_q(Flag.OPENBIT): + out.tell("The case is already open.\n") + return True + # Accept any typing action -- the correct answer is typing a + # word from the poem while you understand it + if state.flags.get("babel_fish_in_ear") and state.flags.get("poem_enjoyed"): + case.fset(Flag.OPENBIT) + state.score += 25 + out.tell( + "You type a word from the poem on the keyboard. The case " + "makes a satisfying click and swings open, revealing an " + "atomic vector plotter.\n" + ) + return True + out.tell( + "You type something on the keyboard. Nothing happens. " + "Perhaps you need to type the right thing.\n" + ) + return True + + if state.prsa == "examine": + out.tell("A small keyboard attached to the glass case.\n") + return True + + return False + + +def _switch_action(state: GameState) -> bool: + out = state.output + if state.prsa in ("push", "press", "flip", "turn on", "turn off"): + out.tell("Click. Nothing obvious happens.\n") + return True + if state.prsa == "examine": + out.tell("A small switch on the front of the glass case.\n") + return True + return False + + +def _plotter_action(state: GameState) -> bool: + out = state.output + case = state.world.get("GLASS-CASE") + + if state.prsa == "take": + if not case.fset_q(Flag.OPENBIT): + out.tell("The plotter is sealed inside the glass case.\n") + return True + plotter = state.world.get("PLOTTER") + plotter.move_to(state.protagonist) + out.tell("You take the atomic vector plotter.\n") + return True + + if state.prsa == "examine": + out.tell( + "It's an atomic vector plotter -- a small, sleek device " + "of obviously advanced technology.\n" + ) + return True + + return False + + +def _satchel_action(state: GameState) -> bool: + out = state.output + satchel = state.world.get("SATCHEL") + + if state.prsa == "examine": + out.tell("It's Ford's battered leather satchel.\n") + return True + + if state.prsa in ("open", "look in"): + visible = satchel.contents_string() + if visible: + descs = [obj.a_desc() for obj in visible] + out.tell("The satchel contains " + ", ".join(descs) + ".\n") + else: + out.tell("The satchel is empty.\n") + return True + + return False + + +def _towel_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + out.tell( + "It's a large, fluffy towel. About the most massively " + "useful thing an interstellar hitchhiker can have.\n" + ) + return True + return False + + +def _guide_action(state: GameState) -> bool: + out = state.output + if state.prsa in ("read", "examine", "consult"): + out.tell( + "The cover of the Guide reads, in large friendly letters: " + '"DON\'T PANIC"\n\n' + "The Guide has a lot to say on many subjects. It is the " + "standard repository of all knowledge and wisdom.\n" + ) + return True + return False + + +def _peanuts_action(state: GameState) -> bool: + out = state.output + if state.prsa in ("eat", "open"): + out.tell( + "You eat the peanuts. They were rather good, as peanuts go.\n" + ) + state.world.get("PEANUTS").move_to(state.world.local_globals) + return True + if state.prsa == "examine": + out.tell("A small packet of salted peanuts.\n") + return True + return False + + +def _ford_vogon_action(state: GameState) -> bool: + out = state.output + if state.prsa == "examine": + if state.flags.get("ford_sleeping"): + out.tell("Ford is slumped against the wall, fast asleep.\n") + else: + out.tell( + "Ford looks somewhat the worse for wear, but his eyes " + "are sharp and alert.\n" + ) + return True + if state.prsa in ("tell", "ask", "talk"): + if state.flags.get("ford_sleeping"): + out.tell("Ford is asleep and doesn't respond.\n") + else: + out.tell('"Don\'t panic," says Ford.\n') + return True + if state.prsa == "wake": + if state.flags.get("ford_sleeping"): + out.tell("Ford grunts and rolls over but doesn't wake.\n") + else: + out.tell("Ford is already awake.\n") + return True + return False + + +# ---- Timed event handlers ---- + +def _i_groggy(state: GameState) -> bool: + """Groggy countdown -- player must act before passing out.""" + out = state.output + counter = state.flags.get("groggy_counter", 0) + + if not state.flags.get("groggy"): + return False + + if state.here and state.here.id != "HOLD": + return False + + counter += 1 + state.flags["groggy_counter"] = counter + + if counter == 1: + out.tell( + "\nYour head is spinning. You feel very groggy and confused.\n" + ) + return True + if counter == 2: + out.tell( + "\nThe room lurches sickeningly. You really don't feel well.\n" + ) + return True + if counter >= 3: + state.jigs_up( + "The room spins wildly and you pass out on the filthy floor. " + "You are woken some time later by a Vogon guard who drags " + "you off and throws you out of an airlock." + ) + return True + + return False + + +def _i_ford(state: GameState) -> bool: + """Ford gives the player the Guide and goes to sleep.""" + out = state.output + + if state.here and state.here.id != "HOLD": + return False + + ford = state.world.get("FORD-VOGON") + guide = state.world.get("GUIDE") + satchel = state.world.get("SATCHEL") + towel = state.world.get("TOWEL") + + out.tell( + '\nFord rummages through his satchel. "Here," he says, handing ' + 'you a battered book. "The Hitchhiker\'s Guide to the Galaxy. ' + "It's the most remarkable book ever to come out of the great " + 'publishing corporations of Ursa Minor."\n\n' + "He also hands you a towel.\n\n" + '"A towel," says Ford, "is about the most massively useful thing ' + 'an interstellar hitchhiker can have."\n\n' + "With that, Ford slumps against the wall and falls asleep.\n" + ) + guide.move_to(state.protagonist) + towel.move_to(state.protagonist) + satchel.move_to(state.here) + state.flags["ford_sleeping"] = True + return True + + +def _i_announcement(state: GameState) -> bool: + """Intercom announcement.""" + out = state.output + + if state.here and state.here.id != "HOLD": + return False + + out.tell( + "\nThere is a crackle from the intercom. A Vogon voice " + 'announces: "Attention. This is your Captain speaking. We will ' + "shortly be arriving at our destination. Passengers are reminded " + 'that the airlock is NOT an emergency exit."\n' + ) + return True + + +def _i_guards(state: GameState) -> bool: + """Guards arrive to take you to the Captain.""" + out = state.output + + if state.here and state.here.id != "HOLD": + return False + + out.tell( + "\nThe door bursts open. Two Vogon guards stomp in, grab you " + "and Ford by your collars, and drag you down the corridor to " + "the Captain's quarters.\n" + ) + # Move player and Ford to the Captain's quarters + captains_quarters = state.world.get_room("CAPTAINS-QUARTERS") + state.here = captains_quarters + state.protagonist.move_to(captains_quarters) + ford = state.world.get("FORD-VOGON") + ford.move_to(captains_quarters) + state.flags["ford_sleeping"] = False + + # Start the captain sequence + state.flags["captain_counter"] = 0 + state.clock.queue(_i_captain, -1, name="I-CAPTAIN") + + return True + + +def _i_captain(state: GameState) -> bool: + """Poetry reading sequence in the Captain's quarters.""" + out = state.output + + if state.here and state.here.id != "CAPTAINS-QUARTERS": + return False + + counter = state.flags.get("captain_counter", 0) + state.flags["captain_counter"] = counter + 1 + has_fish = state.flags.get("babel_fish_in_ear", False) + + if counter == 0: + out.tell( + '\nThe Vogon Captain clears his throat. "I\'m now going to ' + "read you some of my poetry,\" he announces.\n" + ) + return True + + # Read poetry lines (counter 1-5) + if 1 <= counter <= 5: + line_idx = counter - 1 + if has_fish: + line = POETRY_LINES[line_idx] + else: + line = _gibberish_line() + out.tell(f"\nThe Captain reads: {line}\n") + if counter == 5: + if has_fish: + out.tell( + "\nThe Captain pauses and looks at you expectantly.\n" + ) + else: + out.tell( + "\nThe Captain finishes. It sounded like someone " + "gargling with quicksand.\n" + ) + return True + + # After the first verse + if counter == 6: + enjoyed = state.flags.get("poem_enjoyed", False) + if enjoyed and has_fish: + out.tell( + '\nThe Captain is delighted. "You liked it! Oh how ' + "wonderful! Let me read you another!\"\n" + ) + state.flags["captain_counter"] = 100 # jump to second verse + return True + else: + _eject_to_airlock(state) + return True + + # Second verse (counters 101-103) + if 101 <= counter <= 103: + line_idx = counter - 101 + if line_idx < len(POETRY_VERSE_2): + line = POETRY_VERSE_2[line_idx] + out.tell(f"\nThe Captain reads: {line}\n") + if line_idx == len(POETRY_VERSE_2) - 1: + # After second verse, eject + out.tell( + '\n"Now," says the Captain, "did you enjoy that?"\n' + "Before you can answer, the guards grab you.\n" + ) + return True + + if counter == 104: + _eject_to_airlock(state) + return True + + return False + + +def _eject_to_airlock(state: GameState) -> None: + """Guards drag you from the Captain's quarters to the hold, then airlock.""" + out = state.output + + out.tell( + "\nThe Vogon guards grab you and Ford, drag you struggling " + "down the corridor, through the hold, and hurl you into the " + "airlock.\n" + ) + + # Disable the captain timer + state.clock.queue(_i_captain, 0, name="I-CAPTAIN") + for entry in state.clock.entries: + if entry.routine is _i_captain: + entry.enabled = False + + # Move to airlock + airlock = state.world.get_room("AIRLOCK") + state.here = airlock + state.protagonist.move_to(airlock) + ford = state.world.get("FORD-VOGON") + ford.move_to(airlock) + + state.flags["airlock_counter"] = 0 + state.clock.queue(_i_airlock, -1, name="I-AIRLOCK") + + +def _i_airlock(state: GameState) -> bool: + """Airlock countdown -- Ford's dialogue then ejection into space.""" + out = state.output + + if state.here and state.here.id != "AIRLOCK": + return False + + counter = state.flags.get("airlock_counter", 0) + counter += 1 + state.flags["airlock_counter"] = counter + + if counter == 1: + out.tell( + '\nFord says: "I wonder if we\'ll be picked up by another ' + "ship. The chances are pretty remote, of course, but it's " + 'our only hope."\n' + ) + return True + + if counter == 2: + out.tell( + "\nFord is rummaging in his pockets for something. " + '"I had a Sub-Etha Sens-O-Matic somewhere..." he mutters.\n' + ) + return True + + if counter == 3: + out.tell( + '\nFord says: "If we hold our breath, we might survive for ' + "about thirty seconds in the vacuum of space. It's not " + 'long, but it might just be enough."\n' + ) + return True + + if counter >= 4: + out.tell( + "\nWith a great hiss, the outer airlock door opens. The " + "air rushes out. You and Ford are sucked into the cold " + "vacuum of space.\n\n" + "You gasp. Stars wheel around you. The cold is incredible.\n" + ) + + # Disable airlock timer + for entry in state.clock.entries: + if entry.routine is _i_airlock: + entry.enabled = False + + # Set heart probability for Heart of Gold sequence + state.flags["heart_prob"] = 100 + + # Transition to DARK room + dark = state.world.rooms.get("DARK") + if dark: + state.here = dark + state.protagonist.move_to(dark) + else: + # If DARK room doesn't exist yet, end the sequence + out.tell( + "\nYou drift in the void of space...\n" + "Everything goes dark.\n" + ) + return True + + return False + + +# ---- Room action handlers ---- + +def _hold_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is a squalid room filled with grubby mattresses, " + "unwashed cups, and unidentifiable bits of smelly alien " + "underwear. A door lies to port, and an airlock lies to " + "starboard.\n" + ) + return True + + if rarg == "M-END": + hold = state.world.get_room("HOLD") + + if hold.fset_q(Flag.REVISITBIT): + # Revisit -- guards stun you + state.jigs_up( + "A Vogon guard spots you wandering around and stuns you " + "with a blast from his kill-o-zap gun." + ) + return True + + if not hold.fset_q(Flag.TOUCHBIT): + # First visit + hold.fset(Flag.TOUCHBIT) + hold.fset(Flag.REVISITBIT) + + # Give player peanuts + peanuts = state.world.get("PEANUTS") + peanuts.move_to(state.protagonist) + + # Move Ford here + ford = state.world.get("FORD-VOGON") + ford.move_to(hold) + + # Set groggy state + state.flags["groggy"] = True + state.flags["groggy_counter"] = 0 + + # Add score + state.score += 8 + + out.tell( + "\nYou wake up. The room is spinning very gently round " + "your head. Or at least it would be if you could see it, " + "which you can't.\n\n" + "You are lying on what feels like a pile of old mattresses " + "in a room that smells like the inside of a pair of " + "Vogon-ripper trainers.\n\n" + 'Ford Prefect is here. "It\'s OK," he says, "we\'re safe ' + 'now. We\'re on a Vogon ship."\n\n' + "He hands you a small packet of peanuts.\n" + ) + + # Queue timed events + state.clock.queue(_i_groggy, 3, name="I-GROGGY") + state.clock.queue(_i_ford, 6, name="I-FORD") + state.clock.queue(_i_announcement, 18, name="I-ANNOUNCEMENT") + state.clock.queue(_i_guards, 36, name="I-GUARDS") + + return True + + return False + + +def _hold_east_exit(state: GameState) -> "Room | None": + """Exit from hold east to airlock -- requires inner door open.""" + out = state.output + door = state.world.get("INNER-DOOR") + if not door.fset_q(Flag.OPENBIT): + out.tell("The inner airlock door is closed.\n") + return None + return state.world.get_room("AIRLOCK") + + +def _captains_quarters_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This is the cabin of the Vogon Captain. You and Ford are " + "strapped into poetry appreciation chairs.\n" + ) + return True + + return False + + +def _airlock_action(state: GameState, rarg: str) -> bool: + out = state.output + + if rarg == "M-LOOK": + out.tell( + "This airlock has massive doors to port and starboard.\n" + ) + return True + + return False + + +# ---- Registration ---- + +def register(world: World, state: GameState) -> None: + """Create all Vogon ship rooms and objects.""" + + # ---- Initialize state flags ---- + + state.flags.setdefault("groggy", False) + state.flags.setdefault("groggy_counter", 0) + state.flags.setdefault("gown_hung", False) + state.flags.setdefault("panel_blocker", None) + state.flags.setdefault("item_on_satchel", None) + state.flags.setdefault("fish_counter", 5) + state.flags.setdefault("babel_fish_in_ear", False) + state.flags.setdefault("captain_counter", 0) + state.flags.setdefault("poem_enjoyed", False) + state.flags.setdefault("airlock_counter", 0) + state.flags.setdefault("guards_counter", 0) + state.flags.setdefault("ford_sleeping", False) + + # ---- Global objects for the ship ---- + + inner_door = world.register(GameObject( + "INNER-DOOR", desc="inner door", + synonyms=["door", "inner"], + adjectives=["inner", "airlock"], + flags={Flag.DOORBIT, Flag.NDESCBIT}, + action=_inner_door_action, + )) + inner_door.move_to(world.local_globals) + + airlock_obj = world.register(GameObject( + "AIRLOCK-OBJ", desc="airlock", + synonyms=["airlock"], + flags={Flag.NDESCBIT}, + action=_airlock_obj_action, + )) + airlock_obj.move_to(world.local_globals) + + # ---- HOLD ---- + + hold = world.register(Room( + "HOLD", desc="Vogon Hold", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_hold_action, + global_objects=[inner_door, airlock_obj], + )) + + # Babel fish dispenser and puzzle objects + dispenser = world.register(GameObject( + "DISPENSER", desc="babel fish dispenser", + synonyms=["dispenser", "machine"], + adjectives=["babel", "fish"], + flags={Flag.NDESCBIT}, + action=_dispenser_action, + )) + dispenser.move_to(hold) + + dispenser_button = world.register(GameObject( + "DISPENSER-BUTTON", desc="dispenser button", + synonyms=["button"], + adjectives=["dispenser"], + flags={Flag.NDESCBIT}, + action=_dispenser_button_action, + )) + dispenser_button.move_to(hold) + + hook = world.register(GameObject( + "HOOK", desc="hook", + synonyms=["hook", "peg"], + adjectives=["small", "wall"], + flags={Flag.NDESCBIT}, + action=_hook_action, + )) + hook.move_to(hold) + + drain = world.register(GameObject( + "DRAIN", desc="drain", + synonyms=["drain", "grate"], + adjectives=["small", "floor"], + flags={Flag.NDESCBIT}, + action=_drain_action, + )) + drain.move_to(hold) + + robot_panel = world.register(GameObject( + "ROBOT-PANEL", desc="robot panel", + synonyms=["panel"], + adjectives=["robot", "small", "wall"], + flags={Flag.NDESCBIT}, + action=_robot_panel_action, + )) + robot_panel.move_to(hold) + + babel_fish = world.register(GameObject( + "BABEL-FISH", desc="babel fish", + synonyms=["fish"], + adjectives=["babel", "small", "yellow"], + flags={Flag.NDESCBIT, Flag.INVISIBLE}, + action=_babel_fish_action, + )) + babel_fish.move_to(world.local_globals) + + # Glass case with plotter + glass_case = world.register(GameObject( + "GLASS-CASE", desc="glass case", + synonyms=["case"], + adjectives=["glass", "sealed"], + flags={Flag.CONTBIT, Flag.NDESCBIT, Flag.SEARCHBIT}, + action=_glass_case_action, + )) + glass_case.move_to(hold) + + keyboard = world.register(GameObject( + "KEYBOARD", desc="keyboard", + synonyms=["keyboard", "keys"], + adjectives=["small"], + flags={Flag.NDESCBIT}, + action=_keyboard_action, + )) + keyboard.move_to(hold) + + switch = world.register(GameObject( + "SWITCH", desc="small switch", + synonyms=["switch"], + adjectives=["small"], + flags={Flag.NDESCBIT}, + action=_switch_action, + )) + switch.move_to(hold) + + plotter = world.register(GameObject( + "PLOTTER", desc="atomic vector plotter", + synonyms=["plotter", "device"], + adjectives=["atomic", "vector"], + flags={Flag.TAKEBIT, Flag.NDESCBIT}, + size=4, + action=_plotter_action, + )) + plotter.move_to(glass_case) + + # Ford (Vogon ship version -- separate from earth Ford) + ford = world.register(GameObject( + "FORD-VOGON", desc="Ford Prefect", + synonyms=["ford", "prefect"], + adjectives=["ford"], + flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT}, + action=_ford_vogon_action, + )) + + # Items Ford gives or that appear on the ship + peanuts = world.register(GameObject( + "PEANUTS", desc="small packet of peanuts", + synonyms=["peanuts", "packet", "nuts"], + adjectives=["small"], + flags={Flag.TAKEBIT}, + size=2, + action=_peanuts_action, + )) + + satchel = world.register(GameObject( + "SATCHEL", desc="satchel", + synonyms=["satchel", "bag"], + adjectives=["ford's", "leather", "battered"], + flags={Flag.TAKEBIT, Flag.CONTBIT, Flag.SEARCHBIT, Flag.OPENBIT}, + size=10, capacity=20, + action=_satchel_action, + )) + + towel = world.register(GameObject( + "TOWEL", desc="towel", + synonyms=["towel"], + adjectives=["large", "fluffy"], + flags={Flag.TAKEBIT}, + size=4, + action=_towel_action, + )) + + guide = world.register(GameObject( + "GUIDE", desc="Hitchhiker's Guide", + synonyms=["guide", "book"], + adjectives=["hitchhiker's", "electronic"], + flags={Flag.TAKEBIT, Flag.READBIT, Flag.NARTICLEBIT}, + size=3, + text='The cover reads: "DON\'T PANIC"', + action=_guide_action, + )) + + # ---- CAPTAIN'S QUARTERS ---- + + captains_quarters = world.register(Room( + "CAPTAINS-QUARTERS", desc="Captain's Quarters", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_captains_quarters_action, + )) + + vogon_captain = world.register(GameObject( + "VOGON-CAPTAIN", desc="Vogon Captain", + synonyms=["captain", "vogon"], + adjectives=["vogon"], + flags={Flag.NARTICLEBIT, Flag.ACTORBIT, Flag.NDESCBIT}, + action=_vogon_captain_action, + )) + vogon_captain.move_to(captains_quarters) + + poetry = world.register(GameObject( + "POETRY", desc="poetry", + synonyms=["poetry", "poem", "verse"], + adjectives=["vogon"], + flags={Flag.NDESCBIT, Flag.NARTICLEBIT}, + action=_poetry_action, + )) + poetry.move_to(captains_quarters) + + chair = world.register(GameObject( + "POETRY-APPRECIATION-CHAIR", desc="poetry appreciation chair", + synonyms=["chair", "chairs"], + adjectives=["poetry", "appreciation"], + flags={Flag.VEHBIT, Flag.NDESCBIT}, + action=_chair_action, + )) + chair.move_to(captains_quarters) + + # ---- AIRLOCK ---- + + airlock = world.register(Room( + "AIRLOCK", desc="Airlock", + flags={Flag.RLANDBIT, Flag.ONBIT}, + action=_airlock_action, + global_objects=[inner_door, airlock_obj], + )) + + # ---- Connect rooms ---- + + hold.exits = { + Direction.WEST: BlockedExit( + "The door to the corridor is locked (from the outside)." + ), + Direction.EAST: ConditionalExit(_hold_east_exit), + } + + # Captain's quarters has no exits -- you arrive via guards + captains_quarters.exits = {} + + # Airlock exits are blocked + airlock.exits = { + Direction.WEST: BlockedExit( + "The inner door has sealed behind you." + ), + Direction.EAST: BlockedExit( + "The outer door is massive and immovable. Not that you'd " + "want to open it." + ), + } diff --git a/h2g2/engine/parser.py b/h2g2/engine/parser.py index fc9a560..7331a4c 100644 --- a/h2g2/engine/parser.py +++ b/h2g2/engine/parser.py @@ -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) diff --git a/h2g2/engine/state.py b/h2g2/engine/state.py index 912db96..f96466a 100644 --- a/h2g2/engine/state.py +++ b/h2g2/engine/state.py @@ -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 diff --git a/h2g2/engine/verbs.py b/h2g2/engine/verbs.py index 4bce029..6e9babc 100644 --- a/h2g2/engine/verbs.py +++ b/h2g2/engine/verbs.py @@ -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 diff --git a/h2g2/main.py b/h2g2/main.py index c030349..97fad03 100644 --- a/h2g2/main.py +++ b/h2g2/main.py @@ -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: "