Index
NetLibs.java


/*
 *  "Net Libs" Copyright (C) by Michael Benson - 7/27/97
 *
 *  Loosely based on "Mac Libs" which is loosely based on the old
 *  "Mad Libs" books for creating stories from forms.
 */

package NetLibs;

import java.awt.*;
import java.applet.Applet;
import java.util.*;
import java.io.*;
import java.net.*;

import COM.bensoft.base.*;
import COM.bensoft.widgets.*;


/**
 * NetLibs is a sort of game which generates silly stories.  It is
 * loosely based on Mac Libs which is a more sophisticated Macintosh
 * version, and that is loosely based on the old Mad Libs books.
 *  
 * The user can choose from several story templates and then generate a
 * story automatically by retrieving the words from predefined dictionaries, 
 * or by typing the words in a form.  The templates contain prompts
 * which correspond to certain word categories such as noun, verb, adjective, etc.
 * Each category has its own dictionary file containing words.  When the story
 * is generated automatically, the prompts are used to find the categories and
 * then choose a word at random.  When the user is typing the words in a form,
 * the prompts appear next to a text field to show the user what type of word
 * is needed.  After all words are chosen, the story template is scanned and the
 * prompts are replaced by the chosen words.  The resulting story is then
 * displayed in the window.
 *  
 * The story template files refer to the prompts by putting the index in angle
 * brackets like this:
 *  
 *		One day, <1> went to the <2>.
 *  
 * This enables the same word to appear in different places in the story.
 *  
 * The template files all end with the ".txt" suffix and have a corresponding
 * prompt file with the same name and the ".prompts" suffix.  In the prompts
 * file, each prompt is on a line separated by a 'new line' character.
 * The number in the angle brackets in the template correspond to the line
 * number in the prompt file (starting at 1).
 *  
 * The prompts in the prompt file consist of the name of a dictionary file
 * without the ".words" suffix.  There are also currently two special ways 
 * prompts can be used.  To generate a random number, a prompt like this can 
 * be used:
 *  
 *		number from 1 to 100
 *  
 * Also, any prompt can begin with a captial letter.  In that case, the chosen
 * word will automatically be capitalized.
 *  
 * The word files all end with the ".words" suffix.  Each word is on a line
 * separated by a 'new line' character.  When a random word is selected, the
 * appropriate word file is read in and the words are separated into a vector.
 * Then a random number is generated to select the nth word.  The word vectors
 * are cached, so if a story template requests 5 nouns, the word file will not
 * be read and scanned 5 times.  Also, if you generate another story with the
 * same template, it will be much faster the second time.
 *  
 * Note: In the Macintosh version ("Mac Libs"), the user can create and edit
 * templates and dictionaries.  Templates and the resulting stories can contain
 * formatted text and graphics.  Also, the prompts can reference properties
 * and rules.  The rules can be implemented in the dictionary in a
 * HyperCard-like language.  For example, the "verb" dictionary has rules for
 * "past_participle", "present_participle", etc.  This eliminates the need to
 * have separate dictionaries for each type.
 *  
 * @author	Michael Benson
 */
public class NetLibs extends Applet implements NetLibsConst, CommandListener
{
	private static final int		_WRAP_FACTOR = 80;
	private static final int		_PROGRESS_HEIGHT = 11;
	private static final int		_PROGRESS_INDENT = 70;
	private static final String		_extraSpace = " ";
	private static final String		_genStory = "Generate Story";
	private static final String		_autoGen = "Automatic";
	private static final String		_askGen = "Prompt for words";
	private CheckboxGroup			_prompter;
	private Choice					_storyChoice;
	private TextArea				_storyTextArea;
	private Button					_genStoryButton;
	private ProgressMeter			_progressMeter;
	private StoryTemplate			_storyTemplate;
	private NetLibsCommand			_netLibsCommand;

	/**
	 * NetLibs initialization.  Creates main applet frame.  Fills the
	 * Story Templates choice box by reading the file "templates.prompts".
	 */
	public void init() 
	{
		Vector		storyNames = null;
		Label		header;
		
		/*
	 		+------------------------------------------------------------+
	 		| +-----------------------northPanel-----------------------+ |
	 		| | +-------------------------p1-------------------------+ | |
	 		| | |              icon   "Mac Libs"   icon              | | |
	 		| | +-------------------------p2-------------------------+ | |
	 		| | |                +------p3------+                    | | |
	 		| | |  _storyChoice  | o Automatic  |  "Generate Story"  | | |
	 		| | |                | o Prompt     |                    | | |
	 		| | |                +--------------+                    | | |
	 		| | +----------------------------------------------------+ | |
	 		| +-----------------------centerPanel----------------------+ |
	 		| |                                                        | |
	 		| |                     _storyTextArea                     | |
	 		| |                                                        | |
	 		| +-----------------------southPanel-----------------------+ |
	 		| |                     _progressMeter                     | |
	 		| +--------------------------------------------------------+ |
	 		+------------------------------------------------------------+
	 		
	 		East and west have empty panels for margin space.
	 		Also, p3 is a 1 by 2 grid.  The others are border layouts.
		 */
		
		Panel northPanel = new Panel();
		northPanel.setLayout(new BorderLayout());
		
		Panel p1 = new Panel();
		p1.add(new ImageCanvas(this, "images/maclibs.gif", 32, 32));
		p1.add(header = new Label("Net Libs", Label.CENTER));
		p1.add(new ImageCanvas(this, "images/resume.gif", 32, 32));
		Font headerFont = new Font("Helvetica", Font.PLAIN, 24);
		header.setFont(headerFont);
		northPanel.add("North", p1);
		
		Panel p2 = new Panel();
		p2.add(new Label("Story template:", Label.RIGHT));		
		p2.add(_storyChoice = new Choice());
		_storyTemplate = new StoryTemplate();
		
		// Add story names to the popup menu:
		try {
			storyNames = _storyTemplate.getPrompts(this, "templates");
		} catch (IOException e) {
			System.out.println(e);
		}
		for (int i = 0;  i < storyNames.size();  i++) {
			_storyChoice.addItem((String)storyNames.elementAt(i));
		}
		p2.add(new Label(_extraSpace, Label.CENTER));
		
		Panel p3 = new Panel();
		p3.setLayout(new GridLayout(2, 1));
		_prompter = new CheckboxGroup();
		p3.add(new Checkbox(_autoGen, _prompter, true));
		p3.add(new Checkbox(_askGen, _prompter, false));
		p2.add(p3);
		p2.add(new Label(_extraSpace, Label.CENTER));
		_genStoryButton = new Button(_genStory);
		p2.add(_genStoryButton);
		northPanel.add("Center", p2);
		
		Panel centerPanel = new Panel();
		centerPanel.setLayout(new GridLayout(1, 1));
		centerPanel.add(_storyTextArea = new TextArea());
		
		Panel southPanel = new Panel();
		_progressMeter = new ProgressMeter();
		_progressMeter.resize(MAXWIDTH - _PROGRESS_INDENT, _PROGRESS_HEIGHT);
		southPanel.add(_progressMeter);
		
		setLayout(new BorderLayout());
		add("North", northPanel);
		add("Center", centerPanel);
		add("South", southPanel);
		add("East", new Label(_extraSpace, Label.CENTER));
		add("West", new Label(_extraSpace, Label.CENTER));
						
		// NetLibsCommands handles listeners and delegating commands:
		_netLibsCommand = new NetLibsCommand(this);
		_netLibsCommand.addCommandListener(this);
		
		resize(MAXWIDTH, MAXHEIGHT);
	}
	
	/**
	 * Get the NetLibsCommand object to issue a command.
	 *
	 * @return	the NetLibsCommand object.
	 */
	public NetLibsCommand getNetLibsCommand()
	{
		return _netLibsCommand;
	}
	
	/**
	 * Overrides the action method in Component to check for
	 * button clicks.
	 *
	 * @param	evt		the event that caused the action.
	 * @param	arg		the action.
	 * @return			true if the event has been handled; false if not.
	 * @see				java.awt.Component#action()
	 */
	public boolean action(Event evt, Object arg)
	{
		boolean ret = true;
	
		// "Generate Story" button clicked:
		if (_genStory.equals(arg)) {
			if (_autoGen.equals(_prompter.getCurrent().getLabel())) {
				// Generate story automatically:
				_netLibsCommand.doCommand(CMD_AUTO_GENERATE_STORY);
				_netLibsCommand.doCommand(CMD_STORY_GENERATED);
			} else {
				// Prompt user for words:
				_netLibsCommand.doCommand(CMD_PROMPT_GENERATE_STORY);
			}
		} else {
			ret = false;
		}
		
		return ret;
	}
		
	/**
	 * Generates a story automatically from the dictionaries.  Reads the prompt
	 * file, gets a random word from a dictionary for each prompts, then
	 * substitutes the words into the story.
	 */
	public void autoGenerateStory()
	{
		try {
			_autoGenerateStory();
		} catch (IOException e) {
			System.out.println(e);
			_storyTextArea.setText("");
		}
	}
	
	/**
	 * Generates a story automatically from the dictionaries.  Reads the prompt
	 * file, gets a random word from a dictionary for each prompts, then
	 * substitutes the words into the story.
	 *
	 * @exception	IOException if error reading prompt file.
	 * @exception	MalformedURLException if error finding prompt file.
	 */
	private void _autoGenerateStory() throws IOException, MalformedURLException
	{
		Vector	strings = new Vector();
		String	word;
			
		// Get the word prompts:
		Vector	prompts = _storyTemplate.getPrompts(this, _storyChoice.getSelectedItem());

		// For each prompt, get a random word:
		_progressMeter.setLimits(0, (prompts.size() * 3) / 2);
		for (int i = 0;  i < prompts.size();  i++) {
			word = _storyTemplate.getRandomWord(this, (String)(prompts.elementAt(i)));
			strings.addElement(word);
			_progressMeter.increment(1);
		}
		
		// Put the words into the story:
		substituteWords(_storyChoice.getSelectedItem(), strings);
	}

	/**
	 * Generates a story by prompting the user for words.  Reads the prompt
	 * file, then opens the prompt window.  When the user clicks "OK" in
	 * that window, the words will be substituted into the story.
	 */
	public void promptGenerateStory()
	{
		try {
			_promptGenerateStory();
		} catch (IOException e) {
			System.out.println(e);
			_storyTextArea.setText("");
		}
	}
	
	/**
	 * Generates a story by prompting the user for words.  Reads the prompt
	 * file, then opens the prompt window.  When the user clicks "OK" in
	 * that window, the words will be substituted into the story.
	 *
	 * @exception	IOException if error reading prompt file.
	 * @exception	MalformedURLException if error finding prompt file.
	 * @see			NetLibs.PromptWindow#action()
	 */
	private void _promptGenerateStory() throws IOException, MalformedURLException
	{
		// Get the word prompts:
		Vector	prompts = _storyTemplate.getPrompts(this, _storyChoice.getSelectedItem());
		
		// Set up and open the prompt window:
		_progressMeter.setLimits(0, prompts.size());
		PromptWindow pwin = new PromptWindow(this, prompts, _storyChoice.getSelectedItem());
		pwin.show();
	}
	
	/**
	 * Gets the story template and substitutes the chosen words into the story.
	 *
	 * @param	storyName	the name of the story template.
	 * @param	strings		the list of words to be substituted into the story.
	 */
	public void substituteWords(String storyName, Vector strings)
	{
		try {
			String template = _storyTemplate.getTemplate(this, storyName);
			_showStory(template, strings);
		} catch (IOException e) {
			System.out.println(e);
		}
	}
	
	/**
	 * Scan through the story template, substitute the chosen words, and
	 * display the resulting story.
	 *
	 * @param	template	the story template text.
	 * @param	strings		the list of words to be substituted into the story.
	 */
	private void _showStory(String template, Vector strings)
	{
		StringBuffer	story = new StringBuffer();
		int				wrapIndex = 0;
		boolean			prefixAorAn = false;
		boolean			capitalize = false;
		StringBuffer	strIndex;
		Integer			intIndex;
		int				index;
		char			theChar;
		String			str;
		String			aoran;
		
		// Figure out the progress meter increments:
		int incr = template.length() / (_progressMeter.getMax() - _progressMeter.getValue());
		int count = 0;
		
		// Look through the template character by character and
		// substitute words from the vector whenever a "<" character
		// is encountered:
		int i = 0;
		while (i < template.length()) {
		
			theChar = template.charAt(i);

			// To get around the word wrapping bug in some versions
			// of Java, let's just keep track of it here:
			if (wrapIndex >= _WRAP_FACTOR && theChar == ' ') {
				// Insert a carriage return:
				story.append('\n');
				wrapIndex = 0;
			}
			else if (theChar == '<') {
				// Find the number or prompt in the "" brackets:
				i++;
				strIndex = new StringBuffer();
				while (template.charAt(i) != '>') {
					strIndex.append(template.charAt(i));
					i++;
				}
				
				// Look for "a" prompt (indefinite article):
				if (strIndex.charAt(0) == 'a') {
					prefixAorAn = true;
					capitalize = false;
				} else if (strIndex.charAt(0) == 'A') {
					prefixAorAn = true;
					capitalize = true;
				} else {
					// Convert the string to a number:
					index = 0;
					try {
						intIndex = new Integer(strIndex.toString());
						index = intIndex.intValue();
					} catch (NumberFormatException e) {
						System.out.println(e);
					}
					
					// Substitute the corresponding word:
					if (index > 0) {
						str = (String)(strings.elementAt(index - 1));
						if (prefixAorAn) {
							// Prefix "a" or "an" if necessary:
							aoran = BensUtils.getIndefiniteArticle(str, capitalize);
							wrapIndex += aoran.length() + 1;
							story.append(aoran);
							story.append(" ");
							prefixAorAn = false;
							capitalize = false;
						}
						wrapIndex += str.length();
						story.append(str);
					}
				}
			}
			else {
				// Copy the character to the story buffer, but skip
				// one redundant space in the beginning of the line:
				if (theChar == ' ' && wrapIndex == 0) {
					wrapIndex++;
				}
				else if ( ! (theChar == ' ' && prefixAorAn)) {
					story.append(theChar);
				
					// Take care of the kludgy word wrapping:
					if (theChar == '\n') {
						wrapIndex = 0;
					} else {
						wrapIndex++;
					}
				}
			}
			
			i++;	// Next character.
			
			// Increment the progress meter if necessary:
			count++;
			if (count > incr) {
				count = 0;
				_progressMeter.increment(1);
			}
		}
		
		// Show the story:
		_storyTextArea.setText(story.toString());
		_storyTextArea.select(0, 0);
	}
	
	/**
	 * The story is about to be generated.  Take care of various interface
	 * elements.  For example, disable the "Generate Story" button, reset
	 * the progress meter, etc.
	 */
	private void _preGenerateStory()
	{
		_progressMeter.setValue(0);
		_genStoryButton.disable();
		_genStoryButton.update(_genStoryButton.getGraphics());
	}

	/**
	 * The story generation is complete.  Take care of various interface
	 * elements.  For example, enable the "Generate Story" button, reset
	 * the progress meter, etc.
	 */
	private void _postGenerateStory()
	{
		_progressMeter.setValue(0);
		_genStoryButton.enable();
		_genStoryButton.update(_genStoryButton.getGraphics());
	}

	//
	// CommandListener interface:
	//
	
	/**
	 * This is a "listener" which is called whenever a command is
	 * selected.  It is used for doing things like updating buttons,
	 * menu items, etc.
	 *
	 * @param	command		the command which was executed.
	 * @see		NetLibsConst
	 */
	public boolean commandSelected (int command)
	{
		boolean		ret = true;
		
		switch(command) {
		case CMD_AUTO_GENERATE_STORY:
			_preGenerateStory();
			break;
		case CMD_PROMPT_GENERATE_STORY:
			_preGenerateStory();
			break;
		case CMD_STORY_GENERATED:
			_postGenerateStory();
			break;
		default:
			// No action for default case.
		}
		
		return ret;
	}

}