Commit graph

212 commits

Author SHA1 Message Date
4e8459df5f
Convert combat resolution to POV templates 2026-02-13 23:21:52 -05:00
2b21257d26
Add POV template engine for combat messages 2026-02-13 23:05:19 -05:00
292557e5fd
Add power up/down commands
Implements power level management system with tick-based power-up loop.
Players can raise PL toward max_pl (costs stamina per tick), lower PL
instantly, set exact PL targets, and cancel ongoing power-ups.
2026-02-13 23:01:33 -05:00
593bfd3028
Add prompt command for customization 2026-02-13 22:48:59 -05:00
780501ceed
Add render_prompt with modal templates 2026-02-13 22:48:59 -05:00
9729e853e1
Add color markup engine for prompt templates 2026-02-13 22:48:59 -05:00
525b2fd812
Refactor look command to use structured room display
- Add Where: header with zone description
- Add Location: line with quadrant and coordinates
- Add Nearby: line showing entities in viewport (not on player's tile)
- Add Exits: line showing available cardinal directions
- Replace 'Here:' with individual entity lines showing posture
- Replace 'Portals:' with individual 'You see {name}.' lines
- Add look <thing> routing to examine command
- Add comprehensive tests for new structured output
- Update existing tests to match new output format
2026-02-13 22:20:48 -05:00
d7d4fff701
Add render/room.py with structured room display functions 2026-02-13 22:20:48 -05:00
1f7db3a205
Add posture property to Entity for room display 2026-02-13 22:06:18 -05:00
61ab785b59
Send telegraph messages to player when mobs attack 2026-02-12 18:22:30 -05:00
a4a95694f8
Remove player from zone contents on disconnect
Player objects were removed from the players dict on quit/disconnect
but never removed from zone._contents, leaving ghost * markers on
other players' maps.
2026-02-12 17:00:19 -05:00
b63b054997
Show entities at player position beneath the map
Adds a "Here: goblin, Ally" line after the map grid listing mobs and
other players sharing the tile. Dead mobs are excluded.
2026-02-12 17:00:19 -05:00
4cff1475c3
Remove player from zone contents on disconnect
Player objects were removed from the players dict on quit/disconnect
but never removed from zone._contents, leaving ghost * markers on
other players' maps.
2026-02-12 16:41:04 -05:00
200cc00129
Dedupe MSDP vitals to avoid spamming idle clients
Cache last-sent values on Player and skip send_msdp() when
nothing changed. Idle players no longer get a packet every second.
2026-02-12 16:31:17 -05:00
aeb3d31702
Add client command to show protocol and terminal info 2026-02-12 15:58:54 -05:00
ee0dc839d8
Offer GMCP/MSDP during connection and guard tick sends
The server never proactively offered GMCP or MSDP to clients, so
telnetlib3 logged "cannot send MSDP without negotiation" every second.
Now the server sends WILL GMCP and WILL MSDP on connection, and
send_msdp_vitals checks negotiation state before attempting to send.
2026-02-12 15:58:54 -05:00
f418805b78
Fix portal type narrowing in zone tests 2026-02-11 23:17:50 -05:00
3d386fbf99
Fix GMCP and MSDP support for rich clients 2026-02-11 23:13:14 -05:00
ca282851e3
Update test_char_vitals_sent_on_rest_complete to expect both Status and Vitals 2026-02-11 23:13:14 -05:00
6ed71873b5
Fix line length in test_gmcp.py 2026-02-11 23:13:14 -05:00
e247d70612
Send Char.Status on combat end and rest state changes 2026-02-11 23:13:14 -05:00
e9f70ebd2f
Send Char.Vitals on combat resolution and rest completion 2026-02-11 23:13:14 -05:00
d253012122
Add GMCP and MSDP support for rich clients
Implements Phase 7 foundation:
- gmcp.py module with package builders for Char.Vitals, Char.Status,
  Room.Info, Room.Map, and MSDP vitals
- Player helper methods send_gmcp() and send_msdp() for convenience
- Full test coverage for all GMCP/MSDP functions and edge cases
2026-02-11 22:52:16 -05:00
058ba1b7de
Add zone TOML export 2026-02-11 22:38:14 -05:00
3a756cc589
Add ambient message support to zones 2026-02-11 22:38:14 -05:00
c3884e236b
Add per-zone mob spawn rules
Zones can now define spawn rules in TOML:
- [[spawns]] sections specify mob type, max count, and respawn timer
- SpawnRule dataclass stores configuration
- load_zone() parses spawn rules from TOML
- Added example spawn rules to treehouse zone (squirrel, crow)

This is configuration infrastructure only - actual spawning logic
will be handled by the game loop in a future phase.
2026-02-11 22:38:14 -05:00
b123d55fbd
Add hub zone with portal connections 2026-02-11 22:38:14 -05:00
56c82700b0
Add tutorial zones (flower and treehouse)
Implements the new player funnel with two tutorial zones:
- Flower: 7x7 sealed zone with translucent petals, spawn at center
- Treehouse: 20x15 platform zone with rope ladder and branch exits

Both zones are bounded (non-toroidal) and include portals for progression.
2026-02-11 22:38:14 -05:00
7154dd86d3
Add portal to tavern zone 2026-02-11 22:38:14 -05:00
d6920834c8
Add auto-trigger portal on movement 2026-02-11 22:38:14 -05:00
b3801f780f
Add portal loading from zone TOML files
Zone TOML files can now define portals using [[portals]] sections.
Each portal specifies coordinates (x, y), a target (zone_name:x,y),
and a label. Optional aliases are supported. Portals are
automatically created and placed in the zone when it loads.
2026-02-11 22:38:14 -05:00
cb3ad6a547
Add spawn point support to zones 2026-02-11 22:00:06 -05:00
e724abb926
Add TOML verb support for thing templates
Thing templates can now define verbs in TOML using [verbs] section with
module:function references. Verbs are resolved at spawn time and bound
to the spawned object instance using functools.partial. Works for both
Thing and Container instances.
2026-02-11 21:47:33 -05:00
6ce57ad970
Add key-based unlock as first verb interaction
Implements unlock_handler that checks for a key in player inventory
and unlocks containers. Tests cover error cases (non-container,
not locked, no key), success case, key aliasing, and state preservation.
2026-02-11 21:47:33 -05:00
d2de6bdc16
Add use command for verb-based interaction
Implements a TDD-built 'use' command that lets players invoke
object verbs with optional targets:
- use X - calls X's use verb
- use X on Y - calls X's use verb with Y as args
- Proper error messages for missing objects/verbs
- Tests cover all edge cases including inventory/ground search

Also fixes type checking issue in verb dispatch where get_verb
could return None.
2026-02-11 21:47:33 -05:00
9534df8f9c
Add examine command for object inspection
Implements a global examine/ex command that shows detailed descriptions
of objects. Searches inventory first, then ground at player position.
Works with Things, Containers, and Mobs.
2026-02-11 21:47:33 -05:00
fcfa13c785
Add verb infrastructure on Object
Verbs let any Object have interactive handlers players can trigger.
Uses @verb decorator to mark methods that auto-register on instantiation.

- Object._verbs dict stores verb name to async handler mapping
- Object.register_verb(), get_verb(), has_verb() API
- @verb decorator marks methods with _verb_name attribute
- __post_init__ scans for decorated methods and registers them
- find_object() helper searches inventory then ground by name/alias
- Bound methods stored in _verbs (self already bound)
- Works on Object and all subclasses (Thing, Entity, etc)
- 18 tests covering registration, lookup, decoration, inheritance
2026-02-11 21:47:33 -05:00
7d4a75f973
Show portals in look output
Look command now displays portals separately from ground items.
Portals at the player's position are shown after ground items with
the format "Portals: name1, name2". This separates portals from
regular items since they serve a different purpose in gameplay.
2026-02-11 20:58:55 -05:00
aa720edae5
Add enter command for portal zone transitions
Implements portal-based zone transitions with the enter command.
Players can enter portals at their position to move to target zones
with specified coordinates. Includes departure/arrival messaging to
nearby players and automatic look output in the destination zone.
Portals are matched by partial name or exact alias match.
2026-02-11 20:58:55 -05:00
557fffe5fa
Add put and take-from commands for containers 2026-02-11 20:58:55 -05:00
68161fd025
Add container support to thing template loader
Extended ThingTemplate with optional container fields (capacity, closed, locked).
When a template includes capacity, spawn_thing now creates a Container instead
of a regular Thing.

Added two example container templates:
- chest.toml: non-portable, capacity 5, starts closed
- sack.toml: portable, capacity 3, starts open
2026-02-11 20:58:55 -05:00
3be4370b2f
Show container state in look and inventory display
Containers now display their state when viewed:
- Closed containers show "(closed)"
- Open empty containers show "(open, empty)"
- Open containers with items show "(open, containing: item1, item2)"

This applies to both ground items in the look command and inventory items.
Added _format_thing_name helper to both look.py and things.py to handle
the display formatting consistently.
2026-02-11 20:58:55 -05:00
d18f21a031
Add zone TOML loader and tavern interior zone
Implements load_zone() and load_zones() functions to parse zone
definitions from TOML files. Wires zone loading into server startup
to register all zones from content/zones/ directory. Updates player
zone lookup to use the registry instead of hardcoded overworld check.

Includes tavern.toml as first hand-built interior zone (8x6 bounded).
2026-02-11 20:58:55 -05:00
5b9a43617f
Add open and close commands for containers 2026-02-11 20:58:55 -05:00
b3471a8b94
Add zone registry with register and lookup
Implements a module-level zone registry for looking up zones by name.
Includes register_zone() and get_zone() functions with comprehensive
tests covering single/multiple zones, unknown lookups, and overwrites.
2026-02-11 20:40:31 -05:00
303ce2c89e
Add Portal class with target zone and coordinates
Portals are non-portable Things that exist in zones and define
transitions to other zones via target coordinates.
2026-02-11 20:38:47 -05:00
621c42b833
Add Container class with capacity and open/closed state 2026-02-11 20:38:40 -05:00
05a739da74
Add test for duplicate item persistence 2026-02-11 20:29:59 -05:00
6081c90ad1
Add inventory persistence to player saves
Inventory saved as JSON list of thing template names in an inventory
column. Migration adds column to existing databases. load_player_data
returns inventory list, save_player serializes Thing names from contents.
2026-02-11 20:29:58 -05:00
c43b3346ae
Add Thing templates, TOML loading, and spawning
ThingTemplate dataclass mirrors MobTemplate pattern. load_thing_template
and load_thing_templates parse TOML files from content/things/. spawn_thing
creates Thing instances from templates. Includes rock and fountain examples.
2026-02-11 20:01:15 -05:00
2e79255aec
Show ground items in look command
After the viewport, look lists Things at the player's position
in a "On the ground: item1, item2" line. No output when empty.
2026-02-11 20:01:10 -05:00
e96fd50de5
Add inventory command with alias "i"
Lists Thing objects in player.contents with 2-space indented format.
Shows "You aren't carrying anything." when inventory is empty.
2026-02-11 20:01:05 -05:00
7c12bf3318
Add Object.move_to(), get and drop commands
Object.move_to() handles containment transfer: removes from old location's
contents, updates location pointer and coordinates, adds to new location.
get/drop commands use move_to to transfer Things between zone and inventory.
Supports name and alias matching for item lookup.
2026-02-11 19:57:38 -05:00
9437728435
Add Thing class and Entity.can_accept() for inventory
Thing is an Object subclass with description, portable flag, and aliases.
Entity.can_accept() returns True for portable Things, enabling the
containment model where entities carry items in their contents.
2026-02-11 19:55:58 -05:00
957a411601
Clean up global state, migrate broadcast_to_spectators to Zone
Removes dependency on global players dict for spatial queries by using
Zone.contents_at() for spectator lookup. Makes _world local to run_server()
since it's only used during initialization to create the overworld Zone.
Updates test fixtures to provide zones for spatial query tests.
2026-02-11 19:42:12 -05:00
f5646589b5
Migrate look to use player.location (Zone)
- Removed world module-level variable from look.py
- look.cmd_look() now uses player.location.get_viewport() instead of world.get_viewport()
- look.cmd_look() uses zone.contents_near() to find nearby entities instead of iterating global players/mobs lists
- Wrapping calculations use zone.width/height/toroidal instead of world properties
- Added type check for player.location being a Zone instance
- Removed look.world injection from server.py
- Updated all tests to remove look.world injection
- spawn_mob() and combat commands also migrated to use Zone (player.location)
- Removed orphaned code from test_mob_ai.py and test_variant_prefix.py
2026-02-11 19:36:46 -05:00
1349c2f860
Add zone_name to persistence schema
Adds zone_name field to PlayerData and accounts table to track which
zone a player is in. Defaults to 'overworld'. Includes migration logic
to handle existing databases without the column. Server now resolves
zone from zone_name when loading player data.
2026-02-11 19:33:23 -05:00
875ded5762
Migrate fly to use player.location (Zone)
Removed module-level world variable and replaced all world.wrap() calls
with player.location.wrap(). Added Zone assertion for type safety,
matching the pattern in movement.py. Updated tests to remove fly.world
injection since it's no longer needed.
2026-02-11 19:33:15 -05:00
404a1cdf0c
Migrate movement to use player.location (Zone)
Movement commands now access the zone through player.location instead of
a module-level world variable. send_nearby_message uses
zone.contents_near() to find nearby entities, eliminating the need for
the global players dict and manual distance calculations.

Tests updated to create zones and add entities via location assignment.
2026-02-11 19:28:27 -05:00
66c6e1ebd4
Create overworld Zone at startup, set player.location 2026-02-11 19:19:15 -05:00
6f58ae0501
Add contents_near() spatial query to Zone 2026-02-11 19:17:17 -05:00
b4fca95830
Add Zone class with terrain, spatial queries, and viewport
Zone(Object) is a spatial area with a terrain grid. Supports
toroidal wrapping and bounded clamping, passability checks,
viewport extraction, and contents_at(x, y) spatial queries.
Zones are top-level containers (location=None) that accept
everything via can_accept().
2026-02-11 19:08:30 -05:00
d9e9d1b785
Add Object base class with containment primitives
Object provides name, location, x/y, contents reverse-lookup, and
can_accept() — the foundation for the containment tree that zones,
things, and inventory will build on.
2026-02-11 18:40:31 -05:00
6d7b404365
Add pytest regression harness for z-machine game compatibility
Implements Phase 4 of the z-machine compatibility plan.

Creates automated regression tests that smoke-test all supported games
(V3, V5, V8) by loading each story, executing basic commands, and verifying
the interpreter doesn't crash.

Key features:
- Parametrized test covering 7 games (zork1, curses, photopia, Tangle,
  shade, LostPig, anchor)
- QuietScreen class that disables [MORE] prompts for unattended testing
- AutoInputStream that auto-feeds commands then exits cleanly
- Tests verify: no crashes, unimplemented opcodes, and minimum instruction count
- All tests pass in ~2 seconds

Tests skip gracefully if story files aren't present, making this safe to
run in CI or on systems without all game files.
2026-02-10 17:10:29 -05:00
bc1a2e5489
Add undo command support 2026-02-10 16:49:46 -05:00
5f12a4f841
Suppress upper window writes in MudScreen to fix Lost Pig output
V5+ games write room names to the upper window (status line) via
select_window(1). Since select_window was a no-op, status line text
leaked into the main output buffer, causing ">Outside" on prompt lines.

Track the active window and only buffer writes to window 0 (lower).
2026-02-10 14:36:42 -05:00
602da45ac2
Fix IF bugs: case-insensitive story lookup, double prompt, phantom restore command
- _find_story() now compares path.stem.lower() so "lostpig" matches "LostPig.z8"
- Server no longer writes its own prompt in IF mode (game handles prompting)
- Suppress phantom game output on restore (saved PC past sread causes garbage)
- Route .z5/.z8 files to EmbeddedIFSession now that V5+ is supported
2026-02-10 14:16:19 -05:00
11d939a70f
Relax version gates to accept V8 story files
V8 uses the same format as V5 (object model, opcodes, stack) with
two differences: packed address scaling (×8 instead of ×4) and max
file size (512KB instead of 256KB).

zmemory: add V8 size validation and packed_address case
zobjectparser: accept version 8 alongside 4-5 in all checks
zstackmanager: allow V8 stack initialization
V6-7 remain unsupported (different packed address format with offsets).
2026-02-10 13:37:22 -05:00
8526e48247
Fix Quetzal Stks field mapping: return_pc to caller, varnum to frame
return_pc for each frame belongs on the caller's program_counter (the
resume address when this routine exits). varnum is the store variable
that receives the return value, stored as return_addr on the frame
itself. Also handle flags bit 4 (discard result → return_addr=None).
2026-02-10 12:39:40 -05:00
15e1d807aa
Move z-machine restore before interpreter thread start
Replaces the async _do_restore() (called after thread launch) with a
synchronous _try_restore() called before the thread starts. This
eliminates the race condition where restore mutates z-machine state
while the interpreter thread is running.

The restore prefix message is now part of start()'s return value
instead of being sent separately in play.py.
2026-02-10 11:51:45 -05:00
b6d933acc5
Add tests for embedded z-machine MUD integration
Unit tests for MUD UI components (screen, input stream, filesystem)
and integration tests with real zork1.z3 (session lifecycle, escape
commands, save/restore round-trip, state inspection).
2026-02-10 11:18:19 -05:00
1ffc4e14c2
Add round-trip save/restore integration test
Verifies complete save/restore pipeline by generating save data with
QuetzalWriter and restoring it with QuetzalParser. Tests cover:
- Basic round-trip with memory, stack, and PC restoration
- Multiple nested call frames
- Preservation of unchanged memory bytes (run-length encoding)
- Empty stack (no routine frames)

Each test confirms that state modified after save is correctly
restored to original values from the save data.
2026-02-10 10:13:45 -05:00
b0fb9b5e2c
Wire op_restore to QuetzalParser and filesystem
Implement V3 restore opcode:
- Add QuetzalParser.load_from_bytes() to parse save data from memory
- Wire op_restore to call filesystem.restore_game() and parse result
- Validate IFhd matches current story (release/serial/checksum)
- Restore dynamic memory, call stack, and program counter
- Branch true on success, false on failure/cancellation

Fix IFF chunk padding bug:
- Add padding byte to odd-length chunks in QuetzalWriter
- Ensures proper chunk alignment for parser compatibility

Add comprehensive tests:
- Branch false when filesystem returns None
- Branch false without zmachine reference
- Branch true on successful restore
- Verify memory state matches saved values
- Handle malformed save data gracefully
2026-02-10 10:13:45 -05:00
a5053e10f2
Wire op_save to QuetzalWriter and filesystem
Implement full save functionality for V3 z-machine:
- Fixed QuetzalWriter._generate_anno_chunk() to return bytes
- Added QuetzalWriter.generate_save_data() to produce IFF container
- Updated QuetzalWriter.write() to use new method and binary mode
- Added zmachine reference to ZCpu for QuetzalWriter access
- Added _program_counter property to ZCpu for Quetzal access
- Implemented op_save to call QuetzalWriter and filesystem
- Updated tests for op_save (success, failure, IFF validation)
- Added filesystem mock to MockUI for testing
- Added _call_stack to MockStackManager for QuetzalWriter

All tests pass. Save now generates valid IFF/FORM/IFZS data with
IFhd, CMem, Stks, and ANNO chunks.
2026-02-10 10:13:45 -05:00
69b1ef8a59
Implement QuetzalWriter CMem and Stks chunk generators
Adds comprehensive test coverage for CMem chunk generation:
- test_cmem_all_unchanged: Empty output when memory unchanged
- test_cmem_single_byte_change: Single byte modification
- test_cmem_multiple_scattered_changes: Multiple changes
- test_cmem_roundtrip_with_parser: Writer/parser integration
- test_cmem_consecutive_zeros: Run-length encoding validation
2026-02-10 10:13:33 -05:00
2b8c177977
Fix off-by-one in QuetzalParser return_pc parsing 2026-02-10 10:13:33 -05:00
0c6eadb0da
Implement QuetzalWriter._generate_ifhd_chunk()
The IFhd chunk contains 13 bytes of metadata identifying the story
and current execution state:
- Release number (2 bytes) from header
- Serial number (6 bytes) from header
- Checksum (2 bytes) from header
- Program counter (3 bytes) from CPU state

This allows save files to be validated against the story file.
2026-02-10 09:47:24 -05:00
2ce82e7d87
Add unit tests for op_test, op_verify, and op_get_child
Adds comprehensive test coverage for three newly implemented Z-machine opcodes:

- op_test: Tests bitmap flag checking with all flags set, some missing, zero
  flags, and identical values
- op_verify: Tests checksum verification with matching and mismatched checksums
- op_get_child: Tests getting first child of object with and without children

Also extends MockMemory with generate_checksum() and MockObjectParser with
get_child() to support the new tests.
2026-02-09 21:52:08 -05:00
311a67e80a
Fix bugs found running Zork 1 through hybrid interpreter
Spec fixes: implement op_test (bitwise AND branch), add missing branch
to op_get_child, handle call-to-address-0 as no-op in op_call/_call.
I/O fixes: correct keyboard API (keyboard_input.read_line), non-TTY
fallbacks in trivialzui, stdout flush for immediate output. Graceful
handling of unmapped ZSCII characters. Add instruction trace buffer
for debugging.
2026-02-09 21:36:30 -05:00
fb8cbf7219
Implement op_verify and wire ZLexer into sread for Zork 1
op_verify now performs actual checksum validation against the header
instead of raising NotImplemented. ZLexer is injected into ZCpu and
sread tokenizes input into the parse buffer per the V3 spec.
2026-02-09 20:57:15 -05:00
72dd047b7b
Port 5 complex opcodes to hybrid z-machine interpreter
Implement op_sread (text input), op_save/restore (file I/O stubs),
op_restart (exception-based reset), op_input_stream and op_sound_effect
(no-op stubs). Add ZCpuRestart exception. All implementations follow TDD
with comprehensive unit tests.
2026-02-09 20:44:22 -05:00
c76ee337d3
Port 4 medium opcodes to hybrid z-machine interpreter
Implements op_print_addr, op_print_num, op_ret, and op_show_status following
TDD approach with tests first. Each opcode now properly decodes/prints text,
handles signed numbers, returns from routines, or acts as a no-op as appropriate.
2026-02-09 20:44:22 -05:00
1b9d84f41a
Port 10 object tree opcodes to hybrid z-machine interpreter
Implemented 10 opcodes for object tree manipulation: get_sibling,
test_attr, set_attr, clear_attr, jin, remove_obj, print_obj,
get_prop_addr, get_next_prop, and get_prop_len.

Added 6 supporting methods to ZObjectParser: set_attribute,
clear_attribute, remove_object, get_property_data_address,
get_next_property, and get_property_length.

Fixed bug in insert_object where sibling chain walk never advanced
the prev pointer, potentially causing infinite loops.

Added 16 unit tests with MockObjectParser to verify CPU opcode
behavior. All tests passing.
2026-02-09 20:44:21 -05:00
dcc952d4c5
Port 12 trivial opcodes to hybrid z-machine interpreter 2026-02-09 20:44:21 -05:00
54cd0f6656
Strip dfrotz prompt even without preceding newline 2026-02-09 17:55:14 -05:00
b90e61c4fc
Add tests for save overwrite auto-confirm and failure detection 2026-02-09 17:11:59 -05:00
de58209fd0
Add test for chunked IF response reading 2026-02-09 17:07:43 -05:00
43fce6a4ed
Fix line length issues in IF session tests 2026-02-09 16:45:36 -05:00
76488139c8
Add error handling for process communication failures in IF save/restore 2026-02-09 16:45:12 -05:00
bb1f9bbbd8
Prevent double-save when quitting IF sessions 2026-02-09 16:44:13 -05:00
8893525647
Sanitize player names in IF save paths to prevent path traversal 2026-02-09 16:43:09 -05:00
57afe9a3ce
Wire restore into play command
When starting an IF game, check for existing save file and restore
if present. Shows 'restoring saved game...' message and broadcasts
restored game state to spectators.

Also cleaned up redundant tests that didn't properly mock the
auto-save functionality now present in ::quit and stop().
2026-02-09 16:39:15 -05:00
6879da0964
Wire ::save escape command 2026-02-09 16:32:50 -05:00
5f0fec14ef
Add restore functionality to IF sessions 2026-02-09 16:32:23 -05:00
e2bd260538
Add save functionality to IF sessions 2026-02-09 16:31:47 -05:00
6ea82a8496
Add save directory helpers for IF sessions 2026-02-09 16:31:12 -05:00
3dd095b9ea
Add broadcast_to_spectators helper
Helper function sends messages to all players at the same x,y coordinates
as the source player, skipping the source player themselves. Used for IF
spectator broadcasting.
2026-02-09 16:24:48 -05:00
c3f8c8cf12
Add spectator broadcasting tests for IF mode
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.
2026-02-09 16:24:07 -05:00
bc69f46d1a
List stories when cannot find 2026-02-09 16:14:21 -05:00
6308248d14
Fix IF session cleanup on failure and player disconnect 2026-02-09 16:11:28 -05:00