65.9K
CodeProject 正在变化。 阅读更多。
Home

开始“准备就绪”项目规则

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2013年9月26日

CPOL

5分钟阅读

viewsIcon

24174

downloadIcon

242

“准备就绪”规则的开始。

引言

我写这篇文章是为了展示和教授我个人的一项事业,一个名为“规矩”的游戏项目。这是一个用C#从零开始编写的日式麻将游戏。我将记录我的工程决策和实现过程,包括所有的bug。我希望通过看到概念的实际应用能够帮助开发者们进步。

这是什么游戏?

麻将是一种中国的四人桌面游戏,在世界各地有许多变体。我经常将其描述为一种介于 Gin Rummy 和扑克之间的游戏,但使用的是牌而不是牌。牌会被洗好并堆成一堵墙放在玩家面前,很像桌子中间一副牌。每个玩家摸取初始手牌 13 张,然后轮流摸一张牌并打出一张牌,直到一名玩家凑成一对胡牌——通常是 4 组三张牌和一对。玩家也可以——在某些条件下——声明拿走另一位玩家打出的牌来组成其中一组牌,或者凑成胡牌。

这是日式麻将游戏《雀魂》(Tenhou)的截图,来自Osamuko's Mahjong Blog

有关日式麻将的更多信息,以下是一些可以访问的网站

要解决的问题

每个项目都需要一个起点,所以我将从麻将牌开始。日式麻将总共有 136 张牌(日式术语用斜体表示

  • 每张牌有 4 张,共有 34 种不同的牌面设计
  • 其中 27 张是花牌(supai):3 种花色,每种 9 张
    • 这 3 种花色分别是 筒子(pinzu)、索子(sozu)和 万子(manzu
    • 这些花色从 1 到 9 编号,1 和 9 是老头牌(rotohai
  • 7 张是字牌(jiihai
    • 4 张是风牌(kazehai),分别是:东(Ton)、南(nan)、西(sha)和北(Pe
    • 3 张是三元牌(sangenpai):发财(hatsu)、中(chun)和白板(haku
  • 老头牌和字牌合称为么九牌(yaochuhai
  • 有四张红五牌(两张红五筒,一张红五索,一张红五万),可以用来替代普通的花牌。这些是“宝牌”(dora),可以增加胡牌的分数。

麻将牌将是游戏中许多方面都会用到的数据。这个类的一些用途将是确定玩家手中有什么样的三张牌组合,玩家手中是否有某种特定的牌,哪些牌可以帮助玩家更接近胡牌?所有这些问题都有一个相似的主题:“一个或多个集合中的牌是否符合某个条件?”

第一次实现

我从一个单独的麻将牌类开始,使用一个 `enum` 来表示牌的类型和分类。

首先,一个用于基本牌类型的 `enum`。我遵循 0 作为错误值的约定。

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

对于花牌,我需要花色、数字,以及它是否为红牌。对于字牌,我只需要知道它是什么风牌或三元牌。所有这些分类在 C# 中用一个 flags `enum` 来实现会更好。

[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
}
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;
    }
}

构造函数中的参数检查是一种代码坏味道:一个错误创建的牌只有在运行时才能被发现。使用 `MahjongTile` 对象怎么样?让我们看看回答一些问题时的代码会是什么样子。

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();

虽然这对于简单的问题来说效果足够好,但复杂的问题将需要复杂的 `Where` 过滤函数。例如,游戏中的一个核心问题是“我需要多少张牌才能胡牌?”以及“我需要什么牌才能胡牌?”这些问题并不容易回答。我可以接受这个,但它还是让我有些担忧。结合构造函数中的代码坏味道,我应该重新考虑我的设计。(我还得到了一些 CodeProject 社区的帮助。)

第二次实现

值得注意的是,一张麻将牌一旦创建就不会改变。这意味着,如果我为麻将牌 定义值相等性,我就可以将相等牌的细节封装在类中,而无需使用 `public` 属性来实现。此外,对一副牌进行排序似乎是合理的,至少对于用户界面来说是这样,这意味着牌之间应该是可以比较的。

我在让代码可读而不是仅仅正确方面遇到了一些挑战。以下是我的观察

/// <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

}

以下是我的观察

  • 处理不相交子类的相等性是一个有趣的旅程。我需要一种解决方案,让每个子类确定自己定义相等、比较和生成哈希码的方法,以及一种机制让基类能够调用正确的方法来处理各种情况。受保护的 `abstract` 方法利用了 C# 内置的机制来实现这一点。
  • 令我惊讶的是,即使子类包含实现,我也无需在子类中显式实现接口。
  • 由于我定义了 `MahjongTile` 对象的运算符,我小心翼翼地没有使用任何运算符来比较它们。在与 `null` 比较时使用 `Object.ReferenceEquals` 可能是多余的,但我认为这样更安全。
  • 在我进行相等性定义的细节工作时,单元测试既帮助我发现了拼写错误,也确保了重构后行为的正确性。
  • 枚举决定了可接受值的范围和牌的顺序。这包括用于花牌的重载构造函数,该构造函数接受一个整数作为花色数字。

来源

该项目的源代码位于 ruleofready.codeplex.com。我也有单元测试在那里。

下一篇

下次,我将创建用于使用这些牌的引擎。

历史

  • 2013 年 9 月 25 日:创建文章
  • 2013 年 9 月 28 日:添加了第二次实现和麻将描述
© . All rights reserved.