Home Let's Build an RPG! Deconstructing B/X D&D Adventure Generator! B/X D&D Character Generator Various Java Contact
 

First PagePrevious PageBack to overviewNext PageLast Page

(Advanced) Random Names

Role-Playing Games are a world of the imagination, and words are the thread from which their reality is woven.

So in order to have an immersive and fun experience, we have to have a fairly decent way to generate random but reasonably logical text.

We also don't want to have to write huge if-then or case blocks to just choose a random word out of a list! Nor do we want to have big arrays of words, because that is hard to read and edit.

To support this, we'll put together a structured table definition language, using XML, and write an interpreter to walk through the tables and generate results.

This is somewhat Heady Stuff. If the implementation gets you down, don't let it get to you! All you really need to know for now is how to use the provided tables, and maybe how to make your own! If you start getting a headache, just skip down to the “Test It!” section, and then go to the next chapter!

An XML-Based Structured Table Language

In its simplest form, we'll just want a table that randomly rolls an entry from a list of lines, and returns it. Let's design some tables, first. Then we'll explore the code for rolling values on the tables (or resolving them).

Let's make a table of barbarian names first! We'll structure such a table something like this:

<tablegroup description="Barbarian Names" comment="Borrowed from The Age of Fable (http://www.apolitical.info/webgame/tables)"> 

	<table name="##DEFAULT" simple="true"> 
		Abban 
		Adalbert 
		... snip ... (more barbarian names) 
		Wine 
		Wybert 
	</table> 
	 
</tablegroup>

Each XML table file can contain multiple tables, and each table has a name. The first table that is rolled on is named ”##DEFAULT”. In this case, the table is marked as “simple”, indicating that we just want to pick a random line from the table and return it. Tables that aren't simple can specify entries that output a result, roll on another table with a particular name, or both!

In the above example, the table file only contains male barbarian names. Wouldn't it be nice if we could pass in variables that control the the behavior of the tables when we make a roll? Let's do that!

<tablegroup description="Barbarian Names" comment="Borrowed from The Age of Fable (http://www.apolitical.info/webgame/tables)"> 

	<table name="##DEFAULT"> 
		<entry> 
			<if expr="gender=female"> 
				<true> 
					<rolltable name="barbarian_female" /> 
				</true> 
				<false> 
					<rolltable name="barbarian_male" /> 
				</false> 
			</if> 
		</entry> 
	</table> 
	 
	<table name="barbarian_female" simple="true"> 
		Adalbjorg 
		Aeronwen 
		... snip ... (more female barbarian names) 
		Vigdis 
		Yrsa 
	</table> 
	 
	<table name="barbarian_male" simple="true"> 
		Abban 
		Adalbert 
		... snip ... (more male barbarian names) 
		Wine 
		Wybert 
	</table> 
	 
</tablegroup>

Here we see the default table with only one entry (the stuff between the bracketed “entry” tags), so that entry is always picked! When that entry is picked, the table resolving code resolves each element of the entry in order. In this case, there is only one element, the “if” block. The table resolving code looks at the value of the “gender” variable that is passed in when we make a roll on the table. If the value is “female” (true), then it branches to the table named “barbarian_female”. Otherwise, if the value is not “female” (false), it branches to the “barbarian_male” table instead.

Since the “barbarian_female” and “barbarian_male” tables are both marked “simple”, it just chooses a random line out of the table when the table is resolved.

Clear as mud? Good! Let's look at a more complex example!

Let's port a legendary creatures titles table from Age of Fable! Then, we can use the two tables together to produce names similar to “Slappy Jenkins, the Salesman of Used Cars”!

This table will produce results that read “the something of something else”. We'll use one set of tables for lawful (generally, good) creatures (alignment passed in as a variable when we resolve the table), and another set of tables for chaotic (generally, evil) creatures.

We'll structure the default table something like this, to branch to a different set of tables depending on the creature's alignment:

<table name="##DEFAULT"> 
	<entry> 
		the <!-- This outputs a literal "the". --> 
		<if expr="alignment=lawful"> 
			<true> 
				<!-- if alignment=lawful, go to the lawful table. --> 
				<rolltable name="lawful" /> 
			</true> 
			<false> 
				<if expr="alignment=chaotic"> 
					<true> 
						<!-- otherwise, if alignment=chaotic, go to the chaotic table. --> 
						<rolltable name="chaotic" /> 
					</true> 
					<false> 
						<!-- otherwise, alignment is neither lawful nor chaotic, so it must be neutral. --> 
						<!-- so, we'll go to either the lawful or chaotic table randomly. --> 
						<rolldice expr="d2"> 
							<result expr="1"> 
								<rolltable name="lawful" /> 
							</result> 
							<result expr="2"> 
								<rolltable name="chaotic" /> 
							</result> 
						</rolldice> 
					</false> 
				</if> 
			</false> 
		</if> 
	</entry> 
</table>

Once we get to the table that we're actually rolling on, we'll need to produce different results depending on whether the creature is male or female.

<table name="lawful"> 
	<entry> 
		<!-- produce a different result depending on the creature's gender. --> 
		<if expr="gender=female"> 
			<true> 
				Mother 
			</true> 
			<false> 
				Father 
			</false> 
		</if> 
		<!-- then output a literal "of". --> 
		of 
		<!-- and finally, go to yet another table to add another random piece. --> 
		<rolltable name="lawful_2" /> 
	</entry> 
	<entry> 
		<if expr="gender=female"> 
			<true> 
				Queen 
			</true> 
			<false> 
				King 
			</false> 
		</if> 
		of 
		<rolltable name="lawful_2" /> 
	</entry> 
... snip ... 

Here we see several entries. One of the entries is picked randomly off of the table. A different word is added to the output depending on the gender of the creature (as passed in as a variable when we resolve the table). Then a literal “of” is added, and we branch to yet another table to pick the final word.

If you want to take a gander at the complete XML table files, here are links to them in WebSVN!

FIXME Add link
FIXME Add link

Would it have been easier to implement this particular table group just in Java? Probably! But that wouldn't exercise our coding juices nearly as much, would it? :P

Using a decent text editor that can record and play back macros (I like to use VIM for this), you can transform a few big lists of words into such a structured table fairly quickly.

A Table Resolution Class

Here is our table resolution class and XML interpreter. It looks frightening, but if you follow along with the comments, it's not too bad!

XML, despite being rather klunky, is really handy for things like this. We can use the system XML parsers to parse the whole document into a DOM graph, and then just walk over the node tree as we interpret it!

/* 
 * Copyright (C) 2012 L. Adamson 
 *  
 * This software is provided 'as-is', without any express or implied warranty. 
 * In no event will the authors be held liable for any damages arising from the 
 * use of this software. 
 *  
 * Permission is granted to anyone to use this software for any purpose, 
 * including commercial applications, and to alter it and redistribute it 
 * freely, subject to the following restrictions: 
 *  
 * 1. The origin of this software must not be misrepresented; you must not claim 
 * that you wrote the original software. If you use this software in a product, 
 * an acknowledgment in the product documentation would be appreciated but is 
 * not required. 
 *  
 * 2. Altered source versions must be plainly marked as such, and must not be 
 * misrepresented as being the original software. 
 *  
 * 3. This notice may not be removed or altered from any source distribution. 
 *  
 * L. Adamson leaf@dizzydragon.net 
 */ 
 
package net.dizzydragon.rustybox.tables; 
 
import java.io.IOException; 
import java.io.InputStream; 
import java.util.HashMap; 
import java.util.concurrent.ConcurrentHashMap; 
 
import javax.xml.parsers.DocumentBuilder; 
import javax.xml.parsers.DocumentBuilderFactory; 
import javax.xml.parsers.ParserConfigurationException; 
 
import net.dizzydragon.rustybox.util.Dice; 
 
import org.w3c.dom.Document; 
import org.w3c.dom.Element; 
import org.w3c.dom.Node; 
import org.w3c.dom.NodeList; 
import org.xml.sax.SAXException; 
 
/** 
 * This generic table class reads a table specification from an xml document. 
 * Random results can then be obtained from the tables, while passing in 
 * variables to control the way that the tables generate the results. 
 *  
 * If you wish to learn how these table specifications work, please examine the 
 * included XML files and peruse the documentation at 
 * http://www.dizzydragon.net. 
 *  
 * Hopefully some better documentation on the format will be forthcoming... 
 */ 
public class Table 
{ 
	// Somewhat cheesy hack to prevent buggy codebase_lookup=false for applets 
	// running on buggy Sun java plugins that partially ignore the 
	// codebase_lookup parameter from hitting the server a million times trying 
	// to request an external javax.xml.parsers.DocumentBuilderFactory. It 
	// doesn't solve the problem entirely, but it does greatly reduce the 
	// number of server hits. It probably makes this code non-thread-safe, 
	// though, but we could probably fix that by using a ThreadLocal here 
	// instead, if the need arises. 
	final private static DocumentBuilderFactory											dbf				= DocumentBuilderFactory.newInstance(); 
 
	/** 
	 * A concurrent map of loaded tablegroups. If a tablegroup has been loaded 
	 * previously, it will be reused when another object referencing the same 
	 * tablegroup is subsequently instantiated, instead of reloading a duplicate 
	 * of everything. This should help save some memory and avoid needlessly 
	 * repeating XML parsing. It does, however, prevent tables from being 
	 * dynamically reloaded from disk if they are changed (but that's not going 
	 * to happen with a .jar, anyway). 
	 */ 
	final private static ConcurrentHashMap<String,ConcurrentHashMap<String,Element>>	loadedTables	= new ConcurrentHashMap<String,ConcurrentHashMap<String,Element>>(); 
 
	/** 
	 * Parse a "key=value" list of variables into a HashMap. 
	 *  
	 * @param variables 
	 *            the variables to be parsed, passed as "x=y", "p=q", etc. 
	 * @return a HashMap containing the parsed variables. 
	 */ 
	final public static HashMap<String,String> parseVariables( String... variables ) 
	{ 
		// Loop over the varargs list, split at the '=' sign, assign left side 
		// to key and right side to value, and insert them into the hashmap. 
		HashMap<String,String> vars = new HashMap<String,String>(); 
		if( variables != null ) 
		{ 
			for( int i = 0; i < variables.length; i++ ) 
			{ 
				String s = variables[i]; 
				int index = s.indexOf( '=' ); 
				String key = null; 
				String value = null; 
				if( index >= 0 ) 
				{ 
					key = s.substring( 0, index ).trim(); 
					value = s.substring( index + 1 ).trim(); 
					if( "".equals( key ) ) 
						key = null; 
					if( "".equals( value ) ) 
						value = null; 
				} 
				if( key != null ) 
					vars.put( key, value ); 
			} 
		} 
		return vars; 
	} 
 
	/** The name of the table that this object references. */ 
	final private String					tableName; 
 
	/** The default variables associated with this instance of the named table. */ 
	final private HashMap<String,String>	defaultVariables; 
 
	/** 
	 * Instantiates a new table. 
	 *  
	 * @param tableName 
	 *            the name of the XML table specification file to load, sans 
	 *            file extension. 
	 * @param defaultVariables 
	 *            the default variables to associate with this instance of the 
	 *            named table. 
	 */ 
	public Table( String tableName, HashMap<String,String> defaultVariables ) 
	{ 
		// Set table name. 
		this.tableName = tableName; 
		// Make a copy of the default variables table, in case the original is 
		// changed. 
		this.defaultVariables = new HashMap<String,String>( defaultVariables ); 
		// Check and see if the table is already cached in memory. If not, load 
		// it. 
		if( !loadedTables.containsKey( tableName ) ) 
		{ 
			// Dynamically calculate the path to the needed XML file. We're 
			// doing it this way so it won't break if we refactor package names. 
			String path = Table.class.getName(); 
			path = path.substring( 0, path.length() - Table.class.getSimpleName().length() ).replace( '.', '/' ) + tableName + ".xml"; 
			// Try to open the file. Crash out if opening fails. 
			InputStream in = Table.class.getClassLoader().getResourceAsStream( path ); 
			if( in == null ) 
				throw new RuntimeException( "No such table \"" + tableName + "\" at " + path + "!" ); 
 
			// Create a map to put the top element of each table in the file 
			// into. They are keyed by the table name. 
			ConcurrentHashMap<String,Element> table = new ConcurrentHashMap<String,Element>(); 
 
			// Parse the XML document. 
			// DocumentBuilderFactory dbf = 
			// DocumentBuilderFactory.newInstance(); 
			DocumentBuilder builder; 
			try 
			{ 
				builder = dbf.newDocumentBuilder(); 
			} 
			catch( ParserConfigurationException ex ) 
			{ 
				throw new RuntimeException( ex ); 
			} 
			Document doc; 
			try 
			{ 
				doc = builder.parse( in ); 
			} 
			catch( SAXException ex ) 
			{ 
				throw new RuntimeException( ex ); 
			} 
			catch( IOException ex ) 
			{ 
				throw new RuntimeException( ex ); 
			} 
			doc.getDocumentElement().normalize(); 
 
			// Loop over all <table> children of the root <tablegroup> node, and 
			// index them into the table map. 
			for( Node node = doc.getFirstChild().getFirstChild(); node != null; node = node.getNextSibling() ) 
			{ 
				if( "table".equals( node.getNodeName() ) && node instanceof Element ) 
				{ 
					Element eTable = (Element)node; 
					String tName = eTable.getAttribute( "name" ); 
					// If a table is marked as "simple" it just contains a 
					// series of lines, one for each table entry, rather than 
					// XML tags specifying complex behaviors. So we parse out 
					// the text content of the node into an <entry> element for 
					// each line, and delete the text content. 
					if( "true".equals( eTable.getAttribute( "simple" ) ) ) 
					{ 
						String textContent = eTable.getTextContent(); 
						while( eTable.getFirstChild() != null ) 
							eTable.removeChild( eTable.getFirstChild() ); 
						String[] lines = textContent.split( "\n" ); 
						for( String line : lines ) 
						{ 
							line = line.trim(); 
							if( !"".equals( line ) ) 
							{ 
								Element e = doc.createElement( "entry" ); 
								e.setTextContent( line ); 
								eTable.appendChild( e ); 
							} 
						} 
					} 
					if( !"".equals( tName ) ) 
					{ 
						table.put( tName, (Element)node ); 
					} 
					else 
					{ 
						throw new RuntimeException( "No table name specified!" ); 
					} 
				} 
			} 
 
			try 
			{ 
				in.close(); 
			} 
			catch( IOException ex ) 
			{ 
				throw new RuntimeException( ex ); 
			} 
 
			// Now that the table is loaded, insert it into the concurrent map 
			// of cached tablegroups. 
			loadedTables.put( tableName, table ); 
		} 
	} 
 
	/** 
	 * Instantiates a new table. 
	 *  
	 * @param tableName 
	 *            the name of the XML table specification file to load, sans 
	 *            file extension. 
	 * @param defaultVariables 
	 *            the default variables to associate with this instance of the 
	 *            named table. 
	 */ 
	public Table( String tableName, String... defaultVariables ) 
	{ 
		this( tableName, parseVariables( defaultVariables ) ); 
	} 
 
	/** 
	 * Return a random result from a subtable. 
	 *  
	 * @param subtableName 
	 *            the name of the subtable to obtain a result from 
	 * @param vars 
	 *            the union of the variables to control resolution 
	 * @return a randomly rolled subtable entry. 
	 */ 
	final private String random( String subtableName, HashMap<String,String> vars ) 
	{ 
		// Resolve the subtable we need. 
		Element subtable = loadedTables.get( tableName ).get( subtableName ); 
		// Pick a random entry off the subtable. 
		NodeList nodes = subtable.getElementsByTagName( "entry" ); 
		Element entry = (Element)nodes.item( Dice.d( nodes.getLength() ) - 1 ); 
		// Resolve the entry to a string. 
		return resolveEntry( entry, vars ); 
	} 
 
	/** 
	 * Recursively resolve an XML entry (or sub-element) to a string. 
	 *  
	 * @param entry 
	 *            the entry to resolve 
	 * @param vars 
	 *            the union of the variables to control resolution 
	 * @return the resolved string or string slice 
	 */ 
	final private String resolveEntry( Element entry, HashMap<String,String> vars ) 
	{ 
		StringBuilder sb = new StringBuilder(); 
		for( Node n = entry.getFirstChild(); n != null; n = n.getNextSibling() ) 
		{ 
			if( n.getNodeType() == Node.TEXT_NODE ) 
			{ 
				// If the node is a text node, then it's literal text that we 
				// want to append to the output. 
				if( sb.length() > 0 && sb.charAt( sb.length() - 1 ) != ' ' ) 
					sb.append( " " ); 
				sb.append( n.getTextContent().trim() ); 
			} 
			else if( n instanceof Element ) 
			{ 
				// Otherwise, we process the element based on the node name. 
				Element e = (Element)n; 
				String eName = e.getNodeName(); 
				if( "if".equals( eName ) ) 
				{ 
					// Resolve an "if" element. 
					String expr = e.getAttribute( "expr" ); 
					boolean status = false; 
					int eqIndex = expr.indexOf( '=' ); 
					if( eqIndex > 0 ) 
					{ 
						String var = expr.substring( 0, eqIndex ).trim(); 
						String val = expr.substring( eqIndex + 1 ).trim(); 
						if( !"".equals( var ) ) 
						{ 
							if( val == null ) 
								val = ""; 
							String varsVal = vars.get( var ); 
							if( varsVal == null ) 
								varsVal = ""; 
							status = val.equals( varsVal ); 
						} 
					} 
					// Find the true or false child node, based on what the 
					// expression resolved to, and recursively resolve it. 
					String cmp; 
					if( status ) 
						cmp = "true"; 
					else 
						cmp = "false"; 
					for( Node child = e.getFirstChild(); child != null; child = child.getNextSibling() ) 
					{ 
						if( cmp.equals( child.getNodeName() ) && child instanceof Element ) 
						{ 
							Element eChild = (Element)child; 
							if( sb.length() > 0 && sb.charAt( sb.length() - 1 ) != ' ' ) 
								sb.append( " " ); 
							sb.append( resolveEntry( eChild, vars ) ); 
						} 
					} 
				} 
				else if( "rolltable".equals( eName ) ) 
				{ 
					// Resolve on a "roll_table" element by recursively rolling 
					// on the specified table and appending the result to our 
					// output. 
					String subtableName = e.getAttribute( "name" ); 
					if( sb.length() > 0 && sb.charAt( sb.length() - 1 ) != ' ' ) 
						sb.append( " " ); 
					sb.append( random( subtableName, vars ) ); 
				} 
				else if( "rolldice".equals( eName ) ) 
				{ 
					// Resolve a "rolldice" element by rolling a die... 
					String expr = e.getAttribute( "expr" ); 
					String sNum; 
					String sSides; 
					int dInd = expr.indexOf( 'd' ); 
					if( dInd >= 0 ) 
					{ 
						sNum = expr.substring( 0, dInd ).trim(); 
						sSides = expr.substring( dInd + 1 ).trim(); 
						if( "".equals( sNum ) ) 
							sNum = "1"; 
						if( "".equals( sSides ) ) 
							sSides = "6"; 
					} 
					else 
					{ 
						sNum = "1"; 
						sSides = expr.trim(); 
					} 
					// And then recursively resolving the node corresponding to 
					// the number we rolled. 
					String result = Integer.toString( Dice.d( Integer.parseInt( sNum ), Integer.parseInt( sSides ) ) ); 
					for( Node child = e.getFirstChild(); child != null; child = child.getNextSibling() ) 
					{ 
						if( "result".equals( child.getNodeName() ) && child instanceof Element ) 
						{ 
							Element eChild = (Element)child; 
							if( result.equals( eChild.getAttribute( "expr" ) ) ) 
							{ 
								if( sb.length() > 0 && sb.charAt( sb.length() - 1 ) != ' ' ) 
									sb.append( " " ); 
								sb.append( resolveEntry( eChild, vars ) ); 
							} 
						} 
					} 
				} 
				else 
				{ 
					// This happens when we run across an unexpected node. 
					if( sb.length() > 0 && sb.charAt( sb.length() - 1 ) != ' ' ) 
						sb.append( " " ); 
					sb.append( "?" + eName + "?" ); 
				} 
			} 
		} 
		// And finally, once the string is fully built, we return our result. 
		return sb.toString().trim(); 
	} 
 
	/** 
	 * Return a random result from the table. 
	 *  
	 * @param variables 
	 *            optional variables to override the defaults with. 
	 * @return a randomly rolled table entry. 
	 */ 
	final public String random( HashMap<String,String> variables ) 
	{ 
		HashMap<String,String> vars = new HashMap<String,String>( defaultVariables ); 
		vars.putAll( variables ); 
		return random( "##DEFAULT", vars ); 
	} 
 
	/** 
	 * Return a random result from the table. 
	 *  
	 * @param variables 
	 *            optional variables to override the defaults with. 
	 * @return a randomly rolled table entry. 
	 */ 
	final public String random( String... variables ) 
	{ 
		return random( parseVariables( variables ) ); 
	} 
}

Test it!

FIXME STUFF

What's Next?

FIXME STUFF

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/006_random_names.txt · Last modified: 2012/09/09 00:29 (external edit)

Copyright © 2009, 2012-2013 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!