Commit graph

192 commits

Author SHA1 Message Date
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
8a8e3dd6e8
Fix line-too-long lint errors in IF mode tests 2026-02-09 16:10:29 -05:00
b133f2febe
Add play command for starting interactive fiction games 2026-02-09 16:10:29 -05:00
dc342224b1
Wire IF mode into server shell loop and player model 2026-02-09 15:57:24 -05:00
d210033f33
Add IFSession class for interactive fiction subprocess management
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.
2026-02-09 15:54:47 -05:00
af941b329b
Add spawn command and wire mobs into server
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().
2026-02-09 11:54:29 -05:00
d15238eb4e
Add mob AI for combat decisions
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.
2026-02-09 11:54:29 -05:00
91c1af86e2
Handle mob defeat in combat resolution
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.
2026-02-09 11:54:29 -05:00
ca53357730
Render mobs as * in viewport
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).
2026-02-09 11:54:29 -05:00
e6bfd77464
Add mob target resolution in combat commands
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.
2026-02-09 11:54:29 -05:00
84cd75e3a3
Add mob templates, registry, and spawn/despawn/query
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.
2026-02-09 11:54:29 -05:00
f36085c921
Add rest command for stamina recovery 2026-02-08 22:16:47 -05:00
0f7404cb12
Fix prefix matching for combat move variants
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.
2026-02-08 14:41:40 -05:00
77c2e40e0e
Add reload command for hot-reloading TOML content 2026-02-08 14:32:51 -05:00
8c83130f67
Add test for alias exact match over prefix 2026-02-08 13:48:32 -05:00
6e9a89f21e
Update tests for clean command listings 2026-02-08 13:41:36 -05:00
7ae56106d6
Update tests for alias removal 2026-02-08 13:39:58 -05:00
841714d6ca
Update tests for alias removal 2026-02-08 13:37:40 -05:00
e00591b6bf
Fix test fixture to not interfere with other tests 2026-02-08 13:34:46 -05:00
20f33f45e1
Implement standalone help command 2026-02-08 13:33:46 -05:00
7c313ae307
Implement prefix matching in dispatch 2026-02-08 13:33:22 -05:00
5efdbaf4e6
Add help command tests 2026-02-08 13:32:50 -05:00
1a122b85e5
Add collision detection tests 2026-02-08 13:32:49 -05:00
f1e4cfa4dd
Add prefix matching tests 2026-02-08 13:32:32 -05:00
529320acb5
Split commands and skills into separate listings 2026-02-08 13:15:04 -05:00
a8917da59e
Fix typecheck warnings for nullable current_move 2026-02-08 13:05:16 -05:00
d0c33911f3
Wire edit command to open combat TOML files 2026-02-08 12:44:56 -05:00
1b63f87da7
Add detail view to commands command 2026-02-08 12:41:02 -05:00
6fb48e9ae1
Add commands command to list available commands 2026-02-08 12:35:50 -05:00
3c5c1490e6
Replace defense lock with sleep-based commitment blocking
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.
2026-02-08 12:28:17 -05:00
2de1ebd59e
Fix variant defense mode and test cleanup
- 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
2026-02-08 12:28:17 -05:00
1b3684dc65
Add 30-second idle timeout for combat encounters
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.
2026-02-08 12:28:17 -05:00
e368ed1843
Add defense commitment lock and defense-everywhere support
Defenses now work outside combat mode with stamina cost, recovery lock
(based on timing_window_ms), and broadcast to nearby players. Lock
prevents spamming defenses — you commit to the move. Stamina deduction
moved from encounter.defend() to do_defend command layer. Defense
commands registered with mode="*" instead of "combat".
2026-02-08 12:28:17 -05:00
cf423fb22b
Add attack switching (feints) during telegraph and window phases
Attacker can change their move mid-telegraph or mid-window without
resetting the timer. Old move's stamina is refunded, new move charged.
Defender gets a fresh telegraph on switch. Feedback says "switch to"
instead of "use" when swapping attacks.
2026-02-08 12:28:17 -05:00
9054962f5d
Add combat resolution messages with both-POV feedback
resolve() returns ResolveResult dataclass with attacker_msg, defender_msg,
damage, countered, and combat_ended fields. process_combat is now async
and sends messages to both participants on resolve. Counter, hit, and
slam messages give each player their own perspective on what happened.
2026-02-08 12:28:17 -05:00
6344c09275
Restructure combat moves: single-word commands with variant args
The DREAMBOOK always described "punch right/left [target]" as one command
with a direction argument, but the implementation had separate TOML files
and multi-word command names that the dispatcher couldn't reach (it only
matches the first word). Aliases like "pr" also couldn't pass targets
because the shared handler tried to re-derive the move from args.

Changes:
- Merge punch_left/right, dodge_left/right, parry_high/low into single
  TOML files with [variants] sections
- Add command/variant fields to CombatMove for tracking move families
- load_move() now returns list[CombatMove], expanding variants
- Handlers bound to moves via closures at registration time:
  variant handler for base commands (punch → parses direction from args),
  direct handler for aliases and simple moves (pr → move already known)
- Core logic in do_attack/do_defend takes a resolved move
- Combat doc rewritten as rst with architecture details
- Simplify mud.tin aliases (pr/pl/etc are built-in MUD commands now)
2026-02-08 00:20:52 -05:00
d3df09f4de
Fix editor search/replace parsing, dirty flag, and cursor tracking 2026-02-07 23:06:47 -05:00
0574457404
Add syntax highlighting to editor buffer display 2026-02-07 23:01:55 -05:00
a799b6716c
Add editor mode shell integration and edit command
Integrates the Editor class into the MUD server's shell loop, allowing
players to enter and use the text editor from the game.

Changes:
- Add editor field to Player dataclass
- Modify shell input loop to check player mode and route to editor
- Add edit command to enter editor mode from normal mode
- Use inp (not command.strip()) for editor to preserve indentation
- Show line-numbered prompt in editor mode
- Pop mode and clear editor when done=True
- Add comprehensive integration tests
- Fix test isolation issue in test_movement_updates_position
2026-02-07 22:59:37 -05:00
23507d0e70
Add editor class with buffer, commands, and undo 2026-02-07 22:55:53 -05:00
b0fcb080d3
Wire client capabilities into Player & terrain
Parse MTTS from telnetlib3 writer during connection and store capabilities
on Player.caps field. Add convenience property Player.color_depth that
delegates to caps.color_depth for easy access by rendering code.

Changes:
- Add caps field to Player with default 16-color ANSI capabilities
- Parse MTTS in server shell after Player creation using parse_mtts()
- Add Player.color_depth property for quick capability checks
- Add tests verifying Player caps integration and color_depth property
2026-02-07 22:44:45 -05:00
6549d09683
Add 256-color and truecolor SGR helpers
Extended ansi.py with fg_256, bg_256, fg_rgb, and bg_rgb functions
for generating 256-color and truecolor escape sequences. All functions
include value clamping to valid ranges (0-255).
2026-02-07 22:44:23 -05:00
388e693f8c
Add MTTS capability parsing module with client color detection
Parses MTTS bitfield values from telnetlib3 ttype3 into a ClientCaps dataclass.
Includes color_depth property that returns the best available color mode
(truecolor, 256, or 16) based on client capabilities.
2026-02-07 22:44:23 -05:00
54998c29c5
Add save on logout and disconnect
Player state is now saved when using the quit command or when the connection
is lost unexpectedly. Ensures progress is preserved even without auto-save.
2026-02-07 21:42:16 -05:00
3fe51f2552
Add login and registration flow with server integration
Adds login/registration prompts on connection, database initialization on
startup, and periodic auto-save every 5 minutes in the game loop. Player
state is now tied to authenticated accounts.
2026-02-07 21:42:12 -05:00
485302fab3
Add store module with SQLite account persistence
Implements account management with password hashing (pbkdf2_hmac with SHA256)
and constant-time comparison. Includes player state serialization for position
and inventory persistence across sessions.
2026-02-07 21:42:07 -05:00
dbb976be24
Add data-driven combat system with TOML move definitions
Combat moves defined as TOML content files in content/combat/,
not engine code. State machine (IDLE > TELEGRAPH > WINDOW > RESOLVE)
processes timing-based exchanges. Counter relationships, stamina
costs, damage formulas all tunable from data files.

Moves: punch right/left, roundhouse, sweep, dodge right/left,
parry high/low, duck, jump. Combat ends on knockout (PL <= 0)
or exhaustion (stamina <= 0).
2026-02-07 21:16:12 -05:00
d159a88ca4
Add TOML content loader for declarative command definitions
Scan content/commands/ for .toml files at startup and register them
as commands alongside Python-defined ones. Two flavors: handler-based
(points to a Python callable via module:function) and message-based
(auto-generates a handler from inline text). Includes example MOTD
command, type validation, error logging, and full test coverage.
2026-02-07 20:27:29 -05:00
8fbee01c11
Use contextlib.suppress for cleaner exception handling 2026-02-07 16:17:41 -05:00
075a6ce303
Add game loop skeleton for periodic tick processing 2026-02-07 16:17:20 -05:00
d220835f7d
Add mode stack to Player and mode check in dispatch 2026-02-07 16:17:01 -05:00
dcc8b961bb
Add CommandDefinition and migrate command registry 2026-02-07 16:15:21 -05:00
bea2a73c98
Read world config from TOML instead of hardcoding 2026-02-07 16:14:03 -05:00
9948a36f5f
Add world cache to speedup startup 2026-02-07 15:00:07 -05:00
a10f3d4e70
Stagger cloud trail dissolution so tiles fade one at a time
Each cloud in the trail gets a slightly longer TTL than the one
before it (0.15s stagger). The origin cloud dissolves first, then
each subsequent tile follows. Two consecutive flights produce a
trail where the oldest clouds are already gone.
2026-02-07 14:48:42 -05:00
93ad4523e2
Make flying a toggle state
fly with no args toggles flying on/off. Movement commands (fly east,
etc) only work while airborne. "You aren't flying." if you try to
move without toggling on first. Player.flying field tracks the state.
2026-02-07 14:48:42 -05:00
9844749edd
Add fly command with cloud trail effects
fly <direction> moves the player 5 tiles, ignoring terrain. Leaves
a trail of bright white ~ clouds that fade after 2 seconds. Effects
system supports arbitrary timed visual overlays on the viewport.
TinTin aliases: fn/fs/fe/fw/fne/fnw/fse/fsw.
2026-02-07 14:48:42 -05:00
8934397b1e
Make world wrap seamlessly in both axes
Tileable Perlin noise: each octave wraps its integer grid coordinates
with modulo at the octave's frequency, so gradients at opposite edges
match and the noise field is continuous across the boundary.

Coarse elevation grid interpolation wraps instead of padding boundary
cells. Rivers can flow across world edges. All coordinate access
(get_tile, is_passable, get_viewport) wraps via modulo. Movement,
spawn search, nearby-player detection, and viewport relative positions
all handle the toroidal topology.
2026-02-07 13:50:06 -05:00
0d0c142993
Add seed-based terrain world with movement and viewport
1000x1000 tile world generated deterministically from a seed using
layered Perlin noise. Terrain derived from elevation: mountains,
forests, grasslands, sand, water, with rivers traced downhill from
peaks. ANSI-colored viewport centered on player.

Command system with registry/dispatch, 8-direction movement (n/s/e/w
+ diagonals), look/l, quit/q. Players see arrival/departure messages.

Set connect_maxwait=0.5 on telnetlib3 to avoid the 4s CHARSET
negotiation timeout — MUD clients reject CHARSET immediately via MTTS.
2026-02-07 13:27:44 -05:00
3ebff56017 Use telnetlib3 readline correctly 2026-02-07 12:20:00 -05:00
04230eb152 Use async readline with local telnetlib3 2026-02-07 10:23:56 -05:00
a83248b387 Add bare echoing telnet server 2026-02-07 10:05:34 -05:00
541415e011 Add first generation 2026-02-07 09:45:48 -05:00