679639df9f
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>
198 lines
6.0 KiB
Python
198 lines
6.0 KiB
Python
"""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
|