Click here to Skip to main content
15,881,709 members
Articles / Programming Languages / C# 5.0

Beginning the Rule of Ready Project

Rate me:
Please Sign up or sign in to vote.
4.33/5 (3 votes)
26 Sep 2013CPOL5 min read 23.3K   207   13   2
The beginning of Rule of Ready

Introduction

I'm writing to show and teach a personal undertaking, a game project called The Rule of Ready. This is a Japanese Mahjong game written from scratch in C#. I'll be documenting my engineering decisions and the implementations, bugs and all. I hope that seeing concepts in action will help developers improve.

What is This Game?

Mahjong is a Chinese four player tabletop game that has many variants around the world. I often describe it as a cross between gin rummy and poker, but played with tiles instead of cards. The tiles are shuffled and gathered into a wall in front of the players, much like a deck of cards in the middle of a table. Each player draws an initial hand of 13 tiles, and take turns drawing a tile and discarding one from their hand, until one player has a winning hand--4 sets of three tiles and a pair, typically. Players can also--under certain conditions--claim another player's discard to complete one of the sets of tiles, or to complete the hand.

Here's a screen shot from the Japanese Mahjong game Tenhou, from Osamuko's Mahjong Blog:

For more information on Japanese Mahjong, here are some sites to visit:

  • A wiki for information about Japanese Mahjong
  • A through 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

Problem to Solve

Every project needs a place to start, so I'll start with a Mahjong tile. Japanese has a total of 136 tiles (Japanese terms are in itallics):

  • There are 4 copies of each tile, and 34 different designs of tiles
  • 27 of these are suit tiles (supai): 3 suits of 9 tiles each
    • the 3 suits are Dots (pinzu) , Bamboos (sozu) , and Characters (manzu)
    • the suits are numbered 1 to 9, and 1 and 9 are Terminals (rotohai)
  • 7 are Honor Tiles (jiihai)
    • 4 are Wind Tiles (kazehai), one of each direction: East (Ton), South (nan), West (sha), and North (Pe)
    • 3 are Dragon Tiles (sangenpai): Green Dragon (hatsu), Red Dragon (chun), White Dragon (haku)
  • The Terminals and Honors together are called Majors (yaochuhai)
  • There a four Red Five suit tiles that can be substituted for the normal suit tiles (two 5-Dot, one 5-Bamboo, and one 5-Character). These are bonus (dora) tiles that add to a winning hand's score.

A Mahjong tile is going to be a piece of data that will be used in many aspects of the game. Some uses of this class will be determining what sets of 3 tiles are in a player's hand, are any tiles of a particular type in a player's hand, what tiles can help a player get closer to winning? All of these questions have a similar theme: "do the tiles in one or more collections match a condition?"

First Implementation

I started with a single class for a mahjong tile, with an enum for the type and classification of the tile.

First, an enum for the basic tile type. I'm using the convention that 0 is an error value.

C#
public enum MahjongTileType
{
    UnknownTile = 0,
    SuitTile,
    HonorTile
}

For Suit tiles, I need a suit, a number, and whether it's a Red tile or not. For Honor Tiles, I just need what dragon or wind tile to make. All of this categorization has a better implementation in C#, as a flags enum.

C#
[Flags]
public enum MahjongTileType
{
    UnknownTile = 0,
    // suit tiles
    Bambooo = 0x1,
    Character = 0x2,
    Dot = 0x4,
    
    // honor tiles
    GreenDragon = 0x8,
    RedDragon = 0x10,
    WhiteDragon = 0x20,
    EastWind = 0x40,
    SouthWind = 0x80,
    WestWind = 0x100,
    NorthWind = 0x200,
    
    // categories
    DragonTile = GreenDragon | RedDragon | WhiteDragon,
    WindTile = EastWind | SouthWind | WestWind | NorthWind,
    HonorTile = DragonTile | WindTile,
    SuitTile = Bambooo | Character | Dot
}
C#
public class MahjongTile 
{
    public MahjongTileType TileType { get; private set; }
    public int SuitNumber { get; private set; }
    public bool IsRedTile { get; private set; }

    public MahjongTile(MahjongTileType tileType)
    {
        if (tileType != MahjongTileType.HonorTile) 
            throw new ArgumentException("This constructor overload is only for honor tiles", 
                                        "tileType");
        this.TileType = tileType;
        this.SuitNumber = 0;
        this.IsRedTile = false;
    }

    public MahjongTile(MahjongTileType tileType, int suitNumber, bool isRed)
    {
        if (tileType != MahjongTileType.SuitTile) 
            throw new ArgumentException("This constructor overload is only for suit tiles",
                                        "tileType");
        if (suitNumber < 1 || suitNumber > 9) 
            throw new ArgumentException("Suit tiles have values from 1 and 9", 
                                        "suitNumber");
        this.TileType = tileType;
        this.SuitNumber = suitNumber;
        this.IsRedTile = isRed;
    }
}

The argument checks on the constructors are a code smell: an incorrectly created tile won't be caught until runtime. How about using MahjongTile objects? Let's see how code for answering some of the questions would look like:

C#
List<mahjongtile> hand =  new List<mahjongtile>() 
{
    //...
};

// how many EastWind tiles does the hand contain?
int hasEastWind = hand.Where(mt => mt.TileType == MahjongTileType.EastWind).Count();

// are there any honor tiles in the hand?
bool hasHonorTiles = hand.Where(mt => mt.TileType == MahjongTileType.HonorTile).Any();

While this works well enough for simple questions, complicated ones will require complex Where filter functions. For example, one of the central questions to the game is "how many tiles do I need to win?" along with "what tiles do I need to win?" These questions are not trivial to answer. I could live with this, but it's concerning. Combined with the constructor code smell, I should reconsider my design. (I had some help from the CodeProject community as well.)

Second Implementation

One item of note is that a Mahjong tile doesn't change after it's been created. This means that if I define value equality for a Mahjong tile, I can encapsulate the details for equating tiles within the class without needing public properties to do so. Also, sorting the tiles in a hand seems reasonable, at least for the user interface, which means tiles should be able to be compared to one another.

I had a few challenges in getting the code to be readable, rather than merely correct. Observations are below:

C#
/// <summary>
/// Types of Suits for Mahjong tiles
/// </summary>
public enum MahjongSuitType
{
    Bamboo = 1,
    Character,
    Dot
}

/// <summary>
/// The allowed values of Suit Mahjong Tiles
/// </summary>
public enum MahjongSuitNumber
{
    One = 1,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine
}

/// <summary>
/// Types of Mahjong Honor Tiles 
/// </summary>
public enum MahjongHonorType
{
    EastWind = 1,
    SouthWind,
    WestWind,
    NorthWind,
    RedDragon,
    WhiteDragon,
    GreenDragon
}

/// <summary>
/// A Mahjong Tile. (This implements IEquatable and IComparable)
/// </summary>
public abstract class MahjongTile : IEquatable<mahjongtile>, IComparable<mahjongtile>
{

    #region Public Methods

    /// <summary>
    /// Override for equality, when checked against something that's not a MahjongTile
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public override bool Equals(object obj)
    {
        return this.EqualToImpl(obj as MahjongTile);
    }

    /// <summary>
    /// Override for determining hashcode, must return same value for equal objects
    /// </summary>
    /// <returns></returns>
    public override int GetHashCode()
    {
        return this.GetHashCodeImpl();
    }

    #region IEquatable Implementation

    /// <summary>
    /// Definition of equality for when compared to another MahjongTile object
    /// </summary>
    /// <param name="other">another MahjongTile</param>
    /// <returns></returns>
    public bool Equals(MahjongTile other)
    {
        return this.EqualToImpl(other);
    }

    #endregion

    #region IComparable Implementation

    /// <summary>
    /// Definition of ordering when compared to another MahjongTile. 
    /// This is used to implement operator overloads.
    /// </summary>
    /// <param name="other">another MahjongTile</param>
    /// <returns></returns>
    public int CompareTo(MahjongTile other)
    {
        return this.CompareToImpl(other);
    }

    #endregion


    #region Operator overloads

    /// <summary>
    /// Definition of '==' for MahjongTiles. Handles null values first.
    /// </summary>
    /// <param name="left">left operand</param>
    /// <param name="right">right operand</param>
    /// <returns></returns>
    public static bool operator ==(MahjongTile left, MahjongTile right)
    {
        bool leftIsNull = Object.ReferenceEquals(left, null);
        bool rightIsNull = Object.ReferenceEquals(right, null);

        if (leftIsNull && rightIsNull)
            return true;
        else if (leftIsNull || rightIsNull)
            return false;
        else
            return left.EqualToImpl(right);
    }

    /// <summary>
    /// Definition of '!=' for Mahjong Tiles
    /// </summary>
    /// <param name="left">left operand</param>
    /// <param name="right">right operand</param>
    /// <returns></returns>
    public static bool operator !=(MahjongTile left, MahjongTile right)
    {
        return !(left == right);
    }

    /// <summary>
    /// Definition of '<' for Mahjong Tiles
    /// </summary>
    /// <param name="left">left operand</param>
    /// <param name="right">right operand</param>
    /// <returns></returns>
    public static bool operator <(MahjongTile left, MahjongTile right)
    {
        return left.CompareTo(right) < 0;
    }

    /// <summary>
    /// Definition of '>' for Mahjong Tiles
    /// </summary>
    /// <param name="left">left operand</param>
    /// <param name="right">right operand</param>
    /// <returns></returns>
    public static bool operator >(MahjongTile left, MahjongTile right)
    {
        return left.CompareTo(right) > 0;
    }

    /// <summary>
    /// Definition of '<=' for Mahjong Tiles
    /// </summary>
    /// <param name="left">left operand</param>
    /// <param name="right">right operand</param>
    /// <returns></returns>
    public static bool operator <=(MahjongTile left, MahjongTile right)
    {
        return left.CompareTo(right) <= 0;
    }

    /// <summary>
    /// Definition of '>=' for Mahjong Tiles
    /// </summary>
    /// <param name="left">left operand</param>
    /// <param name="right">right operand</param>
    /// <returns></returns>
    public static bool operator >=(MahjongTile left, MahjongTile right)
    {
        return left.CompareTo(right) >= 0;
    }

    #endregion

    #endregion

    #region Protected Abstract Members 

    /// <summary>
    /// Abstract method for the implementation of comparing Mahjong Tiles
    /// </summary>
    /// <param name="other">another MahjongTile</param>
    /// <returns></returns>
    protected abstract int CompareToImpl(MahjongTile other);

    /// <summary>
    /// Abstract method for the implementation of Equating Mahjong Tiles
    /// </summary>
    /// <param name="other">another MahjongTile</param>
    /// <returns></returns>
    protected abstract bool EqualToImpl(MahjongTile other);

    /// <summary>
    /// Abstract method for the implementation of getting a hashcode value Mahjong Tiles
    /// </summary>
    /// <param name="other">another MahjongTile</param>
    /// <returns></returns>
    protected abstract int GetHashCodeImpl();

    #endregion

}

/// <summary>
/// A Mahjong Tile that's a suit
/// </summary>
public class MahjongSuitTile : MahjongTile
{
    #region Public Properties (read only)

    /// <summary>
    /// Type of the suit
    /// </summary>
    public MahjongSuitType SuitType { get; private set; }

    /// <summary>
    /// Number of the suit
    /// </summary>
    public MahjongSuitNumber SuitNumber { get; private set; }

    /// <summary>
    /// Is this tile a Red Bonus (akidora) Tile?
    /// </summary>
    /// <remarks>
    /// This has no effect on Equality for a mahjong tile
    /// </remarks>
    public bool IsRedBonus { get; private set; }

    #endregion

    #region Constructor

    /// <summary>
    /// Create a new Mahjong suit tile, with a give suit type and number, 
    /// and optionally if the tile is a Red Bonus
    /// </summary>
    /// <param name="suitType">suit of the tile</param>
    /// <param name="suitNumber">number of the tile</param>
    /// <param name="isRedBonus">flag of the</param>
    public MahjongSuitTile(MahjongSuitType suitType, MahjongSuitNumber suitNumber, 
                           bool isRedBonus = false)
    {
        if (!Enum.IsDefined(typeof(MahjongSuitType), suitType))
            throw new ArgumentException(
                string.Format("'{0}' is not a valid suit type", 
                suitType), "suitType");
        if (!Enum.IsDefined(typeof(MahjongSuitNumber), suitNumber))
            throw new ArgumentException(
                string.Format("'{0}' is not a valid suit number", 
                suitNumber), "suitNumber");

        this.SuitType = suitType;
        this.SuitNumber = suitNumber;
        this.IsRedBonus = isRedBonus;
    }

    /// <summary>
    /// Create a new Mahjong suit tile, with a give suit type and number, 
    /// and optionally if the tile is a Red Bonus
    /// </summary>
    /// <param name="suitType">suit of the tile</param>
    /// <param name="suitNumber">number of the tile</param>
    /// <param name="isRedBonus">flag of the</param>
    public MahjongSuitTile(MahjongSuitType suitType, int suitNumber, bool isRedBonus = false)
        : this(suitType, (MahjongSuitNumber)suitNumber, isRedBonus) { }

    #endregion

    #region Protected Override Members

    /// <summary>
    /// Override for implementation details of equating Mahjong Tiles
    /// </summary>
    /// <param name="other">another Mahjong Tile</param>
    /// <returns></returns>
    protected override bool EqualToImpl(MahjongTile other)
    {
        if (Object.ReferenceEquals(other, null))
            return false;
        if (Object.ReferenceEquals(other, this))
            return true;

        MahjongSuitTile otherSuitTile = other as MahjongSuitTile;
        if (Object.ReferenceEquals(otherSuitTile, null))
            return false;

        return (this.SuitType == otherSuitTile.SuitType) &&
               (this.SuitNumber == otherSuitTile.SuitNumber);
    }

    /// <summary>
    /// Override for implementation details of getting the hash code value for Mahjong Tiles
    /// </summary>
    /// <param name="other">another Mahjong Tile</param>
    /// <returns></returns>
    protected override int GetHashCodeImpl()
    {
        return this.SuitType.GetHashCode() ^ (this.SuitNumber.GetHashCode() << 4);
    }

    /// <summary>
    /// Override for implementation details of comparing Mahjong Tiles
    /// </summary>
    /// <param name="other">another Mahjong Tile</param>
    /// <returns></returns>
    protected override int CompareToImpl(MahjongTile other)
    {
        if (Object.ReferenceEquals(other, null))
            return 1;
        MahjongSuitTile otherAsSuit = other as MahjongSuitTile;
        if (Object.ReferenceEquals(otherAsSuit, null))
            return -1; //suits are smaller
        else
        {
            int suitCompare = this.SuitType - otherAsSuit.SuitType;
            if (suitCompare != 0)
                return suitCompare;
            else return this.SuitNumber - otherAsSuit.SuitNumber;
        }
    }

    #endregion
}

/// <summary>
/// A Mahjong tile that's an honor tile
/// </summary>
public class MahjongHonorTile : MahjongTile
{

    #region Public Properties (read only)

    public MahjongHonorType HonorType { get; private set; }

    #endregion

    #region Constructor

    public MahjongHonorTile(MahjongHonorType honorType)
    {
        if (!Enum.IsDefined(typeof(MahjongHonorType), honorType))
            throw new ArgumentException(
                string.Format("'{0}' is not a valid honor type", 
                honorType), "honorType");

        this.HonorType = honorType;
    }

    #endregion

    #region Protected Override Members

    /// <summary>
    /// Override for implementation details of equating Mahjong Tiles
    /// </summary>
    /// <param name="other">another Mahjong Tile</param>
    /// <returns></returns>
    protected override bool EqualToImpl(MahjongTile other)
    {
        if (Object.ReferenceEquals(other, null))
            return false;
        if (Object.ReferenceEquals(other, this))
            return true;

        MahjongHonorTile otherHonorTile = other as MahjongHonorTile;
        if (Object.ReferenceEquals(otherHonorTile, null))
            return false;

        return this.HonorType == otherHonorTile.HonorType;
    }

    /// <summary>
    /// Override for implementation details of getting the hash code value for Mahjong Tiles
    /// </summary>
    /// <param name="other">another Mahjong Tile</param>
    /// <returns></returns>
    protected override int GetHashCodeImpl()
    {
        return this.HonorType.GetHashCode();
    }

    /// <summary>
    /// Override for implementation details of comparing Mahjong Tiles
    /// </summary>
    /// <param name="other">another Mahjong Tile</param>
    /// <returns></returns>
    protected override int CompareToImpl(MahjongTile other)
    {
        if (Object.ReferenceEquals(other, null))
            return 1;

        MahjongHonorTile otherAsHonor = other as MahjongHonorTile;
        if (object.ReferenceEquals(otherAsHonor, null))
            return 1; // honors are bigger
        else
            return this.HonorType - otherAsHonor.HonorType;
    }

    #endregion

}

Here are my observations:

  • Working through equality for disjoint subclasses was an interesting journey. I needed both a solution that would have each subclass determine its own definition for equating, comparing, and generating a hash code and a mechanism for the base class to call the correct method for each possible situation. The protected abstract methods leverage C#'s built-in mechanism for doing this.
  • What was really surprising for me is that I didn't have to specifically implement the interfaces in the subclasses, even though they contained the implementation.
  • Since I was defining operators for MahjongTile objects, I was careful to not use any operators to compare them. It's arguably paranoid to use Object.ReferenceEquals when comparing to null, but I felt it was better to be safe.
  • As I was working out the details of defining equality, unit-testing both helped me find typos and ensure correct behaviour as a re-factored.
  • The enums determine both the range of acceptable values and the ordering of tiles. This includes the overloaded constructor for suit tiles that takes in an integer for the suit number.

Source

The source code for this project is at ruleofready.codeplex.com. I have unit tests there as well.

Next

Next time, I'll be creating the engine that will make use of the tiles.

History

  • Sep. 25, 2013: Created article
  • Sep. 28, 2013: Added in second implementation and description of Mahjong

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

 
GeneralMy vote of 5 Pin
Pete O'Hanlon28-Sep-13 23:03
mvePete O'Hanlon28-Sep-13 23:03 
QuestionA Point on Refactoring Pin
Nagy Vilmos26-Sep-13 3:34
professionalNagy Vilmos26-Sep-13 3:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.