Home Lawrence's Labyrinth Deconstructing B/X D&D Adventure Generator! B/X D&D Character Generator Other Contact
 

First PagePrevious PageBack to overviewNext PageLast Page

(Advanced) ActionStrings!

In a role-playing game, there are many actors, and many different points of view! When the game prints messages to the player, it must be able to deal with all of these different actors and points of view.

For example, if a generic Orc takes a swipe at the player, the combat system must print:

The orc takes a swipe at you!

Then, if you take a swipe at the orc, it must print:

You take a swipe at the orc!

Likewise, if the orc and that scary Brynhildr from the previous chapters are going at it, the combat system must print:

The orc takes a swipe at Brynhildr, the Mistress of Fear!

or

Brynhildr, the Mistress of Fear takes a swipe at the orc!

This is a lot of different text and conditionals for the game to deal with!

Wouldn't it be nice if we could just define a generic message, “<Something> takes a swipe at <something else>!”, then feed it the objects referencing the actors and viewer, whenever needed, and get a happily sensible string back out of it?

Let's write a class to do that!!! We'll call it ActionString!

Design

When we instantiate an ActionString, we'll feed its constructor a format string that contains placeholders for various pronoun, verb, and name substitutions!

Then any time we want to print the message we've specified in the format string, we'll feed our instantiated ActionString some Critters: The critter who will be viewing the message (the viewer), and a list of critters to substitute pronouns and verbs for (the actors), and it will give us back a properly formatted and substituted string!

The format string will look something like this:

$Name$0 $|poke|$0 $name$1 with a sharp pointy stick!  $HeSheIt$1 $|howl|$1 in pain!

Substitutions are enclosed in “$” symbols, optionally followed by the number of the critter in the actor list to whom the substitution refers.

From this rather obtuse format string, we can resolve many different sorts of messages, depending on who the viewer and actors are!

You poke Brynhildr, the Mistress of Fear with a sharp pointy stick!  She howls in pain! 
You poke the orc with a sharp pointy stick!  He howls in pain! 
Brynhildr, the Mistress of Fear pokes you with a sharp pointy stick!  You howl in pain! 
Brynhildr, the Mistress of Fear pokes the orc with a sharp pointy stick!  He howls in pain! 
The orc pokes you with a sharp pointy stick!  You howl in pain! 
The orc pokes Brynhildr, the Mistress of Fear with a sharp pointy stick!  She howls in pain!

The substitutions we will support are…

  • $heSheIt$ - He, she, or it. He picks up the fishing rod.”
  • $HeSheIt$ - As above, but with the first letter capitalized (for use at the beginning of a sentence).
  • $himHerIt$ - Him, her, or it. “The fishing rod belongs to her.”
  • $HimHerIt$ - Ditto.
  • $hisHerIts$ - His, her, or its. Its fishing rod is on fire!”
  • $HisHerIts$ - Ditto.
  • $hisHersIts$ - His, hers, or its. “The fishing rod is hers.”
  • $HisHersIts$ - Ditto.
  • $himHerItself$ - Himself, herself, or itself. “It tangles itself up in the fishing line and burns to a crisp!”
  • $HimHerItself$ - Ditto.
  • $name$ - The name of the specified critter.
  • $Name$ - The name of the specified critter, with the first letter capitalized. To be used at the beginning of sentences. Useful for critters with generic names, like “the orc”.
  • $|<first person verb>[|<third person form>]|$ - A first-person verb. When the viewer is also the actor that the verb refers to, the first-person form is used. Otherwise an “s” is appended to turn it into the third person form. In the case of verbs with a nonstandard third-person form, the third person form may optionally be specified with a second pipe symbol. “$|look|$” “$|slash|slashes|$

Implementation

This section is rather intermediate! If you're not comfortable with this sort of thing yet, don't sweat it! As with the tables we explored earlier, just see if you can figure out how the class is used even if you aren't quite to a place where you can figure out exactly how it works yet.

We'll be back to doing easier things in the next chapter!

String Tokenizing

String processing is generally a very expensive operation, consuming a great deal of CPU time! To avoid as much of this as possible, we will preprocess our ActionString format strings and tokenize them into a sequence of tokens that we can scan over and process quickly each time we resolve an ActionString!

Let's begin defining our class by declaring an array of generic Objects to hold the different tokens.

final public class ActionString 
{ 
	final private Object[]	tokens; 
} 

We'll be using different types of objects for the different sorts of tokens.

  • String - Sections of literal text that will be copied straight into the output string verbatim.
  • Integer - (A boxed “int”; if you're not familiar with boxing, don't worry about it for now) To denote which actor a substitution refers to.
  • An enumerated Token type - To replace the substitution strings (like $heSheIt$), because it is much faster to test equality for enums than for strings!

So we need to define that enumerated token type!

final public class ActionString 
{ 
	private static enum Token 
	{ 
		// If you add a token, don't forget to add a block of code to do 
		// something with it in resolve(), below. 
		heSheIt, 
		HeSheIt, 
		himHerIt, 
		HimHerIt, 
		hisHerIts, 
		HisHerIts, 
		hisHersIts, 
		HisHersIts, 
		himHerItself, 
		HimHerItself, 
		name, 
		Name, 
		VERB; // This token is not implicitly matched! 
	} 
 
	final private Object[]	tokens; 
 
} 

Now we'll write the constructor. We pass the format string into the constructor, and tokenize it.

Given this format string:

$Name$0 $|poke|$0 $name$1 with a sharp pointy stick!  $HeSheIt$1 $|howl|$1 in pain!

After the constructor finishes tokenizing it, the “tokens” array will contain the following items, in this order:

Token: Name 
Integer: 0 
String: " " 
Token: VERB 
Integer: 0 
String: "poke" 
String: "pokes" 
String: " " 
Token: name 
Integer: 1 
String: " with a sharp pointy stick!  " 
Token: HeSheIt 
Integer: 1 
String: " " 
Token: VERB 
Integer: 1 
String: "howl" 
String: "howls" 
String: " in pain!"

The code is a little long and dense, but give it a gander and see if you can follow what's going on! If you're still learning the ins and outs of Java and are not quite at a place where you can grok every little detail, don't worry about it! Just see if you can get a general idea of what is going on!

The ArrayLists are sort of like object-oriented versions of arrays, except they have no fixed size. You can add and delete items from them at whim, and there aren't empty spaces in the middle when you delete items, unlike arrays!

Hold onto your hat! Here comes a wall o' text! Don't panic!

	public ActionString( String fmt ) 
	{ 
		// Tokenize the fmt string.  We start with an Object list containing 
		// only the format string.  Then we loop through the each value of the 
		// Token enumeration and split each string at each token we find.  We 
		// insert the information pertaining to the token we found between 
		// the halves of the string. 
		// 
		// It would probably be far cleaner to do this recursively!  Some 
		// people seem to love recursion too much!  Out in the real world 
		// recursion is very slow, because there is a lot of stuff that goes 
		// on behind the scenes on the execution stack when you call a method! 
		// The compiler's optimizer can often inline methods to make things 
		// faster and smaller, but it is not practical for the optimizer to 
		// unroll/inline recursive calls to large methods!  So it is definitely 
		// worth learning how to do things iteratively instead of recursively 
		// for those times when speed is important! 
		// 
		// That being said, speed isn't all that important here, as the only 
		// thing that would get slowed down is startup time (assuming all of 
		// your ActionStrings are marked static.  But I got bored and fell into 
		// that "premature optimization" trap that we talked about previously! 
		// Shame on me!!! 
 
		ArrayList<Object> tokens = new ArrayList<Object>(); 
		tokens.add( fmt ); 
		ArrayList<Object> out = new ArrayList<Object>(); 
		// Parse for enum Tokens. 
		for( Token token : Token.values() ) 
		{ 
			// Produce a token string to look for from the literal name of 
			// the enumeration. 
			String tString = "$"+token.toString()+"$"; 
			// Process each entry in the tokens list sequentially and append to 
			// the "out" list. 
			for( int i = 0; i < tokens.size(); i++ ) 
			{ 
				// Pull the object that we're processing out of the token list. 
				Object slice = tokens.get( i ); 
				// If the object is a string, process it. 
				if( slice instanceof String ) 
				{ 
					// Cast the object we're processing to a string, so our 
					// code is cleaner... 
					String s = (String)slice; 
					// We chop bits off the left side of the string and process 
					// them.  When the string is empty, then we know we're 
					// done. 
					while( !"".equals( s ) ) 
					{ 
						// Find the first instance of the token we're looking 
						// for in what is left of the string. 
						int index = s.indexOf( tString ); 
						if( index == -1 ) 
						{ 
							// No token was found.  Copy the rest of the string 
							// to the output and move on to the next token. 
							out.add( s ); 
							s = ""; 
						} 
						else 
						{ 
							// We found the token we were looking for. 
							// If the token isn't at the very beginning of the 
							// string, append the part of the string that comes 
							// before the token to the output list. 
							if( index > 0 ) 
								out.add( s.substring( 0, index ) ); 
							// Chop of the beginning of the string, up to the 
							// end of the token. 
							s = s.substring( index+tString.length() ); 
							// Add the token that we found to the output list. 
							out.add( token ); 
							// Search for the actor number following the token. 
							for( index=0; index<s.length(); index++ ) 
								if( !Character.isDigit( s.charAt( index ) ) ) 
									break; 
							// If there was no actor number specified, default 
							// to zero.  Otherwise turn it from a string into 
							// a real Integer.  Append the value to the output 
							// list. 
							if( index == 0 ) 
								out.add( new Integer(0) ); 
							else 
								out.add( new Integer( Integer.parseInt( s.substring( 0, index ) ) ) ); 
							// Chop the actor number (if any) off of the string. 
							s = s.substring( index ); 
						} 
					} 
				} 
				else 
				{ 
					// The object we pulled out is not a string (so it must be 
					// a token we found previously), so just copy it to the 
					// output list and continue! 
					out.add( slice ); 
				} 
			} 
			// Swap references to the two lists, so that the output list 
			// becomes the token list and the old token list becomes the new 
			// output list.  Clear the new output list in preparation for the 
			// next iteration through the token loop. 
			ArrayList<Object> tmp = tokens; 
			tokens = out; 
			out = tmp; 
			out.clear(); 
		} 
		// Now we've handled everything but the $|verb substitutions|$, which 
		// require different processing. 
 
		// Parse verb substitutions.  $|look|$ or $|slash|slashes|$ 
		for( Object token : tokens ) 
		{ 
			if( token instanceof String ) 
			{ 
				String s = (String)token; 
 
				// The outer loops are the same as above.  The following block 
				// is what is different! 
 
				// We keep chopping on the string until we've found all the 
				// verbs! 
				while( !"".equals( s ) ) 
				{ 
					// Look for an opening "$|" 
					int vOpen = s.indexOf( "$|" ); 
					if( vOpen == -1 ) 
					{ 
						// If we didn't find an opening "$|", copy the string 
						// verbatim to the output list and exit the loop! 
						out.add( s ); 
						s = ""; 
					} 
					else 
					{ 
						// We found an opening "$|". 
						// Copy the portion of the string preceeding the "$|" (if 
						// any) to the output list. 
						if( vOpen > 0 ) 
							out.add( s.substring( 0, vOpen ) ); 
						// Chop off the beginning of the string, up to and 
						// including the opening "$|". 
						s = s.substring( vOpen+2 ); 
						// Look for a closing "|$"; 
						int vClose = s.indexOf( "|$" ); 
						if( vClose == -1 ) 
						{ 
							// If we didn't find a closing "|$" (which means we 
							// have an improperly formatted string!), just copy 
							// the remainder of the string to the output list  
							// and exit the loop! 
							out.add( s ); 
							s = ""; 
						} 
						else 
						{ 
							// We found a closing "|$".  Copy the text that was 
							// between the "$|" and "|$" to the "vSpec" variable to 
							// process in a moment. 
							String vSpec = s.substring( 0, vClose ).trim(); 
							// Chop off the beginning of the string, up to and 
							// including the closing "|$". 
							s = s.substring( vClose+2 ); 
							// Only process vSpec if there is some text in it. 
							// To do otherwise would be silly! 
							if( !"".equals( vSpec ) ) 
							{ 
								// Try to split vSpec at the "|" character 
								// separating the first and third person forms of  
								// the verb. 
								String[] vSplit = vSpec.split( Pattern.quote( "|" ) ); 
								// Check and make sure the array has at least one 
								// element.  It should never /not/ have at least 
								// one element, but...... 
								if( vSplit.length > 0 ) 
								{ 
									// Append the VERB token to the output list. 
									out.add( Token.VERB ); 
									// Store the first-person form of the verb in 
									// a variable to process in a moment. 
									String firstPerson = vSplit[0]; 
									// Store the third-person form of the verb in 
									// a variable to process in a moment.  If no 
									// third-person form was specified, then we 
									// append "s" to the first-person form and use 
									// that! 
									String thirdPerson; 
									if( vSplit.length > 1 ) 
										thirdPerson = vSplit[1]; 
									else 
										thirdPerson = vSplit[0]+"s"; 
									// Look for the actor number, append it to the 
									// output list, and chop it off the string, 
									// like we did before. 
									int index; 
									for( index=0; index<s.length(); index++ ) 
										if( !Character.isDigit( s.charAt( index ) ) ) 
											break; 
									if( index == 0 ) 
										out.add( new Integer(0) ); 
									else 
										out.add( new Integer( Integer.parseInt( s.substring( 0, index ) ) ) ); 
									s = s.substring( index ); 
									// Add the verb forms to the output list. 
									out.add( firstPerson ); 
									out.add( thirdPerson ); 
								} 
							} 
						} 
					} 
				} 
			} 
			else 
			{ 
				// The object we pulled out is not a string (so it must be 
				// a token we found previously), so just copy it to the 
				// output list and continue! 
				out.add( token ); 
			} 
		} 
		// Swap lists as before. 
		ArrayList<Object> tmp = tokens; 
		tokens = out; 
		out = tmp; 
		out.clear(); 
 
		// Convert the local tokens list to an array (faster access) and assign 
		// it to the private variable. 
		this.tokens = tokens.toArray(); 
	}

CAUTION: In really, really ancient versions of Java, the instanceof operator was very very slow! Even slower than the rest of Java was back in the Bad Old Days! They fixed it sometime around Java 4 (along with most of the other Java slowness issues from back then), but if for some weird reason you are stuck using a junky old 20-year-old JDK, you might want to try to avoid using instanceof!

Resolving ActionStrings

Now that the format string is tokenized, passing some critters in and resolving the string is fairly simple!

	final public String resolve( Critter viewer, Critter... actors ) 
	{ 
		// Building strings with a StringBuilder is much faster than 
		// concatenating strings together with "+"; 
		StringBuilder out = new StringBuilder(); 
		// Since the format string is already parsed out into an array of 
		// tokens, this is pretty easy!  Loop through the tokens with an index 
		// variable... 
		for( int index=0; index<tokens.length; index++ ) 
		{ 
			// Pull out the token we're looking at. 
			Object token = tokens[index]; 
			// Pull out the actor number and actor Critter (even if we're not 
			// going to use them).  If the actor number exceeds the number of 
			// actors in the actor list, it tries to use the last critter in the 
			// actor list.  If there are no critters in the actor list, it uses 
			// the viewer instead. 
			int actorNum = -1; 
			if( index+1 < tokens.length && tokens[index+1] instanceof Integer ) 
				actorNum = (Integer)tokens[index+1]; 
			if( actorNum >= actors.length ) 
				actorNum = actors.length - 1; 
			Critter actor; 
			if( actorNum < 0 ) 
				actor = viewer; 
			else 
				actor = actors[actorNum]; 
			if( actor == null ) 
				actor = Critter.UNKNOWN; 
			// Now we process the tokens, depending on what type of token it 
			// is! 
			if( token instanceof String ) 
			{ 
				// If it's a String, we just copy it straight to the output! 
				out.append( token ); 
			} 
			else if( token instanceof Token ) 
			{ 
				// If it's a Token, we fall through this switch block! 
				// NOTE: If you add more tokens to the Token enumeration, this 
				// is where you process them for output! 
				switch( (Token)token ) 
				{ 
					case heSheIt: 
						// Increment the index to skip over the actor number 
						// on the next loop iteration. 
						index++; 
						// If the actor is the viewer, we copy the appropriate 
						// second-person pronoun to the output.  Otherwise we 
						// fetch the proper gender-specific pronoun from the 
						// actor. 
						if( actor == viewer ) 
							out.append( "you" ); 
						else 
							out.append( actor.getGender().heSheIt() ); 
						break; 
					case HeSheIt: 
						// As above, but capitalized. 
						index++; 
						if( actor == viewer ) 
							out.append( "You" ); 
						else 
							out.append( capitalize(actor.getGender().heSheIt()) ); 
						break; 
					case himHerIt: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "you" ); 
						else 
							out.append( actor.getGender().himHerIt() ); 
						break; 
					case HimHerIt: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "You" ); 
						else 
							out.append( capitalize(actor.getGender().himHerIt()) ); 
						break; 
					case hisHerIts: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "your" ); 
						else 
							out.append( actor.getGender().hisHerIts() ); 
						break; 
					case HisHerIts: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "Your" ); 
						else 
							out.append( capitalize(actor.getGender().hisHerIts()) ); 
						break; 
					case hisHersIts: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "yours" ); 
						else 
							out.append( actor.getGender().hisHersIts() ); 
						break; 
					case HisHersIts: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "Yours" ); 
						else 
							out.append( capitalize(actor.getGender().hisHersIts()) ); 
						break; 
					case himHerItself: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "yourself" ); 
						else 
							out.append( actor.getGender().himHerItself() ); 
						break; 
					case HimHerItself: 
						// Ditto. 
						index++; 
						if( actor == viewer ) 
							out.append( "Yourself" ); 
						else 
							out.append( capitalize(actor.getGender().himHerItself()) ); 
						break; 
					case name: 
						index++; 
						// If the actor is the viewer, we copy the appropriate 
						// second-person pronoun to the output.  Otherwise we 
						// fetch the actor's name. 
						if( actor == viewer ) 
							out.append( "you" ); 
						else 
							out.append( actor.getName() ); 
						break; 
					case Name: 
						// As above, but capitalized. 
						index++; 
						if( actor == viewer ) 
							out.append( "You" ); 
						else 
							out.append( capitalize(actor.getName()) ); 
						break; 
					case VERB: 
						// Increment the index to skip over the actor number 
						// on the next loop iteration. 
						index++; 
						// Fetch the first-person form of the verb, using a 
						// preincrement operator to increment the index before 
						// it indexes the tokens array. 
						String vFirstPerson = (String)tokens[++index]; 
						// Fetch the third-person form of the verb, using a 
						// preincrement operator to increment the index before 
						// it indexes the tokens array. 
						String vThirdPerson = (String)tokens[++index]; 
						// Append the appropriate form of the verb to the 
						// output, depending on whether the viewer is the 
						// actor or not. 
						if( actor == viewer ) 
							out.append( vFirstPerson ); 
						else 
							out.append( vThirdPerson ); 
						break; 
					default: 
						// This block is only reached if a token is added to 
						// the Tokens enumeration later, and not handled here. 
						out.append( "?Unhandled Token: " ); 
						out.append( token ); 
						out.append( "?" ); 
				} 
			} 
			else 
			{ 
				// If this block is ever reached, then there is a bug in this 
				// file somewhere!!! 
				out.append( "?" ); 
				out.append( token ); 
				out.append( "?" ); 
			} 
		} 
		// Convert the output to a regular string and return it! 
		return out.toString(); 
	} 
 
	private String capitalize( String s ) 
	{ 
		StringBuilder out = new StringBuilder(); 
		out.append( s.substring( 0, 1 ).toUpperCase() ); 
		out.append( s.substring( 1 ) ); 
		return out.toString(); 
	}

Here is an example of failure of perfect encapsulation! In an ideal world, the code to handle what to do with a token when the string is resolved would be defined in the same place where the token is defined, in the Tokens enumeration! But doing that would require Much Ugliness! At least it's in the same file, though, and we documented our encapsulation violation!

You can see the whole file in the Subversion repository.

Brynhildr, the Mistress of Fear!

Since that Brynhildr seems to be appearing so often, she now has her own convenience class!

package test; 
 
import net.dizzydragon.rustybox.PlayerCharacter; 
 
public class Brynhildr extends PlayerCharacter 
{ 
	public Brynhildr() 
	{ 
		super( "Brynhildr, the Mistress of Fear", Gender.FEMALE, Alignment.CHAOTIC ); 
	} 
}

Test It!

package test; 
 
import net.dizzydragon.rustybox.ActionString; 
import net.dizzydragon.rustybox.Critter; 
import net.dizzydragon.rustybox.Critter.Gender; 
import net.dizzydragon.rustybox.simpleio.SimpleIO; 
import net.dizzydragon.rustybox.simpleio.SimpleRunnable; 
import net.dizzydragon.rustybox.PlayerCharacter; 
 
public class Test extends SimpleRunnable 
{ 
	private static final long	serialVersionUID	= 1L; 
 
	public static void main( String[] args ) 
	{ 
		runLocal( new Test() ); 
	} 
 
	// Since we declare the ActionString as static, it is instantiated and 
	// tokenized only once, when the program first starts, rather than during 
	// runtime as would be the case if we instantiated it as a local variable 
	// in a method body.  Try to make all of your ActionStrings static!  If you 
	// cannot, then at least try instantiate them only once, in a constructor. 
	// Try to avoid instantiating ActionStrings as temporary objects in a method 
	// body whenever possible, because this will cause tokenization to happen 
	// every time the method is called! 
	final private static ActionString stickPokey = new ActionString(  
			"$Name$0 $|poke|$0 $name$1 with a sharp pointy stick!  $HeSheIt$1 $|howl|$1 in pain!" ); 
 
	@Override 
	public void run( SimpleIO io ) 
	{ 
		Critter[] critters =  
			{  
				new PlayerCharacter( "Sneaky McPlayer", null, null ),  
				new Brynhildr(),  
				new PlayerCharacter( "the orc", Gender.MALE, null )  
			}; 
 
		for( Critter c1 : critters ) 
		{ 
			for( Critter c2 : critters ) 
			{ 
				if( c1 != c2 ) 
					io.println( stickPokey.resolve( critters[0], c1, c2 ) ); 
			} 
		} 
	} 
}

Was All of this Really Necessary?

Probably not. We could probably have achieved similar results with similar levels of performance using Regular Expressions, but I think the ActionString format we've devised here is easier to use for those of us who are bad at regexps! I don't know about you, but when we start getting into implementing that little combat loop game we've been talking about, I want to be implementing rather than digging through grep documentation!

What's Next?

In the next chapter, we'll finally implement that simple little combat loop! We'll roll two characters and pit them against each other in an icy thunderdome of death and mayhem!!!

Source Code

The source code for this chapter can be found in the Subversion Repository.

If you are interested in changing/modifying the code, you can download it into your Eclipse workspace using Subclipse as detailed here: TUTORIAL: Using Subclipse.

First PagePrevious PageBack to overviewNext PageLast Page

rustybox/book/009_actionstrings.txt · Last modified: 2012/09/25 12:58 by leaf

Copyright © 2009, 2012-2014 by L. Adamson, unless otherwise stated.
If you see something here that you like, and find it useful or learn something, please consider making a quick, easy, and secure donation via PayPal.
Your support is what keeps this whole thing going!