Week 1 — Dungeon Crawler

Terminal Utility · Java · Advanced


The Idea

You're building a text-based dungeon crawler — a game where the player moves through a network of rooms, encounters enemies, picks up items, and tries to survive long enough to reach the end.

The terminal output will look something like this:

== The Damp Corridor ==
A narrow passage. Torches flicker on the walls.
Exits: north, east
Enemies: Skeleton (12/20 hp), Rat (5/5 hp)
Items: Rusty Sword

> go north
> attack skeleton
> pick up rusty sword

Simple on the surface — but underneath it's a system of interacting objects that all need to be modelled carefully. A Room is connected to other rooms. An Enemy has its own AI behaviour and responds to being attacked. An Item has an effect when picked up or used. The player has stats that change as the game progresses.

The interesting design challenge is that rooms, enemies, and items are all quite different — but they all need to participate in the same game loop. How do you model that cleanly?

What you must build by the end of the project:

Procedural generation is not required — you can design your dungeon by hand. Two optional extensions are available if you want more of a challenge:

Optional extension A — Data-driven dungeon loading Instead of hardcoding your rooms in Java, define your dungeon in a JSON or plain-text file and write a loader that reads it at startup. This separates your game data from your game logic — a principle used in virtually every real game engine. A room in your file might look something like this:

{
  "id": "crypt",
  "name": "The Crypt",
  "description": "Cold stone and the smell of decay.",
  "exits": { "north": "great_hall", "east": "armory" },
  "enemies": ["skeleton", "skeleton"],
  "items": ["rusty_sword"]
}

Your loader reads the file, constructs the right objects, and wires the rooms together by their IDs. Changing your dungeon then requires no recompilation — just editing the file.

Optional extension B — Procedural generation Write a generator that creates a dungeon at runtime: place rooms, connect them, and populate them with enemies and items according to rules you define. Even a basic generator that guarantees a connected path from start to finish is a worthwhile challenge.


Week 1 — Build the Whole Foundation

This project is advanced enough that rather than introducing one topic at a time, week 1 asks you to design and implement the full object model upfront. All four OOP topics are in play from the start — not because they're required for the sake of it, but because this project genuinely needs all of them to be built well.

By the end of week 1 you should have a small but structurally complete dungeon: a few rooms wired together, enemies and items placed in them, and the player able to move and see their surroundings. No combat yet — but the objects that combat will depend on should already exist and be correctly structured.


Classes & Subclasses

There are three distinct hierarchies in this project. Each models something that varies in type but shares a common structure.

Entities — everything that lives in the dungeon and can act or be acted upon.

Items — things the player picks up and uses.

Rooms — nodes in the dungeon graph.


Inheritance & Polymorphism

The game loop must never check what type of entity it is dealing with. When it is an enemy's turn, the loop calls takeTurn() on each enemy in the room — and the right behaviour happens automatically:

for (Entity e : room.getEntities()) {
    if (e.isAlive()) e.takeTurn(ctx);
}

No instanceof, no casting, no if (enemy is Troll). Each subclass owns its own logic entirely. The same rule applies to items — use(player) is called on whatever item the player selects, and the item handles the rest.


Containers

Three containers are required and each serves a distinct purpose:


Interfaces

Two interfaces must be defined:

interface Describable {
    String describe();
}

interface Interactable {
    void interact(Player p);
}

Describable should be implemented by Room, Enemy, and Item — anything the game needs to print a description of. The room display loop can then call describe() uniformly on everything it renders.

Interactable should be implemented by anything the player can directly act on — items (picking up or using), and optionally NPCs or environmental objects if your dungeon has them. This keeps the command parser decoupled: when the player types interact chest, the parser doesn't need to know what a chest is — it just calls interact(player) on whatever object is at that name.

Why interfaces and not abstract methods in the base classes? Room and Enemy have nothing else in common — there is no shared base class that covers both. Describable is a capability that cuts across unrelated types. That is exactly the right use case for an interface.


By the End of Week 1

Wire up a hand-authored dungeon of three or four rooms. Place enemies and items in them. Implement describe() on rooms, enemies, and items so the player sees a proper description when they enter a room. Let the player move between rooms using the exits HashMap.

No combat yet — but the full object model should be in place and structurally sound. If you find yourself wanting to write the game loop before the model is solid, stop — a shaky foundation here will make everything harder later.

What you decide:

The hardest design question this week: Player and Enemy both extend Entity and both have a takeTurn() method — but Player.takeTurn() needs to wait for user input while Enemy.takeTurn() runs its AI immediately. How do you handle that split without making Entity know anything about the terminal or the scanner? There is more than one valid answer — be ready to explain yours.