Click here to Skip to main content
15,905,963 members
Articles / .NET / .NET4.5

Toolsmithing -- The Rule of Ready

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
20 Oct 2013CPOL3 min read 9.6K   1  
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

C#
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.

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

C#
/// <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()

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
I'm a software developer that works with a small team on websites in .Net. Software development is a career of constant learning, and here I'm learning by sharing.

Comments and Discussions

 
-- There are no messages in this forum --