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).
RDSTable t = new RDSTable();
t.AddEntry(new MyItem("Item 1"), 10);
t.AddEntry(new MyItem("Item 2"), 10);
t.AddEntry(new MyItem("Item 3"), 10);
t.AddEntry(new MyItem("Item 4"), 10);
t.AddEntry(new MyItem("Item 5"), 10);
MyItem m6 = new MyItem("Item 6");
t.AddEntry(m6, 10);
t.rdsCount = 2;
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);
}
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:
RDSTable t = new RDSTable();
RDSTable subtable1 = new RDSTable();
RDSTable subtable2 = new RDSTable();
RDSTable subtable3 = new RDSTable();
t.AddEntry(subtable1, 10);
t.AddEntry(subtable2, 10);
t.AddEntry(subtable3, 10);
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.
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.
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)
{
if (mdynamic)
{
rdsProbability *= 1.05;
}
}
public override void OnRDSHit(EventArgs e)
{
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:
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:
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.
public class GoblinTable : RDSTable
{
public GoblinTable(int arealevel)
{
AddEntry(new Shaman(arealevel - 2), 100);
AddEntry(new Shaman(arealevel - 1), 200);
AddEntry(new Shaman(arealevel ), 500);
AddEntry(new Shaman(arealevel + 1), 200);
AddEntry(new Shaman(arealevel + 2), 100);
AddEntry(new Warrior(arealevel - 2), 100);
AddEntry(new Warrior(arealevel - 1), 200);
AddEntry(new Warrior(arealevel ), 500);
AddEntry(new Warrior(arealevel + 1), 200);
AddEntry(new Warrior(arealevel + 2), 100);
AddEntry(new BOB(arealevel * 2), 1);
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.
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.
RDSTable gold = new RDSTable();
gold.AddEntry(new GoldDrop(baselevel, moblevel, playerlevel), 1);
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:
public class MiniDungeon : RDSTable
{
public MiniDungeon()
{
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:
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);
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.
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