RogueSharp v5.0 Pre-Release
New features:
- Weighted pool class for randomly picking items.
- Map and cell classes now have generic versions that are easier to inherit from.
- Pathfinder class caches graph and dijkstra result for speedier performance with multiple lookups.
- Added Map.GetCellsInRectangle method. (Thanks to Andre Odendaal)
Breaking changes:
- Bug fix – Change Map.Create to be generic, returning the same map type as the creation strategy’s map. (Thanks to Andre Odendaal)
- Bug fix – Update Map Save and Restore methods to track IsExplored status. (Thanks to Andre Odendaal)
- Map.Initialize is virtual so that it can be overridden in derived classes
- Map.Clone is now virtual and generic so that it can be overridden in derived classes.
RogueSharp v5.0 NuGet Package: https://www.nuget.org/packages/RogueSharp/5.0.0-pre2
RogueSharp Source Code: https://github.com/FaronBracy/RogueSharp
Weighted Pools
It is common in Roguelike computer games to have a sort of randomness when it comes to choosing treasure, placing monsters, or creating dungeons. A lot of tabletop games also have a draw mechanism whether it is choosing a card at random from a deck, picking a token at random from a bag, or rolling some dice and consulting a table.
Random Drawing Example
In the board game Quartz players draw crystal from a bag. There are 68 total crystals in the bag.
- 18 Obsidian
- 15 Quartz
- 12 Amethyst
- 10 Emerald
- 7 Sapphire
- 4 Ruby
- 2 Amber
When a player draws a crystal, they keep it. It would no longer be available for another player to draw. We can simulate this using a RougeSharp WeightedPool
First we need to create a type representing the crystal. For this simple example we’ll just use an enumeration
public enum CrystalType { Obsidian = 0, Quartz = 1, Amethyst = 2, Emerald = 3, Sapphire = 4, Ruby = 5, Amber = 6 }
Next we’ll create a helper method that will add a set amount of crystals into the pool.
public void AddCrystalToPool( WeightedPool<CrystalType> pool, CystalType crystalType, int numberToAdd ) { for ( int i = 0; i < numberToAdd; i++ ) { pool.Add( crystalType, 1 ); } }
Notice that the weight for each crystal is 1. This means that each individual crystal has an equal chance to be drawn. There will still be a higher likelihood to draw Obsidian over Amber though, as there are 18 Obsidian in the bag and only 2 Amber.
Finally we’ll setup the pool and then draw 10 crystals from it. Notice that by using the Draw()
we are actually removing the item from the pool.
public void RandomDrawExample() { WeightedPool<CrystalType> pool = new WeightedPool<CrystalType>(); AddCrystalToPool( pool, CrystalType.Obsidian, 18 ); AddCrystalToPool( pool, CrystalType.Quartz, 15 ); AddCrystalToPool( pool, CrystalType.Amethyst, 12 ); AddCrystalToPool( pool, CrystalType.Emerald, 10 ); AddCrystalToPool( pool, CrystalType.Sapphire, 7 ); AddCrystalToPool( pool, CrystalType.Ruby, 4 ); AddCrystalToPool( pool, CrystalType.Amber, 2 ); for ( int x = 0; x < 10; x++ ) { CrystalType crystal = pool.Draw(); Console.WriteLine( $"You drew a {crystal} from the bag" ); } }
The output from running this would look something like the following:
You drew a Quartz from the bag You drew a Obsidian from the bag You drew a Emerald from the bag You drew a Sapphire from the bag You drew a Obsidian from the bag You drew a Quartz from the bag You drew a Obsidian from the bag You drew a Amber from the bag You drew a Quartz from the bag You drew a Sapphire from the bag
Note that since there are only 68 total crystals in the bag if we attempted to draw more than that we would hit an exception when trying to draw the 69th.
System.InvalidOperationException: Add items to the pool before attempting to draw one
Treasure Table Example
For the next example, consider the following treasure table used to determine random treasure in a tabletop role playing game.
As you can see in this table the player would roll 1d100
and then look at the table and choose the reward based on what number they rolled. Let’s see how RogueSharp can simulate this effect using the new WeightedPool
class as a similar feature could be useful in a Roguelike game.
In order to re-create this in code we would first want to make a class to represent the treasure.
public class Treasure { public string Name { get; set; } public int GoldPieceValue { get; set; } public Treasure( string name, int goldPieceValue ) { Name = name; GoldPieceValue = goldPieceValue; } public static Treasure Clone( Treasure other ) { return new Treasure( other.Name, other.GoldPieceValue ); } }
The Clone
method is important here because we want to make sure that we generate a copy of the treasure and not remove the treasure from the table so that there is a equal chance of generating the treasure again in the future.
public void GenerateTreasure() { WeightedPool<Treasure> pool = new WeightedPool<Treasure>( Singleton.DefaultRandom, Treasure.Clone ); pool.Add( new Treasure( "Ruby Necklace", 100 ), 14 ); pool.Add( new Treasure( "Potion of Healing", 50 ), 14 ); pool.Add( new Treasure( "Longsword + 1", 150 ), 16 ); pool.Add( new Treasure( "Ring of Fireballs", 300 ), 14 ); pool.Add( new Treasure( "Leather Armor", 25 ), 14 ); pool.Add( new Treasure( "Small Shield", 25 ), 14 ); pool.Add( new Treasure( "Teleportation Scroll", 100 ), 14 ); for ( int x = 0; x < 10; x++ ) { Treasure treasure = pool.Choose(); Console.WriteLine( $"You found a {treasure.Name} worth {treasure.GoldPieceValue} gp" ); } }
Notice that when we create the WeightedPool
we are passing in the Clone
method from our Treasure
class. If this method was not passed in to the constructor we would get an exception System.InvalidOperationException: A clone function was not defined when this pool was constructed
When we call pool.Add
notice that we assign a weight to each item. This matches the values in the treasure table we are simulating and the values add up to 100 because the original table was a 1d100
role. It’s not necessary for the weights to add up to 100 though.
We call pool.Choose()
to select an item from the table and make a copy of it. This is different than the example above where we call pool.Draw()
which removes the item from the pool.
The output of this function will be something like the following:
You found a Small Shield worth 25 gp You found a Teleportation Scroll worth 100 gp You found a Longsword + 1 worth 150 gp You found a Small Shield worth 25 gp You found a Small Shield worth 25 gp You found a Ring of Fireballs worth 300 gp You found a Ruby Necklace worth 100 gp You found a Potion of Healing worth 50 gp You found a Teleportation Scroll worth 100 gp You found a Ring of Fireballs worth 300 gp
See how the Small Shield was picked multiple times, but the Leather Armor didn’t make the list once. Weights could be adjusted if you wanted a higher likelihood of obtaining a certain item.