【译】开始The Rule of Ready项目
发表于2016-03-07
版权:作者版权声明为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。我已经做过单元测试了。
下一次
下一次我将创建一个引擎能够使用这些麻将块。