【译】开始The Rule of Ready项目

发表于2016-03-07
评论0 846浏览
版权:作者版权声明为http://www.codeproject.com/info/cpol10.aspx,可以翻译
  这篇文章是记录我的一个个人游戏项目The Rule of Ready。这是一个用C#从头开始编写的日本麻将游戏。我将记录我的工程决策和实现方案以及遇到的bug等各种问题。我希望这篇文章会对开发人员有帮助。

这是什么游戏?
  麻将是中国的一种4人桌面游戏,在全世界有很多变体。我经常把它描述程介于扑克和gin rummy的中间体,但是用麻将块代替指派。麻将块被打混然后在每个玩家面前擂成一排就像一叠扑克。每个玩家最初有13张牌,然后轮流摸一张牌并打出一张牌,直到一个玩家赢得比赛-4个三张以及一堆。在某些情况下,玩家还能要别人丢弃的牌然后丢掉手里的一张牌,这是一个日本麻将游戏Tenhou的截图,来自Osamuko's Mahjong Blog


要了解更多日本麻将的信息,请看面网站
· A wiki 关于日本麻将的信息
· A through PDF 详细的关于规则方面的信息
· ReachMahjong.com 职业选手的社区网站,上面还有一些翻译的由日本职业选手写的文章

要解决的问题
每个项目都需要一个奇点,我会从麻将块开始。日本麻将一共有136张牌(术语叫条)
· 每张牌有4个一样的,并且有34种不同的。
· 27牌是套牌:3套9张。
· 3套分别是点(pinzu),竹子(sozu)和人物(manzu)
· 编号是1到9,1和9是两端的终点
· 7张荣誉牌
· 4张风牌,分别是东西南北
· 3张是龙牌:绿龙(hatsu),红龙(chun),白龙(haku)
· 套牌和荣誉牌一起被成为专业牌(yaochuhai)

第一次实现
我是从一个单独的类来表示麻将块,有一个枚举来定义类型和分类。
首先,有一个枚举来定义基本的块。我用0来标记错误
public enum MahjongTileType
{
    UnknownTile= 0,
    SuitTile,
    HonorTile
}
对于套牌,我需要一个suit变量,一个数字,还有它是否是红色。对于荣誉块,我只是需要标记龙牌或者风牌。
[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 MahjongTileTypeTileType { 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("Thisconstructor 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("Thisconstructor overload is only for suit tiles",
                                        "tileType");
        if (suitNumber < 1 || suitNumber > 9)
            throw new ArgumentException("Suit tileshave values from 1 and 9",
                                        "suitNumber");
        this.TileType = tileType;
        this.SuitNumber =suitNumber;
        this.IsRedTile = isRed;
    }
}
List hand =  new List()
{
    //...
};
// 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();
  虽然这在简单的情况下工作的非常棒,但是复杂的问题需要一些复杂的过滤函数,比如游戏的核心问题是“我还需要多少牌才能赢”以及我需要那张牌。这些问题并不好回答。我可以忍受,但是代码已经有一些坏味道的,我应该重构自己的设计。

第二个实现
  需要注意的是麻将块创建出来以后它的值不会再改变,这意味这如果我顶一个判断是否相等操作符,我可以把这个操作封装起来而无需暴露出来。此外,手牌需要比较排序,这意味麻将块需要比较操作
  代码的挑战是不仅仅是正确的,还要是方便阅读的,观察下面的代码:
///
/// Types of Suits for Mahjong tiles
///
public enum MahjongSuitType
{
    Bamboo = 1,
    Character,
    Dot
///
/// The allowed values of Suit Mahjong Tiles
///
public enum MahjongSuitNumber
{
    One = 1,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine
///
/// Types of Mahjong Honor Tiles
///
public enum MahjongHonorType
{
    EastWind = 1,
    SouthWind,
    WestWind,
    NorthWind,
    RedDragon,
   WhiteDragon,
    GreenDragon
///
/// A Mahjong Tile. (This implements IEquatable andIComparable)
///
public abstract class MahjongTile : IEquatable, IComparable
    #region Public Methods 
    ///
    /// Override forequality, when checked against something that's not a MahjongTile
    ///
    ///
    ///
    public override bool Equals(object obj)
    {
        return this.EqualToImpl(obj as MahjongTile);
    } 
    ///
    /// Override fordetermining hashcode, must return same value for equal objects
    ///
    ///
    public override int GetHashCode()
    {
        return this.GetHashCodeImpl();
    } 
    #region IEquatableImplementation 
    ///
    /// Definition ofequality for when compared to another MahjongTile object
    ///
    /// another MahjongTile
    ///
    public bool Equals(MahjongTileother)
    {
        return this.EqualToImpl(other);
    }
    #endregion
    #region IComparableImplementation 
    ///
    /// Definition ofordering when compared to another MahjongTile.
    /// This is used toimplement operator overloads.
    ///
    /// another MahjongTile
    ///
    public int CompareTo(MahjongTile other)
    {
        return this.CompareToImpl(other);
    } 
    #endregion  
    #region Operatoroverloads
    ///
    /// Definition of '=='for MahjongTiles. Handles null values first.
    ///
    /// left operand
    /// right operand
    ///
    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);
    } 
    ///
    /// Definition of '!='for Mahjong Tiles
    ///
    /// left operand
    /// right operand
    ///
    public static bool operator !=(MahjongTile left,MahjongTile right)
    {
        return !(left == right);
    } 
    ///
    /// Definition of '<' for Mahjong Tiles
    ///
    /// left operand
    /// right operand
    ///
    public static bool operator <(MahjongTileleft, MahjongTile right)
    {
        return left.CompareTo(right) < 0;
    } 
    ///
    /// Definition of '>'for Mahjong Tiles
    ///
    /// left operand
    /// right operand
    ///
    public static bool operator >(MahjongTileleft, MahjongTile right)
    {
        return left.CompareTo(right) > 0;
    } 
    ///
    /// Definition of '<=' for Mahjong Tiles
    ///
    /// left operand
    /// right operand
    ///
    public static bool operator <=(MahjongTileleft, MahjongTile right)
    {
        return left.CompareTo(right) <= 0;
    } 
    ///
    /// Definition of'>=' for Mahjong Tiles
    ///
    /// left operand
    /// right operand
    ///
    public static bool operator >=(MahjongTileleft, MahjongTile right)
    {
        return left.CompareTo(right) >= 0;
    } 
    #endregion 
    #endregion
    #region ProtectedAbstract Members 
    ///
    /// Abstract method forthe implementation of comparing Mahjong Tiles
    ///
    /// another MahjongTile
    ///
    protected abstract int CompareToImpl(MahjongTile other); 
    ///
    /// Abstract method forthe implementation of Equating Mahjong Tiles
    ///
    /// another MahjongTile
    ///
    protected abstract bool EqualToImpl(MahjongTile other); 
    ///
    /// Abstract method forthe implementation of getting a hashcode value Mahjong Tiles
    ///
    /// another MahjongTile
    ///
    protected abstract int GetHashCodeImpl(); 
    #endregion 
///
/// A Mahjong Tile that's a suit
///
public class MahjongSuitTile : MahjongTile
{
    #region PublicProperties (read only) 
    ///
    /// Type of the suit
    ///
    public MahjongSuitTypeSuitType { get; private set; } 
    ///
    /// Number of the suit
    ///
    public MahjongSuitNumberSuitNumber { get; private set; } 
    ///
    /// Is this tile a RedBonus (akidora) Tile?
    ///
    ///
    /// This has no effecton Equality for a mahjong tile
    ///
    public bool IsRedBonus { get; private set; } 
    #endregion 
    #region Constructor 
    ///
    /// Create a new Mahjongsuit tile, with a give suit type and number,
    /// and optionally ifthe tile is a Red Bonus
    ///
    /// suit of the tile
    /// number of the tile
    /// flag of the
    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;
    } 
    ///
    /// Create a new Mahjongsuit tile, with a give suit type and number,
    /// and optionally ifthe tile is a Red Bonus
    ///
    /// suit of the tile
    /// number of the tile
    /// flag of the
    public MahjongSuitTile(MahjongSuitType suitType, int suitNumber, bool isRedBonus = false)
        : this(suitType,(MahjongSuitNumber)suitNumber, isRedBonus) { } 
    #endregion 
    #region ProtectedOverride Members
     ///
    /// Override forimplementation details of equating Mahjong Tiles
    ///
    /// another Mahjong Tile
    ///
    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);
    } 
    ///
    /// Override forimplementation details of getting the hash code value for Mahjong Tiles
    ///
    /// another Mahjong Tile
    ///
    protected override int GetHashCodeImpl()
    {
        return this.SuitType.GetHashCode()^ (this.SuitNumber.GetHashCode()<< 4);
    }
    ///
    /// Override forimplementation details of comparing Mahjong Tiles
    ///
    /// another Mahjong Tile
    ///
    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
///
/// A Mahjong tile that's an honor tile
///
public class MahjongHonorTile : MahjongTile
{
    #region PublicProperties (read only) 
    public MahjongHonorTypeHonorType { 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 ProtectedOverride Members 
    ///
    /// Override forimplementation details of equating Mahjong Tiles
    ///
    /// another Mahjong Tile
    ///
    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;
    } 
    ///
    /// Override forimplementation details of getting the hash code value for Mahjong Tiles
    ///
    /// another Mahjong Tile
    ///
    protected override int GetHashCodeImpl()
    {
        return this.HonorType.GetHashCode();
    }
     ///
    /// Override forimplementation details of comparing Mahjong Tiles
    ///
    /// another Mahjong Tile
    ///
    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 
}

我观察到的
· 通过判断是否相等来连接子类是非常有意思的过程,我需要一个解决方案,每个子类有它自己定义的相等、比较和生成hash值。
· 真正让人惊讶的是在子类中没有特殊实现,即使它们包含了实现
· 在我定义是否相等的过程中,单元测试帮我找到问题并重构
· 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 aninteger for the suit number.
· 枚举定义可以接受值的范围以及麻将块的顺序。这包括一个重载的构造函数来让麻将块有一个值。

源代码
源代码在ruleofready.codeplex.com。我已经做过单元测试了。

下一次
下一次我将创建一个引擎能够使用这些麻将块。


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引