A turn-based dungeon game, built twice: once in Java and once in Ruby, from the same class design.
Coursework for PDOO — Object Oriented Programming and Design, year 3 of the double degree in Computer Science and Business Administration, University of Granada (2024–2025). Solo work.
The course builds one project across five practicals. Each one adds a layer to the same design, and the last two ask for the same model in a second language. That constraint is the point of the assignment: you cannot paper over a weak design, because you have to express it twice.
Build a game where several players move through a labyrinth, fight the monsters they walk into, and race to reach the exit. The rules:
- The labyrinth is a grid with walls, monsters, players and one exit.
- On each turn a player moves up, down, left or right. Walking into a monster starts a fight.
- A fight is a series of rounds. Each side rolls its attack, the defender rolls to block, and whoever runs out of tries loses.
- Players collect weapons and shields, which add to attack and defence and wear out after a few uses.
- The game ends when a player reaches the exit or the round limit runs out.
Two design constraints came with it: every random decision goes through a single Dice
class, and the game core must not know what the interface looks like.
One place for randomness. Dice holds every random draw in the game: where characters
spawn, how much damage a hit does, whether a shield is discarded. Nothing else calls a
random number generator. That is what makes the game reproducible and testable at all — if
random draws were scattered across ten classes, no test could pin down behaviour.
What a monster and a player share, and where they split. Both stand on a cell, have
health and can fight, so that goes in an abstract base. Only the player carries weapons,
shields and a name, so that stays in Player:
LabyrinthCharacter (abstract) position, health, attack, defend, fight
├── Monster
└── Player + name, number, weapons, shields
└── FuzzyPlayer same player, picks its move at random
FuzzyPlayer extends Player and only overrides how the next move is chosen. Every other
rule is inherited untouched. It is what lets the game play itself while testing, without a
second copy of the rules.
Weapons and shields are the same thing. Both add power, both wear out, both get
discarded. CombatElement holds that behaviour, and Weapon and Shield only differ in
what they contribute to. In Java the decks that hand them out are one generic class bounded
by that base:
abstract class CardDeck<T extends CombatElement> ├── WeaponCardDeck extends CardDeck<Weapon> └── ShieldCardDeck extends CardDeck<Shield>
The Ruby version has no equivalent. Without generics the deck buys much less, and the practical that introduced it only asked for Java.
The core does not know about the screen. Controller runs the turn loop and holds a
UI view, an interface. GraphicUI implements it with Swing. No model class imports
anything from the UI package, so the game core never learns what a screen is.
The idea works, the execution is half done: TextUI prints the board to the terminal but
never declares implements UI, so Controller cannot take it. That is why the text UI is
commented out in Main instead of being a one-line swap. See
known limitations.
GameState exists for the same reason: instead of letting the UI reach into the model, the
game hands out a flat snapshot of the board, the players, the monsters and the log.
src/java/irrgarten/ Java version: model, controller, text UI and Swing UI
src/ruby/irrgarten/ Ruby version: same model, text UI only
src/exercises/ 62 theory exercises from the problem sets, Java and Ruby
docs/diagrams/ class diagrams handed in for practical 4
docs/assignment/ the exam brief
docs/latex/ the theory write-ups src/exercises is generated from
docs/notes/ two worked explanations written alongside them
docs/variants/ alternative builds kept for exam practice, see its README
tools/ the extractor
Makefile build and run, both languages
src/exercises/ is the other half of the course. The game is the project; the problem sets
are where the design ideas were worked out first:
| Folder | Files | Topic |
|---|---|---|
problem-set-1/ |
6 | classes, references, copying |
problem-set-2/ |
1 | associations |
problem-set-3/ |
34 | inheritance hierarchies, abstract classes, polymorphism |
visibility/ |
16 | access modifiers, packages, encapsulation |
inheritance/ |
5 | slide exercises on overriding |
They came out of the same LaTeX documents the PDFs are built from, using each listing's
style=customjava or style=customrb to pick the language.
The four earlier practicals (P1 to P4) are not in the tree. They were delivered as zip
archives holding mostly generated Javadoc, .class files and NetBeans project files, and
their sources are superseded by src/. They are attached to the
v1.0 release instead, so
the record is kept without carrying 8 MB of build output in every clone.
- JDK 17 — tested with OpenJDK 17.0.19. The sources target Java 11 and compile on both.
- Ruby 3.2 — tested with Ruby 3.2.3. Standard library only.
- make
No external libraries. The Swing UI ships with the JDK.
# Java: compile make build-java # Java: play. Main wires the Swing UI, so this opens a window make run-java # Java: run the practical 1 checks make test-java # Ruby: play in the terminal make run-ruby # Rebuild the API docs into build/javadoc make docs-java
make run-ruby needs a real terminal: the text UI reads single keypresses through
io/console, so it fails if you pipe input into it.
The Ruby build renders the labyrinth in the terminal. - is a free cell, X a wall, M a
monster, and a digit is the player with that number:
nRows: 7, nCols: 7, exitRow: 6, exitCol: 6
Laberinto:
[ - ] [ - ] [ - ] [ - ] [ - ] [ - ] [ - ]
[ - ] [ - ] [ M ] [ - ] [ X ] [ - ] [ - ]
[ - ] [ M ] [ - ] [ - ] [ X ] [ - ] [ - ]
[ - ] [ - ] [ - ] [ M ] [ - ] [ - ] [ - ]
[ 0 ] [ - ] [ - ] [ - ] [ - ] [ - ] [ X ]
The Java build shows the same board in a Swing window.
The class diagrams handed in for practical 4 are in docs/diagrams/:
DC-P4-1.pdf is the full diagram and P4-1.pdf the written design notes.
- Writing the same design twice is the real exercise. The two versions match everywhere
the design was about the model, and drift apart exactly where it leaned on a language
feature.
CardDeck<T>is the clearest case: it is a Java answer, and Ruby never grew one. - One
Diceclass was worth more than any test. Funnelling every random draw through one place turned "the game does something odd sometimes" into a bug I could actually find. - Half an abstraction is worth less than none. I wrote the
UIinterface, pointedControllerat it, and then never madeTextUIimplement it. The design is right and the code does not honour it, so the swap I designed for is a comment inMain. Declaring an interface is the easy half. - The tests are not tests.
TestP1prints values to stdout and I check them by eye. That was enough to hand in and it is not enough to trust. Proper assertions were the obvious next step and I did not take it.
TextUIdoes not implementUI.Controllertakes aUI, so the text interface cannot be passed to it.Main.javahardcodesGraphicUIand leaves theTextUIlines commented out. Fixing it is oneimplements UIplus whatever signatures do not line up.- The Ruby version stops earlier than the Java one. No
CardDeck, no weapon or shield decks, no graphic UI. Only Java has the complete game. src/ruby/irrgarten/test_p1.rbfails. It callsgamestate1.getlabyrinth(), a Java-style name left over from the port;GameStateexposesattr_reader :labyrinth. The game itself runs fine.src/java/irrgarten/TestP2.javais commented out in full, so it never compiles into a class. Its Ruby counterpart,test_p2.rb, does exist.GameCharacteris an enum, not a class. It tags a cell asPLAYERorMONSTER. The name suggests a base class and it is not one.
Ismael Sallami Moreno. Released under the MIT licence (see LICENSE).