14,981,762 members
Articles / Programming Languages / C#
Article
Posted 16 Jul 2012

43.8K views
32 bookmarked

# Loot-Tables, Random Maps and Monsters - Part II

Rate me:
Bringing RDS to life - How it all works.

## Introduction

So you have gone all the long path of dry theory through Part I and want to see how it runs? Welcome to Part II of the RDS article!

I will show you now the "how-to's" with a group of small demos (you find all the demo code in the downloadable source code) - it's a simple Console Application as I promised in Part I "no fancy graphics, no designers, just code" that will output the results of RDS.

## Demo 1 - A Simple "pick 2 out of 6" Table

We create a `RDSTable` object, add 6 items to the table and then let RDS pick two of them at random. We then play around with the table, make one of the entries `rdsAlways=true` to see, that this one will then be included in the result with every query. Play with the probabilities of the items to see, how the drops change.

The code is very simple and straightforward: Create a `RDSTable`, add 6 entries and set the `rdsCount=2`. This will make the system loot 2-out-of-6. (You see, adding entries "by hand" is not the way to go for the future. There's a designer tool needed to have a good supporting GUI to set up and modify your tables and to just load them from a file or database at run time).

C#
```RDSTable t = new RDSTable();
// Add 6 items with equal probability to the table
MyItem m6 = new MyItem("Item 6"); // We need this item later
// Tell the table we want to have 2 out of 6
t.rdsCount = 2;
// First demo: Simply loot 2 out of the 6
Console.WriteLine("Step 1: Just loot 2 out 6 - 3 runs");
for (int i = 0; i < 3; i++)
{
Console.WriteLine("Run {0}", i + 1);
foreach (MyItem m in t.rdsResult)
Console.WriteLine("    {0}", m);
}
// Now set Item 6 to drop always
m6.rdsAlways = true;
Console.WriteLine("Step 2: Item 6 is now set to Always=true - 3 runs");
for (int i = 0; i < 3; i++)
{
Console.WriteLine("Run {0}", i + 1);
foreach (MyItem m in t.rdsResult)
Console.WriteLine("    {0}", m);
}```

Here's an output of Demo 1 (As this is a random system, your output will likely looks different, when you run the demo):

```*** DEMO 1 STARTED ***
----------------------
Step 1: Just loot 2 out 6 - 3 runs
Run 1
Item 3
Item 1
Run 2
Item 6
Item 2
Run 3
Item 4
Item 5
Step 2: Item 6 is now set to Always=true - 3 runs
Run 1
Item 6
Item 2
Run 2
Item 6
Item 2
Run 3
Item 6
Item 4
-----------------------
*** DEMO 1 COMPLETE ***```

## Demo 2 - Simple Recursion. A Table Containing Three Tables and Play with rdsUnique = true

A simple recursive structure is set up:

C#
```RDSTable t = new RDSTable();
RDSTable subtable1 = new RDSTable();
RDSTable subtable2 = new RDSTable();
RDSTable subtable3 = new RDSTable();
t.AddEntry(subtable1, 10); // we add a table to a table thanks to the interfaces
subtable1.AddEntry(new MyItem("Table 1 - Item 1"), 10);
subtable1.AddEntry(new MyItem("Table 1 - Item 2"), 10);
subtable1.AddEntry(new MyItem("Table 1 - Item 3"), 10);
subtable2.AddEntry(new MyItem("Table 2 - Item 1"), 10);
subtable2.AddEntry(new MyItem("Table 2 - Item 2"), 10);
subtable2.AddEntry(new MyItem("Table 2 - Item 3"), 10);
subtable3.AddEntry(new MyItem("Table 3 - Item 1"), 10);
subtable3.AddEntry(new MyItem("Table 3 - Item 2"), 10);
subtable3.AddEntry(new MyItem("Table 3 - Item 3"), 10);```

In the first step, you see the recursion happening, in the second step, we increase the count to 10 and set table 2 to `rdsUnique=true`. You can see, all tables get multiple hits, but there's only 1 records from table 2 in the result set (no matter, how many items or even more subtables are contained in table2!).

You see that even when we set `rdsCount=10`, there is not always really 10 items in the result! The reason for this is the `rdsUnique=true`, as RDS skips all subsequent hits from table 2. This is why you get a smaller count in the result as you might have expected.

C#
```Step 1: Loot 3 items - 3 runs
Run 1
Table 2 - Item 1
Table 1 - Item 3
Table 2 - Item 2
Run 2
Table 3 - Item 2
Table 2 - Item 1
Table 3 - Item 1
Run 3
Table 2 - Item 3
Table 2 - Item 1
Table 2 - Item 3
Step 2: Table 2 is now unique, loot 10 items - 3 runs
Run 1
Table 1 - Item 2
Table 2 - Item 2
Table 3 - Item 1
Table 3 - Item 3
Table 1 - Item 3
Table 1 - Item 3
Table 1 - Item 3
Run 2
Table 1 - Item 1
Table 2 - Item 2
Table 3 - Item 2
Table 3 - Item 1
Table 3 - Item 1
Table 3 - Item 2
Table 3 - Item 2
Run 3
Table 2 - Item 3
Table 1 - Item 1
Table 3 - Item 2
Table 3 - Item 1
Table 3 - Item 2
Table 1 - Item 2```

## Demo 3 - Dynamic Formulas. Changing Probabilities at Runtime

Catching the `PreResultEvaluation` and modifying parameters before a result is calculated. For this demo, the class `MyItem` has been derived to `MyItemDemo3`. This class will override the `OnPreResultEvaluation` method from the `RDSObject` base class and dynamically modify the probability based on a simple formula: With every result requested, our probability increases by 5% until we get hit. When we are hit, the probability is reset to the default of 1.

`MyItemDemo3` will set its own probability in the constructor based on the parameter "`dynamic`", then overrides two events (the `Pre` and the `Hit`) to control the probability and makes an output when hit.

If the item is dynamic, it starts with a probability of `1`, otherwise with `100`. This is for the demo to show you the increase of the probability until the item finally gets hit.

C#
```public class MyItemDemo3 : MyItem
{
public MyItemDemo3(string name, bool isdynamic)
: base(name)
{
mdynamic = isdynamic;
rdsProbability = (mdynamic ? 1 : 100);
}
private bool mdynamic = false;
public override void OnRDSPreResultEvaluation(EventArgs e)
{
// My probability increases by 5% with every query until i get hit...
if (mdynamic)
{
rdsProbability *= 1.05;
}
}
public override void OnRDSHit(EventArgs e)
{
// i am hit! Reset to default probability
if (mdynamic)
{
rdsProbability = 1;
Console.WriteLine("Dynamic hit! Reset probability to 1");
}
}
...
...
...```

The running code for this demo looks like this: We set up a simple table with 5 items, one of them being the dynamic one. Then we loop through the results until the dynamic item gets hit:

```Loot until we hit the dynamic item
Dynamic is now: Item 1 @ 1,0000
Loot: Item 2
Dynamic is now: Item 1 @ 1,0500
Loot: Item 3
Dynamic is now: Item 1 @ 1,1025
Loot: Item 2
Dynamic is now: Item 1 @ 1,1576
Loot: Item 3
Dynamic is now: Item 1 @ 1,2155
Loot: Item 4
Dynamic is now: Item 1 @ 1,2763
Loot: Item 4
Dynamic is now: Item 1 @ 1,3401
Loot: Item 4
Dynamic is now: Item 1 @ 1,4071
Loot: Item 4
Dynamic is now: Item 1 @ 1,4775
Loot: Item 5
...
...
...
Dynamic is now: Item 1 @ 38,8327
Loot: Item 2
Dynamic is now: Item 1 @ 40,7743
Loot: Item 4
Dynamic is now: Item 1 @ 42,8130
Loot: Item 3
Dynamic is now: Item 1 @ 44,9537
Loot: Item 5
Dynamic is now: Item 1 @ 47,2014
Loot: Item 3
Dynamic is now: Item 1 @ 49,5614
Dynamic hit! Reset probability to 1
Loot: Item 1 @ 1,0000```

## Demo 4 - Creating (Spawning) a Group of Monsters, Maybe Even With a Rare Mob?

Ok, we need a group of Goblins. Urgent! Shamans, Warriors and, with luck, the almighty BOB! The Goblin the world fears since it heard of him the first time! . Let's find out how we can create a random set up group of Monsters. This demo shows the usage of the `RDSCreatableObject` class.

The preparation for this demo includes creating a "`Goblin`" base class (which is basically just the same as the "`MyItem`" class from the other Demos) , from which we derive the `Warrior` and the `Shaman`. The almighty BOB, our rare mob will be of course a `Warrior`, so we derive `BOB : Warrior`. We then set up a `RDSTable` that will contain 5 `Shaman`s, 5 `Warrior`s... and `BOB`.

Why 5 of each class? As I want to show in the demo, one possible way to have monsters spawn with different levels. For the Demo, we set a variable "`AreaLevel = 10`" as the level of the Area where we want to spawn our group of monsters. We then add 1 `Shaman` with `AreaLevel-2`, `1` with `AreaLevel-1`, one at par with `AreaLevel`, and one with `+1` and `+2`. Same for the `Warrior`s. The `+2/-2` mobs have a lower probability to spawn, and the even level mobs have the highest probability.

And last but not least, we add `BOB` with a significantly lower probability to spawn. `BOB` is `rdsUnique` of course... there can be only one BOB.

Play around with this demo, run it over and over again, until you finally hit `BOB`. See how the group of monsters looks like in their distribution of levels and types (`Shaman`, `Warrior`) and you will see, that this spawns totally random groups of 10 shamans each.

Maybe you want to enhance this demo to make the count of `Goblin`s spawned random, too. Try to add a `NullValue` or set up another table of `RDSValue<T>` objects (or just roll a dice) to determine the `rdsCount` for the table.

Here is one possible output of Demo 4, showing the different levels of `Goblin`s based on their probability settings:

```Enter Area Level: 20
Spawning Goblins in a Level 20 area:
Shaman - Level 20
Warrior - Level 20
Shaman - Level 22
Shaman - Level 21
Shaman - Level 20
Warrior - Level 22
Shaman - Level 20
Shaman - Level 20
Warrior - Level 20
Shaman - Level 22```

You see a well spread random group of `Goblin`s, in this case slightly more `Shaman`s than `Warrior`s, but the next group could very well be a bunch of `Warrior`s with almost no `Shaman`s...

By entering a zero as the area level in this demo, you make it loop until `BOB` is finally discovered in a group of `Goblin`s. The output then looks like this:

```Enter 0 as area level to loop random levels until you hit BOB!
Enter Area Level: 0
BOB IS HERE! ON YOUR KNEES, WORLD! *haaarharharhar*
BOB found in group #281 in a Level 30 area:
Warrior - Level 28
BOB - Level 60
Warrior - Level 28
Shaman - Level 32
Warrior - Level 32
Shaman - Level 29
Warrior - Level 28
Warrior - Level 30
Shaman - Level 29
Warrior - Level 30```

Here is some of the code written to create this demo. Look at the `Goblin`s and their override of `rdsCreateInstance()`. This will return a `new` `Goblin` to the result set, so each `Monster` contained is its own, living instance.

The `Goblin`s are created for this demo as simple as possible:

C#
```// The Goblin base class needs at least a level
// In a real game scenario you will likely have a
// base class "Monster" or even "NPC", which will
// have the level. For this Demo, a Goblin is enough.
public class Goblin : RDSCreatableObject
{
public Goblin(int level) { Level = level; }

public int Level = 0;
public override string ToString()
{
return this.GetType().Name + " - Level " + Level.ToString();
}
}```

The three `Goblin`s derive from this class, they look all the same in this demo, so I just show the `Shaman` as a representative for all three:

C#
```public class Shaman : Goblin
{
public Shaman(int level) : base(level) { }
public override IRDSObject rdsCreateInstance()
{
return new Shaman(Level);
}
}```

NEW in this demo is the `GoblinTable` class. We do not use `RDSTable` directly, we derive from it, add a custom constructor and add the entries in the derived table. Look at the different levels, probabilities and the extremely low chance for `BOB`, to appear.

C#
```public class GoblinTable : RDSTable
{
public GoblinTable(int arealevel)
{
// Shamans with different level based on the arealevel
// With a probability curve peaked at the area level
// Same for Warriors
// BOB is double the arealevel - a real hard one!
rdsCount = 10;
}
}
```

I think, if not already happened so far, NOW you see some of the power and comfort, this library has to offer for your design of random content!

## Demo 5 - Playing with RDSValue<T>. Random Gold Drops and Other Values

Finally. BOB is dead! What did he drop? How rich has he been really?

The imported part in this short demo is, that you derive a class from `RDSValue<T>` to contain a Gold drop. The value is calculated when it is constructed based on the constructor parameters `AreaLevel`, `MobLevel` and `PlayerLevel`.

The formula taken is: Base Gold amount is `10 * AreaLevel`. Now add/subtract `MonsterLevel`-`AreaLevel` and `AreaLevel`-`Playerlevel` (to punish highlevel players in lowlevel areas). You could as well use some Random formula here, I just wanted to show the dynamic assignment of a value, as well as introducing the `RDSValue<T>` a bit.

C#
```Enter Area Level: 20
Enter Monster Level: 22
Enter Player Level: 24
Querying Gold drop: 198,00```

This is really a very short and simple demo, only to show the access to a `RDSValue<T>` object and what you can do with it. Play around with some `RDSValues`, I am sure you will find a lot of usage scenarios.

C#
```// This table contains only 1 entry to demonstrate the RDSValue<T> class.
// In a real scenario, a gold drop is only one of many entries for the loot
// of a mob of course.
RDSTable gold = new RDSTable();
Console.WriteLine("Querying Gold drop: " +
((GoldDrop)gold.rdsResult.First()).rdsValue.ToString("n2"));
public class GoldDrop : RDSValue<double>
{
public GoldDrop(int arealevel, int moblevel, int playerlevel):
base(0, 1)
{
rdsValue = 10 * arealevel + (moblevel - arealevel) + (arealevel - playerlevel);
}
}```

## Demo 6 - Random Generating a Simple Map

Short demo of selecting 25 map pieces randomly to create a 5x5 map. You can create any map size with this system, of course.

The setup here is to demonstrate a new technique: Dynamically enabling and disabling entries of one single table in the `PreResult` override, based on the exits a map segment has.

We create a class named `MiniDungeon : RDSTable`. This table contains lots of `MapSegment` objects, that derive from `RDSObject`. Each Segment has four exits: `North`, `East`, `South` and `West`. Those boolean flags represent the possible exits of a `Segment` and are used to modify the states of the contents of the `MiniDungeon`.

`MapSegment` gets a constructor that takes four boolean parameters, each one describing one of the possible exits. We want to loot only `Segment`s, that can fulfill the needs of the map (i.e., have the desired exits).

In the `PreResult` override, each `MapSegment` disables/enables itself based on the requested exits, so that only those `Segment`s stay active that can fulfill the desired exits.

The algorithm of the `Map` is clearly not the most high sophisticated you have ever seen, but that's not the point of the demo. A demo output of a 5x5 map could look like this, in simple semigraphic console output:

```███████ ████ ████████████
███████ ████ ████████████
██      ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
████           ██
██ ████ █████████ ███████
██ ████ █████████ ███████
██ ████ █████████ ███████
██ ████ █████████ ███████
██
███████ ████ ████ ████ ██
███████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██           ██        ██
██ █████████ ████ ████ ██
██ █████████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██   ██
██ ███████████████████ ██
██ ███████████████████ ██```

That's enough for a few lines of code and to give you a base for further experiments. Let's take a closer look at the `MiniDungeon` class and how this all works:

I have set up this table containing every possible combination of the 4 exits, except the 0000 (no exit). This leaves us with 15 entries in the table, all with the same probability for simplicity:

C#
```public class MiniDungeon : RDSTable
{
public MiniDungeon()
{
// Add all possible combinations of exits except the 0000 (no exit)
// All have the same probability for this demo, in a real scenario
// you could and probably will make some combinations
// more rare than others of course or have more different segments
// with the same exits... don't forget, this is just a demo!
AddEntry(new MapSegment(false, false, false, true ), 10);
AddEntry(new MapSegment(false, false, true , false), 10);
AddEntry(new MapSegment(false, false, true , true ), 10);
AddEntry(new MapSegment(false, true , false, false), 10);
AddEntry(new MapSegment(false, true , false, true ), 10);
AddEntry(new MapSegment(false, true , true , false), 10);
AddEntry(new MapSegment(false, true , true , true ), 10);
AddEntry(new MapSegment(true , false, false, false), 10);
AddEntry(new MapSegment(true , false, false, true ), 10);
AddEntry(new MapSegment(true , false, true , false), 10);
AddEntry(new MapSegment(true , false, true , true ), 10);
AddEntry(new MapSegment(true , true , false, false), 10);
AddEntry(new MapSegment(true , true , false, true ), 10);
AddEntry(new MapSegment(true , true , true , false), 10);
AddEntry(new MapSegment(true , true , true , true ), 10);
rdsCount = 1;
}
...
...
...```

A `MapSegment` is very simple in its design, too:

C#
```public class MapSegment : RDSObject
{
public MapSegment(bool exitnorth, bool exiteast, bool exitsouth, bool exitwest)
{
North = exitnorth;
East = exiteast;
South = exitsouth;
West = exitwest;
}
public bool North = false;
public bool East = false;
public bool South = false;
public bool West = false;
public override void OnRDSPreResultEvaluation(EventArgs e)
{
base.OnRDSPreResultEvaluation(e);
// Look up what our table needs
// Every RDSObject has a pointer to the table where it is contained
MiniDungeon t = rdsTable as MiniDungeon;
rdsEnabled = ((t.NeedEast && East) || !t.NeedEast) &&
((t.NeedWest && West) || !t.NeedWest) &&
((t.NeedNorth && North) || !t.NeedNorth) &&
((t.NeedSouth && South) || !t.NeedSouth);
}
...
...
...```

The demo algorithm focuses on the exits of a neighbor field to determine, what elements are allowed to drop for the next field. Take a close look at the `override OnRDSPreResultEvaluation` method:

• First new thing here: Each `RDSObject` has a pointer to the table where it is contained, the `rdsTable` field. It is set by the `AddEntry` method of a `RDSTable` object. You can use this field to get runtime data from the table, in this case, what exits are needed for the next field.
• The `MapSegment` sets its own `rdsEnabled` property based on the needed exits and the exits it self can support. If this results in `false`, this `Segment` can not drop. It's as easy as that.

The `MiniDungeon` class now got a method `GenerateMap(..,..)` that plays around with the boolean flags of needed exits based on the position of the generation where it currently is. In the top row, only a `South` exit is really needed, same as in the most left or right column we need an `East` or `West` exit, and for all the fields in the middle of the map, `NeedNorth` and `NeedWest` are set based on the exits of the neighbour fields, so we get `Segment`s that fit with their neighbors.

C#
```// Generates a random map with a given dimension
public MapSegment[,] GenerateMap(int sizeX, int sizeY)
{
MapSegment[,] map = new MapSegment[sizeX, sizeY];
for (int y = 0; y < sizeY; y++)
{
for (int x = 0; x < sizeX; x++)
{
if (y == 0)
{
NeedNorth = false;
NeedSouth = true;
}
else if (y == sizeY - 1)
{
NeedNorth = true;
NeedSouth = false;
}
else
{
NeedNorth = (map[x, y - 1].South);
NeedSouth = !NeedNorth;
}
if (x == 0)
{
NeedEast = true;
NeedWest = false;
}
else if (x == sizeX - 1)
{
NeedEast = false;
NeedWest = true;
}
else
{
NeedWest = (map[x - 1, y].East);
NeedEast = !NeedWest;
}
map[x, y] = (MapSegment)rdsResult.First();
}
}
return map;
}```

Again: This is a very simple and far from perfect algorithm and I honestly don't think it can be used in its current state for any real game. But I also think, it is enough of base work to get you on track and to make you see, what is possible with RDS.

It's the same scheme every time, for every random content. No matter if you have a drop system like Diablo (where a ZOD rune drops only once in a zillion of drops), or you want to generate maps, spawn `Monster`s at random positions and random amount, for everything you want to create dynamically.

I hope you have now a good idea of what RDS can do for you. I think it is a library with very high value that takes away lots of decision work from you if you agree to really implement (inherit) the RDS classes. It all works together fine and you have almost every thinkable freedom with lots of `virtual` methods to override.

I hope you have fun with this library,

Yours,

Mike.

## History

• 2012-07-13: First draft started

## Share

 Software Developer (Senior) Austria
Software Developer since the late 80's, grew up in the good old DOS-Era, switched to windows with Win95 and now doing .net since early 2002 (beta).
Long year c# experience in entertainment software, game programming, directX and XNA as well as SQLServer (DBA, Modelling, Optimizing, Replication, etc) and Oracle Databases in Enterprise environments. Started with Android development in 2014.

My Android Label (mbar Software)
My Android Apps in Play Store

 First Prev Next
 My vote of 5 Member 1124143917-Nov-14 9:11 Member 11241439 17-Nov-14 9:11
 ResetResult Lazerath7-Jan-14 7:49 Lazerath 7-Jan-14 7:49
 Re: ResetResult Mike Barthold7-Jan-14 18:39 Mike Barthold 7-Jan-14 18:39
 My Vote of 5 Vanzanz4-Dec-13 6:17 Vanzanz 4-Dec-13 6:17
 Re: My Vote of 5 Mike Barthold4-Dec-13 20:35 Mike Barthold 4-Dec-13 20:35
 Great Job Jeremy Airey20-Nov-13 10:48 Jeremy Airey 20-Nov-13 10:48
 Re: Great Job Mike Barthold4-Dec-13 20:34 Mike Barthold 4-Dec-13 20:34
 Good Work MrWiggels9-Oct-12 8:23 MrWiggels 9-Oct-12 8:23
 This is done really nicely Mike. I've also been studying doing something like this for quite a while, but this is by far the best implementation. Keep up the good work
 Re: Good Work Mike Barthold9-Oct-12 20:30 Mike Barthold 9-Oct-12 20:30