What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Build a playable command-line adventure by modeling rooms, items and player state separately, then connecting them with a line-based command loop. This guide targets Java 25 or newer and uses conventional Java classes; the core design also works on older Java releases if you avoid newer syntax.
The finished game lets you explore connected rooms, collect a key, unlock a door, and win. It also handles multi-word commands, invalid input and end-of-file without crashing.
What a text adventure needs
A text adventure is a state-management and input-processing program. Each turn follows the same sequence:
- Describe the current room and its exits.
- Read one complete line of input.
- Parse the line into a command and its argument.
- Check whether the action is valid.
- Update game state and print the result.
- Repeat until the player wins or quits.
You do not need a game engine, graphics framework, database or threads for this project. The useful separation is between the world (rooms and items), the player’s state, and the code that reads and responds to commands.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsSet up a Java project
Java 25 is a long-term-support release, and Java 26 is the newer feature release as of August 2026. Neither is required for the game’s basic concepts, but the example below uses records and text blocks, so use Java 25 or newer for it as written. See Oracle’s Java 25 release information and JetBrains’ Java 26 overview. A multi-class project is easier to grow with ordinary source files than with compact source-file syntax.
You can use an IDE or compile from a terminal. IntelliJ IDEA’s project wizard supports its own builder, Maven and Gradle, and can help select a JDK; its project wizard guide covers the setup. The unified IntelliJ IDEA distribution includes free core functionality, with advanced capabilities available through Ultimate; a paid IDE is not needed for this console game. See JetBrains’ distribution details.
For a plain project, use this layout:
text-adventure/
└── src/main/java/adventure/
├── Main.java
├── Game.java
├── GameState.java
├── Player.java
├── Room.java
├── Item.java
├── Command.java
├── Parser.java
└── WorldFactory.java
On a Unix-like shell, compile and run it with:
mkdir -p out
javac -d out $(find src/main/java -name '*.java')
java -cp out adventure.Main
On Windows PowerShell, use:
New-Item -ItemType Directory -Force out
javac -d out (Get-ChildItem -Recurse src/main/java -Filter *.java)
java -cp out adventure.Main
The file-list syntax differs by shell. Maven or Gradle can handle compilation and project layout consistently across environments, but neither is necessary for the first playable version. Gradle’s Java application tutorial explains project initialization, running and bundling; IntelliJ’s Maven support guide covers its Maven integration.
Give each part of the game a responsibility
Keep the first design small. Each class below owns one kind of information:
| Class | Responsibility |
|---|---|
Room |
Name, description, exits and items on the floor |
Item |
Immutable item name and description |
Player |
Current room and inventory |
GameState |
Player, objective room and flags such as completion |
Parser |
Turns an input line into a verb and argument |
Game |
Runs the turn loop and applies command behavior |
WorldFactory |
Creates rooms, connections, items and the starting state |
Classes are useful when they clarify ownership. In particular, avoid scattering game flags in static variables: a fresh GameState makes restarting and testing a second game straightforward.
Model rooms, items and the player
Store exits by direction rather than creating separate fields for north, south, east and west. A map also allows exits such as up, inside or portal.
package adventure;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public final class Room {
private final String name;
private final String description;
private final Map<String, Room> exits = new HashMap<>();
private final Map<String, Item> items = new HashMap<>();
public Room(String name, String description) {
this.name = name;
this.description = description;
}
public String name() { return name; }
public String description() { return description; }
public void connect(String direction, Room destination) {
exits.put(direction.toLowerCase(java.util.Locale.ROOT), destination);
}
public Room exit(String direction) {
return exits.get(direction.toLowerCase(java.util.Locale.ROOT));
}
public Set<String> directions() {
return Collections.unmodifiableSet(exits.keySet());
}
public void addItem(Item item) {
items.put(item.name().toLowerCase(java.util.Locale.ROOT), item);
}
public Item removeItem(String name) {
return items.remove(name.toLowerCase(java.util.Locale.ROOT));
}
public Collection<Item> items() {
return Collections.unmodifiableCollection(items.values());
}
}
Normalize lookup strings with Locale.ROOT rather than the machine’s default locale so commands and item names behave consistently across systems. Keep display names unchanged if you want capitalization in the printed story.
Rank #2
An item can be a record because its data does not need to change:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →package adventure;
public record Item(String name, String description) {}
Records are a standard Java feature from Java 16 onward. See the Java versions supported by IntelliJ IDEA for its language support information.
Use a map for inventory when commands look items up by name:
package adventure;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public final class Player {
private Room location;
private final Map<String, Item> inventory = new HashMap<>();
public Player(Room startingLocation) {
this.location = startingLocation;
}
public Room location() { return location; }
public void moveTo(Room room) { location = room; }
public boolean addItem(Item item) {
return inventory.put(key(item.name()), item) == null;
}
public Item removeItem(String name) {
return inventory.remove(key(name));
}
public boolean hasItem(String name) {
return inventory.containsKey(key(name));
}
public Collection<Item> inventory() {
return Collections.unmodifiableCollection(inventory.values());
}
private static String key(String name) {
return name.toLowerCase(Locale.ROOT);
}
}
The unmodifiable collection view prevents other classes from changing the player’s inventory behind the player’s methods. If a game later allows multiple objects with the same name, use unique item IDs or provide a way for the player to distinguish them.
Build a connected world
Put world construction in one factory instead of mixing it into the command loop. The example has four rooms and a key in the courtyard:
package adventure;
public final class WorldFactory {
private WorldFactory() {}
public static GameState create() {
Room gate = new Room("Gate", "You stand before an old stone gate.");
Room courtyard = new Room("Courtyard", "Weeds cover a silent courtyard.");
Room tower = new Room("Tower", "A narrow tower rises above the courtyard.");
Room treasure = new Room("Treasure Room", "A locked chamber glitters in the torchlight.");
gate.connect("north", courtyard);
courtyard.connect("south", gate);
courtyard.connect("up", tower);
tower.connect("down", courtyard);
tower.connect("east", treasure);
treasure.connect("west", tower);
courtyard.addItem(new Item("key", "A small iron key."));
return new GameState(new Player(gate), treasure);
}
}
A connection only goes one way: adding gate.connect("north", courtyard) does not create a south exit automatically. Add the reverse connection when the player should be able to return. In a larger world, also check that every intended room is reachable from the starting room.
Track mutable state explicitly
The player’s location and inventory belong to Player; game-wide progress belongs to GameState. The state object can expose only the operations the game needs:
package adventure;
public final class GameState {
private final Player player;
private final Room treasureRoom;
private boolean finished;
public GameState(Player player, Room treasureRoom) {
this.player = player;
this.treasureRoom = treasureRoom;
}
public Player player() { return player; }
public Room treasureRoom() { return treasureRoom; }
public boolean isFinished() { return finished; }
public void finish() { finished = true; }
}
Unlocking the treasure room in this version is checked when the player tries to enter it, so the game does not need an extra “door open” flag. If opening a door should persist as a distinct world change, represent that explicitly in state rather than only printing a message.
Parse complete command lines
Read a whole line, then split it into a verb and the rest of the line. This preserves multi-word item names such as brass key.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →package adventure;
import java.util.Locale;
public record Command(String verb, String argument) {
public boolean hasArgument() {
return argument != null && !argument.isBlank();
}
public static Command empty() {
return new Command("", "");
}
}
public final class Parser {
public Command parse(String input) {
if (input == null || input.isBlank()) return Command.empty();
String normalized = input.trim().toLowerCase(Locale.ROOT);
String[] parts = normalized.split("\s+", 2);
String argument = parts.length == 2 ? parts[1].trim() : "";
return new Command(parts[0], argument);
}
}
The split limit of two means the parser keeps everything after the first word as the argument. Thus take brass key becomes verb take and argument brass key. Using split(" ") instead is fragile: repeated spaces can create empty tokens, and treating every word as a separate field makes multi-word arguments awkward.
Keep canonical commands simple at first: look, go, take, use, inventory, help and quit. Once these work, aliases such as l for look or i for inventory can improve convenience, but every alias adds another input case to test.
Read input and run the game loop
BufferedReader.readLine() is a natural fit for a line-command game: it returns one complete line and returns null when input reaches end-of-file. The Java 25 API documentation specifies that behavior. A Scanner can also work for a small exercise, but its token-oriented methods are less convenient for commands with multi-word arguments, and mixing token reads with nextLine() often surprises beginners.
Here is the loop and a compact set of command handlers. It uses the classes already introduced:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutepackage adventure;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Collection;
import java.util.Locale;
import java.util.stream.Collectors;
public final class Game {
private final GameState state;
private final Parser parser = new Parser();
public Game(GameState state) {
this.state = state;
}
public void run(BufferedReader reader) throws IOException {
System.out.println("Welcome to the adventure.");
describeLocation();
while (!state.isFinished()) {
System.out.print("> ");
String line = reader.readLine();
if (line == null) {
System.out.println();
System.out.println("Input ended. Goodbye.");
return;
}
execute(parser.parse(line));
}
System.out.println("You win!");
}
private void execute(Command command) {
switch (command.verb()) {
case "" -> System.out.println("Enter a command.");
case "help" -> showHelp();
case "look" -> describeLocation();
case "inventory" -> showInventory();
case "go" -> go(command.argument());
case "take" -> take(command.argument());
case "use" -> use(command.argument());
case "quit" -> state.finish();
default -> System.out.println("I do not understand that command. Type "help" for a list.");
}
}
private void describeLocation() {
Room room = state.player().location();
System.out.println();
System.out.println(room.name());
System.out.println(room.description());
if (!room.items().isEmpty()) {
System.out.println("Items: " + room.items().stream()
.map(Item::name).sorted().collect(Collectors.joining(", ")));
}
if (!room.directions().isEmpty()) {
System.out.println("Exits: " + room.directions().stream()
.sorted().collect(Collectors.joining(", ")));
}
}
private void showHelp() {
System.out.println("Commands: look, go <direction>, take <item>, use <item>, inventory, help, quit");
}
private void showInventory() {
Collection<Item> items = state.player().inventory();
if (items.isEmpty()) {
System.out.println("Your inventory is empty.");
return;
}
System.out.println("You are carrying:");
items.stream().map(Item::name).sorted().forEach(item -> System.out.println("- " + item));
}
private void go(String direction) {
if (direction.isBlank()) {
System.out.println("Go where?");
return;
}
Room destination = state.player().location().exit(direction.toLowerCase(Locale.ROOT));
if (destination == null) {
System.out.println("You cannot go that way.");
return;
}
if (destination == state.treasureRoom() && !state.player().hasItem("key")) {
System.out.println("The door is locked. You need a key.");
return;
}
state.player().moveTo(destination);
describeLocation();
if (destination == state.treasureRoom()) state.finish();
}
private void take(String itemName) {
if (itemName.isBlank()) {
System.out.println("Take what?");
return;
}
Item item = state.player().location().removeItem(itemName);
if (item == null) {
System.out.println("There is no such item here.");
return;
}
state.player().addItem(item);
System.out.println("You take the " + item.name() + ".");
}
private void use(String itemName) {
if (itemName.isBlank()) {
System.out.println("Use what?");
return;
}
if (!state.player().hasItem(itemName)) {
System.out.println("You are not carrying that.");
return;
}
if (itemName.equalsIgnoreCase("key")
&& state.player().location().name().equals("Tower")) {
System.out.println("The key is ready for the eastern door.");
return;
}
System.out.println("Nothing happens.");
}
}
Because entering the treasure room itself is the win condition, the example checks for the key before moving into that room and ends the game after a successful move. The use key command is a simple demonstration hook; a more elaborate puzzle can instead store an unlocked-door flag and require the player to use the key before the exit becomes available.
Rank #4
The game loop handles ordinary input mistakes with messages rather than exceptions. The crucial order for a gated action is validate first, mutate second: never move the player and only then discover that the key is missing.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Connect the program’s entry point
Keep main as an assembly point: it creates a fresh world, connects the game to standard input, and starts execution.
package adventure;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public final class Main {
private Main() {}
public static void main(String[] args) throws IOException {
Game game = new Game(WorldFactory.create());
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in))) {
game.run(reader);
}
}
}
Now a test or restart can call WorldFactory.create() again to get a clean player, inventory and world instead of inheriting stale static state.
Recommended Free Tools
Handle common input and world failures
- Blank line: the parser returns an empty command and the game prompts for a command again.
- Repeated spaces: trimming and splitting on
\s+parsesgo northasgo north. - Unknown command: explain that the verb was not recognized and point to
help; this is normal player input, not a Java error. - Missing argument: answer
Take what?orGo where?instead of indexing into a missing array element. - Invalid exit or absent item: leave the player and world unchanged, and give a clear message.
- Repeated item pickup: remove an item from its room when taken, so a second attempt fails cleanly.
- End-of-file: check for
nullafterreadLine()and exit. Otherwise redirected input or a closed console can leave the loop in a bad state. - Quit that does not quit: make the command change the loop condition, as
state.finish()does. - One-way exit: add both directions explicitly when a passage should be reversible.
Test parsing and game rules
Test the parser independently from the console. It should preserve the complete argument and handle empty input:
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class ParserTest {
@Test
void parsesMultiWordArgument() {
Command command = new Parser().parse("take brass key");
assertEquals("take", command.verb());
assertEquals("brass key", command.argument());
}
@Test
void parsesEmptyLine() {
Command command = new Parser().parse(" ");
assertEquals("", command.verb());
assertEquals("", command.argument());
}
}
For the world rules, verify state changes rather than relying only on exact printed text. A small test suite should cover:
- The player starts in the gate room.
- A valid movement changes location; an invalid one does not.
- Taking the key removes it from the courtyard and adds it to inventory.
- The player cannot enter the treasure room without the key.
- Entering the objective room with the key ends the game.
To test the loop without a physical terminal, give it a BufferedReader over a StringReader:
String commands = """
look
go north
take key
inventory
quit
""";
BufferedReader reader = new BufferedReader(new StringReader(commands));
This works because the game reads complete lines and keeps input acquisition separate from parsing. Use a fresh GameState for each test so one test’s inventory or room position cannot affect another.
Free tools Windows power users keep installed
One-click scans. No signup required.
Best Value
Choose when to add more structure
Keep a switch while commands are few
A switch is clear for a small game and helps a beginner see the full command vocabulary in one place. If the game grows to many commands with substantial behavior, move each handler into a class or dispatch through a map of command names to handlers. A map is flexible, but can obscure the flow if introduced before the basic loop is understood.
Use enums when directions are fixed
Normalized strings are quick to prototype and permit custom exits. If the world has a fixed set of directions, an enum such as Direction.NORTH makes spelling mistakes harder and can pair with EnumMap. It requires parsing the player’s text into an enum, so it is a useful refactor rather than a prerequisite.
Move special rules out of command code as the game grows
The example’s locked-room condition is intentionally hard-coded. That is understandable for one puzzle; many puzzles will make room names, item names and conditionals accumulate in the command handlers. At that point, represent locked exits or requirements as data, or give a dedicated rules object responsibility for validating actions.
Keep the core dependency-free
Maven or Gradle becomes useful when you add JUnit tests, dependencies or repeatable packaging. Gradle’s official guide shows how to initialize, run and bundle a Java application: Gradle Java application tutorial. A runnable JAR needs a manifest identifying the main class or equivalent build-tool configuration; IntelliJ’s Java application tutorial covers running and packaging an application. A paid build tool, IDE or JDK is not required to compile this game.
Extend the game after the first win
Once movement, inventory and the objective work, add one feature at a time:
drop <item>to move an item from inventory back into the room.examine <item>to display an item’s description.- Characters, dialogue and multiple endings.
- Health and combat, if the story needs them.
- Save and load files, after deciding how world state will be serialized.
- A map display or randomized encounters.
- External JSON or YAML content files after the world model is stable.
- A GUI or web interface that reuses the same game rules and state.
A small game built with clean state and command boundaries is easier to expand than one long method. The key design test is simple: adding a room, item or command should have a clear place in the code, and invalid actions should leave the world in a valid state.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




