工具制造 -- “准备就绪”规则





4.00/5 (1投票)
“准备就绪”项目继续开发一些实用功能。
引言
这是“准备就绪原则”项目的第二篇文章,这是一个用 C# 编写的开源日本麻将游戏项目。 我之前讨论了代表游戏中麻将牌的类,即 MahjongTile
基类以及两种类型麻将牌的子类。
有关日本麻将的更多信息,请访问以下网站
- 一个维基,提供有关日本麻将的信息
- 一份详尽的 PDF,详细介绍了游戏规则和日语术语,由一位仅被称为 Barticle 的玩家提供
- ReachMahjong.com,一个面向游戏专业玩家的社区网站,其中包含来自日本专业玩家的翻译文章
- 一个日本麻将 Flash 游戏
组合工具
首先,我创建了一个 TileEngine
类,用于一些核心功能:创建游戏所需的麻将牌,并维护列表以供参考。 我打算让 TileEngine
成为存储使用 MahjongTile
对象常见功能的地方。 软件项目的挑战之一(至少对我而言)是保持井井有条。 我最喜欢的启发式方法是“知道在哪里可以找到东西”,然后将代码放在我将来希望找到它的地方。 如果在一周或一个月后我很难找到某些代码,那么我将重新评估代码的放置位置。
诚然,Visual Studio 具有多种工具可以帮助查找代码,但我认为这些工具在良好的组织下效果更好。
MahjongSequence
和 MahjongPartialSequence
类是构成获胜牌局的部分麻将牌的集合。 我将在下一篇文章中深入探讨它们。 我还在这个类中找到了一些领域知识:特别是用于日本麻将游戏的特定麻将牌。
TileEngine
public class TileEngine
{
#region Private Fields
/// <summary>
/// Constant: number of identical tiles of each tile type in the game
/// </summary>
private const int TilesPerTypeInGame = 4;
/// <summary>
/// When red bonus tiles are used (Japanese: akidora),
/// this is the suit number used for them
/// </summary>
private const MahjongSuitNumber redBonusNumber = MahjongSuitNumber.Five;
/// <summary>
/// When the red bonus tiles are used, these are the number of tiles per suit
///(there are normally 4 tiles)
/// </summary>
private readonly IReadOnlyDictionary<MahjongSuitType, int> numRedTilesPerSuit =
new Dictionary<MahjongSuitType, int>()
{
{MahjongSuitType.Bamboo, 1},
{MahjongSuitType.Character, 1},
{MahjongSuitType.Dot, 2},
};
#endregion
#region Public (read-only) Properties
/// <summary>
/// A list of Honor types, in order
/// </summary>
public IEnumerable<MahjongHonorType> HonorTileTypes { get; private set; }
/// <summary>
/// A list of Suit types, in order
/// </summary>
public IEnumerable<MahjongSuitType> SuitTileTypes { get; private set; }
/// <summary>
/// A list of suit numbers, in order
/// </summary>
public IEnumerable<MahjongSuitNumber> SuitTileNumbers { get; private set; }
/// <summary>
/// A list of all Terminals (suit tiles that are 1 or 9)
/// (Japanese: Rōtōhai)
/// </summary>
public IEnumerable<MahjongTile> TerminalTiles { get; private set; }
/// <summary>
/// A list of all Major tiles, (Terminals + Honors)
/// (Japanese: Yaochūhai)
/// </summary>
public IEnumerable<MahjongTile> MajorTiles { get; private set; }
/// <summary>
/// A list of all Simple tiles (suit tiles from 2 thru 8)
/// (also known as Minor or Middle Tiles)
/// (Japanese: Chunchanpai or Tanyaohai)
/// </summary>
public IEnumerable<MahjongTile> SimpleTiles { get; private set; }
/// <summary>
/// A Dictionary, keyed by Suit Type of all possible sequences
/// </summary>
public IReadOnlyDictionary<MahjongSuitType, IEnumerable<MahjongSequence>>
SequencesBySuit { get; private set; }
/// <summary>
/// A list of all sequences: 3 consecutive tiles of the same suit,
/// (e.g. 3-dot 4-dot 5-dot)
/// </summary>
public IEnumerable<MahjongSequence> Sequences
{
get { return SequencesBySuit.Values.SelectMany(seq => seq); }
}
/// <summary>
/// A list of all partial sequences: 2 tiles out of a sequence
/// </summary>
public IEnumerable<MahjongPartialSequence> PartialSequences
{
get { return Sequences.SelectMany(seq => seq.PartialSequences); }
}
#endregion
#region Constructor
/// <summary>
/// Create a new Tile Engine. (This is a somewhat heavy object)
/// </summary>
public TileEngine()
{
this.HonorTileTypes = Enum.GetValues
(typeof(MahjongHonorType)).Cast<MahjongHonorType>();
this.SuitTileTypes = Enum.GetValues
(typeof(MahjongSuitType)).Cast<MahjongSuitType>();
this.SuitTileNumbers = Enum.GetValues
(typeof(MahjongSuitNumber)).Cast<MahjongSuitNumber>();
this.TerminalTiles = this.GenerateTerminalTiles();
this.SimpleTiles = this.GenerateSimpleTiles();
this.MajorTiles = this.HonorTileTypes.Select
(honorType => new MahjongHonorTile(honorType))
.Concat(this.TerminalTiles);
this.SequencesBySuit = new Dictionary<MahjongSuitType, IEnumerable<MahjongSequence>>(3)
{
{MahjongSuitType.Bamboo,
this.GenerateSequencesForSuit(MahjongSuitType.Bamboo)},
{MahjongSuitType.Character,
this.GenerateSequencesForSuit(MahjongSuitType.Character)},
{MahjongSuitType.Dot,
this.GenerateSequencesForSuit(MahjongSuitType.Dot)}
};
}
#endregion
#region Public Methods
/// <summary>
/// Create a new tile set of 136 tiles, 4 of each type
/// </summary>
/// <returns></returns>
public IList<MahjongTile> CreateGameTileSet()
{
return CreateGameTileSet(useRedBonusTiles:false);
}
/// <summary>
/// Create a new tile set of 136 tiles, 4 of each type,
/// optionally swapping in the red tiles
/// </summary>
/// <param name="useRedBonusTiles">Swap in red tiles?</param>
/// <returns></returns>
public IList<MahjongTile> CreateGameTileSet(bool useRedBonusTiles)
{
var tileSet = new List<MahjongTile>();
foreach (MahjongSuitType suitType in this.SuitTileTypes)
{
foreach (MahjongSuitNumber suitNumber in this.SuitTileNumbers)
if (!useRedBonusTiles || !(suitNumber == TileEngine.redBonusNumber))
tileSet.AddRange(CreateTilesForSet(suitType, suitNumber));
else
tileSet.AddRange(CreateRedTilesForSet(suitType, suitNumber));
}
foreach (MahjongHonorType honorType in this.HonorTileTypes)
{
tileSet.AddRange(CreateTilesForSet(honorType));
}
return tileSet;
}
#endregion
#region Private Methods
/// <summary>
/// Create tiles for the given suit and number for the game
/// </summary>
/// <param name="suitType">suit to create</param>
/// <param name="suitNumber">number to create</param>
/// <returns></returns>
private IEnumerable<MahjongTile> CreateTilesForSet(MahjongSuitType suitType,
MahjongSuitNumber suitNumber)
{
return Enumerable.Repeat
(new MahjongSuitTile(suitType, suitNumber), TileEngine.TilesPerTypeInGame);
}
/// <summary>
/// Create tile for the given honor for the game
/// </summary>
/// <param name="honorType">honor to create</param>
/// <returns></returns>
private IEnumerable<MahjongTile> CreateTilesForSet(MahjongHonorType honorType)
{
return Enumerable.Repeat(new MahjongHonorTile(honorType),
TileEngine.TilesPerTypeInGame);
}
/// <summary>
/// Create tiles for the given suit and number for the game,
/// with Red Bonus tiles swapped in as
/// appropriate
/// </summary>
/// <param name="suitType">suit to create</param>
/// <param name="suitNumber">number to create</param>
/// <returns></returns>
private IEnumerable<MahjongTile> CreateRedTilesForSet(MahjongSuitType suitType,
MahjongSuitNumber suitNumber)
{
if (suitNumber != TileEngine.redBonusNumber)
return this.CreateTilesForSet(suitType,suitNumber);
int numRedTiles = this.numRedTilesPerSuit[suitType];
int numNormalTiles = TileEngine.TilesPerTypeInGame - numRedTiles;
return Enumerable.Repeat(new MahjongSuitTile(suitType, suitNumber, isRedBonus: true),
numRedTiles)
.Concat(Enumerable.Repeat(new MahjongSuitTile(suitType, suitNumber),
numNormalTiles));
}
/// <summary>
/// Generates a list of all sequences for a suit
/// </summary>
/// <param name="suitType">suit to make the sequences</param>
/// <returns></returns>
private IEnumerable<MahjongSequence> GenerateSequencesForSuit(MahjongSuitType suitType)
{
IList<MahjongSuitTile> tiles =
this.SuitTileNumbers
.Select(number => new MahjongSuitTile(suitType, number))
.ToList();
for(int startingIdx = 0; tiles.Count - startingIdx >= 3; startingIdx += 1)
{
yield return new MahjongSequence(tiles.Skip(startingIdx).Take(3));
}
}
/// <summary>
/// Generate all of the Terminal tiles (1 and 9 of each suit)
/// </summary>
/// <returns>list of Terminals</returns>
private IEnumerable<MahjongTile> GenerateTerminalTiles()
{
int lowTerminal = 1;
int highTerminal = this.SuitTileNumbers.Count();
foreach (MahjongSuitType suitType in this.SuitTileTypes)
{
yield return new MahjongSuitTile(suitType, lowTerminal);
yield return new MahjongSuitTile(suitType, highTerminal);
}
}
/// <summary>
/// Generate all of the Simple Tiles (2 thru 8 of each suit)
/// </summary>
/// <returns>list of Simples</returns>
private IEnumerable<MahjongTile> GenerateSimpleTiles()
{
int lowestSimple = 2;
int highestSimple = this.SuitTileNumbers.Count() - 1;
foreach (MahjongSuitType suitType in this.SuitTileTypes)
{
for (int suitNumber = lowestSimple; suitNumber <= highestSimple; suitNumber++)
{
yield return new MahjongSuitTile(suitType, suitNumber);
}
}
}
#endregion
}
其次,我创建了一些集合类本身所需的功能,这些功能不是 .NET 的一部分:对对象列表进行洗牌,以及一些用于 LinkedList
的辅助方法。 我正在考虑使用 LinkedList
作为游戏的 Wall,从前面抽取麻将牌,从后面抽取 Dead Wall。 我可以使用队列,但我更喜欢使用或构建与任务有直观连接的数据类型——在这种情况下,LinkedList
最适合该任务。
StackOverflow 提供了一些有用的答案,我觉得很有用。
- C# 随机数生成器是线程安全的吗?:一个优雅的答案,用于创建本身随机播种的伪随机数生成器
- 在 C# 中随机化列表<t>:一个用于洗牌列表的简单线性算法
这里有趣的说明是锁定种子的来源,以便 ThreadSafeRandom
的任何两个实例都不会使用静态 Random
的相同结果。 我坦率地承认我复制了 Random
的 public
方法 - 包括文档。 (我相当感觉到 .NET 中缺少 IRandom
接口。)
ThreadSafeRandom
/// <summary>
/// A Random number generator that's seeded randomly.
/// </summary>
public class ThreadSafeRandom
{
#region Private Fields
/// <summary>
/// Source of seeds
/// </summary>
private static readonly Random globalRandom = new Random();
/// <summary>
/// A field for storing the internal (local) random number generator
/// </summary>
[ThreadStatic]
private static Random localRandom;
#endregion
#region Constructor
/// <summary>
/// Create a new pseudo-random number generator that is seeded randomly
/// </summary>
public ThreadSafeRandom()
{
if (ThreadSafeRandom.localRandom == null)
{
int seed;
lock (ThreadSafeRandom.globalRandom)
{
seed = ThreadSafeRandom.globalRandom.Next();
}
localRandom = new Random(seed);
}
}
#endregion
#region Public Methods
/// <summary>
/// Returns a nonnegative number
/// </summary>
/// <returns>A 32-bit signed integer greater than
/// or equal to zero and less than int.MaxValue.
/// </returns>
public int Next()
{
return ThreadSafeRandom.localRandom.Next();
}
/// <summary>
/// Returns a nonnegative random number less than the specified maximum.
/// </summary>
/// <param name="maxValue">
/// The exclusive upper bound of the random number to be generated.
/// maxValue must be greater than or equal to zero.
/// </param>
/// <returns>
/// A 32-bit signed integer greater than or equal to zero, and less than maxValue;
/// that is, the range of return values ordinarily includes zero but not maxValue.
/// However, if maxValue equals zero, maxValue is returned.
/// </returns>
public int Next(int maxValue)
{
return ThreadSafeRandom.localRandom.Next(maxValue);
}
/// <summary>
/// Returns a random number within a specified range.
/// </summary>
/// <param name="minValue">The inclusive lower bound of the random number returned.
/// </param>
/// <param name="maxValue">
/// The exclusive upper bound of the random number returned.
/// maxValue must be greater than or equal to minValue.
/// </param>
/// <returns>
/// 32-bit signed integer greater than or equal to minValue and less than maxValue;
/// that is, the range of return values includes minValue but not maxValue.
/// If minValue equals maxValue, minValue is returned.
/// </returns>
public int Next(int minValue, int maxValue)
{
return ThreadSafeRandom.localRandom.Next(minValue, maxValue);
}
/// <summary>
/// Fills the elements of a specified array of bytes with random numbers.
/// </summary>
/// <param name="buffer">An array of bytes to contain random numbers. </param>
public void NextBytes(byte[] buffer)
{
ThreadSafeRandom.localRandom.NextBytes(buffer);
}
/// <summary>
/// Returns a random number between 0.0 and 1.0.
/// </summary>
/// <returns>
/// A double-precision floating point number greater than or equal to 0.0,
/// and less than 1.0.
/// </returns>
public double NextDouble()
{
return ThreadSafeRandom.localRandom.NextDouble();
}
#endregion
}
这是我的集合扩展方法。 对于 Shuffle
,我需要集合实现 IList<T>
才能使用索引器。 如果没有,此算法会慢得多。 对于 LinkedList
方法,我使用 LINQ 方法 <a href="http://msdn.microsoft.com/en-us/library/bb337697.aspx">Any()</a>
来确定是否存在任何元素。
我个人认为 !list.Any()
比 list.FirstOrDefault() == null
或(假设 list 实现 ICollection<T>
) list.Count == 0
具有更清晰的意图。
IList<T>.Shuffle()
public static class CollectionExtensions
{
private static readonly ThreadSafeRandom random = new ThreadSafeRandom();
/// <summary>
/// In place shuffle of a list (based on Fisher-Yates shuffle)
/// </summary>
/// <typeparam name="T">type of list to shuffle</typeparam>
/// <param name="list">list to shuffle</param>
public static void Shuffle<T>(this IList<T> list)
{
int shuffleToIdx = list.Count;
while (shuffleToIdx > 1)
{
shuffleToIdx -= 1;
int shuffleFromIdx = random.Next(shuffleToIdx + 1);
T value = list[shuffleFromIdx];
list[shuffleFromIdx] = list[shuffleToIdx];
list[shuffleToIdx] = value;
}
}
/// <summary>
/// Removes and returns the first element of the list
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list"></param>
/// <returns></returns>
public static T PopFirst<T>(this LinkedList<T> list)
{
if (list == null || !list.Any())
return default(T);
T element = list.First.Value;
list.RemoveFirst();
return element;
}
/// <summary>
/// Removes and returns the first element of the list
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list"></param>
/// <returns></returns>
public static T PopLast<T>(this LinkedList<T> list)
{
if (list == null || !list.Any())
return default(T);
T element = list.Last.Value;
list.RemoveLast();
return element;
}
}
下一步?
下次,我将介绍如何在游戏中表示麻将牌组。 您可以在 ruleofready.codeplex.com 找到完整的源代码。
历史
- 2013年10月20日:初始版本