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:
- We randomly pick a monster to fight and beat.
- 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.
- We generate the base stats for the generated base item.
- 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.
- We look up the monster's TC in TreasureClassEx.txt.
- For that TC, we randomly choose one of three drops listed in the file.
- 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.
- 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.
- Pick a random monster: there is only one monster in monstats.txt,
hell_bovine, so we pick it.
- 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:".
- 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.
- 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".
- 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:
- A method to randomly choose a monster from monstats.txt.
- A method to extract a monster's treasure class from monstats.txt.
- A method to generate a base item found in TreasureClassEx.txt given a
treasure class.
- A method to generate the base stats found in armor.txt for a base item.
- A method to randomly choose an affix from MagicPrefix.txt or
MagicSuffix.txt.
- 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.