Tests verify:
- Spectators at same location see IF output with player name header
- Spectators at different locations see nothing
- Game start intro broadcasts to spectators
- broadcast_to_spectators skips the playing player
- Multiple spectators all receive messages
Tests currently fail as broadcast_to_spectators not yet implemented.
TDD implementation of IFSession that manages a dfrotz subprocess.
IFResponse dataclass follows the editor pattern with output/done fields.
IFSession handles spawning dfrotz, routing input, and detecting the prompt.
Escape commands (::quit, ::help) are handled without sending to dfrotz.
Phase 6: spawn command creates mobs at player position from loaded
templates. Server loads mob templates from content/mobs/ at startup,
injects world into combat/commands module, and runs process_mobs()
each game loop tick after process_combat().
Phase 5: process_mobs() runs each tick, handling mob attack and defense
decisions. Mobs pick random attacks from their move list when IDLE,
swap roles if needed, and attempt defense during TELEGRAPH/WINDOW with
a 40% chance of correct counter. 1-second cooldown between actions.
Training dummies with empty moves never fight back.
Phase 4: when combat ends, determine winner/loser. If the loser is a
Mob, despawn it and send a victory message to the winner. If the loser
is a Player fighting a Mob, send a defeat message instead.
Phase 3: look command now collects alive mob positions using the same
wrapping-aware relative position calc as players, and renders them as *
with the same priority as other players (after @ but before effects).
Phase 2: do_attack now searches the mobs registry after players dict
when resolving a target name. Players always take priority over mobs
with the same name. World instance injected into combat/commands module
for wrapping-aware mob proximity checks.
Phase 1 of fightable mobs: MobTemplate dataclass loaded from TOML,
global mobs list, spawn_mob/despawn_mob/get_nearby_mob with
wrapping-aware distance. Mob entity gets moves and next_action_at fields.
The variant handler now supports prefix matching for directional variants.
This allows 'pa hi' to match 'parry high', 'pa lo' to match 'parry low', etc.
Implementation:
- First tries exact match on variant key
- Falls back to prefix matching if no exact match
- Returns unique match if exactly one variant starts with the prefix
- Shows disambiguation message if multiple variants match
- Shows error with valid options if no variants match
Tests cover exact match, prefix match, ambiguous prefix, no match,
single-char prefix, case-insensitivity, and preservation of target args.
Defense moves now asyncio.sleep for timing_window_ms instead of using
a cooldown field. Input queues naturally since the per-player loop is
sequential. Outside combat shows "parry the air!" flavor text.
- Set variant defense registration to mode="*" (both attacks and defenses)
- Strengthen telegraph switch test to verify new move's telegraph text
- Remove unused punch parameter from four idle timeout tests
- Use single time.monotonic() call in attack() method
Encounters track last_action_at (updated on attack and defend). If 30
seconds pass with no actions, combat fizzles out with a message to both
players and combat mode is popped. start_encounter initializes the
timestamp so fresh encounters don't immediately timeout.