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.
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.
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.
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().
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.
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.
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".
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.
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.
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)
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
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
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.