15 KiB
Pre-Commit Test Checklist
Automated smoke test (same as CI): run from the repo root:
./run-tests.sh
This builds the server and mudtool, starts the server with a temporary DB, runs the smoke test below (new player, persistence, admin, registration gate, combat), then cleans up. Use it locally to match what Gitea Actions run on push/pull_request. The script uses MUD_TEST_DB (default ./mudserver.db.test) so it does not overwrite your normal mudserver.db.
Prerequisites: Rust toolchain (cargo), ssh client. In CI, Rust is installed by the workflow.
Run through the checks below before every commit to ensure consistent feature coverage.
Build
cargo buildsucceeds with no errorscargo build --bin mudtoolsucceeds
Server Startup
- Server starts:
RUST_LOG=info ./target/debug/mudserver - World loads all rooms, NPCs, objects, races, classes (check log output)
- Database opens (or creates) successfully
- Tick engine starts and logs tick rate
Character Creation
- New player SSH → gets chargen flow (race + class selection)
- Chargen accepts both number and name input
- All races display with expanded info (size, traits, natural attacks, vision)
- Dragon race shows custom body slots, natural armor, fire breath, vision types
- After chargen, player appears in spawn room with correct stats
- Stats reflect race modifiers (STR, DEX, CON, INT, WIS, PER, CHA)
- Player saved to DB after creation
Player Persistence
- Reconnecting player skips chargen, sees "Welcome back!"
- Room, stats, inventory, equipment all restored from DB
- Status effects persist across logout/login (stored in DB)
- Status effects continue ticking while player is offline
- On reconnect, expired effects are gone; active effects resume
- Verify with:
sqlite3 mudserver.db "SELECT * FROM players;"
Look Command
lookwith no args shows the room (NPCs, objects, exits, players)look <npc>shows NPC description, stats, and attitudelook <object>shows object description (floor or inventory)look <exit>shows where the exit leadslook <player>shows player race, level, combat statuslook <invalid>shows "You don't see X here."
Movement & Navigation
go north,n,south,s, etc. all work- Invalid direction shows error
- Room view shows: NPCs (colored by attitude), objects, exits, other players
- Cannot move while in combat (combat lockout)
NPC Interaction
examine <npc>shows description, stats, attitude labeltalk <friendly>shows greeting dialoguetalk <hostile>shows snarl message- Dead NPCs don't appear in room view
Combat - Tick-Based
attack <npc>enters combat state with any NPC that has combat stats- Attacking friendly/neutral NPCs is allowed but incurs attitude penalties
- Attacking non-hostile NPC: attitude shift -30 individual, -15 faction
- "The locals look on in horror" message when attacking non-hostile
- Combat rounds resolve automatically on server ticks (not on command)
- Player receives tick-by-tick combat output (damage dealt, damage taken)
- Default combat action is "attack" if no other action queued
defend/defsets defensive stance (reduced incoming damage next tick)- NPC death: awards XP, shifts attitude -10, shifts faction -5
- Player death: respawns at spawn room with full HP, combat cleared, effects cleared
- NPCs respawn after configured time
- Combat lockout: can only attack/defend/flee/look/stats/inv/use/quit during combat
fleequeues escape attempt — may fail based on statsuse <item>in combat queues item use for next tick- Multiple ticks of combat resolve correctly without player input
- Combat ends when NPC dies (player exits combat state)
- Combat ends when player flees successfully
- NPCs without explicit [combat] section get default stats (20 HP, 4 ATK, 2 DEF, 5 XP)
Combat - NPC AI
- Hostile NPCs auto-engage players who enter their room
- Aggressive NPCs do NOT auto-engage (only attackable, not initiating)
- NPC attacks resolve on tick alongside player attacks
- NPCs in combat continue attacking even if player sends no commands
Combat - RNG
- Damage varies between hits (not identical each time)
- Multiple rapid attacks produce different damage values
Items & Equipment Slots
take <item>picks up takeable objectsdrop <item>places item in roomequip <weapon>equips tomain_handslot (backwards-compat via kind)equip <armor>equips to appropriate slot (objslotfield or fallback)- Equipping to an occupied slot returns old item to inventory
equipfails if race doesn't have the required slot- Objects with explicit
slotfield use that slot use <consumable>heals and removes item (immediate out of combat)use <consumable>in combat queues for next tickinventoryshows equipped items by slot name + bag itemsstatsshows equipment bonuses and natural bonuses separately
Status Effects
- Poison deals damage each tick, shows message to player
- Regeneration heals each tick, shows message to player
- Status effects expire after their duration (in ticks)
statscommand shows active status effects and remaining duration- Multiple status effects can be active simultaneously
- Negative status effects cleared on player death/respawn
- Status effects on offline players resolve by wall-clock time on next login
Guilds
guild listshows all available guilds with descriptionsguild info <name>shows guild details, growth stats, and spell listguild join <name>adds player to guild at level 1guild joingrants base mana/endurance from guildguild leave <name>removes guild membership- Cannot join a guild twice
- Cannot leave a guild you're not in
- Race-restricted guilds reject restricted races
- Player level requirement enforced on
guild join - Multi-guild: player can be in multiple guilds simultaneously
- Guild membership persists across logout/login (stored in player_guilds table)
statsshows guild memberships with levels
Spells & Casting
spellslists known spells grouped by guild with cost and cooldown- Spells filtered by guild level (only shows spells at or below current guild level)
cast <spell>out of combat: heal/utility resolves immediatelycast <spell>out of combat: offensive spells blocked ("enter combat first")cast <spell>in combat: queues as CombatAction, resolves on tick- Spell resolves: offensive deals damage to NPC, shows damage + type
- Spell resolves: heal restores HP, shows amount healed
- Spell resolves: utility applies status effect
- Mana deducted on cast; blocked if insufficient ("Not enough mana")
- Endurance deducted on cast; blocked if insufficient
- Cooldowns applied after casting; blocked if on cooldown ("X ticks remaining")
- Cooldowns tick down each server tick
- NPC can die from spell damage (awards XP, ends combat)
- Spell partial name matching works ("magic" matches "Magic Missile")
Chargen & Guild Seeding
- Class selection shows "→ joins " when class has a guild
- Creating a character with a class that references a guild auto-joins that guild
- Starting mana/endurance calculated from guild base + racial stat modifiers
- Guild membership saved to DB at character creation
Passive Regeneration
- Players out of combat slowly regenerate HP over ticks
- Players out of combat slowly regenerate mana over ticks
- Players out of combat slowly regenerate endurance over ticks
- Regeneration does not occur while in combat
- HP/mana/endurance do not exceed their maximums
Tick Engine
- Tick runs at configured interval (~3 seconds)
- Tick processes: NPC AI → combat rounds → status effects → respawns → regen
- Tick output is delivered to players promptly
- Server remains responsive to immediate commands between ticks
- Multiple players in separate combats are processed independently per tick
NPC Race & Class
- NPCs with fixed race/class in TOML show that race/class
- NPCs without race get a random non-hidden race at spawn
- NPCs without class: race default_class used, or random non-hidden if no default
look <npc>shows NPC race and classexamine <npc>shows NPC race and class- Rat shows "Beast Creature" (fixed race/class)
- Barkeep shows a random race + Peasant (no fixed race, human default class)
- Thief shows random race + Rogue (no fixed race, fixed class)
- Guard shows random race + Warrior (no fixed race, fixed class)
- On NPC respawn, race/class re-rolled if not fixed in TOML
- Hidden races (Beast) do not appear in character creation
- Hidden classes (Peasant, Creature) do not appear in character creation
Race System
- Existing races (Human, Elf, Dwarf, Orc, Halfling) load with expanded fields
- Dragon race loads with custom body, natural attacks, resistances, traits
- Dragon gets custom equipment slots (forelegs, hindlegs, wings, tail)
- Dragon's natural armor (8) shows in stats and affects defense
- Dragon's natural attacks (fire breath 15dmg) affect effective attack
- Items magically resize — no size restrictions on gear (dragon can use swords)
- Races without explicit [body.slots] get default humanoid slots
- Stat modifiers include PER (perception) and CHA (charisma)
- Race traits and disadvantages display during chargen
- XP rate modifier stored per race (dragon = 0.7x)
- Regen modifiers stored per race (dragon HP regen = 1.5x)
Attitude System
- Per-player NPC attitudes stored in DB
examineshows attitude label per-player- Killing NPC shifts attitude (individual -10, faction -5)
- Verify:
sqlite3 mudserver.db "SELECT * FROM npc_attitudes;"
Admin System
- Non-admin can't use
admincommands (gets error) - Set admin via mudtool:
mudtool players set-admin <name> true admin helpshows admin command listadmin promote <player>grants admin (verify in DB)admin demote <player>revokes adminadmin kick <player>disconnects target playeradmin teleport <room_id>warps to room (shows room list on invalid)admin registration offblocks new player creationadmin registration onre-enables itadmin announce <msg>broadcasts to all playersadmin healheals self;admin heal <player>heals targetadmin info <player>shows detailed stats + attitudesadmin setattitude <player> <npc> <value>modifies attitudeadmin listshows all players with online/offline status
Registration Gate
- With registration open (default), new players can create characters
- With registration off, new SSH connections get rejection message
- Existing players can still log in when registration is closed
MUD Tool - CLI
mudtool validate -w ./worldchecks world data (schemas, references, values)mudtool players listshows all playersmudtool players show <name>shows detailsmudtool players set-admin <name> trueworksmudtool players delete <name>removes player + attitudesmudtool settings listshows settingsmudtool settings set registration_open falseworksmudtool attitudes list <player>shows attitudesmudtool attitudes set <player> <npc> <value>works
MUD Tool - TUI
mudtool tuilaunches interactive interface- Tab/1/2/3 switches between Players, Settings, Attitudes tabs
- Arrow keys navigate rows
- 'a' toggles admin on Players tab
- 'd' prompts delete confirmation on Players tab
- Enter edits value on Settings and Attitudes tabs
- ←→ switches player on Attitudes tab
- 'q' exits TUI
JSON-RPC Interface
list_commandsreturns the currently handleable command list- New commands added in
commands.rsare automatically discovered loginaccepts an existing player name (requires character to be created first)- Command output is stripped of ANSI color codes for API consumption
- Verify manually with:
echo '{"_jsonrpc": "2.0", "method": "list_commands", "params": {}, "id": 1}' | nc localhost 2223
Quick Smoke Test Script
The canonical implementation is ./run-tests.sh (see top of this file). The following is the same sequence for reference; when writing or extending tests, keep run-tests.sh and this section in sync.
# Start server in background (use -d for test DB so you don't overwrite mudserver.db)
TEST_DB=./mudserver.db.test
RUST_LOG=info ./target/debug/mudserver -d "$TEST_DB" &
SERVER_PID=$!
sleep 2
# Test 1: New player creation + basic commands
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
1
1
look
stats
go north
talk barkeep
go south
go south
examine thief
attack thief
flee
quit
EOF
# Test 2: Persistence - reconnect
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
look
stats
quit
EOF
# Test 3: Admin via mudtool (use same test DB)
./target/debug/mudtool -d "$TEST_DB" players list
./target/debug/mudtool -d "$TEST_DB" players set-admin smoketest true
./target/debug/mudtool -d "$TEST_DB" players show smoketest
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
./target/debug/mudtool -d "$TEST_DB" settings list
# Test 4: Admin commands in-game
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
admin help
admin list
admin registration on
admin info smoketest
quit
EOF
# Test 5: Registration gate
./target/debug/mudtool -d "$TEST_DB" settings set registration_open false
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 newplayer@localhost <<'EOF'
quit
EOF
# Test 6: Tick-based combat (connect and wait for ticks)
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
1
1
go south
go south
attack thief
EOF
# Wait for several combat ticks to resolve
sleep 8
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 smoketest@localhost <<'EOF'
stats
quit
EOF
# Verify XP changed (combat happened via ticks)
# Test 7: JSON-RPC interface and dynamic command list
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 rpctest@localhost <<'EOF'
1
1
quit
EOF
echo '{"_jsonrpc": "2.0", "method": "login", "params": {"username": "rpctest"}, "id": 1}' | nc -w 2 localhost 2223
echo '{"_jsonrpc": "2.0", "method": "list_commands", "params": {}, "id": 2}' | nc -w 2 localhost 2223
# Cleanup
./target/debug/mudtool -d "$TEST_DB" settings set registration_open true
./target/debug/mudtool -d "$TEST_DB" players delete smoketest
./target/debug/mudtool -d "$TEST_DB" players delete rpctest
kill $SERVER_PID