Skip to content

1. Preamble and Problem

For many years, Penguin Pty Ltd (a small software business run by Atilla Brungs) has dominated the native application gaming market with hit games. However in recent years, advancements in web technologies mean that the new generation of consumers don’t want to download and run a native application, but instead want to play games online. To adapt to the ever-changing market, Penguin Pty Ltd decided in 2021 to take users back to the 1980s and develop a new game called Dungeonmania, but with a modern twist - designed as a web application.

Together, their team of engineers built an MVP which brought Penguin back to #1 on the charts - but now the users are wanting more! The hard working backend engineers, previous terms’ COMP2511 students have all left. A lull in sales has left only budget for two people rather than the previous five to work on the backend. What’s more, the previous engineers left a series of design issues in their implementation.

[[TOC]]

You have been hired and have inherited the existing codebase for the Dungeonmania game.

You have been given the product specification from the MVP version of Dungeonmania to help you understand the existing code and functionality it provides.

NOTE: All of this functionality in this file has been implemented in the monolith repository we have provided to you. You do not need to implement it yourselves.

In Dungeon Mania you control a Player and have to complete various goals within a series of dungeons to complete the game!

The simplest form of such a puzzle is a maze, where the Player must find their way from the starting point to the exit.

More advanced puzzles may contain things like boulders that need to be pushed onto floor switches,

enemies that need to be fought with weapons, or collectables like potions and treasure.

Entities occupy tiles on the map. Each tile has an (x,y) coordinate. The map is infinite in all directions.

More information on the technical details of the map is available in Section 4.1.

There is only one player per game.

The Player, can be moved up, down, left and right into cardinally adjacent squares, provided another entity doesn’t stop them (e.g. a wall). The Player begins the game with a set amount of health and attack damage. The Player spawns at a set ‘entry location’ at the beginning of a game.

The game contains the following static entities.

Entity Image Description
Wall Blocks the movement of the Player, enemies and boulders.
Exit If the Player goes through it, the puzzle may be complete.
Boulder Acts like a wall in most cases. The only difference is that it can be pushed by the Player into cardinally adjacent squares. The Player is only strong enough to push one boulder at a time. When the player pushes a boulder, they move into the spot the boulder was previously in. Boulders can be pushed onto collectable entities.
Floor Switch Switches behave like empty squares, so other entities can appear on top of them. When a boulder is pushed onto a floor switch, it is triggered. Pushing a boulder off the floor switch untriggers it. Boulders will never spawn on switches.
Door Exists in conjunction with a single key that can open it. If the Player holds the key, they can open the door by moving through it. Once open, it remains open.
Portal Teleports players to a corresponding portal. The player must end up in a square cardinally adjacent to the corresponding portal. The square they teleport onto must also be within movement constraints - e.g. the player cannot teleport and end up on a wall. If all squares cardinally adjacent to the corresponding portal are walls, then the player should remain where they are.
Zombie Toast Spawner Spawns zombie toasts in a square cardinally adjacent to the spawner that does not contain a wall. If all of the cardinally adjacent cells to the spawner contain walls, then the spawner will not spawn any zombies. If the player has a weapon (sword or bow **only**) and is cardinally adjacent to the spawner, they can destroy the zombie spawner by interacting with it. The spawner will be destroyed immediately and the durability of the weapon will be decreased by 1.

In addition to the Player, the game contains the following moving entities.

All enemy entities can be created as part of the initial dungeon. Each tick, all enemies move according to their respective behaviour.

Entity Image Description
Spider Spiders spawn at random locations in the dungeon from the beginning of the game. When the spider spawns, they immediately move the 1 square upwards (towards the top of the screen) and then begin 'circling' their spawn spot (see a visual example below).

Spider Movement Figure 1

Spiders are able to traverse over walls, doors, switches, portals, exits (which all have no effect), but not boulders, in which case it will reverse direction (see a visual example below).
Spider Movement Figure 2

Spiders spawn in a square that is less than or equal to a radius of 20 (via Manhattan distance) around the player’s current position. If there is no available space, a spider is not spawned. Spiders cannot spawn on boulders, or in the same square as the player/enemies. If a spider is stuck between two boulders in its movement path, it should remain still.
Zombie Toast Zombies spawn at zombie spawners and move in random directions. Zombies are limited by the same movement constraints as the Player, except portals have no effect on them.
Mercenary Mercenaries do not spawn; they are only present if created as part of the dungeon. They constantly move towards the Player, stopping only if they cannot move any closer (they are able to move around walls). Mercenaries are limited by the same movement constraints as the Player. All mercenaries are considered hostile, unless the Player can bribe them with a certain amount of gold; in which case they become allies. Mercenaries must be within a certain radius of the player in order to be bribed, which is formed by the diagonally and cardinally adjacent cells in a "square" fashion, akin to the blast radius for bombs. As an ally, once it reaches the Player it simply follows the Player around, occupying the square the player was previously in.

Collectable entities can be picked up by the player by walking over the tile the item is on. This will place the item into the player’s inventory.

Entity Image Description
Treasure Can be picked up by the Player.
Key Can be picked up by the player when they move into the square containing it. The Player can carry only one key at a time, and only one door has a lock that fits the key. Keys disappear once used in any context i.e. opening a door, building an item. If a key is used before opening its door, its corresponding door may be locked forever.
Invincibility Potion When a Player picks up an Invincibility potion, they may consume it at any time. Any battles that occur when the Player has the effects of the potion end immediately after the first round, with the Player immediately winning and taking no damage. Zombies and hostile mercenaries will run away from the player when the player is invincible. Movement of spiders and allied mercenaries remains unaffected. The effects of the potion only last for a limited time.
Invisibility Potion When a player picks up an invisibility potion, they may consume it at any time and they immediately become invisible and can move past all other entities undetected. This means that hostile mercenaries will no longer pursue the player and will now move randomly when the player is invisible, though allies will continue to follow the player. Battles do not occur when a player is under the influence of an invisibility potion.
Wood Can be picked up by the Player.
Arrows Can be picked up by the Player.
Bomb Can be collected by the player. When used it is removed from the inventory it is placed on the map at the player's location. When a bomb is cardinally adjacent to an active switch, it destroys all entities in diagonally and cardinally adjacent cells, except for the player, forming a "square" blast radius. The bomb should detonate when it is placed next to an already active switch, or placed next to an inactive switch that then becomes active. The bomb explodes on the same tick it becomes cardinally adjacent to an active switch. A bomb cannot be picked up once it has been placed down or exploded.
Sword A standard melee weapon. Swords can be collected by the Player and used in battles, increasing the amount of damage they deal by an additive factor. Each sword has a specific durability that dictates the number of battles it can be used before it deteriorates and is no longer usable.

Players can only be under the effect of one potion at a time. It is possible for a player to use another potion while the effects of an existing potion are still lasting (can be of the same or a different type of potion). In this case, the effects are not registered immediately but are instead ‘queued’ and will take place the tick following the previous potion wearing of. For example:

  • On tick 0 the Player consumes an invisibility potion that lasts for 5 ticks and becomes invisible to enemies moving that tick
  • On tick 3 they use an invincibility potion
  • At the end of tick 4 (after all enemy movements) the player becomes visible again and becomes invincible.

Some entities can be built using a ‘recipe’ by the player, where entities are combined to form more complex and useful entities. Once a buildable item has been constructed, it is stored in a player’s inventory. For all buildable entities, once the item is constructed the materials used in that construction have been consumed and disappear from the player’s inventory.

Entity Image Description
Bow Can be crafted with 1 wood + 3 arrows. The bow has a durability which deteriorates after a certain number of battles. Bows give the Player double damage in each round, to simulate being able to attack an enemy at range (it can't actually attack an enemy at range).
Shield Can be crafted with 2 wood + (1 treasure OR 1 key). Shields decrease the effect of enemy attacks. Each shield has a specific durability that dictates the number of battles it can be used before it deteriorates.

A battle takes place when the Player and an enemy are in the same cell at any point within a single tick. The conditions for a battle occurring are the same regardless of whether the player moves onto the same tile as the enemy, or vice versa.

A ‘round’ of a battle occurs as follows:

Player Health = Player Health - (Enemy Attack Damage / 10)
Enemy Health = Enemy Health - (Player Attack Damage / 5)

Damage will be applied simultaneously to the player and enemy in each round.

If the Player’s health is <= 0, then the Player dies, is removed from the game and the game is over. If the enemy’s health is <= 0, then the enemy dies and is removed from the game. If after the above ‘round’, neither the Player nor the enemy is dead, the round repeats until either the Player or enemy is dead.

All rounds of a battle take place within a single tick.

In battles, weapons, allies and other supporting items provide an attack and defence bonus to the player.

These buffs can be additive (+), subtractive (-), multiplicative (*), or reductive (/), and stack onto each other. You can have multiple weapons/allies/supporting items of the same type.

An example of a bow, sword and shield being used in battle is as follows:

player health = 10
player base attack damage = 5
bow attack damage = 2
sword attack damage = 1
shield defence = 2
enemy health = 10
enemy attack damage = 5
Battle occurs:
- Round 1 enemy health = 10 - ((2 * (5 + 1)) / 5) = 7.6
player health = 10 - ((5 - 2) / 10) = 9.7
- Round 2 enemy health = 7.6 - ((2 * (5 + 1)) / 5) = 5.2
player health = 9.7 - ((5 - 2) / 10) = 9.4
- Round 3 ...

All additive/subtractive bonuses are processed before multiplicative/reductive bonuses.

In addition to its layout, each dungeon also has a goal that defines what must be achieved by the player for the dungeon to be considered complete. Basic goals are:

  • Being on the exit;
  • Having a boulder on all floor switches;
  • Collecting a certain number of treasure items (or more). This is the total amount collected, not the amount in the inventory.

Goals are only evaluated after the first tick.

It is possible for a goal to become un-achived, such as if the player gets to the exit (goal achieved) and then steps off of it, or if a boulder is moved off of a floor switch.

More complex goals can be built by logically composing goals. For example:

  • Collecting a certain number of treasure AND getting to an exit
  • Collecting a certain number of treasure OR having a boulder on all floor switches
  • Getting to an exit AND (having a boulder on all floor switches OR collecting all treasure)

All compound goals are binary (they contain two and only two subgoals).

The game is won when all the goals are achieved. The game is lost when the player dies and is removed from the map. There is no set behaviour for ticks occuring in the game after winning or losing.

It is also possible for the game to be in an unwinnable state. In this case, gameplay continues as usual.

The movement of mercenaries follows a Djikstra’s algorithm to take the shortest path towards the player.

Pseudocode for this algorithm
Note: This is not necessarily optimal (A* is probably a better algorithm for our common maze like dungeons), but since this is a design course and not an algorithms course, this is fine.
function Dijkstras(grid, source):
let dist be a Map<Position, Double>
let prev be a Map<Position, Position>
for each Position p in grid:
dist[p] := infinity
previous[p] := null
dist[source] := 0
let queue be a Queue<Position> of every position in grid
while queue is not empty:
u := next node in queue with the smallest dist
for each cardinal neighbour v of u:
if dist[u] + cost(u, v) < dist[v]:
dist[v] := dist[u] + cost(u, v)
previous[v] := u
return previous

📝 All references to radii distances are Manhattan Distances unless otherwise specified.

A tick is a transition from one state to a new state. A tick always starts with user input. This input can include moving, using an item, crafting an object, or interacting with an entity.

Then the game world changes in the tick and ends when another user input is needed. So “tick n” is the transition from the n-th state to the (n+1)-th state. There can be multiple developer-defined phases within one tick deciding the order of changes to the game world. Here is one possible example of a phase sequence to help you understand.

Within a phase of a tick, actions can happen in any order. For example, enemies can move in any order. Or if a player picks up an item and triggers a battle in the same action, this can occur in either order.

A sample UML diagram of this MVP specification has been included in this repository.

Note there may be minor inaccuracies or omissions in this diagram, as it is purely for reference purposes. Refer to the specification for the definitive requirements.

You may choose to create your own personal editable copy of the UML diagram using this LucidChart link. However, you are not required to do so.

All maps are infinite in all directions.

Moving left/right is a decrease/increase in the x co-ordinate of an entity respectively, moving up/down is a decrease/increase in the y co-ordinate of an entity respectively.

Each game requires a dungeon map to be loaded in. Dungeon maps consist of JSON files which contain the following:

  • entities, an array of entities in the map when the game starts; and
  • goal-condition, a specification of the goals for winning the dungeon.

There will be no other fields present in the JSON.

4.1.1 Input Specification - Entities (MVP)

Section titled “4.1.1 Input Specification - Entities (MVP)”

Each entry in the entities JSON Array will be a JSON Object with the following fields:

  • x - the x-position of the entity in the dungeon when the game starts;
  • y - the y-position of the entity in the dungeon when the game starts; and
  • type - the type of the entity.

The combination of x and y will be unique for each entity in on the map - i.e. only one entity will ever be created per tile.

The type field will be a string that starts with one of the following prefixes. For automarking purposes, all entities passed in will have a type in the following table.

Entity JSON Prefix Creatable from Dungeon Map?
Player player Yes
Wall wall Yes
Exit exit Yes
Boulder boulder Yes
Floor Switch switch Yes
Door door Yes
Portal portal Yes
Zombie Toast Spawner zombie_toast_spawner Yes
Spider spider Yes
Zombie Toast zombie_toast Yes
Mercenary mercenary Yes
Treasure treasure Yes
Key key Yes
Invincibility Potion invincibility_potion Yes
Invisibility Potion invisibility_potion Yes
Wood wood Yes
Arrows arrow Yes
Bomb bomb Yes
Sword sword Yes
Bow bow No, since this entity must be built by the player.
Shield shield No, since this entity must be built by the player.

Some entities will contain additional fields in their JSON entry, namely:

  • All entities of type portal will have a field colour. Two portals which have the same colour are linked (travelling through one portal takes you to the other). We will never provide a dungeon which has more than two portals of the same colour, and all portals will have a counterpart of the same colour in the dungeon.
  • All entities of type door and key will have a key field which, in the case of the key is the identifier of the key, and in the case of the door the id of the key which fits that door.

Some entities may have different variations that need to be represented to the player. Currently, this includes portal colours and door status, and the code that handles this can be found in dungeonmania/util/NameConverter.java.

Base Entity Variants Notes
Portal portal_{COLOUR} (eg. portal_red) Portals are always represented with a color, which will be appended after the "portal" prefix.
Door door, door_open When a door is open it is represented with the "_open" suffix, however a closed door is just represented as door.

You may edit NameConverter.java if you find that you need to represent any other varied entities.

A basic goal is represented in the dungeon as:

"goal-condition": {
"goal": <goal>
}

Where <goal> is one of "boulders", "treasure" or "exit".

A complex goal is represented in the dungeon as:

"goal-condition": {
"goal": <supergoal>,
"subgoals": [
{"goal": <goal>},
{"goal": <goal>}
]
}

Where <goal> is one of "boulders", "treasure" or "exit", or another nested goal conjunction/disjunction itself, and <supergoal> is one of "AND" or "OR".

In config_template.json we have specified the template for a configuration file. This file is important as it specifies internal game mechanics which will affect the external behaviour of your application. Rather than hard coding these constants into your classes, you must instead read in these values from the specified file when the game is created.

During automarking, we will be providing our own configuration files with each test dungeon - this allows our tests to set parameters that should ensure behaviours are produced without ambiguity. For this reason, if you do not read in the values correctly, you will likely fail a large number of our autotests.

Configuration fields are all integers unless specifically stated otherwise.

JSON Format
Description
ally_attack Attack bonus each ally gives to the player.
ally_defence Decrease in effect of enemy attack damage each ally gives to the player.
bribe_radius Radius in which a mercenary can be bribed.
bribe_amount Amount of gold required to bribe a mercenary.
bomb_radius Blast radius of bomb.
bow_durability The number of battles that the bow lasts for.
player_health Health of the character.
player_attack Attack damage of the character.
invincibility_potion_duration The effects of the potion only last for x ticks.
invisibility_potion_duration The effects of the potion only last for x ticks.
mercenary_attack Attack damage of the mercenary.
mercenary_health Health of the mercenary.
spider_attack Attack damage of the spider.
spider_health Health of the spider.
spider_spawn_interval Spiders spawn every x ticks, starting from the x'th tick. Spawn rate of 0 means that spiders will never spawn in the game.
shield_durability The number of battles that the shield lasts for.
shield_defence The decrease in the effect of the attack damage of the enemy as a result of the shield.
sword_attack Amount of damage added to a players' attack damage when they use a sword in battle.
sword_durability The number of battles that the sword lasts for.
treasure_goal At least x treasure must be collected to complete the treasure goal
zombie_attack Attack damage of the zombie toast.
zombie_health Health of the zombie toast.
zombie_spawn_interval Zombies spawn every x ticks from each spawner, starting from the x'th tick. Spawn rate of 0 means that zombies will never spawn in the game.

The layer of abstraction is at the level of the controller. In the starter code, we have provided a class DungeonManiaController.

The controller methods interact with a HTTP layer in the form of a web server, which we have written for you.

We have provided the following interface data types for you inside response/models. Similarly as for the assignment, you will need to create objects of these types for the controller to return and communicate information to the server layer.

In case you are interested, the server layer then wraps these objects inside a GenericResponseWrapper, a generic type we have made for you, and converts these objects to JSON using a library called gson to allow them to be communicated to the frontend via a HTTP response.

Constructor Prototype
Description
public DungeonResponse(String dungeonId, String dungeonName, List entities, List inventory, List battles, List buildables, String goals)
  • dungeonId is the unique identifier for the dungeon
  • dungeonName is the name of the dungeon map being used (i.e. maze, which corresponds to the file src/main/resources/dungeons/maze.json
  • entities is a list of all entities currently in the dungeon (all entities in the Player's inventory aren't included); if a Player or enemy dies it is removed from this list
  • inventory is the Player's current inventory
  • buildables is a list of buildable item types that the player can build, given their current inventory and game state
  • battles is the list of battles that has occurred in total in the game so far (see BattleResponse), in the order that they occurred
  • goals is a string containing the goals yet to be completed. An empty string denotes the game has been won. Each goal in the string is preceded with a colon : and is one of the three basic goals listed in Section 2.7. How you represent conjunctions (AND) and disjunctions (OR) is up to you, as the frontend will simply render your string with the goals as images. We will only check for the goal strings in our tests (e.g. :exit). An example of the goals string is ":exit AND (:treasure OR :enemies)"
public BattleResponse(String enemy, List rounds, double initialPlayerHealth, double initialEnemyHealth, List weaponryUsed)
  • enemy is the type of enemy (e.g. spider)
  • rounds represent the rounds of the battle (see RoundResponse).
  • initialPlayerHealth is the initial health of the player before the battle.
  • initialEnemyHealth is the initial health of the enemy before the battle.
  • weaponryUsed is a list of all attack and defence items used in the battle, including potions.
public RoundResponse(double deltaPlayerHealth, double deltaEnemyHealth)
  • deltaPlayerHealth is the change in health of the character in that round of the battle (e.g. -3 is a decrease of 3 in health)
  • deltaEnemyHealth is the corresponding change of health of the enemy in that round of the battle.

Note that each of these deltas can be positive and that the 'sign' of the health matters (e.g. positive deltas correlate to increase and negative deltas correlated to decrease in health).
public EntityResponse(String id, String type, Position position, boolean isInteractable)
  • id is the unique identifier for the respective entity
  • type is the type of the entity (a prefix corresponding to the table in Section 4.1.1)
  • position is the x, y, z (layer) position of the entity
  • isInteractable refers to if the entity can receive interaction updates from frontend, which only pertains to mercenaries and zombie toast spawners. When mercenaries become allies, they are no longer interactable.
public ItemResponse(String id, String type)
  • id is the unique identifier for the item
  • type is the type of item (lowercase, see Section 4.1.1 for names).
public Position(int x, int y, int layer)
  • x, y are the co-ordinates of the cell (the top-left cell is 0,0)
  • layer is the Z-position of the entity on the screen (a higher layer is "in front" of a lower layer visually). The Z-position only matters for frontend rendering and is not something we will test.
public enum Direction { UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0); } Direction of movement for the player.
Method Prototype
Description
Exceptions
public DungeonResponse newGame(String dungeonName, String configName) throws IllegalArgumentException Creates a new game, where dungeonName is the name of the dungeon map (corresponding to a JSON file stored in the model) and configName is the name of the configuration file. IllegalArgumentException:
  • If dungeonName is not a dungeon that exists
  • If configName is not a configuration that exists
public DungeonResponse getDungeonResponseModel() Return the dungeon response for the current state of the game without any side effects on the game. N/A
public DungeonResponse tick(String itemUsedId) throws InvalidActionException Ticks the game state when the player uses/attempts to use an item. The player's action (attempts/using an item) must be carried out first, then enemy movement. As soon as the item is used, it is removed from the inventory. IllegalArgumentException:
  • If itemUsed is not a bomb, invincibility_potion, or an invisibility_potion.


InvalidActionException:
  • If itemUsed is not in the player's inventory
public DungeonResponse tick(Direction movementDirection) Ticks the game state when the player moves in the specified direction one square. The player's movement must be carried out first, then enemy movement. N/A
public DungeonResponse build(String buildable) throws InvalidActionException Builds the given entity, where buildable is one of bow or shield IllegalArgumentException:
  • If buildable is not one of bow or shield


InvalidActionException:
  • If the player does not have sufficient items to craft the buildable
public DungeonResponse interact(String entityId) throws IllegalArgumentException Interacts with a mercenary (where the Player bribes the mercenary) or a zombie spawner, where the Player destroys the spawner. IllegalArgumentException:
  • If entityId is not a valid entity ID


InvalidActionException
  • If the player is not within specified bribing radius to the mercenary, when they are bribing
  • If the player does not have enough gold and attempts to bribe a mercenary
  • If the player is not cardinally adjacent to the spawner, if they are destroying a spawner
  • If the player does not have a weapon and attempts to destroy a spawner

The only two exceptions throwable by the Controller are:

  • IllegalArgumentException (an builtin unchecked exception) on the specified conditions; and
  • InvalidActionException (a custom-defined checked exception inside src/main/java/dungeonmania/exceptions).

You can throw them in any order you like, we will not test any input that would fit multiple exceptions at the same time.

File
Path
Description
Should you need to modify this?
DungeonManiaController.java src/main/java/dungeonmania/DungeonManiaController.java Contains one method for each command you need to implement. Yes
App.java src/main/java/App.java Runs a server for Dungeon Mania. No
Position.java, Direction.java, and FileLoader.java src/main/java/dungeonmania/util/Position.java, src/main/java/dungeonmania/util/FileLoader.java, and src/main/java/dungeonmania/util/Direction.java See section 4.3.1 No - do not modify these as we will rely on them being the same in automarking.
DungeonResponse.java, EntityResponse.java, GenericResponseWrapper.java, and ItemResponse.java src/main/java/dungeonmania/response/models/ See section 4.3.1 No.
Scintilla.java and auxiliary files; Environment.java, PlatformUtils.java, and WebServer.java src/main/java/scintilla Contains a small custom built wrapper around Spark-Java for running a web server. When run it automatically opens a web browser. No.
InvalidActionException.java src/main/java/dungeonmania/exceptions A checked exception to throw when an invalid action is attempted (See Section 4.3.5). No - do not modify this class as we will be relying on it during automarking.

Although you are only responsible for the improvement of the backend, a sample frontend has been provided for you.

The game will require you to select a dungeon file and configuration file. Dungeon and configuration files may be loaded into the frontend by placing them in the main/resources/dungeons and main/resources/configs folders respectively.

The player character can be moved using the WASD keys. Building and using items is performed by clicking the relevant icon in the inventory using the mouse. Interacting is performed by clicking the relative entity on the game map.

As the frontend is not a product requirement, the frontend provided is only a sample and may contain bugs.