In the last blog post on ScalaMUD, I showed how I was able to take advantage of a third party NLP library so that I could tag player input with the appropriate parts of speech like Verb, Noun, Adjective, etc. My goal here is to eventually use this information to match words in player input sentences with physical objects in the game so that when someone types ‘kill the blue dragon’, I will be able to find the ActorRef for the blue dragon, and find a verb handler for kill, and then initiate combat.

In this blog post, I’ve come one step closer to that goal by enabling the dispatching of commands. As players enter input, that input is sent off to a Commander actor which tags up the input with parts of speech. Once the Comander is finished with it, it wraps the tagged words in an EnrichedCommand message and sends that to the player, which now has the trait Commandable, allowing the player to respond to enriched commands. This has an added benefit of allowing NPCs to be scripted in the future by allowing them to pretend to ‘type’ things like “kill player”, etc.

Eventually rooms will add commands to players when they enter the room to allow them to type the name of the exit (e.g. north, south, up, window) as a verb. Mortals and Wizards alike each have a unique set of commands they need to be able to type and these commands are grouped into “command libraries” (my old alma mater MUD, Genesis, an LPMUD, called them command souls). For example, here’s the first version of the Mortal command lib:

package com.kotancode.scalamud.core.cmd

import com.kotancode.scalamud.core.ServerStats
import com.kotancode.scalamud.core.TextMessage
import com.kotancode.scalamud.Game
import com.kotancode.scalamud.core.Implicits._
import akka.actor._
import akka.routing._

abstract class CommandLibMessage
case class AttachCommandLib extends CommandLibMessage

class MortalCommandLib extends Actor {
	def receive = handleMortalCommands

	def handleMortalCommands: Receive = {
		case cmd:EnrichedCommand if cmd.firstVerb == "who" => {
			handleWho(cmd.issuer)
		}
		case AttachCommandLib => {
			attachToSender(sender)
		}
	}

	def handleWho(issuer: ActorRef) = {
		println("player "+ issuer.name + " typed who.")
		var stringOut = "Players logged in:\n"
		for (p: ActorRef <- Game.server.inventory) {
			stringOut += p.name + "\n"
		}
		issuer ! TextMessage(stringOut)
	}

	def attachToSender(sender:ActorRef) = {
		sender ! AddCommand(Set("who"), self)
	}

}

Those of you who program in iOS or Cocoa might recognize some of the “Delegate Pattern” here… when the command lib attaches itself to something capable of issuing commands (anything that carries the Commandable trait, like a player or NPC), it just passes the implicit sender ActorRef so that actor can now dispatch commands to that lib.

Here’s the code in the Commander object that sends enriched commands to the actor that “typed” (virtually or for real) the command:

package com.kotancode.scalamud.core.cmd

import akka.actor._
import akka.routing._

import com.kotancode.scalamud.core.lang.EnrichedWord
import java.util.ArrayList
import edu.stanford.nlp.ling.Sentence
import edu.stanford.nlp.ling.TaggedWord
import edu.stanford.nlp.ling.HasWord
import edu.stanford.nlp.tagger.maxent.MaxentTagger
import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer
import com.kotancode.scalamud.core.cmd._

class Commander extends Actor {
	def receive = {
		case s:String => {
			val words = s.split(" ");
			val wordList = new java.util.ArrayList[String]();
			for (elem <- words) wordList.add(elem)
		    val sentence = Sentence.toWordList(wordList);
		    val taggedSentence = Commander.tagger.tagSentence(sentence).asScala.toList

			var enrichedWords = new ListBuffer[EnrichedWord]
		    for (tw : TaggedWord <- taggedSentence) {
				val ew = EnrichedWord(tw)
				println(ew)
				enrichedWords += ew
			}

			sender ! HandleCommand(EnrichedCommand(enrichedWords, sender))
	}
}
}

object Commander {
	val tagger = new MaxentTagger("models/english-bidirectional-distsim.tagger")
}

The important bit is that the player gets the HandleCommand message, which then goes through a dispatch process in the Commandable trait, and eventually registered verb handlers (like those registered via attached command libraries) get invoked via messages. Here’s the Commandable trait:

package com.kotancode.scalamud.core.cmd

import akka.actor._
import akka.routing._
import scala.collection.mutable.HashMap
import scala.collection.mutable.HashSet

sealed abstract class CommandMessage
case class AddCommand(verbs:Set[String], handlerTarget:ActorRef) extends CommandMessage
case class Removecommand(verb:String) extends CommandMessage
case class HandleCommand(command:EnrichedCommand) extends CommandMessage

trait Commandable {

	private val verbHandlers: HashMap[Set[String], ActorRef] = new HashMap[Set[String], ActorRef]

	def handleCommandMessages:akka.actor.Actor.Receive = {
		case AddCommand(verbs, handlerTarget) => {
			verbHandlers.put(verbs, handlerTarget)
		}

		case HandleCommand(cmd) => {
			dispatch(cmd)
		}
	}

	def dispatch(cmd:EnrichedCommand) = {
		println("handled a command "+ cmd +".")
		println("command's first verb: " + cmd.firstVerb)
		val targetHandlers = verbHandlers.filterKeys(key => key.contains(cmd.firstVerb))
		targetHandlers foreach {case (key, value) => value ! cmd}
	}
}

The dispatching actually happens in the targetHandlers foreach … line where the command is sent to every command handler that declared interest in that verb. In our case, we have a mortal command verb who that displays the list of connected users and the wizard command verb uptime that displays the length of time the server app has been running.

The following is sample session output from telnetting to the game:

Welcome to ScalaMUD 1.0

Login: Kevin
Welcome to ScalaMUD, Kevin
who
Kevin: who
Players logged in:
Bob
Kevin
Kevin
uptime
Kevin: uptime
Server has been up for 6 mins 39 secs.

Now that I can log in with multiple players, see who is online, and dispatch commands to handlers as well as differentiate between mortal and wizard abilities, this game is finally starting to feel like the beginnings of a real MUD.