Wire game transitions end-to-end, add Guide lookup, add 79 tests
Transitions: - Add I-HOUSEWRECK (tick 20) and I-VOGONS (tick 50) timed events to earth.py, queued at startup in main.py - I-VOGONS demolishes Earth and moves player to Vogon Hold - Fix airlock→Dark transition to call Dark room M-ENTER handler - Fix dream-restore to support multiple callbacks (list instead of single) - Add state.finish() call to RAMP for endgame victory Guide system: - Add 16-entry lookup database to GUIDE object (space, towel, vogons, poetry, beast, babel fish, earth, magrathea, marvin, etc.) - "consult guide about X" now returns relevant entry text Tests (79 passing): - test_engine.py (14): containment, flags, articles, clock mechanics - test_parser.py (20): directions, compound verbs, prepositions, synonyms - test_earth.py (21): full opening sequence, puzzles, navigation - test_vogon.py (4): room existence, Hold first-visit sequence - test_dark.py (7): inventory clearing, dream dispatch, probabilities - conftest.py: shared game fixture and send() helper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -237,4 +237,4 @@ def register(world: World, state: GameState) -> None:
|
||||
))
|
||||
|
||||
# Register dream restore callback
|
||||
state._dream_restore_callback = _dream_restore
|
||||
state._dream_restore_callbacks.append(_dream_restore)
|
||||
|
||||
@@ -470,6 +470,57 @@ def pub_action(state: GameState, rarg: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# ---- Timed event handlers ----
|
||||
|
||||
def _i_housewreck(state):
|
||||
"""Bulldozer demolishes house if player is still in BEDROOM or FRONT-PORCH."""
|
||||
if state.here and state.here.id in ("BEDROOM", "FRONT-PORCH"):
|
||||
state.jigs_up(
|
||||
"Astoundingly, a bulldozer pokes through your wall. However, you have "
|
||||
"no time for surprise because the ceiling is collapsing on you as "
|
||||
"your home is unexpectedly demolished to make way for a new bypass."
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _i_vogons(state):
|
||||
"""Vogon fleet arrives, demolishes Earth, beams player to Vogon Hold."""
|
||||
out = state.output
|
||||
state.flags["earth_demolished"] = True
|
||||
|
||||
out.tell("\nWith a noise like a thousand grand pianos being dropped down a "
|
||||
"mineshaft, a huge yellow Vogon Constructor Fleet noisily hurtles "
|
||||
"through the upper atmosphere of the planet.\n\n")
|
||||
out.tell('"People of Earth, your attention please," says a voice. '
|
||||
'"This is Prostetnic Vogon Jeltz of the Galactic Hyperspace '
|
||||
'Planning Council. As you will no doubt be aware, the plans for '
|
||||
'development of the outlying regions of the Galaxy require the '
|
||||
'building of a hyperspatial express route through your star system, '
|
||||
'and regrettably your planet is one of those scheduled for '
|
||||
'demolition. The process will take slightly less than two of your '
|
||||
'Earth minutes. Thank you."\n\n')
|
||||
out.tell("There is a terrible ghastly silence. There is a terrible ghastly "
|
||||
"noise. There is a terrible ghastly silence.\n\n")
|
||||
out.tell("You wake up. You are aboard a Vogon ship.\n\n")
|
||||
|
||||
# Move player to Vogon Hold
|
||||
hold = state.world.rooms.get("HOLD")
|
||||
if hold:
|
||||
# Clear Ford from earth locations
|
||||
ford = state.world.objects.get("FORD")
|
||||
if ford:
|
||||
ford.move_to(state.world.local_globals)
|
||||
|
||||
state.protagonist.move_to(hold)
|
||||
state.here = hold
|
||||
state.lying_down = False
|
||||
|
||||
# Trigger Hold's M-ENTER handler
|
||||
if hold.action:
|
||||
hold.action(state, "M-ENTER")
|
||||
return True
|
||||
|
||||
|
||||
# ---- Registration ----
|
||||
|
||||
def register(world: World) -> None:
|
||||
|
||||
@@ -1069,6 +1069,7 @@ def _ramp_action(state: GameState, rarg: str) -> bool:
|
||||
"industry. This is a great achievement.\n\n"
|
||||
"Congratulations.\n"
|
||||
)
|
||||
state.finish()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
+62
-5
@@ -466,9 +466,49 @@ def _towel_action(state: GameState) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
GUIDE_ENTRIES = {
|
||||
"space": "If you hyperventilate and then empty your lungs, you will last about thirty seconds in the vacuum of space. However, because space is vastly hugely mind-bogglingly big, getting picked up by another ship within those thirty seconds is almost infinitely improbable.",
|
||||
"towel": "A towel is about the most massively useful thing an interstellar hitchhiker can have. You can wrap it around you for warmth, lie on it on the brilliant marble-sanded beaches of Santraginus V, use it to sail a miniraft down the slow heavy River Moth, or wet it for use in hand-to-hand combat.",
|
||||
"vogon": "Vogons are one of the most unpleasant races in the Galaxy. They wouldn't even lift a finger to save their own grandmothers from the Ravenous Bugblatter Beast of Traal without orders signed in triplicate.",
|
||||
"poetry": "Vogon poetry is of course the third worst in the Universe. The very worst poetry was written by Paula Nancy Millstone Jennings of Greenbridge, Essex.",
|
||||
"beast": "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.",
|
||||
"babel": "The Babel fish is small, yellow, leech-like, and probably the oddest thing in the Universe. If you stick one in your ear you can instantly understand anything said to you in any form of language.",
|
||||
"earth": "Mostly harmless.",
|
||||
"magrathea": "An ancient planet, now largely abandoned, where they used to manufacture luxury custom-built planets.",
|
||||
"marvin": "Marvin the Paranoid Android. He has a brain the size of a planet but they only ever ask him to open doors.",
|
||||
"zaphod": "Zaphod Beeblebrox. President of the Imperial Galactic Government. Adventurer, ex-hippie, good-timer, and the worst-dressed sentient being in the known Universe.",
|
||||
"trillian": "A bright young astrophysicist who Arthur once met at a party in Islington and who subsequently went off with Zaphod Beeblebrox.",
|
||||
"ford": "A roving researcher for the Guide who has been posing as an out-of-work actor on Earth for the last fifteen years.",
|
||||
"arthur": "A perfectly ordinary man who has found himself swept up in a very peculiar adventure.",
|
||||
"guide": "The Hitchhiker's Guide to the Galaxy is a wholly remarkable book. It has already supplanted the great Encyclopedia Galactica as the standard repository of all knowledge and wisdom.",
|
||||
"heart": "The Heart of Gold is the sleekest, most advanced, most gorgeous ship ever built. Stolen by Zaphod Beeblebrox.",
|
||||
"damogran": "A planet in the westernmost reaches of the Galaxy, famous for the Presidential Ceremony.",
|
||||
}
|
||||
|
||||
|
||||
def _guide_lookup(state: GameState) -> str | None:
|
||||
"""Look up a topic in the Guide. Returns entry text or None."""
|
||||
# Check indirect object (prsi) synonyms/id
|
||||
if state.prsi:
|
||||
candidates = [state.prsi.id.lower()]
|
||||
if hasattr(state.prsi, 'synonyms') and state.prsi.synonyms:
|
||||
candidates.extend(s.lower() for s in state.prsi.synonyms)
|
||||
for candidate in candidates:
|
||||
for key, entry in GUIDE_ENTRIES.items():
|
||||
if key in candidate or candidate in key:
|
||||
return entry
|
||||
# Check raw indirect noun from parser
|
||||
if hasattr(state, 'indirect_noun') and state.indirect_noun:
|
||||
raw = state.indirect_noun.lower()
|
||||
for key, entry in GUIDE_ENTRIES.items():
|
||||
if key in raw or raw in key:
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def _guide_action(state: GameState) -> bool:
|
||||
out = state.output
|
||||
if state.prsa in ("read", "examine", "consult"):
|
||||
if state.prsa in ("read", "examine"):
|
||||
out.tell(
|
||||
"The cover of the Guide reads, in large friendly letters: "
|
||||
'"DON\'T PANIC"\n\n'
|
||||
@@ -476,6 +516,21 @@ def _guide_action(state: GameState) -> bool:
|
||||
"standard repository of all knowledge and wisdom.\n"
|
||||
)
|
||||
return True
|
||||
if state.prsa == "consult":
|
||||
entry = _guide_lookup(state)
|
||||
if entry:
|
||||
out.tell(
|
||||
"The Guide flickers to life and displays:\n\n"
|
||||
f"{entry}\n"
|
||||
)
|
||||
else:
|
||||
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, but not "
|
||||
"apparently on that one.\n"
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -779,10 +834,12 @@ def _i_airlock(state: GameState) -> bool:
|
||||
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)
|
||||
dark_room = state.world.rooms.get("DARK")
|
||||
if dark_room:
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
else:
|
||||
# If DARK room doesn't exist yet, end the sequence
|
||||
out.tell(
|
||||
|
||||
@@ -56,7 +56,7 @@ class GameState:
|
||||
self.inventory_extras: list[Callable] = []
|
||||
|
||||
# Death system
|
||||
self._dream_restore_callback: Callable | None = None
|
||||
self._dream_restore_callbacks: list[Callable] = []
|
||||
|
||||
def update_lit(self) -> None:
|
||||
"""Recalculate whether the current location is lit."""
|
||||
@@ -83,8 +83,8 @@ class GameState:
|
||||
# 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)
|
||||
for cb in self._dream_restore_callbacks:
|
||||
cb(self)
|
||||
dark = self.world.rooms.get("DARK")
|
||||
if dark:
|
||||
self.here = dark
|
||||
|
||||
@@ -67,6 +67,11 @@ def main() -> None:
|
||||
lambda s: "no tea" if s.flags.get("holding_no_tea") else None
|
||||
)
|
||||
|
||||
# Queue startup timed events
|
||||
from h2g2.content.earth import _i_housewreck, _i_vogons
|
||||
clock.queue(_i_housewreck, 20, name="I-HOUSEWRECK")
|
||||
clock.queue(_i_vogons, 50, name="I-VOGONS")
|
||||
|
||||
# Banner
|
||||
output.tell(
|
||||
"\n *** THE HITCHHIKER'S GUIDE TO THE GALAXY: "
|
||||
|
||||
@@ -10,5 +10,8 @@ dependencies = []
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8.0"]
|
||||
|
||||
[project.scripts]
|
||||
h2g2 = "h2g2.main:main"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
from h2g2.engine.world import World
|
||||
from h2g2.engine.state import GameState
|
||||
from h2g2.engine.output import Output
|
||||
from h2g2.engine.clock import Clock
|
||||
from h2g2.engine.parser import Parser
|
||||
from h2g2.engine.loop import GameLoop
|
||||
from h2g2.content import globals_content, earth, vogon, heart, unearth, dark
|
||||
import h2g2.engine.verbs # noqa: F401 — register handlers
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def game():
|
||||
"""Create a fully initialized game world."""
|
||||
world = World()
|
||||
globals_content.register(world)
|
||||
earth.register(world)
|
||||
|
||||
output = Output()
|
||||
clock = Clock()
|
||||
state = GameState(world, output, clock)
|
||||
|
||||
state.protagonist = world.protagonist
|
||||
state.here = world.get_room("BEDROOM")
|
||||
state.winner = world.protagonist
|
||||
state.lying_down = True
|
||||
|
||||
# Initialize flags (same as main.py)
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
# Register content that needs state
|
||||
vogon.register(world, state)
|
||||
heart.register(world, state)
|
||||
unearth.register(world, state)
|
||||
dark.register(world, state)
|
||||
|
||||
parser = Parser()
|
||||
loop = GameLoop(state, parser)
|
||||
|
||||
return state, parser, loop
|
||||
|
||||
|
||||
def send(game_tuple, command: str) -> str:
|
||||
"""Send a command to the game and return the output text."""
|
||||
state, parser, loop = game_tuple
|
||||
result = parser.parse(command, state)
|
||||
if result:
|
||||
loop._execute(result)
|
||||
text = state.output.flush()
|
||||
return text
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Tests for the DARK dream dispatch room."""
|
||||
|
||||
from tests.conftest import send
|
||||
|
||||
|
||||
def test_dark_room_exists(game):
|
||||
"""Verify the DARK room was registered."""
|
||||
state, _, _ = game
|
||||
dark_room = state.world.get_room("DARK")
|
||||
assert dark_room is not None
|
||||
assert dark_room.id == "DARK"
|
||||
|
||||
|
||||
def test_dark_entry_clears_inventory(game):
|
||||
"""Entering the DARK room should strip all inventory items."""
|
||||
state, _, _ = game
|
||||
# Give player an item
|
||||
gown = state.world.get("GOWN")
|
||||
gown.move_to(state.protagonist)
|
||||
assert len(state.protagonist.children) > 0
|
||||
|
||||
# Enter dark room
|
||||
dark_room = state.world.get_room("DARK")
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
state.output.flush()
|
||||
|
||||
# Inventory should be cleared (items moved to local_globals)
|
||||
assert len(state.protagonist.children) == 0
|
||||
|
||||
|
||||
def test_dark_entry_sets_dreaming(game):
|
||||
"""Entering DARK should set the dreaming flag."""
|
||||
state, _, _ = game
|
||||
dark_room = state.world.get_room("DARK")
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
state.output.flush()
|
||||
|
||||
assert state.dreaming is True
|
||||
|
||||
|
||||
def test_dark_entry_produces_text(game):
|
||||
"""Entering DARK should display flavor text."""
|
||||
state, _, _ = game
|
||||
dark_room = state.world.get_room("DARK")
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
text = state.output.flush()
|
||||
|
||||
assert len(text) > 0
|
||||
|
||||
|
||||
def test_dark_dispatch_heart_prob(game):
|
||||
"""With 100% heart_prob and 0% for others, dispatch should go to ENTRY-BAY."""
|
||||
state, _, _ = game
|
||||
state.flags["heart_prob"] = 100
|
||||
state.flags["vogon_prob"] = 0
|
||||
state.flags["traal_prob"] = 0
|
||||
state.flags["fleet_prob"] = 0
|
||||
|
||||
# Enter dark room
|
||||
dark_room = state.world.get_room("DARK")
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
state.lying_down = False
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
state.output.flush()
|
||||
|
||||
# Set ready and trigger dispatch
|
||||
state.flags["dark_exit_ready"] = True
|
||||
state.flags["dark_hint_given"] = True
|
||||
text = send(game, "south")
|
||||
|
||||
# Should be dispatched to ENTRY-BAY (Heart of Gold)
|
||||
assert state.here.id == "ENTRY-BAY"
|
||||
|
||||
|
||||
def test_dark_dispatch_vogon_prob(game):
|
||||
"""With 100% vogon_prob and 0% for others, dispatch should go to HOLD."""
|
||||
state, _, _ = game
|
||||
state.flags["heart_prob"] = 0
|
||||
state.flags["vogon_prob"] = 100
|
||||
state.flags["traal_prob"] = 0
|
||||
state.flags["fleet_prob"] = 0
|
||||
|
||||
# Enter dark room
|
||||
dark_room = state.world.get_room("DARK")
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
state.lying_down = False
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
state.output.flush()
|
||||
|
||||
# Set ready and trigger dispatch
|
||||
state.flags["dark_exit_ready"] = True
|
||||
state.flags["dark_hint_given"] = True
|
||||
text = send(game, "south")
|
||||
|
||||
assert state.here.id == "HOLD"
|
||||
|
||||
|
||||
def test_dark_look_shows_pitch_dark(game):
|
||||
"""Looking in DARK should describe pitch darkness."""
|
||||
state, _, _ = game
|
||||
dark_room = state.world.get_room("DARK")
|
||||
state.here = dark_room
|
||||
state.protagonist.move_to(dark_room)
|
||||
if dark_room.action:
|
||||
dark_room.action(state, "M-ENTER")
|
||||
state.output.flush()
|
||||
|
||||
# Trigger M-LOOK
|
||||
dark_room.action(state, "M-LOOK")
|
||||
text = state.output.flush()
|
||||
assert "pitch dark" in text.lower()
|
||||
@@ -0,0 +1,228 @@
|
||||
"""Tests for the Earth section of the game."""
|
||||
|
||||
from tests.conftest import send
|
||||
|
||||
|
||||
def test_start_in_dark(game):
|
||||
state, _, _ = game
|
||||
assert not state.lit
|
||||
text = send(game, "look")
|
||||
assert "pitch black" in text.lower()
|
||||
|
||||
|
||||
def test_turn_on_light(game):
|
||||
text = send(game, "turn on light")
|
||||
assert "Good start" in text
|
||||
assert "light is now on" in text
|
||||
state, _, _ = game
|
||||
assert state.lit
|
||||
|
||||
|
||||
def test_turn_off_light(game):
|
||||
send(game, "turn on light")
|
||||
text = send(game, "turn off light")
|
||||
assert "pitch dark" in text
|
||||
state, _, _ = game
|
||||
assert not state.lit
|
||||
|
||||
|
||||
def test_get_out_of_bed(game):
|
||||
send(game, "turn on light")
|
||||
text = send(game, "get out of bed")
|
||||
assert "manage it" in text.lower()
|
||||
state, _, _ = game
|
||||
assert not state.lying_down
|
||||
|
||||
|
||||
def test_cant_walk_while_lying_down(game):
|
||||
send(game, "turn on light")
|
||||
text = send(game, "south")
|
||||
# Player starts in bed, so the message references getting out of bed
|
||||
assert "get out" in text.lower() or "get up" in text.lower()
|
||||
|
||||
|
||||
def test_take_gown_with_headache(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
text = send(game, "get gown")
|
||||
assert "large enough" in text.lower()
|
||||
assert "pocket" in text.lower()
|
||||
|
||||
|
||||
def test_wear_gown(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
text = send(game, "wear gown")
|
||||
assert "wearing" in text.lower()
|
||||
|
||||
|
||||
def test_look_in_pocket(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
text = send(game, "look in pocket")
|
||||
# Gown should open and reveal contents
|
||||
assert "thing" in text.lower() or "fluff" in text.lower() or "analgesic" in text.lower()
|
||||
|
||||
|
||||
def test_eat_tablet(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
send(game, "look in pocket") # opens gown
|
||||
text = send(game, "eat tablet")
|
||||
assert "headache goes" in text.lower()
|
||||
state, _, _ = game
|
||||
assert not state.flags.get("headache")
|
||||
assert state.score == 10
|
||||
|
||||
|
||||
def test_cant_leave_bedroom_with_headache(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "open door")
|
||||
text = send(game, "south")
|
||||
assert "eighteen inches" in text.lower() or "jostles" in text.lower()
|
||||
|
||||
|
||||
def test_leave_bedroom_after_tablet(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
send(game, "look in pocket")
|
||||
send(game, "eat tablet")
|
||||
text = send(game, "south")
|
||||
# Should successfully leave bedroom
|
||||
state, _, _ = game
|
||||
assert state.here.id == "FRONT-PORCH"
|
||||
|
||||
|
||||
def test_cant_leave_house_without_gown(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
send(game, "look in pocket")
|
||||
send(game, "eat tablet")
|
||||
send(game, "south")
|
||||
# Now at front porch, take off gown
|
||||
send(game, "take off gown")
|
||||
text = send(game, "south")
|
||||
assert "indecency" in text.lower()
|
||||
|
||||
|
||||
def test_leave_house_with_gown(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
send(game, "look in pocket")
|
||||
send(game, "eat tablet")
|
||||
send(game, "south")
|
||||
text = send(game, "south")
|
||||
state, _, _ = game
|
||||
assert state.here.id == "FRONT-OF-HOUSE"
|
||||
|
||||
|
||||
def test_navigate_to_pub(game):
|
||||
# Speed through to pub
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
send(game, "look in pocket")
|
||||
send(game, "eat tablet")
|
||||
send(game, "south") # front porch
|
||||
send(game, "south") # front of house
|
||||
send(game, "south") # country lane
|
||||
text = send(game, "south") # pub
|
||||
state, _, _ = game
|
||||
assert state.here.id == "PUB"
|
||||
# Ford should have arrived
|
||||
assert "Ford" in text or state.flags.get("ford_arrived")
|
||||
|
||||
|
||||
def test_drink_three_beers(game):
|
||||
# Speed through to pub
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
send(game, "get gown")
|
||||
send(game, "wear gown")
|
||||
send(game, "look in pocket")
|
||||
send(game, "eat tablet")
|
||||
send(game, "south")
|
||||
send(game, "south")
|
||||
send(game, "south")
|
||||
send(game, "south") # pub
|
||||
|
||||
text1 = send(game, "drink beer")
|
||||
assert "2 pint" in text1.lower()
|
||||
text2 = send(game, "drink beer")
|
||||
assert "1 pint" in text2.lower()
|
||||
text3 = send(game, "drink beer")
|
||||
assert "wriggly" in text3.lower() or "ear" in text3.lower()
|
||||
|
||||
state, _, _ = game
|
||||
assert state.score == 25 # 10 (tablet) + 15 (beers)
|
||||
|
||||
|
||||
def test_score_command(game):
|
||||
text = send(game, "score")
|
||||
assert "0 of a possible 400" in text
|
||||
|
||||
|
||||
def test_inventory_shows_headache(game):
|
||||
text = send(game, "inventory")
|
||||
assert "splitting headache" in text.lower()
|
||||
|
||||
|
||||
def test_inventory_shows_no_tea(game):
|
||||
text = send(game, "inventory")
|
||||
assert "no tea" in text.lower()
|
||||
|
||||
|
||||
def test_examine_bed(game):
|
||||
send(game, "turn on light")
|
||||
text = send(game, "examine bed")
|
||||
assert "bed" in text.lower()
|
||||
|
||||
|
||||
def test_look_under_bed(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
text = send(game, "look under bed")
|
||||
assert "handkerchiefs" in text.lower() or "nothing" in text.lower()
|
||||
|
||||
|
||||
def test_open_curtains_reveals_bulldozer(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
text = send(game, "open curtains")
|
||||
assert "bulldozer" in text.lower()
|
||||
|
||||
|
||||
def test_screwdriver_headache(game):
|
||||
"""Can't take screwdriver while having a headache."""
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
text = send(game, "take screwdriver")
|
||||
assert "dances" in text.lower() or "possessed" in text.lower()
|
||||
|
||||
|
||||
def test_toothbrush_headache(game):
|
||||
"""Can't take toothbrush while having a headache."""
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
text = send(game, "take toothbrush")
|
||||
assert "lunge" in text.lower() or "spins" in text.lower()
|
||||
|
||||
|
||||
def test_phone_action(game):
|
||||
send(game, "turn on light")
|
||||
send(game, "get out of bed")
|
||||
text = send(game, "answer phone")
|
||||
assert "receiver" in text.lower() or "dialling" in text.lower()
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Tests for core engine mechanics: containment, flags, clock, articles."""
|
||||
|
||||
from h2g2.engine.game_object import GameObject, Room, Flag
|
||||
from h2g2.engine.clock import Clock
|
||||
|
||||
|
||||
class TestObjectContainment:
|
||||
def test_move_to_sets_parent(self):
|
||||
room = Room("R1", desc="room")
|
||||
obj = GameObject("O1", desc="thing")
|
||||
obj.move_to(room)
|
||||
assert obj.parent is room
|
||||
assert obj in room.children
|
||||
|
||||
def test_move_to_removes_from_old_parent(self):
|
||||
room1 = Room("R1", desc="room1")
|
||||
room2 = Room("R2", desc="room2")
|
||||
obj = GameObject("O1", desc="thing")
|
||||
obj.move_to(room1)
|
||||
obj.move_to(room2)
|
||||
assert obj not in room1.children
|
||||
assert obj in room2.children
|
||||
assert obj.parent is room2
|
||||
|
||||
def test_contains(self):
|
||||
room = Room("R1", desc="room")
|
||||
obj = GameObject("O1", desc="thing")
|
||||
obj.move_to(room)
|
||||
assert room.contains(obj)
|
||||
|
||||
def test_is_held_by_direct(self):
|
||||
holder = GameObject("H", desc="holder")
|
||||
obj = GameObject("O", desc="thing")
|
||||
obj.move_to(holder)
|
||||
assert obj.is_held_by(holder)
|
||||
|
||||
def test_is_held_by_nested(self):
|
||||
holder = GameObject("H", desc="holder")
|
||||
container = GameObject("C", desc="container")
|
||||
obj = GameObject("O", desc="thing")
|
||||
container.move_to(holder)
|
||||
obj.move_to(container)
|
||||
assert obj.is_held_by(holder)
|
||||
|
||||
def test_is_held_by_false(self):
|
||||
holder = GameObject("H", desc="holder")
|
||||
obj = GameObject("O", desc="thing")
|
||||
assert not obj.is_held_by(holder)
|
||||
|
||||
def test_contents_string_excludes_invisible(self):
|
||||
room = Room("R1", desc="room")
|
||||
visible = GameObject("V", desc="visible")
|
||||
invisible = GameObject("I", desc="invisible", flags={Flag.INVISIBLE})
|
||||
visible.move_to(room)
|
||||
invisible.move_to(room)
|
||||
result = room.contents_string()
|
||||
assert visible in result
|
||||
assert invisible not in result
|
||||
|
||||
def test_contents_string_excludes_ndescbit(self):
|
||||
room = Room("R1", desc="room")
|
||||
visible = GameObject("V", desc="visible")
|
||||
hidden = GameObject("H", desc="hidden", flags={Flag.NDESCBIT})
|
||||
visible.move_to(room)
|
||||
hidden.move_to(room)
|
||||
result = room.contents_string()
|
||||
assert visible in result
|
||||
assert hidden not in result
|
||||
|
||||
|
||||
class TestFlagOperations:
|
||||
def test_fset_and_fset_q(self):
|
||||
obj = GameObject("O", desc="thing")
|
||||
assert not obj.fset_q(Flag.TAKEBIT)
|
||||
obj.fset(Flag.TAKEBIT)
|
||||
assert obj.fset_q(Flag.TAKEBIT)
|
||||
|
||||
def test_fclear(self):
|
||||
obj = GameObject("O", desc="thing", flags={Flag.TAKEBIT})
|
||||
assert obj.fset_q(Flag.TAKEBIT)
|
||||
obj.fclear(Flag.TAKEBIT)
|
||||
assert not obj.fset_q(Flag.TAKEBIT)
|
||||
|
||||
def test_fclear_nonexistent_flag_no_error(self):
|
||||
obj = GameObject("O", desc="thing")
|
||||
obj.fclear(Flag.TAKEBIT) # should not raise
|
||||
|
||||
def test_multiple_flags(self):
|
||||
obj = GameObject("O", desc="thing", flags={Flag.TAKEBIT, Flag.OPENBIT})
|
||||
assert obj.fset_q(Flag.TAKEBIT)
|
||||
assert obj.fset_q(Flag.OPENBIT)
|
||||
assert not obj.fset_q(Flag.WEARBIT)
|
||||
|
||||
|
||||
class TestObjectArticle:
|
||||
def test_default_article(self):
|
||||
obj = GameObject("O", desc="ball")
|
||||
assert obj.article() == "a "
|
||||
assert obj.a_desc() == "a ball"
|
||||
assert obj.the_desc() == "the ball"
|
||||
|
||||
def test_vowel_article(self):
|
||||
obj = GameObject("O", desc="egg", flags={Flag.VOWELBIT})
|
||||
assert obj.article() == "an "
|
||||
assert obj.a_desc() == "an egg"
|
||||
|
||||
def test_no_article(self):
|
||||
obj = GameObject("O", desc="Ford Prefect", flags={Flag.NARTICLEBIT})
|
||||
assert obj.article() == ""
|
||||
assert obj.a_desc() == "Ford Prefect"
|
||||
assert obj.the_desc() == "Ford Prefect"
|
||||
|
||||
|
||||
class TestClock:
|
||||
def test_tick_and_fire(self):
|
||||
clock = Clock()
|
||||
fired = []
|
||||
|
||||
def handler(state):
|
||||
fired.append(True)
|
||||
return True
|
||||
|
||||
clock.queue(handler, 3, name="test")
|
||||
clock.tick_all(None) # tick 1: countdown 3->2
|
||||
assert len(fired) == 0
|
||||
clock.tick_all(None) # tick 2: countdown 2->1
|
||||
assert len(fired) == 0
|
||||
clock.tick_all(None) # tick 3: countdown 1->0, fires
|
||||
assert len(fired) == 1
|
||||
|
||||
def test_one_shot_does_not_refire(self):
|
||||
clock = Clock()
|
||||
fired = []
|
||||
|
||||
def handler(state):
|
||||
fired.append(True)
|
||||
return True
|
||||
|
||||
clock.queue(handler, 1, name="test")
|
||||
clock.tick_all(None) # fires
|
||||
clock.tick_all(None) # should not fire again
|
||||
assert len(fired) == 1
|
||||
|
||||
def test_disable_prevents_fire(self):
|
||||
clock = Clock()
|
||||
fired = []
|
||||
|
||||
def handler(state):
|
||||
fired.append(True)
|
||||
return True
|
||||
|
||||
entry = clock.queue(handler, 2, name="test")
|
||||
clock.tick_all(None) # tick 1
|
||||
clock.disable(entry)
|
||||
clock.tick_all(None) # tick 2 — disabled, should not fire
|
||||
clock.tick_all(None) # tick 3
|
||||
assert len(fired) == 0
|
||||
|
||||
def test_negative_tick_fires_every_turn(self):
|
||||
clock = Clock()
|
||||
fired = []
|
||||
|
||||
def handler(state):
|
||||
fired.append(True)
|
||||
return True
|
||||
|
||||
clock.queue(handler, -1, name="repeating")
|
||||
clock.tick_all(None)
|
||||
clock.tick_all(None)
|
||||
clock.tick_all(None)
|
||||
assert len(fired) == 3
|
||||
|
||||
def test_queue_updates_existing_entry(self):
|
||||
clock = Clock()
|
||||
fired = []
|
||||
|
||||
def handler(state):
|
||||
fired.append(True)
|
||||
return True
|
||||
|
||||
clock.queue(handler, 5, name="test")
|
||||
clock.queue(handler, 1, name="test") # reset to 1
|
||||
clock.tick_all(None) # should fire now (1->0)
|
||||
assert len(fired) == 1
|
||||
|
||||
def test_remove_entry(self):
|
||||
clock = Clock()
|
||||
fired = []
|
||||
|
||||
def handler(state):
|
||||
fired.append(True)
|
||||
return True
|
||||
|
||||
entry = clock.queue(handler, 1, name="test")
|
||||
clock.remove(entry)
|
||||
clock.tick_all(None)
|
||||
assert len(fired) == 0
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Tests for the input parser."""
|
||||
|
||||
from tests.conftest import send
|
||||
|
||||
|
||||
def test_direction_shortcut_n(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("n", state)
|
||||
assert result is not None
|
||||
assert result.verb == "walk"
|
||||
assert result.direction == "NORTH"
|
||||
|
||||
|
||||
def test_direction_shortcut_s(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("s", state)
|
||||
assert result is not None
|
||||
assert result.verb == "walk"
|
||||
assert result.direction == "SOUTH"
|
||||
|
||||
|
||||
def test_direction_shortcut_e(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("e", state)
|
||||
assert result is not None
|
||||
assert result.verb == "walk"
|
||||
assert result.direction == "EAST"
|
||||
|
||||
|
||||
def test_direction_go_north(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("go north", state)
|
||||
assert result is not None
|
||||
assert result.verb == "walk"
|
||||
assert result.direction == "NORTH"
|
||||
|
||||
|
||||
def test_compound_verb_get_out(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("get out of bed", state)
|
||||
assert result is not None
|
||||
assert result.verb == "get out"
|
||||
|
||||
|
||||
def test_compound_verb_turn_on(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("turn on light", state)
|
||||
assert result is not None
|
||||
assert result.verb == "turn on"
|
||||
|
||||
|
||||
def test_compound_verb_turn_off(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("turn off light", state)
|
||||
assert result is not None
|
||||
assert result.verb == "turn off"
|
||||
|
||||
|
||||
def test_compound_verb_look_in(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("look in pocket", state)
|
||||
assert result is not None
|
||||
assert result.verb == "look in"
|
||||
|
||||
|
||||
def test_compound_verb_look_at(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("look at bed", state)
|
||||
assert result is not None
|
||||
assert result.verb == "examine"
|
||||
|
||||
|
||||
def test_compound_verb_pick_up(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("pick up gown", state)
|
||||
assert result is not None
|
||||
assert result.verb == "take"
|
||||
|
||||
|
||||
def test_compound_verb_stand_up(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("stand up", state)
|
||||
assert result is not None
|
||||
assert result.verb == "stand up"
|
||||
|
||||
|
||||
def test_compound_verb_lie_down(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("lie down", state)
|
||||
assert result is not None
|
||||
assert result.verb == "lie down"
|
||||
|
||||
|
||||
def test_compound_verb_put_on(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("put on gown", state)
|
||||
assert result is not None
|
||||
assert result.verb == "put on"
|
||||
|
||||
|
||||
def test_compound_verb_take_off(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("take off gown", state)
|
||||
assert result is not None
|
||||
assert result.verb == "take off"
|
||||
|
||||
|
||||
def test_preposition_splitting(game):
|
||||
state, parser, _ = game
|
||||
# Need objects in scope for "put X in Y" — use gown (in bedroom) and bed
|
||||
result = parser.parse("put gown in bed", state)
|
||||
assert result is not None
|
||||
assert result.prep == "in"
|
||||
|
||||
|
||||
def test_again_command(game):
|
||||
state, parser, _ = game
|
||||
# First command
|
||||
result1 = parser.parse("look", state)
|
||||
assert result1 is not None
|
||||
# Again via "g"
|
||||
result2 = parser.parse("g", state)
|
||||
assert result2 is result1
|
||||
# Again via "again"
|
||||
result3 = parser.parse("again", state)
|
||||
assert result3 is result1
|
||||
|
||||
|
||||
def test_buzz_word_stripping(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("take the gown", state)
|
||||
assert result is not None
|
||||
assert result.verb == "take"
|
||||
# "the" should be stripped, object should resolve to GOWN
|
||||
assert result.direct_obj is not None
|
||||
assert result.direct_obj.id == "GOWN"
|
||||
|
||||
|
||||
def test_verb_synonym_grab(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("grab gown", state)
|
||||
assert result is not None
|
||||
assert result.verb == "take"
|
||||
|
||||
|
||||
def test_verb_synonym_x(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("x bed", state)
|
||||
assert result is not None
|
||||
assert result.verb == "examine"
|
||||
|
||||
|
||||
def test_verb_synonym_i(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("i", state)
|
||||
assert result is not None
|
||||
assert result.verb == "inventory"
|
||||
|
||||
|
||||
def test_negation(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("don't panic", state)
|
||||
assert result is not None
|
||||
assert result.is_negated is True
|
||||
assert result.verb == "panic"
|
||||
|
||||
|
||||
def test_empty_input_returns_none(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("", state)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_only_buzz_words_returns_none(game):
|
||||
state, parser, _ = game
|
||||
result = parser.parse("the a an", state)
|
||||
assert result is None
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Tests for the Vogon ship section."""
|
||||
|
||||
from tests.conftest import send
|
||||
|
||||
|
||||
def test_hold_exists(game):
|
||||
"""Verify the Hold room was registered."""
|
||||
state, _, _ = game
|
||||
hold = state.world.get_room("HOLD")
|
||||
assert hold is not None
|
||||
assert hold.id == "HOLD"
|
||||
|
||||
|
||||
def test_hold_first_visit(game):
|
||||
"""Test that the Hold's first-visit M-END sequence gives peanuts and score."""
|
||||
state, _, loop = game
|
||||
hold = state.world.get_room("HOLD")
|
||||
state.here = hold
|
||||
state.protagonist.move_to(hold)
|
||||
state.lying_down = False
|
||||
# The first-visit logic is in M-END, not M-ENTER
|
||||
if hold.action:
|
||||
hold.action(state, "M-END")
|
||||
text = state.output.flush()
|
||||
# Should mention waking up and give peanuts
|
||||
assert len(text) > 0
|
||||
assert state.score >= 8
|
||||
peanuts = state.world.get("PEANUTS")
|
||||
assert peanuts.parent is state.protagonist
|
||||
|
||||
|
||||
def test_airlock_room_exists(game):
|
||||
"""Verify the Airlock room was registered."""
|
||||
state, _, _ = game
|
||||
airlock = state.world.get_room("AIRLOCK")
|
||||
assert airlock is not None
|
||||
assert airlock.id == "AIRLOCK"
|
||||
|
||||
|
||||
def test_captains_quarters_exists(game):
|
||||
"""Verify the Captain's Quarters room was registered."""
|
||||
state, _, _ = game
|
||||
quarters = state.world.get_room("CAPTAINS-QUARTERS")
|
||||
assert quarters is not None
|
||||
@@ -2,7 +2,78 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2g2"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=8.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user