Analysis of how CircleMUD, DikuMUD2, Evennia, and CoffeeMud handle item quantity commands
Author: Research compiled for Ruby MUD implementation Date: 2025 Codebases Analyzed: CircleMUD 3.1, DikuMUD2, Evennia 4.x, CoffeeMud 5.x
- Executive Summary
- Command Syntax Comparison
- CircleMUD Analysis
- DikuMUD2 Analysis
- Evennia Analysis
- CoffeeMud Analysis
- Comparison Table
- Key Insights & Patterns
- Ruby Implementation Guide
- Code Examples
- Testing Recommendations
- Performance Considerations
- Edge Cases
- Real Code Snippets
- Conclusion
This analysis compares how four major MUD codebases handle item quantity commands like get, drop, put, and give with support for:
- Single items:
get sword - Quantities:
get 3 apples - All items:
get all - All of a type:
get all.sword - Containers:
get apple from bag
┌───────────────────────┬───────────┬──────────┬─────────┬───────────┐
│ Command │ CircleMUD │ DikuMUD2 │ Evennia │ CoffeeMud │
├───────────────────────┼───────────┼──────────┼─────────┼───────────┤
│ get apple │ ✓ │ ✓ │ ✓ │ ✓ │
│ get 3 apples │ ✓ │ ✗ │ ✓ │ ✓ │
│ get all │ ✓ │ ✓ │ ✗ │ ✓ │
│ get all.sword │ ✓ │ ✗ │ ✗ │ ✓ │
│ drop all │ ✓ │ ✓ │ ✗ │ ✓ │
│ get apple from bag │ ✓ │ ✓ │ ✗ │ ✓ │
│ get 3 apples from bag │ ✓ │ ✗ │ ✗ │ ✓ │
└───────────────────────┴───────────┴──────────┴─────────┴───────────┘
- Quantity Position (Universal): All codebases use PREFIX position:
get 3 apples✓ (neverget apples 3) - "All" Notation (Varies):
- CircleMUD:
allorall.sword - DikuMUD2:
allonly - Evennia: No "all" by default
- CoffeeMud:
all,all.sword, ORsword.all
- CircleMUD:
- Parsing Strategies:
- CircleMUD: Destructive in-place modification
- DikuMUD2: Keyword-based with " from " delimiter
- Evennia: Clean Python unpacking, immutable
- CoffeeMud: Centralized English library
- Performance: O(n) for most implementations, but CoffeeMud's numeric suffix approach can be O(n²)
Implement the UNION of all capabilities:
get apple # Basic - single item
get 3 apples # Quantity - specific count
get all # All items visible
get all.sword # All of specific type
drop all # Drop all carried items
get apple from bag # From container
get 3 apples from bag # Quantity from container- Format:
<number> <item> - Position: ALWAYS prefix (universal across all MUDs)
- Examples:
get 3 swords,drop 5 apples
- Basic:
get all- get everything visible - Filtered:
get all.sword- get all matching keyword - Variations: CoffeeMud also supports
sword.all
- Format:
<action> <item> from <container> - Examples:
get apple from bag,get 3 apples from chest - Special: CircleMUD supports
get all from all.bag
Files: /src/act.item.c, /src/handler.c
CircleMUD uses a simple dot-notation parser:
int find_all_dots(char *arg)
{
if (!strcmp(arg, "all"))
return (FIND_ALL); // Matches: "all"
else if (!strncmp(arg, "all.", 4)) {
strcpy(arg, arg + 4); // Modifies arg in-place! "all.sword" → "sword"
return (FIND_ALLDOT); // Matches: "all.sword"
} else
return (FIND_INDIV); // Single item
}ACMD(do_get)
{
// Three-argument parsing for: "get [count] <item> [from] <container>"
one_argument(two_arguments(argument, arg1, arg2), arg3);
// Detect if first arg is a number
if (!*arg2)
get_from_room(ch, arg1, 1); // "get apple" → get 1
else if (is_number(arg1) && !*arg3)
get_from_room(ch, arg2, atoi(arg1)); // "get 3 apples" → get 3
}The get_from_room() function handles quantity:
void get_from_room(struct char_data *ch, char *arg, int howmany)
{
struct obj_data *obj, *next_obj;
int dotmode, found = 0;
dotmode = find_all_dots(arg);
if (dotmode == FIND_INDIV) {
// Single item - find first match, then loop 'howmany' times
if (!(obj = get_obj_in_list_vis(ch, arg, NULL, world[IN_ROOM(ch)].contents)))
send_to_char(ch, "You don't see %s %s here.\r\n", AN(arg), arg);
else {
struct obj_data *obj_next;
while(obj && howmany--) { // KEY: Loop takes 'howmany' items
obj_next = obj->next_content;
perform_get_from_room(ch, obj);
obj = get_obj_in_list_vis(ch, arg, NULL, obj_next);
}
}
} else {
// ALL or ALL.type - iterate through all items in room
for (obj = world[IN_ROOM(ch)].contents; obj; obj = next_obj) {
next_obj = obj->next_content;
if (CAN_SEE_OBJ(ch, obj) &&
(dotmode == FIND_ALL || isname(arg, obj->name))) {
found = 1;
perform_get_from_room(ch, obj);
}
}
}
}CircleMUD has sophisticated container handling:
else {
// Three-argument case: "get <obj> from <container>" or "get <num> <obj> from <container>"
cont_dotmode = find_all_dots(arg2);
if (cont_dotmode == FIND_INDIV) {
// Single container
generic_find(arg2, FIND_OBJ_INV | FIND_OBJ_ROOM, ch, &tmp_char, &cont);
if (!cont) { /* error */ }
else
get_from_container(ch, cont, arg1, mode, amount);
} else {
// Multiple containers: "get sword from all.bag"
for (cont = ch->carrying; cont; cont = cont->next_content)
if ((cont_dotmode == FIND_ALL || isname(arg2, cont->name))) {
if (GET_OBJ_TYPE(cont) == ITEM_CONTAINER)
get_from_container(ch, cont, arg1, FIND_OBJ_INV, amount);
}
}
}Pros:
- Simple dot notation for "all of type":
get all.sword - Numeric quantities work:
get 3 apples - Works with containers:
get 3 apples from bag - Partial failure handling: capacity limits stop the loop early
- Clear separation of single-item vs. all-item logic
- Backward compatible (quantities are optional)
Cons:
- Destructive parsing:
find_all_dots()modifies argument string in-place - Numeric quantities must come first:
get 3 apples(notget apples 3) - Complex container logic requires three-argument parsing
- No feedback on how many items were collected vs. attempted
- No limit on "all" operations (server can spam messages)
Files: /dm-dist-ii/act_obj1.c
DikuMUD2 doesn't use dot notation. Instead, it uses explicit keywords:
void do_get(struct unit_data *ch, char *argument,
const struct command_info *cmd)
{
struct unit_data *from_unit = NULL, *thing;
char arg1[MAX_INPUT_LENGTH], *arg2;
// Look for " from " keyword
if ((arg2 = str_cstr(argument, " from "))) {
strncpy(arg1, argument, arg2 - argument);
arg2 += 6; // Skip " from "
from_unit = find_unit(ch, &arg2, 0, FIND_UNIT_HERE);
} else
strcpy(arg1, argument);
// Check for keywords
if (str_ccmp_next_word(arg1, "all")) {
// "get all" → iterate everything
thing = from_unit ? UNIT_CONTAINS(from_unit) : UNIT_CONTAINS(UNIT_IN(ch));
for (; ok && thing; thing = next_unit) {
next_unit = thing->next;
if (UNIT_WEAR(thing, MANIPULATE_TAKE)) {
int tmp_get = get(ch, thing, from_unit, cmd, oarg);
ok = (tmp_get != 2);
pick = (pick || tmp_get == 0);
}
}
} else if (str_ccmp_next_word(arg1, "money")) {
// "get money" → special keyword for all money
} else {
// Single object or quantity of money
char *tmp = arg1;
amount_t amount = 0;
if (next_word_is_number(arg1)) {
tmp = str_next_word(arg1, arg);
amount = (amount_t) atoi(arg);
}
thing = from_unit ?
find_unit(ch, &tmp, UNIT_CONTAINS(from_unit), 0) :
find_unit(ch, &tmp, 0, FIND_UNIT_SURRO);
if (thing && IS_MONEY(thing)) {
if (amount == 0)
amount = MONEY_AMOUNT(thing); // Take all of this money
// ... split_money() for exact amount
}
}
}- Uses
" from "delimiter instead of "from" keyword alone - No dot notation - uses explicit keywords like
"all","money" - Quantity only works for money objects:
get 5 coins - Non-money items with quantities not supported
Pros:
- Clear keyword-based approach (no destructive parsing)
- Money handling is sophisticated (can split money)
- Capacity checking is explicit:
char_can_carry_w() - Good separation of money vs. items
Cons:
- No "all.type" notation for selective "all" operations
- Quantities only work for money, not regular items
- No container traversal with "all":
get all.bagnot supported - Less flexible than CircleMUD for advanced users
Files: /evennia/commands/default/general.py
Evennia uses object-oriented command parsing:
class NumberedTargetCommand(COMMAND_DEFAULT_CLASS):
"""Extract optional number prefix from input"""
def parse(self):
"""
Parser that extracts a `.number` property from input.
Examples:
"3 apples" → self.number = 3, self.args = "apples"
"apples" → self.number = 0, self.args = "apples"
"""
super().parse()
self.number = 0
if getattr(self, "lhs", None):
# handle self.lhs but don't require it
count, *args = self.lhs.split(maxsplit=1)
if args and count.isdecimal():
self.number = int(count)
self.lhs = args[0]
if self.args:
count, *args = self.args.split(maxsplit=1)
if args and count.isdecimal():
self.args = args[0]
if not self.number:
self.number = int(count)Key Points:
- Pythonic: uses tuple unpacking,
maxsplit=1,isdecimal() - Handles both
self.lhs(left-hand side) andself.args - Priority: lhs number takes precedence
- Immutable: Doesn't modify the string, just parses it
class CmdGet(NumberedTargetCommand):
key = "get"
aliases = "grab"
def func(self):
caller = self.caller
if not self.args:
self.msg("Get what?")
return
# Use search() with stacked parameter for quantity matching
objs = caller.search(
self.args,
location=caller.location,
stacked=self.number # Pass quantity to search
)
if not objs:
return
objs = utils.make_iter(objs) # Ensure list
# Validate all objects before moving any
for obj in objs:
if not obj.access(caller, "get"):
self.msg("You can't get that.")
return
if not obj.at_pre_get(caller):
return
# Move all objects
moved = []
for obj in objs:
if obj.move_to(caller, quiet=True, move_type="get"):
moved.append(obj)
obj.at_get(caller)
if not moved:
self.msg("That can't be picked up.")
else:
obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True)
caller.location.msg_contents(f"$You() $conj(pick) up {obj_name}.", from_obj=caller)Evennia separates concerns:
- Parser (NumberedTargetCommand.parse()): Extracts numeric prefix
- Searcher (obj.search()): Finds matching objects and handles stacking
- Command (CmdGet.func()): Orchestrates validation and movement
Pros:
- Clean separation of parsing, searching, and execution
- Numeric quantities:
get 3 applesworks naturally - Stacked items handled transparently
- Pre-validation before any state changes (atomic)
- Clear error messages distinguish parsing from game logic
- Extensible: custom commands can override parse() or search()
- Object-oriented hooks:
at_pre_get(),at_get()
Cons:
- No "all" keyword support by default
- No "all.type" notation
- No container traversal:
get apple from bagnot in default - Quantity must be prefix (matches Ruby idiom though!)
- Search can be slow for large inventories (no indexed lookup)
Files: /Commands/Get.java, /Commands/Drop.java
CoffeeMud delegates parsing to a centralized library:
public boolean execute(final MOB mob, final List<String> commands, int metaFlags) {
// Parse containers using English library
final java.util.List<Container> containers =
CMLib.english().parsePossibleContainers(mob, commands, Wearable.FILTER_ANY, true);
// Parse max quantity
int maxToGet = CMLib.english().parseMaxToGive(mob, commands, containers.size() == 0, R, true);
if (maxToGet < 0)
return false;
String whatToGet = CMParms.combine(commands, 0);
boolean allFlag = (commands.size() > 0) ?
commands.get(0).equalsIgnoreCase("all") : false;
// Check for dot notation variations
if (whatToGet.toUpperCase().startsWith("ALL.")) {
allFlag = true;
whatToGet = "ALL " + whatToGet.substring(4); // "all.sword" → "ALL sword"
}
if (whatToGet.toUpperCase().endsWith(".ALL")) {
allFlag = true;
whatToGet = "ALL " + whatToGet.substring(0, whatToGet.length() - 4); // "sword.all" → "ALL sword"
}
// Iterate to get all matching items with numeric suffix
while ((c < containers.size()) || (containers.size() == 0)) {
int addendum = 1;
String addendumStr = "";
boolean doBugFix = true;
while (doBugFix || ((allFlag) && (addendum <= maxToGet))) {
doBugFix = false;
Environmental getThis = null;
// Fetch with numeric suffix: "sword", "sword.1", "sword.2", etc.
if ((container != null) && (mob.isMine(container)))
getThis = R.fetchFromMOBRoomFavorsItems(mob, container,
whatToGet + addendumStr, Wearable.FILTER_UNWORNONLY);
else {
if (!allFlag)
getThis = CMLib.english().parsePossibleRoomGold(mob, R, container, whatToGet);
if (getThis == null)
getThis = R.fetchFromRoomFavorItems(container, whatToGet + addendumStr);
}
if (getThis == null)
break;
addendumStr = "." + (++addendum); // "sword.1", "sword.2", "sword.3"
}
}
}CoffeeMud uses a clever technique for handling "get all":
- First call:
fetchFromRoomFavorsItems(..., "sword", ...)→ finds "sword" (1st item) - Second call:
fetchFromRoomFavorsItems(..., "sword.2", ...)→ finds 2nd "sword" - Third call:
fetchFromRoomFavorsItems(..., "sword.3", ...)→ finds 3rd "sword" - Continues until
nullreturned
Pros:
- Sophisticated numeric suffix resolution for "all" operations
- Works with quantities:
get 3 apples,drop 5 swords - Handles both
all.typeandtype.alldot notations - Container support:
get apple from bag - Centralized parsing (English library) is DRY
- Raw material handling: can split bundles
- Capacity awareness: respects weight/count limits
- Partial success: if can only carry 3 of 5 items, carries 3
Cons:
- Complex central parsing logic
- Multiple fetch attempts with numeric suffixes could be slow (O(n²))
- "ALL " transformation is done in-place on whatToGet
- Two variations of dot notation (
.allandall.) might confuse players - Relies on natural numeric suffix matching, not explicit item IDs
| Feature | CircleMUD | DikuMUD2 | Evennia | CoffeeMud |
|---|---|---|---|---|
| Basic Get | get apple |
get apple |
get apple |
get apple |
| Numeric Qty | Yes: get 3 apples |
Money only | Yes: get 3 apples |
Yes: get 3 apples |
| All Items | Yes: get all |
Yes: get all |
No | Yes: get all |
| All Type | Yes: get all.sword |
No | No | Yes: get all.sword OR get sword.all |
| From Container | Yes: get apple from bag |
Yes: get apple from bag |
No (default) | Yes: get apple from bag |
| Qty + Container | Yes: get 3 apples from bag |
Yes (money only) | No (default) | Yes: get 3 apples from bag |
| Qty Position | First: get 3 apples |
First: get 5 coins |
First: get 3 apples |
First: get 3 apples |
| Destructive Parse | Yes (find_all_dots) | No | No | Partial (ALL notation) |
| Stacking Support | No (multiple items) | No | Yes (implicit) | Yes (RawMaterial) |
| Partial Success | Yes (capacity) | Yes (capacity) | Yes (implicit) | Yes (capacity) |
| Feedback | Count in message | Count in message | Count in message | Count in message |
Universal Convention: All four codebases place numeric quantity as a PREFIX:
get 3 apples✓ (correct in all)get apples 3✗ (not supported anywhere)
- CircleMUD:
allorall.type(simple, 2 patterns) - DikuMUD2:
allonly, specialmoneykeyword (least flexible) - Evennia: No "all" by default (most permissive, game-specific)
- CoffeeMud:
all,all.type,type.all(most flexible, potentially confusing)
- CircleMUD:
find_all_dots()modifies argument in-place⚠️ - DikuMUD2: No destructive parsing ✓
- Evennia: No destructive parsing ✓
- CoffeeMud: Partial modification of whatToGet
- CircleMUD: Complex but comprehensive (can do
get all.apples from all.bags) - DikuMUD2: Basic support (single container only)
- Evennia: No container support in default commands
- CoffeeMud: Integrated into centralized parsing
- CircleMUD: Linear search with
get_obj_in_list_vis(), linked list - DikuMUD2:
find_unit()system, keyword-based - Evennia:
search()method with stacked results, OOP - CoffeeMud: Numeric suffix appending (
sword,sword.2,sword.3)
- CircleMUD: Stops on first error, detailed messages per item
- DikuMUD2: Continues until capacity hit, "stop" flag tracks this
- Evennia: All-or-nothing validation before moving anything (atomic)
- CoffeeMud: Continues until capacity hit, tracks items successfully moved
while(item = find_next_matching()) && (count < limit) {
perform_action(item)
count++
}
for item in items {
if !validate(item) return error
}
for item in items {
execute(item)
}
Difference: Pattern 2 is atomic - either all succeed or all fail. Pattern 1 is progressive - partial success is possible.
Universal pattern: Quantity is always a PREFIX integer.
class QuantityParser
def self.extract(args)
parts = args.split(maxsplit: 1)
if parts.size == 2 && parts[0].match?(/^\d+$/)
{ quantity: parts[0].to_i, what: parts[1] }
else
{ quantity: 1, what: args }
end
end
end
# Tests:
QuantityParser.extract("3 apples") # => {quantity: 3, what: "apples"}
QuantityParser.extract("apples") # => {quantity: 1, what: "apples"}Recommended: Support all and all.<keyword>
class AllModeParser
ALL_PATTERN = /^all(?:\.(\w+))?$/i
def self.parse(arg)
if arg == "all"
{ mode: :all, keyword: nil }
elsif (match = arg.match(ALL_PATTERN))
{ mode: :all, keyword: match[1] }
else
{ mode: :single, keyword: nil }
end
end
end
# Tests:
AllModeParser.parse("all") # => {mode: :all, keyword: nil}
AllModeParser.parse("all.sword") # => {mode: :all, keyword: "sword"}
AllModeParser.parse("sword") # => {mode: :single, keyword: nil}Recommended: Support from <container> as last argument
class ContainerParser
FROM_PATTERN = /^(.+?)\s+from\s+(.+)$/i
def self.parse(arg)
if (match = arg.match(FROM_PATTERN))
{ what: match[1], container: match[2] }
else
{ what: arg, container: nil }
end
end
end
# Tests:
ContainerParser.parse("apple from bag") # => {what: "apple", container: "bag"}
ContainerParser.parse("3 apples from sack") # => {what: "3 apples", container: "sack"}┌─────────────────────────────────────────────┐
│ Command Router (orchestrates everything) │
└──────────────────┬──────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌───▼─────┐ ┌────────┐ ┌─────▼──────┐
│ Parser │──▶│ Finder │──▶│ Validator │
└─────────┘ └────────┘ └─────┬──────┘
│
┌──────▼───────┐
│ Executor │
└──────────────┘
module Mud::Parsers
class GetCommandParser
def parse(raw_input)
input = raw_input.strip
# Step 1: Extract container clause ("from <container>")
parts = ContainerParser.parse(input)
what = parts[:what]
container = parts[:container]
# Step 2: Parse quantity ("3 apples" → {quantity: 3, what: "apples"})
qty_parts = QuantityParser.extract(what)
quantity = qty_parts[:quantity]
item_pattern = qty_parts[:what]
# Step 3: Detect all mode ("all.sword" → {mode: :all, keyword: "sword"})
all_parts = AllModeParser.parse(item_pattern)
mode = all_parts[:mode]
keyword = all_parts[:keyword]
{
quantity: quantity,
mode: mode, # :single or :all
keyword: keyword, # for "all.sword" → "sword"
container: container, # for "from bag" → "bag"
raw_pattern: item_pattern # original pattern for matching
}
end
end
endmodule Mud
class ItemFinder
def self.find_items(actor, pattern, location, container: nil, quantity: 1, mode: :single)
source = container || location
items = []
case mode
when :single
# Get first N matching items
suffix = 1
quantity.times do
search_pattern = quantity == 1 ? pattern : "#{pattern}.#{suffix}"
item = source.find_item(actor, search_pattern)
break unless item
items << item
suffix += 1
end
when :all
# Get ALL matching items (respecting keyword filter)
suffix = 1
while (item = source.find_item(actor, "#{pattern}.#{suffix}"))
if pattern.empty? || item.matches_keyword?(pattern)
items << item
end
suffix += 1
end
end
items
end
end
endmodule Mud
class TransferValidator
def self.validate_can_transfer(actor, items, source, destination)
errors = []
items.each do |item|
# Check visibility
errors << "#{item.name}: you can't see that" unless actor.can_see?(item)
# Check removability
if source.is_a?(Room)
errors << "#{item.name}: you can't take that" unless actor.can_take?(item)
elsif source.is_a?(Container)
errors << "#{item.name}: you can't remove that from here" unless actor.can_remove_from?(item, source)
end
# Check destination capacity
if destination.is_a?(Character)
if !destination.can_carry_item?(item)
errors << "#{item.name}: inventory full"
end
end
# Item-specific hooks
if !item.can_be_transferred?(actor, source, destination)
errors << "#{item.name}: #{item.transfer_error_message}"
end
end
[errors.empty?, errors]
end
end
endmodule Mud
class GetAction
def self.perform(actor, items, source, quiet: false)
moved = []
items.each do |item|
# Hooks: pre-transfer
next unless item.before_get(actor)
# State change
item.move_to(actor)
moved << item
# Hooks: post-transfer
item.after_get(actor)
end
# Output
unless quiet
if moved.empty?
actor.puts "You couldn't pick up anything."
else
names = moved.map(&:article_name).join(", ")
actor.puts "You pick up #{names}."
source.broadcast_to_others(actor, "#{actor.name} picks up #{names}.")
end
end
moved.size
end
end
endmodule Mud
module Commands
class Get < Command
def execute(raw_input)
# Step 1: Parse
parser = Mud::Parsers::GetCommandParser.new
parsed = parser.parse(raw_input)
# Step 2: Find items
location = actor.location
container = nil
if parsed[:container]
container = location.find_item(actor, parsed[:container]) ||
actor.find_item(parsed[:container])
unless container
actor.puts "You don't see #{parsed[:container]} here."
return
end
source = container
else
source = location
end
items = Mud::ItemFinder.find_items(
actor,
parsed[:raw_pattern],
source,
quantity: parsed[:quantity],
mode: parsed[:mode]
)
if items.empty?
if parsed[:mode] == :all
actor.puts "There's nothing to get here."
else
actor.puts "You don't see #{parsed[:raw_pattern]} here."
end
return
end
# Step 3: Validate ALL items
valid, errors = Mud::TransferValidator.validate_can_transfer(
actor, items, source, actor
)
unless valid
errors.each { |error| actor.puts error }
return
end
# Step 4: Execute (move items)
Mud::GetAction.perform(actor, items, source)
end
end
end
endclass Item
def before_get(actor)
if cursed?
actor.puts "The curse on #{name} prevents you from taking it!"
return false
end
true
end
def after_get(actor)
if magical?
actor.puts "#{name} glows as you pick it up."
actor.location.broadcast_to_others(actor, "#{name} glows softly.")
end
end
def can_be_transferred?(actor, source, destination)
if source.is_a?(Shrine) && holy?
actor.puts "It would be disrespectful to remove #{name} from here."
return false
end
true
end
endclass Container < Item
attr_accessor :max_weight, :contents
def find_item(actor, pattern)
contents.find do |item|
actor.can_see?(item) && item.matches_pattern?(pattern)
end
end
def has_room_for?(item)
current_weight = contents.sum(&:weight)
current_weight + item.weight <= max_weight
end
def can_remove_from?(item, remover)
if locked?
remover.puts "#{name} is locked."
return false
end
true
end
endclass DropAction
def self.perform(actor, items, destination)
dropped = []
failed = []
items.each do |item|
if item.move_to(destination)
dropped << item
item.after_drop(actor)
else
failed << item
end
end
# Always report what was dropped
if dropped.any?
names = dropped.map(&:article_name).join(", ")
actor.puts "You drop #{names}."
destination.broadcast_to_others(actor, "#{actor.name} drops #{names}.")
end
# Report what couldn't be dropped
if failed.any?
names = failed.map(&:article_name).join(", ")
actor.puts "You couldn't drop: #{names}"
end
dropped.size
end
enddescribe Mud::Parsers::GetCommandParser do
let(:parser) { described_class.new }
describe "quantity extraction" do
it "extracts leading number" do
result = parser.parse("get 3 apples")
expect(result[:quantity]).to eq(3)
expect(result[:raw_pattern]).to eq("apples")
end
it "defaults quantity to 1" do
result = parser.parse("get apple")
expect(result[:quantity]).to eq(1)
end
end
describe "container extraction" do
it "extracts from clause" do
result = parser.parse("get apple from bag")
expect(result[:container]).to eq("bag")
expect(result[:raw_pattern]).to eq("apple")
end
it "handles quantity with container" do
result = parser.parse("get 3 apples from sack")
expect(result[:quantity]).to eq(3)
expect(result[:container]).to eq("sack")
end
end
describe "all-dot notation" do
it "parses all.keyword" do
result = parser.parse("get all.sword")
expect(result[:mode]).to eq(:all)
expect(result[:raw_pattern]).to eq("sword")
end
end
enddescribe GetCommand do
let(:actor) { create_player }
let(:room) { create_room }
let(:apple) { create_item("apple", "fruit", room) }
let(:command) { GetCommand.new(actor) }
before { actor.location = room }
it "gets a single item" do
result = command.execute("get apple")
expect(actor.carrying).to include(apple)
end
it "gets multiple identical items with quantity" do
apple2 = create_item("apple", "fruit", room)
apple3 = create_item("apple", "fruit", room)
result = command.execute("get 2 apples")
expect(actor.carrying).to include(apple, apple2)
expect(room.contains).to include(apple3)
end
it "gets all items with all" do
sword = create_item("sword", "weapon", room)
result = command.execute("get all")
expect(actor.carrying).to include(apple, sword)
expect(room.contains).to be_empty
end
it "respects capacity limits" do
(1..5).each { |i| create_item("apple", "fruit", room) }
actor.max_carry = 3
result = command.execute("get all")
expect(actor.carrying.size).to eq(3)
expect(room.contains.size).to eq(2)
end
endWith "get all", sequential lookups can be O(n²):
find_item(actor, "sword.1")
find_item(actor, "sword.2")
find_item(actor, "sword.3")
... (N times for N items)
class Room
def find_all_matching(actor, keyword)
@items.select { |item| actor.can_see?(item) && item.matches_keyword?(keyword) }
end
endclass Room
def find_items_matching(actor, pattern, max_count = nil)
matching = @items.select do |item|
actor.can_see?(item) && item.matches_pattern?(pattern)
end
max_count ? matching.first(max_count) : matching
end
end
# Usage:
items = room.find_items_matching(actor, "apple", 3) # Get up to 3 apples- Empty input:
get→ "Get what?" - Not found:
get unicorn→ "You don't see that." - Capacity:
get 5 appleswhen can carry 3 → partial success - Ambiguous:
get apple(3 apples) → prompt for numeric suffix - Container locked: "The chest is locked."
- Item too heavy: "That's too heavy."
- Cursed items: "You can't take that."
- Container not container: "That's not a container."
- Partial failure: Report what succeeded and what failed
int find_all_dots(char *arg)
{
if (!strcmp(arg, "all"))
return (FIND_ALL);
else if (!strncmp(arg, "all.", 4)) {
strcpy(arg, arg + 4); /* strcpy: OK (always less) */
return (FIND_ALLDOT);
} else
return (FIND_INDIV);
}def parse(self):
super().parse()
self.number = 0
if getattr(self, "lhs", None):
count, *args = self.lhs.split(maxsplit=1)
if args and count.isdecimal():
self.number = int(count)
self.lhs = args[0]
if self.args:
count, *args = self.args.split(maxsplit=1)
if args and count.isdecimal():
self.args = args[0]
if not self.number:
self.number = int(count)// Handle "all.sword" notation
if (whatToGet.toUpperCase().startsWith("ALL.")) {
allFlag = true;
whatToGet = "ALL " + whatToGet.substring(4);
}
// Handle "sword.all" notation
if (whatToGet.toUpperCase().endsWith(".ALL")) {
allFlag = true;
whatToGet = "ALL " + whatToGet.substring(0, whatToGet.length() - 4);
}The best Ruby implementation would combine:
- ✅ Evennia's parsing model: Clean separation of number/quantity parsing
- ✅ CircleMUD's notation: Support
allandall.type - ✅ Evennia's validation: All-or-nothing before execution
- ✅ CoffeeMud's flexibility: Support both
fromcontainers and quantities - ✅ CoffeeMud's partial success: Handle capacity gracefully
- ✅ Evennia's hooks: Extensible item/container callbacks
- Parse thoroughly: Separate quantity, mode (single/all), keyword, and container
- Validate atomically: Check ALL items before moving ANY
- Handle partial success: Report what was transferred and what failed
- Use hooks: Let items customize their transfer behavior
- Avoid destructive parsing: Don't modify input strings in-place
- Index strategically: Use keyword indexing for "all" operations
- Provide clear feedback: Tell players exactly what happened and why
- Test edge cases: Empty inventory, capacity limits, validation failures
- Quantity position is universal: Always prefix (
get 3 apples) - "All" notation varies: CircleMUD's
all.typeis intuitive - Destructive parsing is risky: Prefer immutable operations
- Atomic validation is cleaner: But prevents partial success
- Performance matters: Avoid O(n²) numeric suffix loops
- Hooks enable extensibility: Let game designers customize behavior
This creates a system that is:
- Flexible: Handles most MUD syntaxes players expect
- Intuitive: Prefix position for quantities matches all codebases
- Safe: Validates before executing (atomic operations)
- Graceful: Handles partial success with clear feedback
- Extensible: Hooks allow customization without modifying core logic
- CircleMUD:
/src/act.item.c(lines 289-599),/src/handler.c(lines 1384-1393) - DikuMUD2:
/dm-dist-ii/act_obj1.c(lines 243-510) - Evennia:
/evennia/commands/default/general.py(lines 384-542) - CoffeeMud:
/Commands/Get.java(lines 102-230),/Commands/Drop.java(lines 109-230)
Document Version: 1.0 Last Updated: 2025 License: Public Domain / CC0