Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save TiuTalk/4c8c8ec73cbeffe1f75ffc0c94a57dec to your computer and use it in GitHub Desktop.
Save TiuTalk/4c8c8ec73cbeffe1f75ffc0c94a57dec to your computer and use it in GitHub Desktop.
MUD Item Quantity Commands: Analysis of CircleMUD, DikuMUD2, Evennia, and CoffeeMud implementations

MUD Item Quantity Commands: Comprehensive Analysis

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


Table of Contents

  1. Executive Summary
  2. Command Syntax Comparison
  3. CircleMUD Analysis
  4. DikuMUD2 Analysis
  5. Evennia Analysis
  6. CoffeeMud Analysis
  7. Comparison Table
  8. Key Insights & Patterns
  9. Ruby Implementation Guide
  10. Code Examples
  11. Testing Recommendations
  12. Performance Considerations
  13. Edge Cases
  14. Real Code Snippets
  15. Conclusion

Executive Summary

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

Quick Comparison Matrix

┌───────────────────────┬───────────┬──────────┬─────────┬───────────┐
│ 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 │     ✓     │    ✗     │    ✗    │     ✓     │
└───────────────────────┴───────────┴──────────┴─────────┴───────────┘

Key Findings

  1. Quantity Position (Universal): All codebases use PREFIX position: get 3 apples ✓ (never get apples 3)
  2. "All" Notation (Varies):
    • CircleMUD: all or all.sword
    • DikuMUD2: all only
    • Evennia: No "all" by default
    • CoffeeMud: all, all.sword, OR sword.all
  3. Parsing Strategies:
    • CircleMUD: Destructive in-place modification
    • DikuMUD2: Keyword-based with " from " delimiter
    • Evennia: Clean Python unpacking, immutable
    • CoffeeMud: Centralized English library
  4. Performance: O(n) for most implementations, but CoffeeMud's numeric suffix approach can be O(n²)

Command Syntax Comparison

Recommended Ruby Support

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

Syntax Breakdown

1. Quantity Specification

  • Format: <number> <item>
  • Position: ALWAYS prefix (universal across all MUDs)
  • Examples: get 3 swords, drop 5 apples

2. "All" Operations

  • Basic: get all - get everything visible
  • Filtered: get all.sword - get all matching keyword
  • Variations: CoffeeMud also supports sword.all

3. Container Operations

  • Format: <action> <item> from <container>
  • Examples: get apple from bag, get 3 apples from chest
  • Special: CircleMUD supports get all from all.bag

CircleMUD Analysis

Files: /src/act.item.c, /src/handler.c

Parsing Mechanism: find_all_dots()

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
}

⚠️ Critical: This function MODIFIES the argument string in-place by stripping the "all." prefix.

Quantity Handling

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
}

Implementation Details

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);
      }
    }
  }
}

Container Support

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/Cons

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 (not get 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)

DikuMUD2 Analysis

Files: /dm-dist-ii/act_obj1.c

Parsing Mechanism: Keyword-Based

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
    }
  }
}

Key Differences

  • 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/Cons

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.bag not supported
  • Less flexible than CircleMUD for advanced users

Evennia Analysis

Files: /evennia/commands/default/general.py

Parsing Mechanism: NumberedTargetCommand

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) and self.args
  • Priority: lhs number takes precedence
  • Immutable: Doesn't modify the string, just parses it

CmdGet Implementation

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)

Key Design Insight

Evennia separates concerns:

  1. Parser (NumberedTargetCommand.parse()): Extracts numeric prefix
  2. Searcher (obj.search()): Finds matching objects and handles stacking
  3. Command (CmdGet.func()): Orchestrates validation and movement

Pros/Cons

Pros:

  • Clean separation of parsing, searching, and execution
  • Numeric quantities: get 3 apples works 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 bag not in default
  • Quantity must be prefix (matches Ruby idiom though!)
  • Search can be slow for large inventories (no indexed lookup)

CoffeeMud Analysis

Files: /Commands/Get.java, /Commands/Drop.java

Parsing Mechanism: English Library

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"
        }
    }
}

Numeric Suffix Resolution

CoffeeMud uses a clever technique for handling "get all":

  1. First call: fetchFromRoomFavorsItems(..., "sword", ...) → finds "sword" (1st item)
  2. Second call: fetchFromRoomFavorsItems(..., "sword.2", ...) → finds 2nd "sword"
  3. Third call: fetchFromRoomFavorsItems(..., "sword.3", ...) → finds 3rd "sword"
  4. Continues until null returned

Pros/Cons

Pros:

  • Sophisticated numeric suffix resolution for "all" operations
  • Works with quantities: get 3 apples, drop 5 swords
  • Handles both all.type and type.all dot 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 (.all and all.) might confuse players
  • Relies on natural numeric suffix matching, not explicit item IDs

Comparison Table

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

Key Insights & Patterns

1. Quantity Parsing Position

Universal Convention: All four codebases place numeric quantity as a PREFIX:

  • get 3 apples ✓ (correct in all)
  • get apples 3 ✗ (not supported anywhere)

2. "All" Notation Approaches

  • CircleMUD: all or all.type (simple, 2 patterns)
  • DikuMUD2: all only, special money keyword (least flexible)
  • Evennia: No "all" by default (most permissive, game-specific)
  • CoffeeMud: all, all.type, type.all (most flexible, potentially confusing)

3. Destructive Parsing Risk

  • CircleMUD: find_all_dots() modifies argument in-place ⚠️
  • DikuMUD2: No destructive parsing ✓
  • Evennia: No destructive parsing ✓
  • CoffeeMud: Partial modification of whatToGet

4. Container Handling

  • 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

5. Item Resolution Strategies

  • 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)

6. Failure Handling

  • 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

7. Design Patterns Observed

Pattern 1: Loop-and-Limit (CircleMUD, DikuMUD2, CoffeeMud)

while(item = find_next_matching()) && (count < limit) {
    perform_action(item)
    count++
}

Pattern 2: Validate-Then-Execute (Evennia)

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.


Ruby Implementation Guide

Core Design Decisions

1. Quantity Parsing

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"}

2. "All" Notation

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}

3. Container Parsing

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"}

Implementation Architecture: 5 Layers

┌─────────────────────────────────────────────┐
│ Command Router (orchestrates everything)    │
└──────────────────┬──────────────────────────┘
                   │
    ┌──────────────┴──────────────┐
    │                             │
┌───▼─────┐   ┌────────┐   ┌─────▼──────┐
│ Parser  │──▶│ Finder │──▶│ Validator  │
└─────────┘   └────────┘   └─────┬──────┘
                                  │
                           ┌──────▼───────┐
                           │   Executor   │
                           └──────────────┘

Layer 1: Parser (Input Validation & Decomposition)

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
end

Layer 2: Item Finder (Object Resolution)

module 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
end

Layer 3: Validator (Game Rule Checking)

module 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
end

Layer 4: Executor (State Changes & Output)

module 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
end

Layer 5: Command Router (Everything Together)

module 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
end

Code Examples

Example 1: Custom Item with Transfer Hooks

class 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
end

Example 2: Container with Capacity Management

class 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
end

Example 3: Partial Success Handling

class 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
end

Testing Recommendations

Parser Tests

describe 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
end

Integration Tests

describe 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
end

Performance Considerations

Issue: Numeric Suffix Lookups Can Be Slow

With "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)

Solution 1: Index by Keyword

class Room
  def find_all_matching(actor, keyword)
    @items.select { |item| actor.can_see?(item) && item.matches_keyword?(keyword) }
  end
end

Solution 2: Batch Fetch (Best)

class 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

Edge Cases

Checklist

  • Empty input: get → "Get what?"
  • Not found: get unicorn → "You don't see that."
  • Capacity: get 5 apples when 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

Real Code Snippets

CircleMUD: find_all_dots() (handler.c:1384-1393)

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);
}

Evennia: NumberedTargetCommand (general.py:354-369)

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)

CoffeeMud: Dot Notation Handling (Get.java:531-540)

// 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);
}

Conclusion

Best Practices Summary

The best Ruby implementation would combine:

  1. Evennia's parsing model: Clean separation of number/quantity parsing
  2. CircleMUD's notation: Support all and all.type
  3. Evennia's validation: All-or-nothing before execution
  4. CoffeeMud's flexibility: Support both from containers and quantities
  5. CoffeeMud's partial success: Handle capacity gracefully
  6. Evennia's hooks: Extensible item/container callbacks

Implementation Checklist

  1. Parse thoroughly: Separate quantity, mode (single/all), keyword, and container
  2. Validate atomically: Check ALL items before moving ANY
  3. Handle partial success: Report what was transferred and what failed
  4. Use hooks: Let items customize their transfer behavior
  5. Avoid destructive parsing: Don't modify input strings in-place
  6. Index strategically: Use keyword indexing for "all" operations
  7. Provide clear feedback: Tell players exactly what happened and why
  8. Test edge cases: Empty inventory, capacity limits, validation failures

Key Takeaways

  • Quantity position is universal: Always prefix (get 3 apples)
  • "All" notation varies: CircleMUD's all.type is 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

References

  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment