3
\$\begingroup\$

I'm writing a text based game, and I hit a snag with the user commands processing: so far I have a huge hardcoded ifelse block, and that does not seem good or scalable.

With single word non session commands it is easy:

> quit

Here I know I simply have to quit; but when it comes to multi word commands, like:

> help skills
> use bag
> take sword from bag

the if/elseif/else pattern is so cumbersome and not scalable.

Also, with regards to commands in one session, like so:

> open box
> look inside box
// you see next items inside box: one, two, three
> take one

how can I process different options depth, when each command received from client is processed from the beginning? How to respond with "first you must open box to take items from it" if user enters "take one" without prior "open box"?

How can I organize and process user input efficiently here?

\$\endgroup\$
5
  • 1
    \$\begingroup\$ This looks pretty broad at present. Can you please edit your question to narrow it down to a specific input problem you'd like help solving? Try including a concrete example of what you're doing now, and describe what it is about it that you find not good, or a particular kind of scaling you feel it won't handle well, then we can try to propose ways to improve these aspects. \$\endgroup\$
    – DMGregory
    Commented Feb 5, 2018 at 10:45
  • 1
    \$\begingroup\$ I've taken the liberty to reword your question so that it looks a bit more like a whole. With the edit you made, it's much clearer than the first version. \$\endgroup\$
    – Vaillancourt
    Commented Feb 5, 2018 at 14:57
  • \$\begingroup\$ First thing: you need to keep state about your world (e.g. is box open?) Next: you should put all commands in an array, and search that for a match, instead of a long if-else. \$\endgroup\$
    – Bram
    Commented Feb 5, 2018 at 16:31
  • \$\begingroup\$ @Bram, world state will be persisted, ofcourse.Could you please elaborate on the array approach? \$\endgroup\$
    – lxknvlk
    Commented Feb 5, 2018 at 17:00
  • \$\begingroup\$ @Bram This seems to be the basis for an answer, could you post it as such so that users can vote on it? Comments are not meant to answer questions. \$\endgroup\$
    – Vaillancourt
    Commented Feb 5, 2018 at 18:30

3 Answers 3

5
\$\begingroup\$

I'd be tempted to write this as a lexer-parser pair, like a script interpreter / compiler front-end.

The lexer reads the string, cuts out the next "token" to handle, skipping over whitespace or irrelevant punctuation, and standardizing the vocabulary of tokens (say, flattening to lowercase or merging synonyms/spelling variants/permitted typos).

The parser can then act on a higher level, describing the sequences of tokens it expects/accepts for particular commands.

The style of lexer I've used in the past exposes two ways of processing the next token:

  • Expect checks to see if the next token matches a particular criterion. If it does, it consumes it and advances to the next token. If it doesn't, it generates an error.

    We can use this for transitive verbs like "Take" by expecting an object word to follow, and complaining "Take what?" if this expectation is not met.

  • Accept checks if the next token matches a criterion. If it does, it consumes it and skips ahead to the next token after it, otherwise it does nothing at all.

    This is good for allowing optional words like "Take screwdriver" or "Take the screwdriver" without a big combinatoric explosion of if/else branches for every possible phrasing.

Similar to abathur's answer, I'd recommend storing a list of command tokens paired with a function pointer / delegate representing the action it performs. Then you can search your array of commands for a match for the first token, and fire off the appropriate logic. If you pass the lexer down into that command, it can use it to recursively check the subsequent tokens as needed.

So we might have something like this:

CommandResult TakeCommand(Scene scene, Lexer Lexer) {
    // Optional A / An / The / That / This
    lexer.Accept(Token.Articles);

    // You can extend this to handle things like 
    // "Take one of / take two / take all"
    // if(lexer.Accept(Token.Quantifier)) ...

    // Or specific flavours of item...
    // "Take the big blue screwdriver"
    // while(lexer.Accept(Token.Descriptor))...

    // Check that they provided an object to take.
    if(lexer.Expect(Token.Identifiers) == false)
        return CommandResult.Error("Take what?")

     // Validate that the named thing is an inventory item.
     var item = GetInventoryItemFromToken(lexer.consumedToken);
     if(item == null)
          return CommandResult.Error("You can't take that.");

     // Validate that there is such a thing in this scene/context.
     if(scene.ContainsItem(item) == false)
          return CommandResult.Error("There's no {0} here.", item.canonicalName);

     // Finally, defer the actual inventory operation to the scene
    // (it might want to implement special rules,
    // like "you can't just take George's false teeth,
    // you'll have to offer something in trade!" or
    // "as soon as you lift the idol, the whole temple
    // begins to shake..."
    return scene.Take(item);
}

Now you can wire up your commands something like...

parser.RegisterCommand(Token.Take, TakeCommand);

Depending on your setup, you may be able to treat these command mappings as data & modular action components, rather than hard-coding them all.

\$\endgroup\$
1
  • \$\begingroup\$ The more I reflect on this, the clearer it becomes that this way of working is better for controlled languages like we use in programming, but won't necessarily scale to natural language if your commands are more nuanced. In particular, localizing this to handle a different input language — especially one in which commands don't start with the imperative verb — would be a nightmare. I'd love to see answers that propose more flexible/localization-friendly approaches. \$\endgroup\$
    – DMGregory
    Commented Feb 8, 2018 at 5:09
2
\$\begingroup\$

Caveat: I'm not sure this recommendation really un-bloats your input processing, (in some sense it can make it worse), but it does enable you to modularize and better understand it.

Let's start with an overly simple explanation of how command-processing works on an old MUD I admin for:

  1. User input arrives, and we perform transformations based on any user-defined aliases or nicknames (i.e., if the player types ef and alias ef = 'eat current_food' and nickname current_food = apple, we'll transform the command down to eat apple).
  2. Next we replace/transform any globally defined aliases (n = north).
  3. Then we take the first "word" as the command, and start asking various objects (in a defined sequence) if they'd like to handle this command by calling some specific method with the command and arguments. If one object returns false, we ask the next.

This model is simple, and it gives you a great amount of flexibility. There's some start-up and computational inefficiency, but I think the trade-off is worth it. We give a lot of objects a chance to handle the command; here's a rough example in pythonic pseudocode:

global_commands = {
    "take": TakeCommand(),
    "use": UseCommand()
}

def process_command(input_string):
    input_string = player.replace_nicknames(input_string)
    input_string = player.replace_aliases(input_string)
    input_string = global_command_replaces(input_string)

    command, arguments = input_string.split(maxsplit=1)

    if not find_command(command, arguments):
        print("no such command")

def find_command(command, arguments):
    if find_command_in(player.inventory, command, arguments):
        return True

    if find_command_in(player.environment.inventory, command, arguments):
        return True

    if player.environment.do(command, arguments):
        return True

    if player.do(command, arguments):
        return True

    if command in global_commands:
        if global_commands[command].do(arguments)
            return True

def find_command_in(items, command, arguments):
    for item in items:
        if item.do(command, arguments):
            return True

All of these objects get to make their own decisions about what does/n't work, and you may be able group some common features/functionality together with inheritance or just common patterns. This also enables you to override global commands in local instances where you want something special to happen.

\$\endgroup\$
1
\$\begingroup\$

When I developed my text-based RPG in Ruby, I had a module that parses input into <verb> <arguments> pairs. Then arguments are passed over to module that runs processing of each verb separately, such as:

module Parser
  @@UNKNOWN = "Don't know this command"
  @@NO = "Hmm?"
  def Parser.parse(str, player) # what, from
    str = str.gsub(/^\s*/,"").gsub(/\s*$/,"")
    unless str == ""
      action, params = str.split(/\s/, 2)
      suc = false
      Commands::LIST.each do |k, f|
        if k=~/^#{Regexp.escape(action)}/i
          $log.info [action, params]
          eval("Commands.#{f}(player, params)")
          suc = true
          break
        end
      end
      player.send @@UNKNOWN unless suc
    else
      player.send @@NO
    end
  end
end

Here is snippet of the commands module commands.rb:

module Commands

  LIST = {
    "look" => "look",
    "tell" => "tell",
    "say" => "say",
    "save" => "save_base",
    "north" => "go_north",
    "south" => "go_south",
    "west" => "go_west",
    "east" => "go_east",
    "down" => "go_down",
...
  def Commands.scan(player, *a)
    player.scan
  end

  def Commands.tell(creature, *args)
    to, msg = args[0].split(/\s/, 2)

    if (pl = Player.find({:name => to})) && pl.online? && pl.visible_for(creature)
      if pl == creature
        creature.send "You wanna talk to yourself?"
        return false
      end
      creature.tell(pl, msg)
    else
      creature.send "There is nobody called #{to} here."
    end

  end

  def Commands.say(creature, *args)
    str = args
    creature.say str[0]
  end
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .