シンプルなスロットマシーンのアルゴリズムを考える

スロットマシーンのアルゴリズムってどうなってるんだろうと思ってちょっと考えてみました。
何気にプログラム入門用的な題材として使いやすいネタなんじゃないかと思ったりしました。
## 絵柄
スロットマシーンには絵柄があるのでまずそれを表現します。
シンプルなので(?)enumにしました。
必要な機能が出てきたらクラスにすればいいんじゃなかろうかと。

```csharp
public enum Symbol
{
    Default = 0,
    Cherry = 1,
    Orange = 2,
    Watermelon = 3,
    Bell = 4,
    BAR = 5,
    Seven = 6,
}
```
柄は適当にそれっぽいのを。
## リール
絵柄が並んだ１列がリールです。
Symbolの配列を持っていて識別番号を持ちます。

```csharp
public class Reel
{
    public const int SYMBOL_COUNT = 10;

    public Reel(int no, IEnumerable<Symbol> symbols)
    {
        No = no;
        Symbols = symbols.ToArray();
    }

    public int No { get; }
    /// <summary>
    /// 絵柄の並び
    /// </summary>
    public Symbol[] Symbols { get; }

    public static Reel Create(int no)
    {
        // ランダムにリールを生成する
        // 実際は役の成立を左右する部分なので調整した出目が設定されるのだろう
        var symbols = Enumerable.Repeat(1, SYMBOL_COUNT)
            .AsParallel()
            .Select(_ => (Symbol)RandomProvider.GetThreadRandom().Next(1, 7));

        return new Reel(no, symbols);
    }
}

public static class SymbolsExtensions
{
    public static Symbol[] GetByPosition(this IEnumerable<Symbol> source, int position)
    {
        return source.Skip(position)
            .Concat(source.Take(position))
            .Take(SlotMachine.ROW_COUNT) // 定数 これは後で出てくる
            .ToArray();
    }
}
```
RandomProviderってのはここからお借りしました。
http://neue.cc/2013/03/06_399.html

## 役
スロットマシーンは揃った絵柄のパターンによって配当が変化しますので
それを表現します。
どうやって役の成立を判定するか、というところが実装を考えるにあたって面白いところな気がします。
今回は絵柄毎に一意なキャラクターを決めて絵柄の並びを文字列に変換することで
出た目のパターン文字列に対し役のパターン文字列をstringのContainsメソッドで判定すれば役判定に使えるんじゃないかと思ってやってみました。
アルファベットに当てはめると絵柄が25種類までという制約がありますが数字を足せば35までいけますし実際そんなに絵柄の種類があるスロットって無いんじゃないかなと思うので案外有りかな…と思ったりしてます。

```csharp
public class Hand
{
    public Hand(int id, string name, IEnumerable<Symbol> pattern, int rate)
    {
        Id = id;
        Name = name;
        PatternToMake = pattern.ToArray();
        Rate = rate;
    }
    public int Id { get; }
    public string Name { get; }
    public Symbol[] PatternToMake { get; }
    public int Rate { get; }

    public bool IsMaking(Symbol[] pattern)
    {
        return pattern.ToIdentifier().Contains(PatternToMake.ToIdentifier());
    }

    public static Hand[] CreateList()
    {
        // 適当に役のリストを用意する
        return new[]
        {
            new Hand(1, "Cherry", new[] { Symbol.Cherry, Symbol.Cherry, Symbol.Cherry }, 1),
            new Hand(2, "Orange", new[] { Symbol.Orange, Symbol.Orange, Symbol.Orange }, 2),
            new Hand(3, "WaterMelon", new[] { Symbol.Watermelon, Symbol.Watermelon, Symbol.Watermelon }, 3),
            new Hand(4, "Bell", new[] { Symbol.Bell, Symbol.Bell, Symbol.Bell }, 5),
            new Hand(5, "BAR", new[] { Symbol.BAR, Symbol.BAR, Symbol.BAR }, 10),
            new Hand(6, "Seven", new[] { Symbol.Seven, Symbol.Seven, Symbol.Seven }, 100),
        };
    }
}

public static class SymbolCharacter
{
    private static Dictionary<Symbol, string> CharacterDict { get; }
        = Enum.GetValues(typeof(Symbol)).Cast<Symbol>()
            .Where(x => x != Symbol.Default)
            .ToDictionary(
                x => x,
                x => SymbolToCharacter(x));

    private static string SymbolToCharacter(Symbol symbol)
    {
        switch (symbol)
        {
            case Symbol.Cherry:
                return "A";
            case Symbol.Orange:
                return "B";
            case Symbol.Watermelon:
                return "C";
            case Symbol.Bell:
                return "D";
            case Symbol.BAR:
                return "E";
            case Symbol.Seven:
                return "F";
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    public static string ToIdentifier(this IEnumerable<Symbol> source)
    {
        return string.Join("", source.Select(x => CharacterDict[x]));
    }
}
```
## スロットマシーン本体
リールと存在する役を保持してプレイできるようにします。

```csharp
public class SlotMachine
{
    /// <summary>
    /// 列数
    /// </summary>
    public const int REEL_COUNT = 3;
    /// <summary>
    /// 行数
    /// </summary>
    public const int ROW_COUNT = 3;

    public SlotMachine()
    {
        Reels = Enumerable.Range(1, REEL_COUNT).Select(x => Reel.Create(x)).ToArray();
        Hands = Hand.CreateList();
    }
    /// <summary>
    /// スロットのリール
    /// </summary>
    private Reel[] Reels { get; set; }
    /// <summary>
    /// 役
    /// </summary>
    private Hand[] Hands { get; set; }

    public int SpinCount { get; private set; }

    public PlayResult Play(int bet)
    {
        // リールの止まったところを抽選
        var results = Reels.AsParallel()
            .Select(x =>
            {
                var random = RandomProvider.GetThreadRandom();
                var position = random.Next(0, Reel.SYMBOL_COUNT);
                return x.Symbols.GetByPosition(position);
            })
            .Aggregate(
                Enumerable.Repeat(0, ROW_COUNT).Select(_ => new List<Symbol>()).ToArray(),
                (acc, x) =>
                {
                    // 各リールの出目を行列変換
                    foreach (var i in Enumerable.Range(0, ROW_COUNT))
                        acc[i].Add(x[i]);
                    return acc;
                })
            .Select(x => x.ToArray())
            .ToArray();

        // 成立した役を取得
        var hands = results.AsParallel()
            .SelectMany(r => Hands.Where(x => x.IsMaking(r)))
            .ToArray();

        // カウントアップ
        SpinCount++;

        return new PlayResult(
            SpinCount,
            hands.Sum(x => x.Rate * bet),
            hands);
    }

    /// <summary>
    /// 生成されたリール確認用
    /// </summary>
    /// <returns></returns>
    public string DumpReel()
    {
        return string.Join(Environment.NewLine,
            Reels.Aggregate(
                Enumerable.Repeat(0, Reel.SYMBOL_COUNT).Select(_ => new List<Symbol>()).ToArray(),
                (acc, x) =>
                {
                    foreach (var i in Enumerable.Range(0, Reel.SYMBOL_COUNT))
                        acc[i].Add(x.Symbols[i]);
                    return acc;
                })
                .Select(x => string.Join(" ", x.Select(s => s.ToString().PadRight(10))))
        );
    }

}

public class PlayResult
{
    public PlayResult(int no, int returns, Hand[] hands)
    {
        No = no;
        Returns = returns;
        MadeHands = hands;
    }
    public int No { get; }
    public int Returns { get; }
    public Hand[] MadeHands { get; }

    public override string ToString()
    {
        return $"{No.ToString("0000")}Play:Returns{Returns}{Environment.NewLine}{string.Join(Environment.NewLine,MadeHands.Select(x => x.Name))}";
    }
}
```
## エントリーポイント
まあ1000回くらい試行してみます。

```csharp

class Program
{
    static void Main(string[] args)
    {
        // slot生成
        var slot = new SlotMachine();
        // Reel生成結果を見る
        Console.WriteLine(slot.DumpReel());
        Console.ReadLine();

        // 1000回試行
        foreach (var _ in Enumerable.Range(1, 1000))
        {
            var result = slot.Play(100); // なんとなく100BET ※単位不明
            Console.WriteLine(result.ToString());
        }

        Console.WriteLine("終わり");
        Console.ReadLine();
    }
}
```
![Slot実行結果.PNG](https://qiita-image-store.s3.amazonaws.com/0/60841/c6c987d3-bc74-d190-9cef-b2900e7ec846.png)
ランダムリールな割にスリーセブンが成立していたりする…！

たまには頭の体操的にいろんなもののアルゴリズムを考えてみるのも面白いですね。
https://github.com/nk9k/SlotMachineSample