Homework 6 - Loot generator

Procedural content generation in the context of video games is the process of generating content such as levels, art, or other assets "on the fly" rather than up-front. For example, instead of designing a series of levels for a game, we may instead provide several level primitives and then write a series of rules as to how these primitives can be combined to form actual levels. Then we can write a series of procedures or methods that automatically combine primitives according to the rules to form complete levels.

Many video games have used procedural content generation to great effect. One of those games is the venerable Diablo series of which Diablo II released in 2000 is the latest iteration. Diablo is a Hack and Slash role-playing game that focuses on leveling up and getting loot for your character by killing monsters. The appeal of Diablo is that both the levels and loot are procedurally generated so that the replay value is huge. Many games have emulated Diablo II's procedural design, in particular, the action role-playing game Torchlight and the first-person shooter/role-playing mash-up Borderlands. However, Diablo II is a class that many people still play online over a decade later.

In this homework, we'll be implementing the loot generation algorithm found in Diablo II. Since Diablo II has been around for over a decade, dedicated fans have put together exactly how the game procedural generates loot as detailed on the Diablo Wiki. For our purposes, the loot generation algorithm is interesting because it highlights both file I/O and how far we've come this semester. We are at the point where we can start understanding and simulating how real-world systems out there!

The algorithm and data files we use are simplified versions taken from the above article. However, the core ideas remain the same, so you can faith that after this assignment, you'll understand how Diablo II procedural generates items!

The loot generation algorithm

Diablo II uses a collection of files in order to randomly generate loot. For this homework, we'll be providing you two sets of these files to power your loot generator. All the files are tab-delimited text files, so you can open them up either in a text editor like jGRASP or a spreadsheet program such as Excel or Openoffice. In the sections that follow, we'll describe the contents of each of the files, but you should look at these text files yourselves in order to get a sense of the layout.

To generate a single piece of loot, our LootGenerator program will go through the following steps:

  1. We randomly pick a monster to fight and beat.
  2. We look up the treasure class of that particular monster and using that treasure class, generate the base item that is dropped by the monster.
  3. We generate the base stats for the generated base item.
  4. We generate affixes (i.e., prefixes and suffixes) for the item and stat modifiers from those affixes.
Like the previous homework, the LootGenerator program will repeatedly prompt the user to go through this process until they decide to quit.

Step 1: Picking the monster

The data file monstats.txt contains the list of possible monsters in the game. It has the following format:

<# of entries>
Class     Type     Level     TreasureClass

The class of the monster is its name. The type and level are irrelevant for our purposes, and the treasure class defines the class of items that the monster drops when it dies. In the next step, we'll look up this treasure class in another file to determine the item the monster drops.

You should choose a random monster from this file to slay. Each monster has equal probability of being chosen. The <# of entries> field at the top of the file will help in accomplishing this task.

Step 2: Looking up the treasure class

Once we've picked a monster and extracted its treasure class (TC), we next go to TreasureClassEx.txt to determine the base item that the monster drops. A base item in Diablo II is an armor or weapon type that we'll build upon to generate a final item. For this assignment, we will only consider armor pieces. Adding weapons is included in the extra credit section.

TreasureClassEx.tx has the following format:

Treasure_Class     Item1     Item2     Item3

Each treasure class entry in this file describes three possible drops that can occur for that TC. For example, here is one line of the TreasureClassEx.txt from the simple data set.

tc:Act_5_(H)_Equip_B     tc:armo60b     tc:armo60b     tc:Act_5_(H)_Equip_A

All TC names are prefixed with "tc:" so that we can easily tell them apart from base items in this file. So the tc:Act_5_(H)_Equip_B TC has three possible drops which are themselves TCs. Note that tc:armo60b appears as two of the three possible drops for tc:Act_5_(H)_Equip_B which means it has 2/3rds of a chance of being picked. Another line from the simple data set:

tc:armor60a     Embossed_Plate     Sun_Spirit     Fury_Visor

Here, the tc:armor60a TC has three possible drops that are all base items.

To determine the drop that actually occurs from a monster, we go through the following process.

  1. We look up the monster's TC in TreasureClassEx.txt.
  2. For that TC, we randomly choose one of three drops listed in the file.
  3. If the drop we choose is a TC, we look up that new TC in the file and randomly choose from one of its drops. We repeat this process until we finally arrive at a base item.
  4. The base item that we finally choose is the randomly generated drop from our monster!

Step 3: Computing base stats for a base item

Since we are not actually simulating any combat mechanics, computing the base stats for the base item we generated in the previous part simply means we generate a String that contains the base statistic for that base item. For armor pieces, the base statistic is defense and should be printed out in the following form:

Defense: <defense value>

The defense value is derived from the entry for the base item in armor.txt which has the following form.

name     minac     maxac

This defense value for armor is simply a random integer in the range minac to maxac inclusive. You will need to generate such a random integer to create the base statistic String.

Part 4: Generating affixes

Finally, we generate a prefix and suffix for our item. A prefix and suffix each have a 1/2 chance of being generated. So our item generator may make an item with both a prefix or suffix, one of a prefix or suffix, or neither a prefix nor a suffix.

Prefixes and suffixes exist in the MagicPrefix.txt and MagicSuffix.txt files respectively and have the following identical formats:

<# of entries>
Name     mod1code     mod1min     mod1max

name is precisely the prefix and suffix that you will attach onto the base item's name. mod1code is the additional statistic text that the affix will introduce to the base item. That statistic will have a single, random integer value in the range mod1min and mod2max inclusive.

Thus, the format of the final item name will be:

<prefix (if exists)> <base item name> <postfix (if exists)>

The format of the additional statistics from the prefix and suffix have the form:

<value> <statistic text>

Each additional statistic should be printed on a separate line with the prefix statistic coming before the suffix statistic. If a prefix or suffix is not generated, then you should not include an extra line for that prefix or suffix.

Format of the output

Each "round" of the loot generator has you squaring off against a randomly-chosen monster, killing it, and then displaying its loot. The format of your output for each round should look as follows:

Fighting <monster name>...
You have slain <monster name>!
<monster name> dropped:
=====
<complete item name>
<base item statistic>
<additional affix statistics>
=====

The prompt has the following output:

Fight again [y/n]? <Echoed user input from Scanner>

Like the previous homework, the prompt should be "generous" in that it is case-insensitive (i.e., "y", "n", "Y", and "N" are all valid responses) and reprompts the user if they do not enter a valid value. The reprompt message is the same as the original prompt messasge.

Note that the strings in the data files are underscore-separated. For example, the entry for the Hell Bovine in monstats.txt is actually Hell_Bovine. This is to make parsing with Scanners much easier (in particular, you don't have to worry about setting custom delimiters). You should use these strings as-is in your program, but when you finally print them to the console, you should convert the underscores to spaces. The replace(oldChar, newChar) method of the String class will be useful here..

Data sets

The set of files armor.txt, MagicPrefix.txt, MagicSuffix.txt, monstats.txt, TreasureClassEx.txt, and weapons.txt comprise a single data set for your program. You should assume that all of these files exist in the same directory as your LootGenerator program.

We provide two data sets for testing purposes. The small dataset consists of a single monster, 6 treasure classes, 9 armor pieces, and 5 affixes. A common technique is to test your program on a toy data set. This toy data set is small enough for you to easily reason about how your program deals with the data. You should start out with this data set and make sure that your program works with it.

The large dataset includes 49 monsters, 68 treasure classes, 202 armor pieces, and 758 affixes. Once you have your program working on the small data set, you can move onto the large data set to further test your code.

One testing technique you may want to try is to temporarily remove the prompt for your code when you use the large dataset, so that the program rapidly generates new items. If your program can do this for an extended period of time without throwing an exception, then you can have confidence that your program works correctly.

When working with a data set, you should copy the files contained in the zip archives linked above into the same directory as your LootGenerator program. Make sure that you do not mix-and-match files from the different data sets.

Example of item generation

To tie everything together, here is an example of generating an item from the small data set. While you read through this example, you should follow along with the data files in small_data.zip.

  1. Pick a random monster: there is only one monster in monstats.txt, hell_bovine, so we pick it.
  2. Get the TC for that monster: we look back in monstats.txt and find that the TC for the hell_bovine is tc:Cow_(H) which we know is a treasure class because it is prefixed with "tc:".
  3. Generate the base item: going to TreasureClassEx.txt, we look at the entry for tc:Cow_(H) and randomly pick one of the three items on that line. Let's say we end up picking tc:armo3. This is a treasure class, so we look at the entry for tc:armo3 and randomly pick again. Let's say we end up picking Leather_Armor. This is not a treasure class (since it does not start with "tc:") so it is the base item that we generate.
  4. Generate base stats:We scan armor.txt for a Leather_Armor entry. As per the instructions, we randomly choose a number between the values minac and maxac which we find is 14 and 17 for Leather_Armor. Say we choose the value 15, so the base statistic for our item is "Defense: 15".
  5. Generate affixes and affix stats: finally we need to generate the affixes for our item. Let's say that we end up only generating a suffix for our item. We go to MagicSuffix.txt and randomly choose one of the entries, for example, say we pick of_the_Titan that has entries "Strength", "16", and "20" for mod1code, mod1min, and mod1max respectively. Let's say that we pick 18 as our statistic value, a random number between 16 and 20 inclusive. Then our affix statistic is the string "18 Strength".

Putting this together, our output for the round should look like:

Fighting Hell Bovine
You have slain Hell Bovine!
Hell bovine dropped:
=====
Leather Armor of the Titan
Defense: 15
18 Strength
=====

Sample Output

Here is a example run of the LootGenerator program against the small data set. Like always, you should be able to reproduce the format of the output exactly.

Design

This is our largest program yet, so decomposition and good design will be essential in order to manage its complexity. The outline of the algorithm at the top of the write-up the suggests the top-level methods that we'll need for our LootGenerator. You need at least one method for each top-level bullet in the algorithm outline. Some of the bullets necessarily require multiple methods, e.g., a separate method to generate armor versus weapon base statistics, but it will be up to you to make that determination. A good rule of thumb is that any task that requires processing a file should be factored out to its own method.

Addendum: To help you along with this assignment, here are the methods that you should implement based on the algorithm outline above:

  1. A method to randomly choose a monster from monstats.txt.
  2. A method to extract a monster's treasure class from monstats.txt.
  3. A method to generate a base item found in TreasureClassEx.txt given a treasure class.
  4. A method to generate the base stats found in armor.txt for a base item.
  5. A method to randomly choose an affix from MagicPrefix.txt or MagicSuffix.txt.
  6. A method to generate the stats for some given affix found in MagicPrefix.txt or MagicSuffix.txt

Note how each of the methods above follows exactly from algorithm outline. This is typically how we implement complex algorithms and is a textbook example of good decomposition techniques.

In addition to these required methods, you should also include at least three other helper methods in your program that do interesting work.

A good approach to tackling this program is to decide up front what top-level methods you need and then writing each independently as if they were separate questions for the homework. For example, you will likely need a method to randomly pick a monster from the monstats.txt file. You should write and test this method independently from the rest. Once all of these methods are written, you can then put them all together in main.

Extra credit: LootGenerator Additions

Weapons

In addition to processing armor, we can also process weapons as well. For this part, please use the small_data_weapons data set which includes an extra data file weapon.txt.

Now, before we generate the base stats for our base item, we must figure out if the item is a weapon or armor. The difference between a weapon and armor is that a weapon deals damage and an armor provides defense. Note that we are not actually simulating the battle mechanics of Diablo II, so for our purposes, the only difference between the two is the text that we generate for each.

We already explained the base statistic for armor above, so now we'll describe the base statistic for weapons. The format of weapon.txt is as follows:

name     mindmg     maxdmg     wclass

The base statistic for weapons is the weapon's damage range and whether it deals one-handed or two-handed damage. Unlike armor, the mindmg and maxdmg fields are exactly the minimum and maximum damage range we need. The wclass field determines if the weapon deals one-handed or two-handed damage according to the following table:

One-handed damage Two-handed damage
1hs, 1ht, ht1 stf, 2hs, 2ht, bow, xbw

The format of the weapon base statistic that we'll print out will be one of the two lines depending on the handedness of the weapon:

One-handed Damage: <min dmg> - <max dmg>
Two-handed Damage: <min dmg> - <max dmg>

You can assume that the base item that you find in TreasureClassEx.txt must appear in exactly one of weapons.txt and armor.txt, i.e., you don't need to write code for the case where you can't find the base item.

For 1 extra credit point, copy your completed LootGenerator class code into a class called LootGeneratorWithWeapons in a file called LootGeneratorWithWeapons that does the above behavior. Note: if you already implemented LootGenerator factoring in weapons with the original data set, please note so in your class comment. You do not need to submit a LootGeneratorWithWeapons class to receive this extra credit point.

Graphics

Currently, our program is entirely text-based which isn't appropriate for a simulation of a video game like Diablo II. We won't be able to add a true graphics engine to our simulator in a reasonable time. Instead, we'll use our DrawingPanel from homework 3 to render an image of the monster we slay and the item that is generated.

For this extra credit, we'll fork our code into a new class LootGeneratorWithImages that is functionally identical to LootGenerator except that in addition to outputting text to the console on each round, we'll also pop up a DrawingPanel that contains images of the monster and the base item. For example, in one round, we may output the following:

Fighting Hell Bovine...
You have slain Hell Bovine!
Hell Bovine dropped:
=====
Brutal Leather Armor
Defense: 17
42 Enhanced Damage %
=====

And thus our new program would pop up the example DrawingPanel above that contains an image of a Hell Bovine and leather armor side by side. The layout of the DrawingPanel is up to you, and you should feel free to add extra graphics as you see fit. However, it should contain at least two images corresponding with the monster and base item that are chosen. Note that you do not need to close the DrawingPanel for the user with each new round.

To draw the images, you will need a way to load images in Java as well as a way to draw them to a DrawingPanel. To load images, you should use the static read method of the ImageIO class:

// Returns an Image object that represents the
// image loaded from the given file.
ImageIO.read(file);

ImageIO exists in the javax.imageio package so you should import it similarly to the other classes we've used so far.

To draw an image, you should get the Graphics object for the DrawingPanel and then call the drawImage method of the Graphics class:

// Draws the given image onto the graphics object at
// (x, y) with the given width and height.
drawImage(image, x, y, width, height, imageobs);
You should pass an image object created by ImageIO.read as the first argument to drawImage. For the last argument, you should pass null, a value that means "no object here". We will discuss null at the end of the course when we talk about reference semantics for objects.

Since we have many monsters, weapons, and armor in our data, instead of hardcoding every possible such entity and the image to load, you should instead create a new data file images.txt that contains an entry for each of these entities along with the image that you should load for it. Your method for creating and rendering the DrawingPanel should use this file in order to find what image files to load.

For 1 extra credit point, you should copy your LootGenerator class code into a new class LootGeneratorWithImages in a file LootGeneratorWithImages that accomplishes the above behavior. You should also inclue a file images.txt that contains entries entries for every entity in the small data set, along with a collection of images to load for the small data set. The images should be distinct for each entity (i.e., you can't load the same gif/png/jpeg for each entity). If you aren't artistically inclined, search around on the Internet (e.g., via Google image search) for some material you can use.

Submission

Please submit your Java source file, LootGenerator.java in addition to any extra files you create for the extra credit electronically via the course website. You will need to place these files into a zip archive called hw06.zip and submit that archive. On Windows you can create a new "Compressed (zipped) Folder" by right-clicking your desktop and selecting "New". On OSX, you can select your files in Finder, right-click and select "Compress". If you decide to only submit LootGenerator.java, you will still need to put it into a zip archive and submit that archive instead.