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

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

2013年10月20日

CPOL

3分钟阅读

viewsIcon

9949

“准备就绪”项目继续开发一些实用功能。

引言

这是“准备就绪原则”项目的第二篇文章,这是一个用 C# 编写的开源日本麻将游戏项目。 我之前讨论了代表游戏中麻将牌的类,即 MahjongTile 基类以及两种类型麻将牌的子类。

有关日本麻将的更多信息,请访问以下网站

组合工具

首先,我创建了一个 TileEngine 类,用于一些核心功能:创建游戏所需的麻将牌,并维护列表以供参考。 我打算让 TileEngine 成为存储使用 MahjongTile 对象常见功能的地方。 软件项目的挑战之一(至少对我而言)是保持井井有条。 我最喜欢的启发式方法是“知道在哪里可以找到东西”,然后将代码放在我将来希望找到它的地方。 如果在一周或一个月后我很难找到某些代码,那么我将重新评估代码的放置位置。

诚然,Visual Studio 具有多种工具可以帮助查找代码,但我认为这些工具在良好的组织下效果更好。

MahjongSequenceMahjongPartialSequence 类是构成获胜牌局的部分麻将牌的集合。 我将在下一篇文章中深入探讨它们。 我还在这个类中找到了一些领域知识:特别是用于日本麻将游戏的特定麻将牌。

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 提供了一些有用的答案,我觉得很有用。

这里有趣的说明是锁定种子的来源,以便 ThreadSafeRandom 的任何两个实例都不会使用静态 Random 的相同结果。 我坦率地承认我复制了 Randompublic 方法 - 包括文档。 (我相当感觉到 .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日:初始版本
© . All rights reserved.