16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C#スクリプトエンジンで神経衰弱を実装した。

Posted at

#カードゲーム
遊戯王好きです。MTGやったことないけど好きです。麻雀とかも好きです。
でもどれも実際に作るのは難しそうなんで一人用の神経衰弱で我慢します。
我慢はしますが、どんどん拡張すると遊戯王とかMTGになる、そんな発展性のある神経衰弱を作りたいです。

#神経衰弱
うーん、でも神経衰弱も実装するのは面倒くさいですね。
トランプ作らないといけないし、何より神経衰弱もルールが複雑です。
神経衰弱のルールを書き下すとこんな感じでしょうか。

##用意するもの
52枚のトランプ。♠♦♥♣の記号が書かれたカードがそれぞれ13枚。
さらにそれぞれのグループには1から13の数字がそれぞれ書かれている。

##ルール

  1. トランプを全て裏にする。
  2. トランプをシャッフルする。
  3. プレイヤーは裏側のトランプを二枚選んで表にする。
  4. 二枚が異なる数字のときは裏に戻す。
  5. 3-4をトランプが全て表になるまで繰り返す。
  6. 全て表になったらゲーム終了。

なんと!適当に書いただけでも6つのルールがあるじゃないですか!これは面倒くさい!

#カードゲームの最小単位

最初はもっともっと簡単なカードゲームがいいな。
よーし、じゃあカードゲームを成り立たせるのに最低限必要な概念だけを取り出してゲームを作るか。
例えばこんな感じのカードゲームはどうでしょうか。

##用意するもの
###ルールカード

カードです。カードにはルールが書かれています。

###スタック

スタックです。ルールカードを積んだり取り出したり出来ます。

###辞書(ノート)

辞書です。何書いてもイイヨ。

##ルール
1.スタックからカードを一枚取り出して、カードに書かれたルールを実行する。
2.スタックが空になるまで1を繰り返す。
3.スタックが空になったらゲームを終了する。

これなら自分でも実装できそうです。

GameEnvironment.cs
  /// <summary>
    /// ルールカード。後々の事を考えてDictionaryを継承させているが、今回はこの部分は使わない。
    /// </summary>
    public class RuleCard : Dictionary<string, object>
    {
     /// <summary>
        /// ルール。なんとなく戻り値の型をboolにしているが、今回はこの戻り値を使わない。
        /// </summary>
        public Func<bool> Effect { get; set; }
    }

    public class GameEnvironment
    {
        /// <summary>
        /// ルールカードの集合。無くてもいいし、Dictionaryに押し込んでも良い。
        /// </summary>
        public Dictionary<string,RuleCard> Universe { get; set; } = null;
        public Dictionary<string,object> Dictionary { get; set; } = new Dictionary<string, object>();
        public Stack<RuleCard> Stack { get; set; } = new Stack<RuleCard>();

        public bool Run()
        {
            while (true)
            {
                if (this.Stack.Count > 0)
                {
                    var p = this.Stack.Pop();
                    p.Effect();
                }
                else
                {
                    Console.WriteLine("ゲームを終了します。");
                    break;
                }
            }
            return true;
        }
        
    }

出来ました。なんて面白そうなゲームなんだろう。
さあさあ皆さん、ルールカードを作ってスタックに乗せて好きなだけ遊んでください。

#ルールカードで神経衰弱を実装する
せっかくですからこれで神経衰弱でも作りましょう。
以下のルールカードがあれば神経衰弱が作れそうです。

  • [生成]:トランプを生成します。具体的にはトランプは辞書に書かれます。
  • [シャッフル]:トランプをシャッフルします。
  • [選択]:裏側のトランプから2枚選んで表にします。異なる数字だったときは裏に戻します。
  • [底]:裏側のトランプが存在するとき、スタックに[底][選択]のカードをスタックに積みます。
  • [開始]:[底][シャッフル][生成]のカードをスタックに積みます。

これでスタックに[開始]のカードを乗せれば、神経衰弱の出来上がりです。
トランプに書かれた数字とか記号とか、表とか裏とか、そもそもトランプとか、これらは単なる情報に過ぎません。だから全部辞書に書いて記録します。ルールカードは辞書に書き込んだり内容を参照出来るのでこのような複雑なゲームも実行できるわけです。まあ、神経衰弱なので全然複雑ではありませんけども。

#C#スクリプト
さて、ルールカードを頑張って作れるのなら、神経衰弱以外にも大富豪とか麻雀とか遊戯王とかいろいろ作れそうな気がします。
特に遊戯王、今でも新しいカードがどんどん発売されてますね。あのカードにもいろいろルールが書かれているのでなんかルールカードっぽいですね。となれば、新しいカードが出るたびにソースコードをコンパイルするのは面倒くさい。
だから、ルールカードの部分はスクリプトに任せましょう。

C#スクリプトの使い方は以下のページなんかで解説されています。
C#スクリプト実行 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
csi.exeコマンド登場! C#スクリプト(.csx)やREPLを動かそう - Build Insider

C#スクリプトを読み込むことでプログラムはそこに記述されたコードを実行することが出来ます。
ルールカードの記述は出来るだけ簡潔にしたいので、属性と静的メソッドでルールカードを表現することにします。
以下のCardMethodsクラスで[CardMethod(name)]が付与されたメソッドはnameという名前のルールカードのルールを表しています。

code.csx
public class CardMethods
{
    public static CardGame.GameEnvironment Environment { get; set; }
    public static string NUMBER = "Number";
    public static string TRUMPTYPE = "TrumpType";
    public static string TRUMPS = "Trumps";
    public static Trump[] Trumps
    {
        get { return (Trump[])Environment.Dictionary[TRUMPS]; }
    }
    public static Stack<RuleCard> GStack
    {
        get { return Environment.Stack; }
    }
    public static Dictionary<string,RuleCard> Universe
    {
        get { return Environment.Universe; }
    }

    /// <summary>
    /// ゲーム開始時に使用されるカード。神経衰弱に必要なカードを生成し、必要なルールをスタックに積む。
    /// </summary>
    /// <returns></returns>
    [CardMethod("開始",true)]
    public static bool FirstCard()
    {
        Console.OutputEncoding = System.Text.Encoding.Unicode;
        Console.WriteLine("神経衰弱を開始します。");
        GStack.Push(Universe["底"]);
        GStack.Push(Universe["シャッフル"]);
        GStack.Push(Universe["生成"]);
        return true;
        
    }

    /// <summary>
    /// スタックの一番下に常に置かれているカード。トランプが全て表になっていなければ、底と選択のカードをスタックに積む。
    /// </summary>
    /// <returns></returns>
    [CardMethod("底")]
    public static bool Bottom()
    {
        if (Trumps.All((v) => v.IsOmote))
        {
            Console.WriteLine("ゲームクリア!");
        }
        else
        {
            GStack.Push(Universe["底"]);
            GStack.Push(Universe["選択"]);
        }
        return true;
    }

    /// <summary>
    /// 伏せているカードを二枚選んで両方の数字が同じならそれらのカードを表にする。違うなら裏に戻す。
    /// </summary>
    /// <returns></returns>
    [CardMethod("選択")]
    public static bool Choice()
    {
        View();
        var input1 = ValidateChoice(true,-1);
        var input2 = ValidateChoice(false,input1);

        var card1 = Trumps[input1];
        var card2 = Trumps[input2];
        Console.WriteLine("一つ目のカードは{0}です", card1.TypeAndNumber);
        Console.WriteLine("二つ目のカードは{0}です", card2.TypeAndNumber);
        if (card1.Number == card2.Number)
        {
            Console.WriteLine("数字が一致しました。");
            card1.IsOmote = true;
            card2.IsOmote = true;
        }
        else
        {
            Console.WriteLine("残念、外れでした。");
        }
        return true;
    }
    public static int ValidateChoice(bool isFirst, int preNumber)
    {
        if (isFirst)
        {
            Console.WriteLine("一つ目のカードを選んでください。");
        }else
        {
            Console.WriteLine("二つ目のカードを選んでください。");
        }
        while (true)
        {
            var input1 = Console.ReadLine();
            int num = -1;
            var result = Int32.TryParse(input1, out num);
            if (!result)
            {
                Console.WriteLine("数字を入力してください");
            }
            else if (num < 0 || num >= Trumps.Length)
            {
                Console.WriteLine("範囲外の数字です");
            }
            else if (Trumps[num].IsOmote)
            {
                Console.WriteLine("選択したカードは既に表です。");
            }
            else if(!isFirst && preNumber == num)
            {
                Console.WriteLine("一つ目のカードと同じカードを選択しています。");
            }
            else
            {
                return num;
            }
        }
    }

    /// <summary>
    /// トランプの生成
    /// </summary>
    /// <returns></returns>
    [CardMethod("生成")]
    public static bool Generate()
    {
        Console.WriteLine("カードを生成します。");
        var p = new List<Trump>();
        foreach(var type in new[] { TrumpType.Heart, TrumpType.Diamond, TrumpType.Club, TrumpType.Spade })
        {
            for (int i=1;i<= 4; i++)
            {
                p.Add(new Trump() { Type = type, Number = i, IsOmote = false });
            }
        }
        Environment.Dictionary[TRUMPS] = p.ToArray();

        return true;
    }
    public static void Shuffle<T>(T[] items)
    {
        var seed = System.Environment.TickCount;
        var rand = new System.Random(seed);
        var count = items.Length * 10;
        for (int k = 0; k < 10; k++)
        {
            for (int i = 0; i < items.Length; i++)
            {
                var j = rand.Next(items.Length);
                var tmp = items[i];
                items[i] = items[j];
                items[j] = tmp;
            }
        }
    }

    /// <summary>
    /// トランプをシャッフルする
    /// </summary>
    /// <returns></returns>
    [CardMethod("シャッフル")]
    public static bool CardShuffle()
    {
        Console.WriteLine("カードをシャッフルします。");
        Shuffle(Trumps);
        return true;
    }
    public static bool View()
    {
        for(int i = 0; i < Trumps.Length; i++)
        {
            Console.Write("[{0}:{1}]", i, Trumps[i]);
        }
        Console.WriteLine("");
        return true;
    }
}
return typeof(CardMethods);
CardInfo.cs
/// <summary>
    /// ルールカードを表すメソッドに付与する。isFirstがTrueのメソッドはゲーム開始時にスタックに積まれる。
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public class CardMethod : Attribute
    {
        public string Name { get; set; }
        public bool IsFirst { get; set; } = false;
        public CardMethod(string name)
        {
            this.Name = name;
        }
        public CardMethod(string name, bool isFirst)
        {
            this.Name = name;
            this.IsFirst = isFirst;
        }
    }

以下はスクリプトを実行するコードと、読み込んだコードからRuleCardを生成するコードです。
CardMethod属性とリフレクションを使って必要なメソッドを取り出して、それをFuncに変換しています。
メソッドをFuncに変換できたら、あとはRuleCardインスタンスに代入するだけです。

CardLoader.cs
public class CardLoader
    {
        /// <summary>
        /// スクリプトを読み込んでゲーム環境を生成する 
        /// </summary>
        /// <param name="script"></param>
        /// <returns></returns>
        public static GameEnvironment RunScript(string script)
        {
            //スクリプトのパス設定
            var ssr = ScriptSourceResolver.Default
        .WithBaseDirectory(Environment.CurrentDirectory);
            var smr = ScriptMetadataResolver.Default
                .WithBaseDirectory(Environment.CurrentDirectory);
            var so = ScriptOptions.Default
                .WithSourceResolver(ssr)
                .WithMetadataResolver(smr);

            //Roslynでスクリプト実行
            var gameRuleClass = CSharpScript.EvaluateAsync<Type>(script,so).Result;

            var env = new GameEnvironment();
            var kv = ExtractRules(gameRuleClass);
            env.Universe = kv.Key;
            env.Stack.Push(kv.Value);

            SetEnvironmentInScript(env, gameRuleClass);
            
            return env;
        }

        /// <summary>
        /// ルールカードの抽出
        /// </summary>
        /// <param name="gemeRuleClass"></param>
        /// <returns></returns>
        public static KeyValuePair<UniverseDic,RuleCard> ExtractRules(Type gemeRuleClass)
        {
            RuleCard firstCard = null;
            var dic = new UniverseDic();

            foreach (MethodInfo info in gemeRuleClass.GetMethods())
            {
                var cardMethodAtt = (CardMethod)Attribute.GetCustomAttribute(info, typeof(CardMethod));
                if (cardMethodAtt != null)
                {
                    var effect = (Func<bool>)info.CreateDelegate(typeof(Func<bool>));
                    var card = new RuleCard() { Effect = effect };
                    card["Name"] = cardMethodAtt.Name;
                    dic[cardMethodAtt.Name] = card;
                    if (cardMethodAtt.IsFirst)
                    {
                        firstCard = card;
                    }
                }
            }
            return new KeyValuePair<UniverseDic, RuleCard>(dic, firstCard);
        }
    }

スクリプトからルールカードを生成出来たら、あとはスタックに積んで実行するだけですね。
さあついに神経衰弱の始まりです!

sinkei.PNG

( ^ω^)つまんね!

#まとめ

  1. カードゲームの最小単位の枠組みを考えてみた
  2. 枠組みを使って神経衰弱を実装した
  3. C#スクリプトを使ってゲームのルールを動的に読み込めるようにした

こんなところでしょうか。神経衰弱くらいなら簡単に実装できましたが、麻雀や遊戯王となると難しそうですね。
そして本格的にカードゲームを作るとなると、GUI、通信対戦、CPUの思考などゲームのルールとは別の方向での実装の難しさが存在します。だから私はここでぶん投げてふて寝します。
おわり。

16
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?