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






4.33/5 (3投票s)
“准备就绪”规则的开始。
引言
我写这篇文章是为了展示和教授我个人的一项事业,一个名为“规矩”的游戏项目。这是一个用C#从零开始编写的日式麻将游戏。我将记录我的工程决策和实现过程,包括所有的bug。我希望通过看到概念的实际应用能够帮助开发者们进步。
这是什么游戏?
麻将是一种中国的四人桌面游戏,在世界各地有许多变体。我经常将其描述为一种介于 Gin Rummy 和扑克之间的游戏,但使用的是牌而不是牌。牌会被洗好并堆成一堵墙放在玩家面前,很像桌子中间一副牌。每个玩家摸取初始手牌 13 张,然后轮流摸一张牌并打出一张牌,直到一名玩家凑成一对胡牌——通常是 4 组三张牌和一对。玩家也可以——在某些条件下——声明拿走另一位玩家打出的牌来组成其中一组牌,或者凑成胡牌。
这是日式麻将游戏《雀魂》(Tenhou)的截图,来自Osamuko's Mahjong Blog
有关日式麻将的更多信息,以下是一些可以访问的网站
- 一个关于日式麻将信息的维基
- 一个由一位名叫 Barticle 的玩家提供的详尽 PDF 文件,详细介绍了游戏规则和日式麻将术语,内容非常详尽。
- ReachMahjong.com,一个面向职业麻将玩家的社区网站,包含日籍职业玩家的文章翻译
- 一个日式麻将 Flash 游戏
要解决的问题
每个项目都需要一个起点,所以我将从麻将牌开始。日式麻将总共有 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 日:添加了第二次实现和麻将描述