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:
- A dungeon of at least six rooms connected in a non-linear map (some branching, not just a corridor)
- At least three distinct enemy types with different behaviours
- At least three item types with different effects
- A player who can move, fight, pick up items, and die
- A win condition — reaching a final room, defeating a boss, collecting a key, your choice
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.
- An abstract base class
Entitywith shared fields (name,maxHp,currentHp,attackPower,defence) and shared concrete methods (takeDamage(int amount),isAlive(),getHpBar()). The method that defines how an entity acts on its turn —takeTurn(GameContext ctx)or similar — should be declared abstract. - A
Playersubclass. Holds an inventory, a reference to the current room, and responds to user input rather than AI logic. - At least three
Enemysubclasses, each with a mechanically distincttakeTurn():- Something straightforward — attacks for fixed damage every turn (e.g.
Skeleton) - Something reactive — changes behaviour based on its own state (e.g.
Trollthat attacks twice when below 50% hp) - Something that requires a different tactic to deal with (e.g.
Wizardthat heals itself,Batthat debuffs the player, your own idea)
- Something straightforward — attacks for fixed damage every turn (e.g.
Items — things the player picks up and uses.
- An abstract base class
Itemwith aname, adescription, anduse(Player p)declared abstract. - At least three concrete subclasses: a healing item, a weapon or damage item, and one more of your choice — a buff potion, a key, a torch, whatever fits your dungeon.
Rooms — nodes in the dungeon graph.
- A
Roomclass with a name, a description, aHashMap<String, Room>of exits, aList<Enemy>of enemies present, and aList<Item>of items on the floo. - A
Dungeonclass that holds all rooms, the starting room, and the goal room.
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:
HashMap<String, Room> exits— held in eachRoom, maps direction names ("north","down","through the mirror") to connected rooms. Using aHashMapmeans any string can be a valid exit direction, not just the four cardinal points.List<Enemy> enemiesandList<Item> items— held in eachRoom. Enemies are removed from the list when defeated; items are removed when picked up.List<Item> inventory— held inPlayer, tracks what the player is carrying. Used when the player typesuseordrop.
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:
- What your three enemy types are and what makes each mechanically distinct
- Your dungeon's theme — castle, cave, spaceship, haunted house, anything
- What your win condition is
- Whether
Roomneeds subclasses (a locked room, a shop, a boss room) — not required, but think about whether your current design would support it
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.