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
private const int TilesPerTypeInGame = 4;
private const MahjongSuitNumber redBonusNumber = MahjongSuitNumber.Five;
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
public IEnumerable<MahjongHonorType> HonorTileTypes { get; private set; }
public IEnumerable<MahjongSuitType> SuitTileTypes { get; private set; }
public IEnumerable<MahjongSuitNumber> SuitTileNumbers { get; private set; }
public IEnumerable<MahjongTile> TerminalTiles { get; private set; }
public IEnumerable<MahjongTile> MajorTiles { get; private set; }
public IEnumerable<MahjongTile> SimpleTiles { get; private set; }
public IReadOnlyDictionary<MahjongSuitType, IEnumerable<MahjongSequence>>
SequencesBySuit { get; private set; }
public IEnumerable<MahjongSequence> Sequences
{
get { return SequencesBySuit.Values.SelectMany(seq => seq); }
}
public IEnumerable<MahjongPartialSequence> PartialSequences
{
get { return Sequences.SelectMany(seq => seq.PartialSequences); }
}
#endregion
#region Constructor
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
public IList<MahjongTile> CreateGameTileSet()
{
return CreateGameTileSet(useRedBonusTiles:false);
}
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
private IEnumerable<MahjongTile> CreateTilesForSet(MahjongSuitType suitType,
MahjongSuitNumber suitNumber)
{
return Enumerable.Repeat
(new MahjongSuitTile(suitType, suitNumber), TileEngine.TilesPerTypeInGame);
}
private IEnumerable<MahjongTile> CreateTilesForSet(MahjongHonorType honorType)
{
return Enumerable.Repeat(new MahjongHonorTile(honorType),
TileEngine.TilesPerTypeInGame);
}
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));
}
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));
}
}
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);
}
}
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.
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
public class ThreadSafeRandom
{
#region Private Fields
private static readonly Random globalRandom = new Random();
[ThreadStatic]
private static Random localRandom;
#endregion
#region Constructor
public ThreadSafeRandom()
{
if (ThreadSafeRandom.localRandom == null)
{
int seed;
lock (ThreadSafeRandom.globalRandom)
{
seed = ThreadSafeRandom.globalRandom.Next();
}
localRandom = new Random(seed);
}
}
#endregion
#region Public Methods
public int Next()
{
return ThreadSafeRandom.localRandom.Next();
}
public int Next(int maxValue)
{
return ThreadSafeRandom.localRandom.Next(maxValue);
}
public int Next(int minValue, int maxValue)
{
return ThreadSafeRandom.localRandom.Next(minValue, maxValue);
}
public void NextBytes(byte[] buffer)
{
ThreadSafeRandom.localRandom.NextBytes(buffer);
}
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();
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;
}
}
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;
}
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