Text Game Engine

Oh, so OO. Play the game, write your own.

Page 1 of 1

3 Replies - 2178 Views - Last Post: 12 January 2009 - 07:47 PM

#1 baavgai  Icon User is offline

  • Dreaming Coder
  • member icon

Reputation: 5643
  • View blog
  • Posts: 12,359
  • Joined: 16-October 07

Text Game Engine

Posted 08 January 2009 - 01:56 PM

I had promised to post an OOP example of a text game. I went with the classic "Adventure" game style, with verb basic syntax. At the end of the day it grew on me to near 15 classes and was not the basic, simple, example I'd planned.

Still, it works and is fun. There's probably a whole lot of room for improvement, but I'm done messing with it for the moment.

Of course, some of those classes are pretty dinky:
package baavgai.textgame.framework;
// this is all you need for a text game
public interface IGameEngine {
	void processAction(String cmd);
	String getPendingMessage();
	boolean isGameOver();
}


When you think about it, the heart of a text game is simplest of conversations. I send a message to the game, the game sends a message back. We keep at this until some kind of end condition is met.

This type of game is a challenge for a number of reasons, but mostly because keeping track of all the elements is a pain. More elements, more pain. OO design can help with this. Objects hold state. If we can make our game simply be a series of objects and have the individual objects do as much work as possible, it's a much easier logistic.

Here's another dinky class (ok, interface):
package baavgai.textgame.framework;

// all objects that can be interacted with are reponsible for this
public interface IActionProcessor {
	// if an object says it can process it, it gets too
	boolean canProcess(UserAction action);

	// this method will be called if above it true
	boolean process(UserAction action, Player player);
}


A collection of objects that I can iterate through, asking if they want to process a request, seemed a good approach.

Now for some meat:
package baavgai.textgame.framework;

import java.util.*;

// everything the user interacts with extends this class
public abstract class ItemBase implements IActionProcessor {
	// the identical action can be described by several verbs
	// we keep the common ones here
	protected static final String[] V_USE = new String[] {"use", "activate", "apply"};
	protected static final String[] V_TAKE = new String[] {"take", "get", "pickup", "steal", "pocket"};
	protected static final String[] V_GO = new String[] {"go", "walk", "head", "crawl"};
	protected static final String[] V_LOOK = new String[] {"look", "examine", "inspect"};
	
	protected List<IActionProcessor> actions = new ArrayList<IActionProcessor>();
	protected final String name;
	protected String lookText;

	public ItemBase(String name, String lookText) { 
		this.name = name;
		this.lookText = lookText;
		initDefaulActions();
	}
	
	public ItemBase(String name) {
		this(name, "Yep, looks like a " + name + ".");
	}
	
	protected void initDefaulActions() {
		actions.add(new ProcessorImpl(this, V_LOOK) {
			public boolean process(UserAction action, Player player) {
				processLook(player);
				return true;
			}
		});
	}
	//protected ItemBase() { }
  
	public String getName() { return name; }
	protected String  getLook(Player player) { return lookText; }
	protected void processLook(Player player) { player.addMessage(getLook(player)); }
	public boolean isVisible(Player player) { return true; }
	public boolean canProcess(UserAction action) { return action.isNounMatch(this.name); }

	protected <T> IActionProcessor getProcessor(UserAction action, Collection<T> list) {
		for(T item : list) {
			IActionProcessor processor = (IActionProcessor)item;
			if (processor.canProcess(action)) { return processor; }
		}
		return null;
	}
	
	public boolean process(UserAction action, Player player) {
		IActionProcessor processor = getProcessor(action, actions);
		return processor!=null && processor.process(action, player);
	}
   
}



The bulk of all objects extend ItemBase. You can already see how this gets used, with the V_LOOK processor being loaded. That's the heard of the item structure; they are containers for action and can be easily extended.

Finally, here's what a game implemented in the framework looks like. It is a complete little game and shows all the variations I could think of.
package baavgai.textgame.example;
import baavgai.textgame.framework.*;

class DemoWorld extends Area {
	public DemoWorld() {
		this.items.add(new InventoryItem("bucket", "It's got a hole in it."));
		
		this.items.add(new InventoryItem("rope", 
				"It's good, strong, hemp rope.  Pity it's not tied at the top of the well"
				+ "\nIt's surprisingly dry.  Don't smoke it."
				) {
			protected void initDefaulActions() {
				super.initDefaulActions();
				actions.add(new ProcessorImpl(this, new String[] {"smoke"}) {
					public boolean process(UserAction action, Player player) {
						player.addMessage("I told you not to do that!\n");
						return true;
					}
				});
			}
		});
		
		this.items.add(new DisplayItem("remains") {
			protected void processLook(Player player) { 
				player.addMessage(
						"They are human remains."
						+ " As you examine them they fall apart into a pile of bones." 
						+ " The skull rolls across the floor."
						+ " You hear a clinking noise."
						);
				player.addNewItemToArea(new DisplayItem("bones", "A pile of human bones.  Ick."));
				player.addNewItemToArea(new InventoryItem("skull"));
				player.addNewItemToArea(new InventoryItem("flint") {
					private boolean firstLook = true;
					protected String getLook(Player player) {
						String msg = "A stone with a fractured beveled edge.  ";
						if (firstLook) {
							firstLook = false;
							msg += "For some reason you can't recall, you recognize this as flint."
							+"  You could even make a fire with it, if you had some steel...";
						} else {
							msg += "If you had some steel, you might be able to make a fire.";
						}
						return msg;
					}
					
					protected void processUse(Player player) {
						if (!player.hasItem("knife")) {
							player.addMessage("You have nothing to strike against to make a spark.");
							return;
						}
						if (!player.hasItem("rope")) {
							player.addMessage("You make some nice sparks.  If only you had something dry and flammable.");
							return;
						}
						player.addMessage("Using skills acquired from watching too much reality TV, you manage to get the end of the rope lit.");
						player.addMessage("  The rope is stiff and makes a serviceable torch.");
						player.items.add(new InventoryItem("torch",
								"It's actually a flaming rope, but it works.  Now you can see more of where you are."));
						
						player.destroyItemOnSelf("rope");
						
					}

				});
				player.destroyItemInArea(this);
			}
		});
		
		Area area1, area2;

		area1 = new AreaImpl("You are in underground caverns.  A luminous fungus coats the walls here."
				+"  You hear the sound of water moving, but don't see any."
				+"  The cave continues east and west."
				);

		//area1.getItems().add(new InventoryItem("torch"));
		
		this.items.add(new Exit(Exit.DIR_N, area1));
		area1.getItems().add(new Exit(Exit.DIR_S, this));
		
		area2 = new AreaImpl("More glowing fungus.  The walls and floor are covered with tasty looking mushrooms.");
		area2.getItems().add(new Shroom());

		area1.getItems().add(new Exit(Exit.DIR_E, area2));
		area2.getItems().add(new Exit(Exit.DIR_W, area1)); 

		area2 = new AreaImpl("There is moving water.  Something glints in the water.");
		area2.getItems().add(new DisplayItem("stream") {
			private boolean firstLook = true;
			protected String getLook(Player player) {
				String msg = "A stream of black water eriely reflects the fungal glow.";
				if (firstLook) {
					firstLook = false;
					msg += "  Strange blind fish dart back and forth.";
					msg += "  You see something reflecting below the surface, looks like a knife.";
					
					player.addNewItemToArea(new Fish());
					player.addNewItemToArea(new InventoryItem("knife") {
						protected void processTake(Player player) {
							if (player.isItemInArea("fish")) {
								player.addMessage("You reach for the knife...\n");
								player.addMessage("Just as your hand closes on the handle, the fish swarm upon you.");
								player.addMessage("  Before you can react, in an explosion of blood and gore, you have no arm.");
								player.addMessage("  After that it gets ugly.  You die.");
								player.endGame();
							} else {
								player.moveItemAreaToSelf(this);
							}
						}
					});
				}
				return msg;
			}
		});

		area1.getItems().add(new Exit(Exit.DIR_W, area2));
		area2.getItems().add(new Exit(Exit.DIR_E, area1)); 
		
	}

	
	class Fish extends DisplayItem {
		public Fish() { 
			super("fish", 
					"Ugly pale blind subterranean fish.  All milky white except for nasty looking black teeth."
					+ "  They look like they'll make a meal of you."
					);
		}
		
		protected void processTake(Player player) {
			player.addMessage("Leave the fish alone.  They look hungry.");
		}

	   
		protected void initDefaulActions() {
			super.initDefaulActions();
			actions.add(new ProcessorImpl(this, new String[] {"feed", "give"}) {
				public boolean process(UserAction action, Player player) {
					boolean hasFood = player.hasItem("mushroom");
					if (!hasFood) {
						player.addMessage("You don't have anything to feed them with.");
					} else {
						player.addNewItemToArea(new DisplayItem("deadfish", 
								"Corpses of the murderous fish you killed are slowly rotting."));
						player.addMessage("You feed the shrooms to the fish...");
						player.addMessage("\nAll the fish descend on the bounty in a feeding frenzy!");
						player.addMessage("  Wait, some of them aren't moving anymore.");
						player.addMessage("  Looks like you killed them all.  I hope you're happy.");
						player.destroyItemOnSelf("mushroom");
						player.destroyItemInArea((ItemBase)owner);
					}
					return true;
				}
			});
		}
	}
	
	class Shroom extends InventoryItem {
		public Shroom() { super("mushroom"); }
	   
		protected void initDefaulActions() {
			super.initDefaulActions();
			String [] vEat = new String[] {"eat"};
			actions.add(new ProcessorImpl(this, vEat) {
				public boolean process(UserAction action, Player player) {
					player.addMessage("Are you an idiot?  You don't eat strange shrooms!");
					return true;
				}
			});
		}
	}
	

	// come back to the escape too often and you die
	protected String getLook(Player player) {
		String msg = "High above you is a well opening.";
		if (this.visits==1) {
			msg += "\nYou are at the bottom of a well ( yes, you're an idiot. )"
			+ "\nLucky for you, it's low tide and receding water has revealed a hole"
			+ "\nyou think you can squeeze into.";
		} else if (this.visits<3) {
			msg += "\nThe water apears to be rising"; 
		} else if (this.visits<8) {
			msg += "\nThe water is rising more quickly now."; 
		} else if (this.visits==9) {
			msg += "\nSuddenly the water comes rushing in."
			+ "\nTonight, you sleep with the fishes.";
			player.endGame();
		}
		if (player.hasItem("torch") && !player.isItemInArea("ladder")) {
			msg += "\nIn the light of your new found torch, you see an ancient ladder fused to the wall!";
			player.addNewItemToArea(new DisplayItem("ladder", 
			"A decaying arrangment of rope and wood can now be seen just above eye level under mossy cover.") {
				protected void initDefaulActions() {
					super.initDefaulActions();
					actions.add(new ProcessorImpl(this, V_USE) {
						public boolean process(UserAction action, Player player) {
							player.addMessage("You climb the ladder to freedom.  Congratulations!");
							player.endGame();
							return true;
						}
					});
				}
				
			});
			
		}
		return msg; 
	}
}



The rest of the code is attached.

Attached File(s)



Is This A Good Question/Topic? 0
  • +

Replies To: Text Game Engine

#2 badjava  Icon User is offline

  • Lux Ex Tenebris
  • member icon

Reputation: 14
  • View blog
  • Posts: 540
  • Joined: 30-October 08

Re: Text Game Engine

Posted 10 January 2009 - 03:07 AM

View Postbaavgai, on 8 Jan, 2009 - 12:56 PM, said:

I had promised to post an OOP example of a text game.


Heya baavgai, I gave it a shot but I don't know enough about netbeans yet to get this to build and run. Would you give a bit of a walk through on how to add all of the included files in to a project and get this to run?
Was This Post Helpful? 0
  • +
  • -

#3 baavgai  Icon User is offline

  • Dreaming Coder
  • member icon

Reputation: 5643
  • View blog
  • Posts: 12,359
  • Joined: 16-October 07

Re: Text Game Engine

Posted 12 January 2009 - 10:49 AM

This got a rewrite. Please do not look at above code, though I'm leaving it for the curious. This is the good one. ;)

Again, here's what a game looks like with the engine:

package baavgai.textgame.example;

import baavgai.textgame.framework.*;

public class DemoTextGame extends GameEngine {
	protected void initItems() {
		Location topLocation = (Location)addItem(new Location() {
			public void processLook(IGameSession session) {
				session.addMessage("High above you is a well opening.");
				if (session.getTurn()==1) {
					session.addMessage("\nYou are at the bottom of a well ( yes, you're an idiot. )"
					+ "\nLucky for you, it's low tide and receding water has revealed a hole"
					+ "\nyou think you can squeeze into.");
				} else if (session.getTurn()<3) {
					session.addMessage("\nThe water apears to be rising"); 
				} else if (session.getTurn()<8) {
					session.addMessage("\nThe water is rising more quickly now."); 
				} else if (session.getTurn()==9) {
					session.addMessage("\nSuddenly the water comes rushing in."
					+ "\nTonight, you sleep with the fishes.");
					session.endGame();
				}

				if (session.hasItemPlayer("torch") && !session.hasItemLoc("ladder")) {
					session.addMessage("\nIn the light of your new found torch, you see an ancient ladder fused to the wall!");
					
					session.addItem(new DisplayItem("ladder", 
					"A decaying arrangment of rope and wood can now be seen just above eye level under mossy cover.") 
					{
						public void processUse(IGameSession session) {
							session.addMessage("You climb the ladder to freedom.  Congratulations!");
							session.endGame();
						}
							
					});
					
				}
			}
			
		});
		
		this.setCurrentLocation(topLocation);
		
		addItem(new InventoryItem("bucket", "It's got a hole in it."));
		
		addItem(new InventoryItem("rope", 
				"It's good, strong, hemp rope.  Pity it's not tied at the top of the well"
				+ "\nIt's surprisingly dry.  Don't smoke it."
				) {
			protected void init() {
				addAction(new Action(new String[] {"smoke"}) {
					public void process(Item parentItem, IGameSession session) {
						session.addMessage("I told you not to do that!\n");
					}
				});
				
			}
		});
		
		addItem(new DisplayItem("remains") {
			public void processLook(IGameSession session) {
				session.addMessage("They are human remains."
							+ " As you examine them they fall apart into a pile of bones." 
							+ " The skull rolls across the floor."
							+ " You hear a clinking noise."
							);
				session.addItem(new DisplayItem("bones", "A pile of human bones.  Ick."));
				session.addItem(new InventoryItem("skull"));
				
				session.addItem(new InventoryItem("flint") {
					private boolean firstLook = true;
					public void processLook(IGameSession session) {
						session.addMessage("A stone with a fractured beveled edge.  ");
						if (firstLook) {
							firstLook = false;
							session.addMessage("For some reason you can't recall, you recognize this as flint.");
							session.addMessage("  You could even make a fire with it, if you had some steel...");
						} else {
							session.addMessage("If you had some steel, you might be able to make a fire.");
						}
					}

					public void processUse(IGameSession session) {
						if (!session.hasItemPlayer("knife")) {
							session.addMessage("You have nothing to strike against to make a spark.");
						} else if (!session.hasItemPlayer("rope")) {
							session.addMessage("You make some nice sparks.  If only you had something dry and flammable.");
						} else {
							session.addMessage("Using skills acquired from watching too much reality TV, you manage to get the end of the rope lit.");
							session.addMessage("  The rope is stiff and makes a serviceable torch.");
							InventoryItem item = new InventoryItem("torch",
									"It's actually a flaming rope, but it works.  Now you can see more of where you are.");
							session.addItem(item);
							item.processTake(session);
							
							session.removeItem(session.findItem("rope"));
							session.removeItem(this);
						}

					}
				});
				session.removeItem(this);
			}
		});
		
		
		
		
		Location loc1, loc2;

		loc1 = new Location() {
			public void processLook(IGameSession session) {
				session.addMessage(
						"You are in underground caverns.  A luminous fungus coats the walls here."
						+"  You hear the sound of water moving, but don't see any."
						+"  The cave continues east and west."
						);
			}
		};
		
		addExitItem(topLocation, ExitItem.DIR_N, loc1, ExitItem.DIR_S);
		
		loc2 = new Location() {
			public void processLook(IGameSession session) {
				session.addMessage(
						"More glowing fungus.  The walls and floor are covered with tasty looking mushrooms."
						);
			}
		};
		addExitItem(loc1, ExitItem.DIR_E, loc2, ExitItem.DIR_W);
		this.setCurrentLocation(loc2);

		addItem(new InventoryItem("mushroom") {
			protected void init() {
				addAction(new Action(new String[] {"eat"}) {
					public void process(Item parentItem, IGameSession session) {
						session.addMessage("Are you an idiot?  You don't eat strange shrooms!");
					}
				});
				
			}
		});
		
		loc2 = new Location() {
			public void processLook(IGameSession session) {
				session.addMessage("There is moving water.  Something glints in the water.");
			}
		};
		addExitItem(loc1, ExitItem.DIR_W, loc2, ExitItem.DIR_E);
		this.setCurrentLocation(loc2);
		addItem(new DisplayItem("stream") {
			private boolean firstLook = true;
			public void processLook(IGameSession session) {
				session.addMessage("A stream of black water eriely reflects the fungal glow.");
				if (firstLook) {
					firstLook = false;
					session.addMessage("  Strange blind fish dart back and forth.");
					session.addMessage("  You see something reflecting below the surface, looks like a knife.");
					
					addItem(new DisplayItem("fish",
							"Ugly pale blind subterranean fish.  All milky white except for nasty looking black teeth."
							+ "  They look like they'll make a meal of you."
							) {

						public void processTake(IGameSession session) {
							session.addMessage("Leave the fish alone.  They look hungry.");
						}
				   
						protected void init() {
							addAction(new Action(new String[] {"feed", "give"}) {
								public void process(Item parentItem, IGameSession session) {
									boolean hasFood = session.hasItemPlayer("mushroom");
									if (!hasFood) {
										session.addMessage("You don't have anything to feed them with.");
									} else {
										session.addItem(new DisplayItem("deadfish", 
											"Corpses of the murderous fish you killed are slowly rotting."));
										session.addMessage("You feed the shrooms to the fish...");
										session.addMessage("\nAll the fish descend on the bounty in a feeding frenzy!");
										session.addMessage("  Wait, some of them aren't moving anymore.");
										session.addMessage("  Looks like you killed them all.  I hope you're happy.");
										session.removeItem(session.findItem("mushroom"));
										session.removeItem(parentItem);
									}
								}
							});
						}
						
					});
					
					addItem(new InventoryItem("knife") {
						public void processTake(IGameSession session) {
							if (session.hasItemLoc("fish")) {
								session.addMessage("You reach for the knife...\n");
								session.addMessage("Just as your hand closes on the handle, the fish swarm upon you.");
								session.addMessage("  Before you can react, in an explosion of blood and gore, you have no arm.");
								session.addMessage("  After that it gets ugly.  You die.");
								session.endGame();
							} else {
								super.processTake(session);
							}
						}
					});
				}
			}
		});
		
		this.setCurrentLocation(topLocation);
	}
	
	public static void main(String[] args) {
		GameWindow game = new GameWindow(new DemoTextGame());
		game.play();
	}
}




It's just source and a jar file, no project.

To make a NetBeans Project out of it, first create an empty Java project. Then just copy the root under src (baavgai) into the src folder of the project.

For Eclipse, to an import.

To just run the thing, fire up the jar file: java -jar GameTest.jar
Attached File  TextGameJavaFinal.zip (30.22K)
Number of downloads: 163
Was This Post Helpful? 0
  • +
  • -

#4 badjava  Icon User is offline

  • Lux Ex Tenebris
  • member icon

Reputation: 14
  • View blog
  • Posts: 540
  • Joined: 30-October 08

Re: Text Game Engine

Posted 12 January 2009 - 07:47 PM

View Postbaavgai, on 12 Jan, 2009 - 09:49 AM, said:

This got a rewrite. Please do not look at above code, though I'm leaving it for the curious. This is the good one. ;)

fire up the jar file: java -jar GameTest.jar
Attachment attachment


Checking this out again, thank you for the tip :)
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1