Files
h2g2/tests/test_engine.py
seppedl 679639df9f 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>
2026-04-03 22:04:22 +02:00

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