Toolsmithing -- The Rule of Ready





4.00/5 (1 vote)
The Rule of Ready project continues with some utility functionality
Introduction
This is the second article for The Rule of Ready project, an open-source Japanese Mahjong game project written in C#. I previously talked about the class that represents a tile in the game, the MahjongTile
base class and subclasses for the two types of tiles.
For more information on Japanese Mahjong, here are some sites to visit:
- A wiki for information about Japanese Mahjong
- A thorough PDF detailing both the rules and the Japanese terms for the game, in exhaustive detail, from a player only known as Barticle
- ReachMahjong.com, a community site for professional players of the game, with translated articles from professional Japanese players
- A Japanese Mahjong Flash game
Putting Together Tools
First, I created a TileEngine
class for some core functionality: creating the tiles needed for a game, and maintain lists for reference. I'm intending the TileEngine
to be the place to store common functionality for using MahjongTile
objects. One of the challenges of a software project--for me, at least--is keeping organized. My favored heuristic for this is "knowing where to find something," and then putting code where I'll expect to find it in the future. If I have a hard time finding some code in the week or month later, then I reassess where I put the code.
Admittedly, Visual Studio has a variety of tools to assist in finding code, but I feel that these tools work even better with good organization.
The MahjongSequence
and MahjongPartialSequence
classes are collections of tiles that are part of a winning hand. I'll go into depth on them in my next article. I've also located some domain knowledge in this class: specifically the specific tiles that are used for a game of Japanese Mahjong.
TileEngine
public class TileEngine
{
#region Private Fields
/// <summary>
/// Constant: number of identical tiles of each tile type in the game
/// </summary>
private const int TilesPerTypeInGame = 4;
/// <summary>
/// When red bonus tiles are used (Japanese: akidora),
/// this is the suit number used for them
/// </summary>
private const MahjongSuitNumber redBonusNumber = MahjongSuitNumber.Five;
/// <summary>
/// When the red bonus tiles are used, these are the number of tiles per suit
///(there are normally 4 tiles)
/// </summary>
private readonly IReadOnlyDictionary<MahjongSuitType, int> numRedTilesPerSuit =
new Dictionary<MahjongSuitType, int>()
{
{MahjongSuitType.Bamboo, 1},
{MahjongSuitType.Character, 1},
{MahjongSuitType.Dot, 2},
};
#endregion
#region Public (read-only) Properties
/// <summary>
/// A list of Honor types, in order
/// </summary>
public IEnumerable<MahjongHonorType> HonorTileTypes { get; private set; }
/// <summary>
/// A list of Suit types, in order
/// </summary>
public IEnumerable<MahjongSuitType> SuitTileTypes { get; private set; }
/// <summary>
/// A list of suit numbers, in order
/// </summary>
public IEnumerable<MahjongSuitNumber> SuitTileNumbers { get; private set; }
/// <summary>
/// A list of all Terminals (suit tiles that are 1 or 9)
/// (Japanese: Rōtōhai)
/// </summary>
public IEnumerable<MahjongTile> TerminalTiles { get; private set; }
/// <summary>
/// A list of all Major tiles, (Terminals + Honors)
/// (Japanese: Yaochūhai)
/// </summary>
public IEnumerable<MahjongTile> MajorTiles { get; private set; }
/// <summary>
/// A list of all Simple tiles (suit tiles from 2 thru 8)
/// (also known as Minor or Middle Tiles)
/// (Japanese: Chunchanpai or Tanyaohai)
/// </summary>
public IEnumerable<MahjongTile> SimpleTiles { get; private set; }
/// <summary>
/// A Dictionary, keyed by Suit Type of all possible sequences
/// </summary>
public IReadOnlyDictionary<MahjongSuitType, IEnumerable<MahjongSequence>>
SequencesBySuit { get; private set; }
/// <summary>
/// A list of all sequences: 3 consecutive tiles of the same suit,
/// (e.g. 3-dot 4-dot 5-dot)
/// </summary>
public IEnumerable<MahjongSequence> Sequences
{
get { return SequencesBySuit.Values.SelectMany(seq => seq); }
}
/// <summary>
/// A list of all partial sequences: 2 tiles out of a sequence
/// </summary>
public IEnumerable<MahjongPartialSequence> PartialSequences
{
get { return Sequences.SelectMany(seq => seq.PartialSequences); }
}
#endregion
#region Constructor
/// <summary>
/// Create a new Tile Engine. (This is a somewhat heavy object)
/// </summary>
public TileEngine()
{
this.HonorTileTypes = Enum.GetValues
(typeof(MahjongHonorType)).Cast<MahjongHonorType>();
this.SuitTileTypes = Enum.GetValues
(typeof(MahjongSuitType)).Cast<MahjongSuitType>();
this.SuitTileNumbers = Enum.GetValues
(typeof(MahjongSuitNumber)).Cast<MahjongSuitNumber>();
this.TerminalTiles = this.GenerateTerminalTiles();
this.SimpleTiles = this.GenerateSimpleTiles();
this.MajorTiles = this.HonorTileTypes.Select
(honorType => new MahjongHonorTile(honorType))
.Concat(this.TerminalTiles);
this.SequencesBySuit = new Dictionary<MahjongSuitType, IEnumerable<MahjongSequence>>(3)
{
{MahjongSuitType.Bamboo,
this.GenerateSequencesForSuit(MahjongSuitType.Bamboo)},
{MahjongSuitType.Character,
this.GenerateSequencesForSuit(MahjongSuitType.Character)},
{MahjongSuitType.Dot,
this.GenerateSequencesForSuit(MahjongSuitType.Dot)}
};
}
#endregion
#region Public Methods
/// <summary>
/// Create a new tile set of 136 tiles, 4 of each type
/// </summary>
/// <returns></returns>
public IList<MahjongTile> CreateGameTileSet()
{
return CreateGameTileSet(useRedBonusTiles:false);
}
/// <summary>
/// Create a new tile set of 136 tiles, 4 of each type,
/// optionally swapping in the red tiles
/// </summary>
/// <param name="useRedBonusTiles">Swap in red tiles?</param>
/// <returns></returns>
public IList<MahjongTile> CreateGameTileSet(bool useRedBonusTiles)
{
var tileSet = new List<MahjongTile>();
foreach (MahjongSuitType suitType in this.SuitTileTypes)
{
foreach (MahjongSuitNumber suitNumber in this.SuitTileNumbers)
if (!useRedBonusTiles || !(suitNumber == TileEngine.redBonusNumber))
tileSet.AddRange(CreateTilesForSet(suitType, suitNumber));
else
tileSet.AddRange(CreateRedTilesForSet(suitType, suitNumber));
}
foreach (MahjongHonorType honorType in this.HonorTileTypes)
{
tileSet.AddRange(CreateTilesForSet(honorType));
}
return tileSet;
}
#endregion
#region Private Methods
/// <summary>
/// Create tiles for the given suit and number for the game
/// </summary>
/// <param name="suitType">suit to create</param>
/// <param name="suitNumber">number to create</param>
/// <returns></returns>
private IEnumerable<MahjongTile> CreateTilesForSet(MahjongSuitType suitType,
MahjongSuitNumber suitNumber)
{
return Enumerable.Repeat
(new MahjongSuitTile(suitType, suitNumber), TileEngine.TilesPerTypeInGame);
}
/// <summary>
/// Create tile for the given honor for the game
/// </summary>
/// <param name="honorType">honor to create</param>
/// <returns></returns>
private IEnumerable<MahjongTile> CreateTilesForSet(MahjongHonorType honorType)
{
return Enumerable.Repeat(new MahjongHonorTile(honorType),
TileEngine.TilesPerTypeInGame);
}
/// <summary>
/// Create tiles for the given suit and number for the game,
/// with Red Bonus tiles swapped in as
/// appropriate
/// </summary>
/// <param name="suitType">suit to create</param>
/// <param name="suitNumber">number to create</param>
/// <returns></returns>
private IEnumerable<MahjongTile> CreateRedTilesForSet(MahjongSuitType suitType,
MahjongSuitNumber suitNumber)
{
if (suitNumber != TileEngine.redBonusNumber)
return this.CreateTilesForSet(suitType,suitNumber);
int numRedTiles = this.numRedTilesPerSuit[suitType];
int numNormalTiles = TileEngine.TilesPerTypeInGame - numRedTiles;
return Enumerable.Repeat(new MahjongSuitTile(suitType, suitNumber, isRedBonus: true),
numRedTiles)
.Concat(Enumerable.Repeat(new MahjongSuitTile(suitType, suitNumber),
numNormalTiles));
}
/// <summary>
/// Generates a list of all sequences for a suit
/// </summary>
/// <param name="suitType">suit to make the sequences</param>
/// <returns></returns>
private IEnumerable<MahjongSequence> GenerateSequencesForSuit(MahjongSuitType suitType)
{
IList<MahjongSuitTile> tiles =
this.SuitTileNumbers
.Select(number => new MahjongSuitTile(suitType, number))
.ToList();
for(int startingIdx = 0; tiles.Count - startingIdx >= 3; startingIdx += 1)
{
yield return new MahjongSequence(tiles.Skip(startingIdx).Take(3));
}
}
/// <summary>
/// Generate all of the Terminal tiles (1 and 9 of each suit)
/// </summary>
/// <returns>list of Terminals</returns>
private IEnumerable<MahjongTile> GenerateTerminalTiles()
{
int lowTerminal = 1;
int highTerminal = this.SuitTileNumbers.Count();
foreach (MahjongSuitType suitType in this.SuitTileTypes)
{
yield return new MahjongSuitTile(suitType, lowTerminal);
yield return new MahjongSuitTile(suitType, highTerminal);
}
}
/// <summary>
/// Generate all of the Simple Tiles (2 thru 8 of each suit)
/// </summary>
/// <returns>list of Simples</returns>
private IEnumerable<MahjongTile> GenerateSimpleTiles()
{
int lowestSimple = 2;
int highestSimple = this.SuitTileNumbers.Count() - 1;
foreach (MahjongSuitType suitType in this.SuitTileTypes)
{
for (int suitNumber = lowestSimple; suitNumber <= highestSimple; suitNumber++)
{
yield return new MahjongSuitTile(suitType, suitNumber);
}
}
}
#endregion
}
Second, I created some functionality needed for collection classes themselves that isn't part of .NET: shuffling a list of objects, and some helper methods for a LinkedList
. I'm looking at using a LinkedList
to be the Wall of the game, drawing tiles from the front, and the Dead Wall from the back. I could have used a queue, but I prefer to use or build data types that have an intuitive connection to the task--and a LinkedList
fits the task best in this case.
StackOverflow had some helpful answers that I found useful here.
- Is C# Random Number Generator thread safe?: An elegant answer for creating pseudo-random number generators that are themselves seeded randomly
- Randomize a List in C#<t>: A simple linear algorithm for shuffling a list
The interesting note here is the lock on the source of seeds, so that no two instances of ThreadSafeRandom
will use the same result of the static Random
. I freely admit that I copied the public
methods of Random
--to include the documentation. (I rather felt the lack of an IRandom
interface in .NET.)
ThreadSafeRandom
/// <summary>
/// A Random number generator that's seeded randomly.
/// </summary>
public class ThreadSafeRandom
{
#region Private Fields
/// <summary>
/// Source of seeds
/// </summary>
private static readonly Random globalRandom = new Random();
/// <summary>
/// A field for storing the internal (local) random number generator
/// </summary>
[ThreadStatic]
private static Random localRandom;
#endregion
#region Constructor
/// <summary>
/// Create a new pseudo-random number generator that is seeded randomly
/// </summary>
public ThreadSafeRandom()
{
if (ThreadSafeRandom.localRandom == null)
{
int seed;
lock (ThreadSafeRandom.globalRandom)
{
seed = ThreadSafeRandom.globalRandom.Next();
}
localRandom = new Random(seed);
}
}
#endregion
#region Public Methods
/// <summary>
/// Returns a nonnegative number
/// </summary>
/// <returns>A 32-bit signed integer greater than
/// or equal to zero and less than int.MaxValue.
/// </returns>
public int Next()
{
return ThreadSafeRandom.localRandom.Next();
}
/// <summary>
/// Returns a nonnegative random number less than the specified maximum.
/// </summary>
/// <param name="maxValue">
/// The exclusive upper bound of the random number to be generated.
/// maxValue must be greater than or equal to zero.
/// </param>
/// <returns>
/// A 32-bit signed integer greater than or equal to zero, and less than maxValue;
/// that is, the range of return values ordinarily includes zero but not maxValue.
/// However, if maxValue equals zero, maxValue is returned.
/// </returns>
public int Next(int maxValue)
{
return ThreadSafeRandom.localRandom.Next(maxValue);
}
/// <summary>
/// Returns a random number within a specified range.
/// </summary>
/// <param name="minValue">The inclusive lower bound of the random number returned.
/// </param>
/// <param name="maxValue">
/// The exclusive upper bound of the random number returned.
/// maxValue must be greater than or equal to minValue.
/// </param>
/// <returns>
/// 32-bit signed integer greater than or equal to minValue and less than maxValue;
/// that is, the range of return values includes minValue but not maxValue.
/// If minValue equals maxValue, minValue is returned.
/// </returns>
public int Next(int minValue, int maxValue)
{
return ThreadSafeRandom.localRandom.Next(minValue, maxValue);
}
/// <summary>
/// Fills the elements of a specified array of bytes with random numbers.
/// </summary>
/// <param name="buffer">An array of bytes to contain random numbers. </param>
public void NextBytes(byte[] buffer)
{
ThreadSafeRandom.localRandom.NextBytes(buffer);
}
/// <summary>
/// Returns a random number between 0.0 and 1.0.
/// </summary>
/// <returns>
/// A double-precision floating point number greater than or equal to 0.0,
/// and less than 1.0.
/// </returns>
public double NextDouble()
{
return ThreadSafeRandom.localRandom.NextDouble();
}
#endregion
}
And here's my extension methods for collections. For Shuffle
, I needed the collection to implement IList<T>
to be able to use an indexer. Without that, this algorithm would be much slower. For the LinkedList
methods, I use the LINQ method <a href="http://msdn.microsoft.com/en-us/library/bb337697.aspx">Any()</a>
to determine whether there are any elements.
I personally feel that !list.Any()
is has a clearer intent than list.FirstOrDefault() == null
or (assuming list implements ICollection<T>
) list.Count == 0
.
IList<T>.Shuffle()
public static class CollectionExtensions
{
private static readonly ThreadSafeRandom random = new ThreadSafeRandom();
/// <summary>
/// In place shuffle of a list (based on Fisher-Yates shuffle)
/// </summary>
/// <typeparam name="T">type of list to shuffle</typeparam>
/// <param name="list">list to shuffle</param>
public static void Shuffle<T>(this IList<T> list)
{
int shuffleToIdx = list.Count;
while (shuffleToIdx > 1)
{
shuffleToIdx -= 1;
int shuffleFromIdx = random.Next(shuffleToIdx + 1);
T value = list[shuffleFromIdx];
list[shuffleFromIdx] = list[shuffleToIdx];
list[shuffleToIdx] = value;
}
}
/// <summary>
/// Removes and returns the first element of the list
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list"></param>
/// <returns></returns>
public static T PopFirst<T>(this LinkedList<T> list)
{
if (list == null || !list.Any())
return default(T);
T element = list.First.Value;
list.RemoveFirst();
return element;
}
/// <summary>
/// Removes and returns the first element of the list
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list"></param>
/// <returns></returns>
public static T PopLast<T>(this LinkedList<T> list)
{
if (list == null || !list.Any())
return default(T);
T element = list.Last.Value;
list.RemoveLast();
return element;
}
}
What's Next?
Next time, I'll go into how I'll be representing Mahjong sets in the game. You can find the complete source at ruleofready.codeplex.com.
History
- 20th October, 2013: Initial version