Resource Generation - UQdeco2800/2022-studio-3 GitHub Wiki
Introduction
As resources are a core aspect of the game, given they resemble both the main gameplay loop of upgrading militia and the game's win condition, it is essential that they can be added to the game easily (See article: Adding a resource), and consistently. The ResourceGenerator class implemented in Sprint 2, allows for customizable Resource addition and balancing within the game, through the addition of a resources.json config file. The ResourceGenerator will read this file, and add all resources defined in it into the game, based on a few key constraints: Minimum number to add, maximum number to add, preferred distance from city centre, tiles wide and tiles high. This is relevant to the Studio's direction, as future sprints call for the necessity of more resources to be added (especially to win the game), and they will be added in unknown quantities and sizes. This feature solves these problems in advance, by allowing the developer to have agency over the qualities of the resource, and add in a variable or fixed number of them to the game.
Algorithm
Master Algorithm
The algorithm to procedurally place each Resource defined in /assets/configs/resources.json is as follows:
- Read resources from resources.json into a list of ResourceSpecification, which will automatically choose a number to generate between minAmount/maxAmount on construction
- Load in all island tiles from the MapGenerator's map into a list of Coordinate (used to speed up iteration)
- Sort the ResourceSpecification list such that the largest resources are at the top -> these are placed first, to maximize the chance of them finding space in the map before it fills up with other resources
- For each ResourceSpecification on the list, run the placement algorithm to allocate them a tile on the island that is unencumbered, and will fit the resource
Placement algorithm
- While this ResourceSpecification has not been placed the desired amount of times, continue placing resources
- Determine the places on the map where this resource may be placed, based on its size, existing resource placements and a constant buffer variable which may be set to allow space between resource placements (Is currently set to 1, to ensure resources don't block off the map)
- If this resource has no valid locations, the map is full, and the resource generation has failed. In this case, the MapGenerator will re-create a new map with the same initial specifications, and the ResourceGenerator will run again. The ResourceGenerator will attempt to re-make the map to fill the desired amount of resources up to 5 times before throwing an exception, to avoid infinite loops
- Assuming now that the resource can be placed, a list of Coordinate is formed, representing valid locations for the resource to be placed
- A weighted tree map is formed from the valid placements list, with each entry's key referring to its weight - which is determined by the preferred distance of the resource (i.e. a Coordinate closer to the preferred distance will have a higher weight)
- A single placement coordinate is chosen randomly from this tree map, implying that desired distances are significantly more likely to be chosen
- The placement of the resource is added to its ResourceSpecification's list of placements, and the internal char[][] map is updated to reflect that these tiles are now occupied
UML Diagram of how Map Generation classes interact
Note that a few fields and functions of MapGenerator were omitted in this summary for conciseness, as they are primarily used for unit testing, and not relevant to the ResourceGeneration of the map.
UML Summary
- A MapGenerator is created with a set of static constants defined in AtlantisTerrainFactory, which calls generateMap() to create the game map
- A ResourceGenerator is created, composed of the MapGenerator, and allocates space for a number of resources on the map, based on a list of ResourceSpecification that is read from a .json config file
- Each entry of this list of ResourceSpecification contains a list of coordinate, representing places on the map to put this resource, which is passed back to MapGenerator and output to the game using its getResourcePlacements() function
Test Plan
The approach to test this feature was 2-pronged: Experimental testing was conducted to visually verify that the code was performing as expected, while Unit testing was run to algorithmically ensure that the Resources could consistently be added within their rule set.
Experimental Testing
As the ResourceGenerator was developed, a function writeCurrentResources() was developed, to output the contents of the map as resources were added, to visually verify that they were being placed correctly (I.e. not in the ocean, not on top of other resources, not inside the city, not outside the map bounds). This provided insight into the algorithmic process as it executed placing each resource to ensure the code performed as intended. A sample output of a generation run is provided below, generating 2x2 stone resources and 1x1 tree resources:
Two .txt files are created, after_Stone.txt and after_Tree.txt, which show how the resource map looks after each resource has been fully placed. after_Stone.txt:
after_Tree.txt
Upon visual examination of these two outputs, the following observations can be made: Stone is placed first, which is correct functionality given it is the larger of the two resources, both resources are placed within the island bounds, neither resource is placed in the city, neither resource is overlapping.
These visual, experimental evaluations were carried out dozens of times over the development process of ResourceGenerator, to ensure its functionality was consistent.
Unit testing
Experimental evaluation wasn't deemed as enough, as edge cases exist where the ResourceGenerator could fail - for example a resource being generated in a large range (e.g {1, 1000} resources added to the map) which may succeed in 90% of runs, but fail when a large number is chosen for the resource multiple times (even despite the concession the algorithm makes to re-generate the map when a resource can't fit up to 5 times). As such, cases like these were tested in the game unit tests (found in src/test/com/deco2800/game/areas/MapGenerator/ResourceGeneratorTest.java)
The core issues tested for include:
- A resource being generated in an invalid location (as defined above)
- A set of ResourceSpecification data defined in resources.json that will sometimes be unable to be correctly placed on the map, possibly due to map size being too small, resource size being too large, or resource quantity being too large
- The inability to correctly parse the resources.json config file
These insulate the project from a developer incorrectly modifying resources.json, leading to a chance of a runtime crash if unlucky values were chosen for the number of resources to add to the map.