C#でチンチロリンゲームをボチボチと進めていること、ご高承の通りです。(

注:「BCCForm and BCCSkelton のサポートセンター」というブログなのに、既にbcc32c.exeという32bitコンパイラーを見限っているところが哀しい。

 

まだまだ完成形のイメージが出来ていない、部品作りの段階ですが、現状を報告しましょう。

 

現在のChinchirorinアプリは次のような構成に仕様(しよう-親爺ギャグ)かと検討しています。

 

「完成形」 Chinchirorin.exe(Windows Formのアプリ)

                 (namespace Chinchirorin) 

                 |

                  Casino(賭博管理者(賭場)クラスーChinchirorinで定義するか?)

「部品」     |       |

                 |       Dice.dll(丼に投賽する「賽子」コントロール、Diceクラス-これはほぼ完成)

                 |

                  Players.dll(ユーザー<手動>、PC<自動>による賭博者主体、Playerクラス

 

現在はこの緑字のPlayerクラスの検討を行っています。その為には「賭場における賭博者の判断、行動」を理解し、その判断、行動に沿ってクラスを設計しなければなりません。では「賭博者」の判断、行動とは何でしょう?(自分の経験から次のように構成してみました。)

 

【賭博者の判断、行動】

(1)博打勝負に勝つ為に、現金(所謂「種銭」)をもって賭場に出かけてゆく。

(2)賭場において、賭場のルールを順守し(アプリに「イカサマ」は導入予定なし)て

 ①種銭額、自分の運・ツキ判断に基づく「強気弱気」に基づき、賭け銭の多寡を判断する。

 ②賭け銭でチンチロ賭博を行う。

 ③勝負の結果から種銭が増減し、自分の運・ツキ判断に変化が生じる(「勝負の振り返り」)

 ④上記「自分の運・ツキ判断」は、累計対戦結果と今回(現在)の勝負結果の統計数値から判断される。

 ⑤具体的には「累計勝率と今回の限界的勝率から、現在の傾向を判断」し、「運があれば強気、そうでなければ弱気」になり、次の上記①に影響を与える。

(3)破産または最低掛け金に届かなくなればゲームオーバー、そうでなければ賭博を継続し、適宜ルールに沿って中断する。

 

こんな感じで考えており、その為のPlayerクラスのプロトタイプに持たせる情報を次のように考えています。

 

        //個性系変数とプロパティ
        public bool IsNew = true;                        //新規か否かのフラグ
        public string Name {get; set;}                    //賭博者の名前(愛称)
        //賭博系プロパティ
        public int Capital {get; set;}                    //掛け金の元手
        public int Bet {get; private set;}                //掛け金(元手×強気弱気係数)
        public int Luck {get; private set;}                //運、強気(bullish)弱気(bearish)の原因
        public int BullBear {get; private set;}            //強気弱気(掛け金の多寡に影響する)
        //統計系プロパティ
        public int TotalGames {get; private set;}        //累計勝負数
        public int TotalWins {get; private set;}        //累計勝ち数
        public int TotalLoses {get; private set;}        //累計負け数
        public double TotalAverage {get; private set;}    //累計勝率
        public int ThisGames {get; private set;}        //今回勝負数
        public int ThisWins {get; private set;}            //今回勝ち数
        public int ThisLoses {get; private set;}        //今回負け数
        public double ThisAverage {get; private set;}    //今回勝率
 

そして記録すべき情報は簡単な(賭博者名).datファイルに書き出して、Playersフォールダーに保存し、次の勝負の際に読み込むようにしてはどうかな、と考えています。

 

このPlayerクラスは、PCにより自動で勝負することもできる反面、手動でユーザーが勝負することもでき、手動の場合、「強気弱気」に関わらず掛け金を変えられるようにする、という考えです。(全部自動にするとまたまた環境ソフト化しますが。)

 

取り敢えず、ご報告まで。

【食い物話】やりますよ、といってまだ何もやっていないので、ちょっとやりますか。

 

実は私、【食い物話】といっても食通でも、食べたがりでもなく、(酒が好きなので、「何かつまみを」ということで発揮する)「どんな味になるんだろう?という好奇心で食べ物を作る」ことが好きなのかもしれません。

 

自分で作ったものを2018年に最初に撮影したのは、将にこの流れでメキシコのチリコンカーン(Chili con carne)。

通算10年(米国5年、東南アジア5年)の海外生活でローカル飯は結構経験したのと、時々そういう味が懐かしく、日本の食材で真似してみたりしましたが、最近は結構調味料製造業者がエスニックスパイスを調合したシーズニングを売っています。

 

そんなことで、昨日そういったシーズニングを使って「改造料理」を作ってみましたのでご報告を。

 

背景として、私、完全リタイアしており、神さんの在不在に関わらず、昼は孤食で結構自分で調理していただきます。そんなことで、ご飯に飽きた時に使おうと「スーパーのOEMソース焼きそば」(注)をよく買います。しかし、添付される「粉末ソース味」「粉末塩味」の調味料ではワンパターンになるので、時々「自作広東風オイスターソース味」「自作和風生姜醤油味」など改造して楽しんでいました。

注:元は\98で3食も入っていましたが、今はご多分に漏れず、\138と値上がりしましたね。

 

今回は時々作るガパオライスの為にS&Bが出しているガパオシーズニングスパイスがあったので、こいつを使ってやれ、ということで作ってみました。

麺と、具材(豚挽き肉、ソーセージ、玉葱、人参、モヤシ、キャベツ、エノキ)を炒め、ガパオスパイス仕上げました。何時も食べたいというほどではないですが、「ソースや塩だけのワンパターン」に飽きた時の味としては(スパイシーなものが好きなので)丁度良かったです。

 

同様に他のシーズニングを使って、定番料理を改造するのも面白いな、と感じました。

 

どうも「Diceコントロール」と「役と出目の判定」の話題を飛び飛びで変えてしまい、申し訳ありません。

【Dice】チンチロリンのルールと出目の判定

では、

(1)等価の条件に共通する独特(unique:ユニーク)な条件を探すか(アルゴリズムの検討)

(2)可能な結果の組み合わせをテーブルにしてチャチャっと検索するか(テーブル化)

を示唆し、末尾にテーブルを載せておきましたが、その際に「和(OR)」と「積(AND)」を表示して、例としてヒフミが

1, 2, 3:6, 6

1, 3, 2:6, 6(これは青字にするのを忘れていましたね。)
2, 1, 3:6, 6

2, 3, 1:6, 6

3, 1, 2:6, 6

3, 2, 1:6, 6

和と積が常に6になる組み合わせであることを示唆しました。

 

実際にBCCSkelton(C++)でDiceを作った際には、このサンプルプログラムは対戦(チンチロリンの勝負)をすることを予定していなかったので、「投賽」と「判定」を一つの関数(C#のメソッド)で行い、「即勝ち」「即負け」「3回振って目が出たか否か」の判定しかしていませんでした。(末尾「参考」参照)

しかし、「役の判定」についてはこの「和」と「積」のユニーク性に着眼して判断しています。BCCSkeltonのコード解説は以下のコメントが解説を行っていますのでそのまま載せます。末尾のコードの特徴は、3つの賽子の目(num[0]~num[2])そのものをチェックするのではなく、最初にNum_OR、Num_AND、Num_XOR、Num_EQという変数にそれぞれOR、AND、XORと二つ以上の賽が同じ値か否かの値をとっておき、これを判定と出目に使っている点でしょうか?

/* 開発時のテスト用
    if(g_JAPANESE) 
        str = 
        "/////////////////////////////////////////////////////////////////\r\n"\
        "// チンチロリンの目、役の判定条件について\r\n"\
        "// 1 → 0 0 0 1\r\n"\
        "// 2 → 0 0 1 0\r\n"\
        "// 3 → 0 0 1 1\r\n"\
        "// 4 → 0 1 0 0\r\n"\
        "// 5 → 0 1 0 1\r\n"\
        "// 6 → 0 1 1 0\r\n"\
        "//「アラシ」\r\n"\
        "// num[0] | num[1] | num[2](== num[0] & num[1] & num[2])== num[0];\r\n"\
        "// 全ての要素のOR(またはAND)がいずれも要素である場合。\r\n"\
        "//「456」\r\n"\
        "// num[0] ^ num[1] ^ num[2] == 7 && num[0] & num[1] & num[2] == 4\r\n"\
        "// 各桁のビットが奇数個立っており、全ての要素が4以上である場合。\r\n"\
        "//「123」\r\n"\
        "// num[0] ^ num[1] ^ num[2] == 0 && num[0] | num[1] | num[2] == 3\r\n"\
        "// 各桁のビットが偶数個立っており、全ての要素が3以下である場合。\r\n"\
        "//「目」\r\n"\
        "// (num[0] == num[1]) | (num[1] == num[2]) | (num[0] == num[2])\r\n"\
        "// 二つの要素のいずれかが等しい場合。\r\n"\
        "// なお、3つの要素の排他的論理和が最後の目となる。\r\n"\
        "/////////////////////////////////////////////////////////////////\r\n";
(英語表記は省略)

*/
 

さて、問題は

このコードは、そのままでは対戦型チンチロリンゲームには使えない

ということです。「役の判定」に関わるサブメソッドとしては使えますが、その際にも戻り値として

 

(1)勝敗(即勝ち、即負けの場合)または未定(出目がある場合)(従ってBoolean型は使えない)

(2)上記未定の場合の出目(6は即勝ち、1は即負けなので2~5の値になる)

 

を返さないと不味なので、

 

(1)これらの情報をまとめた構造体やクラスを作り、それを戻り値にするか否か

(2)関数の戻り値に「一定のルール」を作る(例:0-負け、1-勝ちで1倍付け、2~5-出目、6-勝ちで2倍付け、7-勝ちで3倍付け)

(3)戻り値は「即勝ち(正数)、即負け(負数)の倍率(1~3)」とし、単なる出目は"out"引数を使う

 

という工夫をする必要があるでしょう。

 

更に対戦型チンチロリンゲームを考えてゆくと、

これはチンチロ賭博のRPG型ゲームではなく、偶然性をテーマとしたシミュレーション型ゲームではないか?

という感が強くなってきました。

 

最初は単に「元手」「掛け金」を持ち、「運・ツキ(乱数による偶然の流れに一定の傾向が見られた場合に「運・ツキがある」と評価するメソッドを考えています)」に基づいて「強気、弱気」(これらにより、掛け金の額が変わるはずです)が決定され、結果として「戦績」が記録される「Playerクラス」のインスタンスに投賽させて判定していればよい、と考えましたが、賭博はそんな簡単なものではなく、「賭場という主体(胴元、Bankerクラス-こいつらが投賽や判定を仕切る)」が「賭博の行われる賭場をマネージする」中にPlayerクラスインスタンスがおり、賭け自体は「ヒトがマニュアルで行う」または「PCで自動化して行う」ことでもよいのではないか?と考えを変えてきました。

 

まだまだ先は長そうです。ゆっくりと考え、ボチボチ行きましょう!

 

【参考-BCCSkeltonのDiceの投賽・判定部】

bool CMyWnd::OnRoll() {

    int num[3];    //解説:三つのサイコロの目
    char buff[256];    //解説:文字列化用のバッファ
    CSTR str;    //解説:BCCSkeltonの文字列変数クラスとインスタンス
    PlaySound("IDS_DICESOUND", m_hInstance, SND_RESOURCE | SND_ASYNC);    //解説:wavファイルリソース
    for(int i = 0; i < 3; i++) {    //解説:3つのDiceクラスコントロールに対して
        num[i] = SendItemMsg(IDC_D1 + i, DM_ROLL, 0, 0);    //解説:賽を振るメッセージを送り、賽の出目を採る
        if(g_JAPANESE)    //解説:「日本語版なら」という意味です。
            wsprintf(buff, Str_18, num[i], i);    //解説:賽の目アナウンスコメントを文字列で表示
        else    //解説:「英語版なら」という意味です。
            wsprintf(buff, Str_08, num[i], i);
        str = str + buff + "\r\n";
        SendItemMsg(IDC_EDIT, WM_SETTEXT, 0, (LPARAM)str.ToChar());    //解説:エディットボックスに文字列を表示
    }
    m_Times--;        //賽を振る回数を減らす(目や役が出来なければ、3回迄賽を振れる)
    int Num_OR = num[0] | num[1] | num[2];
    int Num_AND = num[0] & num[1] & num[2];
    int Num_XOR = num[0] ^ num[1] ^ num[2];
    int Num_EQ = (num[0] == num[1]) | (num[1] == num[2]) | (num[0] == num[2]);

    //解説:予め賽の目のOR、AND、XORと二つの目が等しいか否かのEQ(ほぼBoolean)を取っておく

    if(Num_OR == Num_AND) {                    //アラシの場合-これは(Num_OR == num[0~2])と同じです
        if(g_JAPANESE)
            wsprintf(buff, "アラシが出ました!親の総取りです。\r\n");
        else
            wsprintf(buff, "Storm's here! You, Dealer's got everything!\r\n");
        m_Times = 3;                        //初期化される(解説:即勝ちなので「次回3回振れますよ」という意味です。)
    }
    else if(Num_XOR == 7 && Num_AND == 4) {    //456の場合
        if(g_JAPANESE)
            wsprintf(buff, "456が出ました!親の総取りです。\r\n");
        else
            wsprintf(buff, "456's here! You, Dealer's got everything!\r\n");
        m_Times = 3;                        //初期化される
    }
    else if(Num_XOR == 0 && Num_OR == 3) {    //123の場合
        if(g_JAPANESE)
            wsprintf(buff, "なんと、123です!倍付け配等で親落ちです。\r\n");
        else
            wsprintf(buff, "My God, 123's here! Gotta pay doubled bets and quit Dealer!\r\n");
        m_Times = 0;                        //親落ち
    }
    else if(Num_EQ) {
        if(Num_XOR == 6) {                    //出目の場合
            if(g_JAPANESE)
                wsprintf(buff, "親に%dの目がつきました!親の総取りです。\r\n", Num_XOR);
            else
                wsprintf(buff, "You've got Point of %d! You, Dealer's got everything.\r\n", Num_XOR);
            m_Times = 3;                    //初期化される

        }
        else if(Num_XOR == 1) {    //解説:ゾロ目も1ですが、↑でチェックしたelseなのでゾロ目は対象外です。
            if(g_JAPANESE)
                wsprintf(buff, "なんと%dの目です!親の総付けです。\r\n", Num_XOR);
            else
                wsprintf(buff, "My God, You've got Point of %d! Gotta pay everyone.\r\n", Num_XOR);
            m_Times = 0;                    //親落ち
        }
        else {    //解説:出目が1、6以外ですので2~5になります。
            if(g_JAPANESE)
                wsprintf(buff, "親に%dの目がつきました。\r\n", Num_XOR);
            else
                wsprintf(buff, "You've got Point of %d.\r\n", Num_XOR);
            m_Times = 3;                    //初期化される(解説:本来はこれで終わりではない。)
        }
    }
    else
        *buff = NULL;

    //解説:本来は、これから出目を記録して親方、子方の出目を比較しなければならない。
    //目または役が出た場合、ステータスバーに表示
    SBar.SetText(1, buff);
    //最後に賽を振れる残回数を表示
    if(g_JAPANESE) {
        wsprintf(buff, Str_19, m_Times);
        str = str + buff + "\r\n";
    }
    else {
        wsprintf(buff, Str_09, m_Times);
        str = str + buff + "\r\n";
    }
    SendItemMsg(IDC_EDIT, WM_SETTEXT, 0, (LPARAM)str.ToChar());
    //終了チェック
    if(!m_Times) {    //解説:C++でよくやる"m_Times(賽を振れる回数) == 0"の代用式です。
        if(g_JAPANESE) {
            if(MessageBox(m_hWnd, "あなたの負けです。まだ続けますか?", "勝負の終了",
                            MB_YESNO | MB_ICONQUESTION) == IDYES)
                m_Times = 3;
            else
                OnIdok();
        }
        else {
            if(MessageBox(m_hWnd, "You lost. Wanna continue yet?", "Game over",
                            MB_YESNO | MB_ICONQUESTION) == IDYES)
                m_Times = 3;
            else
                OnIdok();
        }
    }
    return TRUE;
}
 

いやー、スミマセン。

前々回、「では、作成したDice.dllが正しく動くかどうかテストしましょう。Diceコントロールを使うC#プログラム(TestDice.cs-これは次回で!)を用意し、動かします。」と書いておきながら、すっかり忘れてしまいました!(最近、可也来ているんですよね、ボケが。)

 

ということで、テストプログラム、

TestDice.csのご紹介を。

【TestDice.cs】

///////////////////////////////////
//TestDice.cs - テスト用プログラム
///////////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using Dices;    //解説:これでDice.dllをロードし、中のnamespace Dicesクラスを呼び込みます。

namespace TestDice
{
    public partial class AppForm : Form
    {
        Dice dice1, dice2, dice3;    //Diceコントロール(解説:ボタンコントロールから派生させています。)
        Button rollbtn, extbtn;       //ボタンコントロール

        [STAThread]    //解説:お忘れなく。
        public static void Main()
        {
            Application.Run(new AppForm());
        }

        public AppForm()
        {
            System.Reflection.Assembly myOwn = System.Reflection.Assembly.GetEntryAssembly();
            this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);    //プログラムアイコンをフォームにつける
            this.ClientSize = new Size(232, 120);
            this.MinimumSize = new Size(232, 120);
            this.Text = "Dice";
            this.Load += AppForm_Load;
            this.SizeChanged += AppForm_SizeChanged;
        }

        private void AppForm_Load(object sender, EventArgs e)
        {
            //Dice1(解説:Diceクラスコントロールの代表として解説します。)
            dice1 = new Dice();    //解説:先ず生成します。
            dice1.Size = new Size(64, 64);    //解説:大きさを決定します。
            dice1.Location = new Point(10, 10);    //解説:位置決めします。
            dice1.Anchor = (AnchorStyles.Top | AnchorStyles.Left);    //解説:左上にアンカーを設定します。
            dice1.Click += Dice1_Click;    //解説:これはクリックされた時の割込み処理です。
            this.Controls.Add(dice1);    //解説:フォームに張り付けます。

            //Dice2
            dice2 = new Dice();
            dice2.Size = new Size(64, 64);
            dice2.Location = new Point(ClientSize.Width / 2 - 32, 10);
            dice2.Anchor = AnchorStyles.Top;
            dice2.Click += Dice2_Click;
            this.Controls.Add(dice2);

            //Dice3
            dice3 = new Dice();
            dice3.Size = new Size(64, 64);
            dice3.Location = new Point(ClientSize.Width - dice3.Width - 10, 10);
            dice3.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            dice3.Click += Dice3_Click;
            this.Controls.Add(dice3);

            //投賽ボタン
            rollbtn = new Button();
            rollbtn.Location = new Point(10, ClientSize.Height - rollbtn.Height - 10);
            rollbtn.Text = "投賽";
            rollbtn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left);
            rollbtn.Click += rollbtn_Click;
            this.Controls.Add(rollbtn);

            //終了ボタン
            extbtn = new Button();
            extbtn.Location = new Point(ClientSize.Width - extbtn.Width - 10, ClientSize.Height - extbtn.Height - 10);
            extbtn.Text = "終了";
            extbtn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            extbtn.Click += extbtn_Click;
            this.Controls.Add(extbtn);
        }

        //終了処理
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.No)
            {
                e.Cancel = true;
            }
        }

        private void Dice1_Click(object sender, EventArgs e)
        {    //解説:クリック時の処理です。代表で説明します。
            //Dice1Roll
            MessageBox.Show((dice1.Roll()).ToString(), "Dice1の賽の目", MessageBoxButtons.OK, MessageBoxIcon.Information);    //解説:メッセージボックスで返り値を文字列にして出目を表示します。
            //dice1.Roll();    //解説:本来はコントロールdice1を振る(Roll)のように書きます。返り値が出目です。
        }

        private void Dice2_Click(object sender, EventArgs e)
        {
            //Dice2
            MessageBox.Show((dice2.Roll()).ToString(), "Dice2の賽の目", MessageBoxButtons.OK, MessageBoxIcon.Information);
            //dice2.Roll();
        }

        private void Dice3_Click(object sender, EventArgs e)
        {
            //Dice3
            MessageBox.Show((dice3.Roll()).ToString(), "Dice3の賽の目", MessageBoxButtons.OK, MessageBoxIcon.Information);
            //dice3.Roll();
        }

        private void rollbtn_Click(object sender, EventArgs e)
        {
            //投賽処理
            dice1.DiceSound(false);    //解説:もう一つの投賽音を出すときには引数falseを与えます。
            dice1.Roll();
            //dice2.DiceSound(false);    //解説:毎回投賽音を出すと変なのでdice1に代表して出してもらいました。
            dice2.Roll();
            //dice3.DiceSound(false);    //解説:同上
            dice3.Roll();
        }

        private void extbtn_Click(object sender, EventArgs e)
        {
            //終了処理
            Close();
        }
    }
}

 

たったこれだけで賽子が振れるようになりました。!

C#って本当に簡単ですねっ!

チンチロリンをテーマにしたゲームの構想はまだ途半ば(というか、スタート直後というか)です。

完成形の絵が見えない(注)」段階では、じっくり考え、熟成させてから手を付けた方がよいでしょう。

注:プログラミングしていると、作りたいものの具体的なイメージが見えるときと見えない時があります。良いプログラムを効率よく書ける時は、矢張り完成成果物の姿、形が心に焼き付いているときであることは経験がそう教えてくれます。

 

こういう時は、いずれ必要となる部品を作って遊ぶに限る!

 

ということで、サイコロコントロール(Diceコントロール)を作ったので、その出目と手役の判定メソッドを先に部品として作っておくことにします。

 

その前に、(サイコロ賭博なんて知らない「良ゐ子」の為に)チンチロリンのルールをお話ししなければなりませんね。

 

チンチロリン」というのは、非常にプリミティブなサイコロ博打(賭博)でそのルールも単純です。以下はBCCSkeltonで作ったDiceの日本語解説です。特に赤字にした「役と目の概要」をよくお読みください。

【Dice日本語解説】

【Diceについて】
Diceは"DICE"ウィンドウクラスのダイアログコントロールの為のサンプルプログラムです。
(日本で「チンチロリン」と呼ばれる)サイコロ賭博を模擬的に行うために作成しました。
「チンチロリン」は親と小方間で行われ、親が先ず3つのサイコロを振ることで始まります。親は(子方が勝負する前に)「役」等(以下参照)により直ちに勝ったり負けたりします。親が賽を振った後、小方も3つのサイコロを振って「目」(二つのサイコロが同じ数の時の三番目のサイコロの目)、またはなにか「役」を出すことで勝負できます。
親が目を出しても、小方がアラシや456を出すと負けますが、それ以外は親と子方は、より強い目を持つことで勝負します。


「役と目の概要」
「手役」 -  勝金  - 説明
 目   -  掛金  - 親が三番目のサイコロの目で6を出すと即勝ちとなり、1を出すと即負けとなる。それ以外は小方の目より親が大きい目を出した場合に勝ちとなる。
 アラシ - 3倍取り - 親または小方が全て同じ目を出した場合に即勝ちとなる。
 シゴロ -  倍取り  - 親または小方が4、5、6の目を出した場合に即勝ちとなる。
 ヒフミ -  倍付け  - 親または小方が1、2、3の目を出した場合に即負けとなる。

詳細は以下参照。
https://ja.wikipedia.org/wiki/%E3%83%81%E3%83%B3%E3%83%81%E3%83%AD%E3%83%AA%E3%83%B3

【チンチロリンのルール】

(1)最初にサイコロで親を決める。(Diceでは、貴方が親になる。)
(2)チンチロリンに参加する子方がコマ(掛け金)を張る。
(3)親が丼にサイコロ3個を廻す。
(4)嵐、シゴロ、目が6(ゾロ目以外の賽の目が6)と出た場合、親の即勝ちとなる。
(5)ヒフミが出た場合、親の即負けとなり、親落ち(親の権利を次に譲ること)する。
(6)目が1、目無し、ションベン(賽が丼から出ること)ならばその場で親の即負けで、親落ちする。
(7)親方が2~5の目ならば、子方も親の右隣から順番に賽を廻してゆく。
(8)アラシ(3倍付)、シゴロ(2倍付)、親より大きな目(1倍付)が出れば勝ちとなる。
    //解説:「3倍付、2倍付」は「親の」です。
(9)目が同じであれば引き分け、ヒフミ(倍付)やそれ以外は負けとなる。    //解説:親の倍付けが正しい
目が出ない場合、3回まで投賽することができる。
 

大体お判りでしょうか?(親が付きまくって「役」や6を出し続けると、子方は賽子も振れないことになります。)

 

さて、ここからはプログラミングのお話です。

賽の目は整数の1~6までですので、仮に

    int D1, D2, D3;

に「DiceクラスインスタンスのRollメソッドの戻り値を代入する」と、D1, D2, D3が三つのサイコロそれぞれの出目となります。

では、D1, D2, D3の値でどうやって「嵐」や「シゴロ」、「ヒフミ」や、目(同じ数の二つのサイコロ以外の三つ目のサイコロの数)の判定をすればよいのでしょうか?(この問いは「解法(アルゴリズム)」に関わるものです。)

 

例えば「嵐」は3つのサイコロのぞろ目(1,1,1や2,2,2や3,3,3...等々)ですので、

    if(D1 == D2 && D2 == D3 && D3 == D1) {}

という条件式が思いつきますが、「ン?」と思った人はこの条件式は

    if(D1 == D2 && D2 == D3) {}

で「十分じゃないか?」(注)ということが分かると思います。

注:"&&(且つ)"は、最初の"D1 == D2"がTRUEである場合のみ次の条件式をチェックしますので、"D2 == D3"をチェックする際には"D1 == D2"がTRUEであることが前提になる(即ち"D1 == D3"と等価となる)からです。又(C#では許してくれませんが)CやC++という低級言語では「TRUEが1」「FALSEが0」であることから、「且つ」の"&&"を積(*)、「または」の"||"を和(+)で処理することがあります。(例:if(condition1 * condition2)、if(condition1 + condition2)ーそれぞれTRUE(= 1)とFALSE(= 0)で条件式がどうなるか確認してみてください。)

 

しかし、「シゴロ」や「ヒフミ」は"4,5,6"が"4,6,5"はもとより、"6,5,4"や"6,4,5"、"5,4,6"や"5,6,4"と等価なので、if条件式を書いていると冗長でプログラムサイズが無用に大きくなってしまいます。

 

こういう場合には、

(1)等価の条件に共通する独特(unique:ユニーク)な条件を探すか(アルゴリズムの検討)

(2)可能な結果の組み合わせをテーブルにしてチャチャっと検索するか(テーブル化)

を行うかとなります。時には(2)をやっていると(1)に行き着くこともあるので、簡単なプログラム(注)を書いてテーブルを作り、眺めてみるのも「ひらめき」を得るためにはよいことでしょう。

注:今回の場合、次のようになります。

////////////////////
//ChinchiroTable.cs
////////////////////

using System;
using System.IO;                        
//StreamWriterを使う為
using System.Text;                      //Encoderを使う為

namespace ChinchiroTable
{
    public class RollTable
    {
        static void Main()
        {
            int D1, D2, D3;        
//賽子の目
            //文字列に総ての出目を記録する
            string result = "";
            for(D1 = 1; D1 <= 6; D1++)
            {
                for(D2 = 1; D2 <= 6; D2++)
                {
                    for(D3 = 1; D3 <= 6; D3++)
                    {
                        result += String.Format("{0},{1},{2}:{3},{4}\r\n", D1, D2, D3, D1 + D2 + D3, D1 * D2 * D3);
                    }
                }
            }
            
//ファイル(UTF-8:Encoding.UTF8)を書く
            using(StreamWriter sw = new StreamWriter(".\\ChinchiroTable.csv", false, Encoding.UTF8))
            {
              
 //ファイルを書く
                sw.Write(result);
                
//ファイルを閉じる
                sw.Close();
            }
            Console.ReadKey();
        }
    }
}

 

<上記プログラムの出力結果(6^3 = 218通り)-ヒフミシゴロ表示修正済>

D1,D2,D3:和,積
--------------
1, 1, 1:3, 1
1, 1, 2:4, 2
1, 1, 3:5, 3
1, 1, 4:6, 4
1, 1, 5:7, 5
1, 1, 6:8, 6
1, 2, 1:4, 2
1, 2, 2:5, 4

1, 2, 3:6, 6
1, 2, 4:7, 8
1, 2, 5:8, 10
1, 2, 6:9, 12
1, 3, 1:5, 3
1, 3, 2:6, 6
1, 3, 3:7, 9
1, 3, 4:8, 12
1, 3, 5:9, 15
1, 3, 6:10, 18
1, 4, 1:6, 4
1, 4, 2:7, 8
1, 4, 3:8, 12
1, 4, 4:9, 16
1, 4, 5:10, 20
1, 4, 6:11, 24
1, 5, 1:7, 5
1, 5, 2:8, 10
1, 5, 3:9, 15
1, 5, 4:10, 20
1, 5, 5:11, 25
1, 5, 6:12, 30
1, 6, 1:8, 6
1, 6, 2:9, 12
1, 6, 3:10, 18
1, 6, 4:11, 24
1, 6, 5:12, 30
1, 6, 6:13, 36
2, 1, 1:4, 2
2, 1, 2:5, 4

2, 1, 3:6, 6
2, 1, 4:7, 8
2, 1, 5:8, 10
2, 1, 6:9, 12
2, 2, 1:5, 4
2, 2, 2:6, 8
2, 2, 3:7, 12
2, 2, 4:8, 16
2, 2, 5:9, 20
2, 2, 6:10, 24

2, 3, 1:6, 6
2, 3, 2:7, 12
2, 3, 3:8, 18
2, 3, 4:9, 24
2, 3, 5:10, 30
2, 3, 6:11, 36
2, 4, 1:7, 8
2, 4, 2:8, 16
2, 4, 3:9, 24
2, 4, 4:10, 32
2, 4, 5:11, 40
2, 4, 6:12, 48
2, 5, 1:8, 10
2, 5, 2:9, 20
2, 5, 3:10, 30
2, 5, 4:11, 40
2, 5, 5:12, 50
2, 5, 6:13, 60
2, 6, 1:9, 12
2, 6, 2:10, 24
2, 6, 3:11, 36
2, 6, 4:12, 48
2, 6, 5:13, 60
2, 6, 6:14, 72
3, 1, 1:5, 3

3, 1, 2:6, 6
3, 1, 3:7, 9
3, 1, 4:8, 12
3, 1, 5:9, 15
3, 1, 6:10, 18

3, 2, 1:6, 6
3, 2, 2:7, 12
3, 2, 3:8, 18
3, 2, 4:9, 24
3, 2, 5:10, 30
3, 2, 6:11, 36
3, 3, 1:7, 9
3, 3, 2:8, 18
3, 3, 3:9, 27
3, 3, 4:10, 36
3, 3, 5:11, 45
3, 3, 6:12, 54
3, 4, 1:8, 12
3, 4, 2:9, 24
3, 4, 3:10, 36
3, 4, 4:11, 48
3, 4, 5:12, 60
3, 4, 6:13, 72
3, 5, 1:9, 15
3, 5, 2:10, 30
3, 5, 3:11, 45
3, 5, 4:12, 60
3, 5, 5:13, 75
3, 5, 6:14, 90
3, 6, 1:10, 18
3, 6, 2:11, 36
3, 6, 3:12, 54
3, 6, 4:13, 72
3, 6, 5:14, 90
3, 6, 6:15, 108
4, 1, 1:6, 4
4, 1, 2:7, 8
4, 1, 3:8, 12
4, 1, 4:9, 16
4, 1, 5:10, 20
4, 1, 6:11, 24
4, 2, 1:7, 8
4, 2, 2:8, 16
4, 2, 3:9, 24
4, 2, 4:10, 32
4, 2, 5:11, 40
4, 2, 6:12, 48
4, 3, 1:8, 12
4, 3, 2:9, 24
4, 3, 3:10, 36
4, 3, 4:11, 48
4, 3, 5:12, 60
4, 3, 6:13, 72
4, 4, 1:9, 16
4, 4, 2:10, 32
4, 4, 3:11, 48
4, 4, 4:12, 64
4, 4, 5:13, 80
4, 4, 6:14, 96
4, 5, 1:10, 20
4, 5, 2:11, 40
4, 5, 3:12, 60
4, 5, 4:13, 80
4, 5, 5:14, 100

4, 5, 6:15, 120
4, 6, 1:11, 24
4, 6, 2:12, 48
4, 6, 3:13, 72
4, 6, 4:14, 96

4, 6, 5:15, 120
4, 6, 6:16, 144
5, 1, 1:7, 5
5, 1, 2:8, 10
5, 1, 3:9, 15
5, 1, 4:10, 20
5, 1, 5:11, 25
5, 1, 6:12, 30
5, 2, 1:8, 10
5, 2, 2:9, 20
5, 2, 3:10, 30
5, 2, 4:11, 40
5, 2, 5:12, 50
5, 2, 6:13, 60
5, 3, 1:9, 15
5, 3, 2:10, 30
5, 3, 3:11, 45
5, 3, 4:12, 60
5, 3, 5:13, 75
5, 3, 6:14, 90
5, 4, 1:10, 20
5, 4, 2:11, 40
5, 4, 3:12, 60
5, 4, 4:13, 80
5, 4, 5:14, 100

5, 4, 6:15, 120
5, 5, 1:11, 25
5, 5, 2:12, 50
5, 5, 3:13, 75
5, 5, 4:14, 100
5, 5, 5:15, 125
5, 5, 6:16, 150
5, 6, 1:12, 30
5, 6, 2:13, 60
5, 6, 3:14, 90

5, 6, 4:15, 120
5, 6, 5:16, 150
5, 6, 6:17, 180
6, 1, 1:8, 6
6, 1, 2:9, 12
6, 1, 3:10, 18
6, 1, 4:11, 24
6, 1, 5:12, 30
6, 1, 6:13, 36
6, 2, 1:9, 12
6, 2, 2:10, 24
6, 2, 3:11, 36
6, 2, 4:12, 48
6, 2, 5:13, 60
6, 2, 6:14, 72
6, 3, 1:10, 18
6, 3, 2:11, 36
6, 3, 3:12, 54
6, 3, 4:13, 72
6, 3, 5:14, 90
6, 3, 6:15, 108
6, 4, 1:11, 24
6, 4, 2:12, 48
6, 4, 3:13, 72
6, 4, 4:14, 96

6, 4, 5:15, 120
6, 4, 6:16, 144
6, 5, 1:12, 30
6, 5, 2:13, 60
6, 5, 3:14, 90

6, 5, 4:15, 120
6, 5, 5:16, 150
6, 5, 6:17, 180
6, 6, 1:13, 36
6, 6, 2:14, 72
6, 6, 3:15, 108
6, 6, 4:16, 144
6, 6, 5:17, 180
6, 6, 6:18, 216

 

このようなテーブルを作成しておき、D1、D2とD3の値から検索するテーブルによる判定の他、3つの数字の順番に関係しないユニークな条件を何か思いつきましたか?

次回は(最適か否かは別として)BCCSkeltonでとったアルゴリズムを紹介しましょう

 

チンチロリンゲームの構想は、プレーヤークラスの設計が重要なので、何を要素とするか考えましたが、それを実戦で検証することも同時に行わないと効率的な開発はできません。そんなこんなでグルグルグルグルと思考が循環する為、先ずは先日C#のスタンドアローン(exeファイル)で作ったBCCSkeltonのDice移植版をDLLに纏めてみました。

 

1.リソース

Diceコントロールには「1」から「6」の目までのビットマップと、投賽の時のサイコロ音(BCCSkeltonのDiceでは二つでしたが、少なくとも一つ)が必要です。

この6つのビットマップを2つのwaveファイルを自作のResWriterでDice.resourcesファイルにします。

ちゃんと書けたか、自作のResReaderでチェックすると、

ビットマップはBitmapクラスとして、waveファイルはByte変数の配列(Byte[])としてDiceResources.resourcesファイルに書かれました。

 

2.Dice.cs

Diceに何をさせたいかというと、前に書いたように、BCCSkeltonで書いたDice.dllと同様の動作をさせたい訳です。それを念頭に次のコードをご覧ください。

///////////////////////////////
//Diceコントロール(Dices.dll)
///////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Resources;            //ResourceManagerを使う為
using System.IO;                //Streamを使う為

namespace Dices
{
    public class Dice : Button        //解説:ボタンコントロールから派生させる(注)ことで作成を簡単にしています。
    {                                           //注:このようにするとカスタムコントロールが簡単に作れます。
        //賽の目(解説:出た目は1-6なので整数にします。)
        public int roll;
        //ビットマップ(解説:賽の目をビットマップ配列に入れます。)
        Bitmap[] bmpD = new Bitmap[6];
        //コンストラクター
        public Dice()
        {
            //このDLLのアッセンブリーを取得する
            System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
            //ダイスの目のイメージを読み込む
            ResourceManager rm = new ResourceManager("Dice", asm);    //ResourceManagerインスタンスの作成
            bmpD[0] = (Bitmap)rm.GetObject("D1");
            bmpD[1]= (Bitmap)rm.GetObject("D2");
            bmpD[2] = (Bitmap)rm.GetObject("D3");
            bmpD[3] = (Bitmap)rm.GetObject("D4");
            bmpD[4] = (Bitmap)rm.GetObject("D5");
            bmpD[5] = (Bitmap)rm.GetObject("D6");
            //さいころの初期値は1
            //this.Image = bmpD[0];    //イメージレイアウトが指定できない
            this.BackgroundImageLayout = ImageLayout.Zoom;
            this.BackgroundImage = bmpD[0];
            //押し下げられた時の処理
            this.Click += Dice_Click;
        }
        //デストラクター
        ~Dice()
        {
            //ビットマップを廃棄(解説:C#はいずれガーベージコレクションで廃棄するのでしょうが、明示的に廃棄します。)
            bmpD[0].Dispose();
            bmpD[1].Dispose();
            bmpD[2].Dispose();
            bmpD[3].Dispose();
            bmpD[4].Dispose();
            bmpD[5].Dispose();
        }
        //Diceがクリックされた時の処理
        private void Dice_Click(object sender, EventArgs e)
        {
            //投賽音
            DiceSound();    //解説:先ず投賽音を鳴らします。
            //賽を振る
            Roll();
        }
        //乱数を使用してさいころの目を返す
        public int Roll()
        {
            //乱数の初期化(解説:毎回同じ乱数配列にならないよう実施時間で変えます。)
            Random rand = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);
            for(int i = 0; i < 10; i++)
            {
                roll = rand.Next(6);
                this.BackgroundImage = bmpD[roll];
                this.Refresh();                        //Invalidate() + Update()の効果がある。

                //解説:この"this.Refresh();"がないと賽の目がくるくると変化しません。
                System.Threading.Thread.Sleep(50);    //好みに応じてwaitを掛ける。
            }
            return roll + 1;
        }
        //Dice Roll時の音
        //解説:何も書かないとtrueで"DiceSound1.wav"、falseだと"DiceSound2.wav"になります。
        public void DiceSound(bool res = true)

        {
/*         //解説:外部waveファイルから読み取るにはこのように書きます。
            string soundl_file = "DiceSound1.wav";
            if(!res) soundl_file = "DiceSound2.wav";
            //サウンドプレイヤーにwavファイルを読み込む
            System.Media.SoundPlayer player = new System.Media.SoundPlayer(soundl_file);
*/

            //このプログラムのアッセンブリーを取得する
            System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
            //Dice.resourcesを読み込む
            ResourceManager rm = new ResourceManager("Dice", asm);    //ResourceManagerインスタンスの作成
            //Waveファイル用Streamクラスインスタンスsound
            Stream sound;
            if(res)
                sound  = new MemoryStream((byte[])rm.GetObject("DiceSound1"));
            else
                sound  = new MemoryStream((byte[])rm.GetObject("DiceSound2"));
            //サウンドプレイヤーにWaveストリームを読み込む
            System.Media.SoundPlayer player = new System.Media.SoundPlayer(sound);
            //解説:メディアプレイヤーのこの手軽さがC#の本領発揮というところでしょう。

            //非同期再生する
            player.Play();
            //使用後サウンドプレイヤーを破棄する(解説:ここでも明示的にメモリーを開放します。)
            player.Dispose();
            //ストリームを廃棄(解説:ここでも明示的にメモリーを開放します。)
            sound.Dispose();
        }
    }
}

たったこれだけでDiceコントロールを作るDice.dllが出来ます。なお、コンパイルする際にはdllになるようにライブラリーを選択してください。

 

では、作成したDice.dllが正しく動くかどうかテストしましょう。Diceコントロールを使うC#プログラム(TestDice.cs-これは次回で!)を用意し、動かします。

単体のサイコロをクリックしても、「投賽」ボタンをクリックしても賽の目が10回クルクルと廻り、目が出ます。

成功!なーんて簡単なんだろう、C#って?

 

何とかMazeに形をつけたので、新しいテーマに取り組もうと思います。それは、

Diceこの間のブログ)

です。

 

その後、webで調べてみると既にゲーム化しているもの(注1)、webシミュレーションもあり、「なんでこんなアンダーグランドのサイコロ賭博(注2)が若い人まで結構知られているのだろうか?」と思いましたが、その理由は(私は知らなかったのですが)賭博漫画に「カイジ」という有名なものがあり、それが若い人がチンチロを知らしめる契機になったようです。

注1:これ。講評が見るも無残。

注2:私は1978年に、阿佐田哲也著「麻雀放浪記(青春篇)」を読んで、初めて知りました。

 

PCゲーム化についていえば、BCCSkeltonのサンプルとして"Dice"

 

を作りましたが、これは単に「プレイヤーの、一回の対戦勝負をシミュレートする」だけなので、何度もやろうという気が起こるものではありませんでした。(注)

注:元々このDiceは「BCCSkeltonでカスタムコントロールを作れるか、どう作るのか」を解説する為に作ったものでした。

 

今回C#で「BCCSkeltonのカスタムコントロールであるDice」と同様のものを作りましたので、

ちゃんとしたゲームを作ろうかな、と考えたものです。

 

しかし、「ちゃんとしたゲーム」として考えると、「オープニングで賭場に入り」、「開帳するチンチロの場のプレーヤーを特定し」、「親決めがあり」、「サイコロを振って親と子方の対戦が続き」、「親になったり、親落ちしたり」して、「成功したり、破産したり」するものを考えますが、赤字部分で動画的興味を出せない(注)」以上、ゲームの内容として興味深いものにしなければ、成功しないと考えました。

注:例えばオープニングでは「掘っ立て小屋に入ってゆく」、勝負では「丼の中で三つのサイコロが飛び跳ねて目を出す」などを動画で表示すると結構面白いのですが、これはかなり大変なリソースサイズと動画のCPU負荷がかかること、そして「私にはまったく絵心がない」ことが致命的理由として挙げられます。

 

どうするか?

 

矢張りプレーヤー(人間)と敵方プレーヤー(PC)との対戦とし、乱数での偶然性の他、対戦相手の特性を偶発的勝敗要因とすることが必要であると考えています。また、プレーヤー毎の対戦結果が歴史的に記録されて分析ができるようになるとそれもまた新しい面白さになるかもしれません。(更に可能であれば、PCプレーヤーの他、人間プレーヤーを複数にして対戦することも面白いといえば面白いでしょう。しかし、これをインターネットを通じたリモート対戦にすると私の技量をはるかに超えてしまいます。)

 

やるかどうか、出来るかどうかは別とし、

 

プレーヤークラス(Class)を作成し、「(ツキ)」、「(自分の運を認知して、それに乗る)技量(注)」、「(技量と相関がある)強気弱気(注)」、「種銭と賭け銭」が勝負を経験するごとに動的に変化し、「親での対戦回数」「子での対戦回数」、「それぞれの勝敗歴」等々をメンバーとして持たせ、statisticsを作成すると面白いかもしれませんね。

注:博打や勝負をやったことがある人はわかると思いますが、対戦には必ず偶発的要素があり、それが自分の追い風になっているか、向かい風になっているかを認知すること、追い風の場合はそれに載って強気で勝負すること、向かい風の場合には怪我をしないように保守的にしのぐことが肝要ですが、これらをPCプレイヤーの擬人的要素として取り入れる、ということです。

 

まぁ、ボチボチ、しこしことゆっくりやりますわ。

 

さて、前回の最後に「『このMaze_DLL.dllとTestMaze.exeを機能改良する例』としてもう一回程度お付き合い下さい」と書いたので、追加仕様を実装しようとして結構苦戦しました。
すったもんだの挙句、何とか完成したので、変更点についてコメント、解説してゆきます。

【変更点-ファイル:Maze_DLL.cs】
<追加>
using System.IO;        //StreamReader/Writerを使う為
using System.Text;     //Encoderを使う為
//解説:これはファイルを扱う為に必須でした。

<追加>
/////////////////
//迷路関連データ
/////////////////
//迷路データの中断ファイル

const string mzFileName = "Maze.tmp";
//解説:C++の"define"による定数定義の代わりに、C#では変更できないconst変数を使います。

//迷路探索者の進行方向
public int mzWhereToGo;        //上-0、右-1、下-2、左-3

//解説:最初「名無し(private)」にして、初期設定を1(右)にしていましたが、中断再開時には異なることもあることから、publicに変更し、初期値はコンストラクターで明示的に決定するようにしました。しかし、このメンバーに直接アクセスしたらコンパイラーに怒られたので別途アクセスメソッドを設けたため、privateのままでよかったように思います。

<追加>
////////////////////////////////////////////////////
//コンストラクタ(引数がない場合の初期値は41 x 41)
////////////////////////////////////////////////////
//迷路情報を初期化

if(!IniMaze())    //中断用設定ファイルがあればそれを使う
{    //なければオリジナル通りの設定となる
    //迷路サイズの設定
    mzWidth = width;
    mzHeight = height;
    MazeData = new int[mzWidth, mzHeight];
    //迷路探索者の初期位置
    mzWhereIam = new Cell();
    mzWhereIam.X = 1;
    mzWhereIam.Y = 1;
    //迷路出口の初期位置
    mzExit = new Cell();
    mzExit.X = mzWidth - 2;
    mzExit.Y = mzHeight - 2;
    //迷路内の進行方向
    mzWhereToGo = 1;    //初期値は右(1)
}
//迷路作成用Cellリスト(解説:上記の条件式から外した)
StartCells = new List<Cell>();

//解説:IniMazeというプログラム中断時のパラメーターを記録したファイル(中断ファイル)を読みだして設定するメソッドを追加したので、それで設定できない場合のみオリジナル仕様の設定(↑)を行い、進行方向の初期設定も行うことにしました。なお、いずれもStartCellsリストは作るのでこれは外出ししました。

<追加>
///////////////////////////
//Mazeの進行方向を取得する
///////////////////////////

public int GetDir()
{
    return mzWhereToGo;
}
//解説:もともとは常に白紙で、右向きで発進する仕様だったのですが、中断ファイルで右以外の方向もあり得るようになったので、↑で「別途アクセスメソッドを設けた」と書いたように、Mazeコントロールを使うアプリが進行方向のデータをMazeから取得できるようにこのメソッドを追加しました。

<追加>
///////////////
//迷路生成処理
///////////////

public int[,] GenerateMaze()
{
    //既に壁データがあれば戻る(解説:中断ファイルを読み込んだ場合は、新たな迷路は作らない為)
    if(MazeData[0, 0] == Wall)
        return null;
//解説:オリジナル仕様では毎回Mazeコントロール(クラスインスタンス)が生成されるたびに一回限りの迷路を作っていたのですが、中断する場合は新規作成する必要がないので、「(常に存在する)左上の壁部分があれば何もしない」で戻る仕様にしました。

<追加>
/////////////////////////////
//Maze.tmpファイルを読み込み
//迷路を初期化する
/////////////////////////////

private bool IniMaze()
{
    string data = "";
    //ファイル(UTF-8:Encoding.UTF8)を読む
    try
    {
        using(StreamReader file = new StreamReader(mzFileName, Encoding.UTF8))
        {
            data = file.ReadToEnd();
            // ファイルを閉じる
            file.Close();
        }
    }
    catch(Exception e)    //FileNotFoundException e
    {
        //Debug用-引数のeを使わないので、コンパイラーから警告が出されます。
        //MessageBox.Show("エラーコード:" + e.ToString(), "初期化ファイル読み込みエラー", MessageBoxButtons.OK, MessageBoxIcon.Error);

        return false;
    }
    //文字列の行分割
    string[] lines = data.Split(new[]{"\r\n","\n","\r"},StringSplitOptions.None);
    //第1行(迷路の幅と高さ)の語分割
    string[] widthheight = lines[0].Split(',');
    if(!Int32.TryParse(widthheight[0], out mzWidth))
    {
        MessageBox.Show("mzWidthの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    if(!Int32.TryParse(widthheight[1], out mzHeight))
    {
        MessageBox.Show("mzHeightの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    //迷路の作成
    MazeData = new int[mzWidth, mzHeight];
    //第2行(UNIT、mzIs3D、mzWhereToGo)の語分割
    string[] others = lines[1].Split(',');
    if(!Int32.TryParse(others[0], out UNIT))
    {
        MessageBox.Show("UNITの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    if(!Boolean.TryParse(others[1], out mzIs3D))
    {
        MessageBox.Show("mzIs3Dの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    if(!Int32.TryParse(others[2], out mzWhereToGo))
    {
        MessageBox.Show("mzWhereToGoの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    //第3行(迷路探索者の位置)の語分割
    string[] where = lines[2].Split(',');
    mzWhereIam = new Cell();
    int x, y;    //TryParse用変数
    if(Int32.TryParse(where[0], out x))
        mzWhereIam.X = x;
    else
    {
        MessageBox.Show("mzWhereIam.Xの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    if(Int32.TryParse(where[1], out y))
        mzWhereIam.Y = y;
    else
    {
        MessageBox.Show("mzWhereIam.Xの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    //第4行(迷路出口の位置)の語分割
    string[] exitpos = lines[3].Split(',');
    mzExit = new Cell();
    if(Int32.TryParse(exitpos[0], out x))
        mzExit.X = x;
    else
    {
        MessageBox.Show("mzExit.Xの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    if(Int32.TryParse(exitpos[1], out y))
        mzExit.Y = y;
    else
    {
        MessageBox.Show("mzExit.Yの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        return false;
    }
    //第5行以降(迷路データ)の語分割
    for(int i = 0; i < mzHeight; i++)
    {
        string[] dt = lines[4 + i].Split(',');
        for(int j = 0; j < mzWidth; j++)
        {
            if(!Int32.TryParse(dt[j], out MazeData[i, j]))
            {
                MessageBox.Show("MazeDataの初期化に失敗しました。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
        }
    }
    return true;
}
/////////////////////////////
//Maze.tmpファイルを書き込み
//中断する処理
/////////////////////////////

public void Abort()
{
    //第1行(迷路の幅と高さ)
    string data = mzWidth.ToString() + "," + mzHeight.ToString() + Environment.NewLine;
    //第2行(UNIT、mzIs3D、mzWhereToGo)
    data += UNIT.ToString() + "," + mzIs3D.ToString() + "," + mzWhereToGo.ToString() + Environment.NewLine;
    //第3行(迷路探索者の位置)
    data += mzWhereIam.X.ToString() + "," + mzWhereIam.Y.ToString() + Environment.NewLine;
    //第4行(迷路出口の位置)
    data += mzExit.X.ToString() + "," + mzExit.Y.ToString() + Environment.NewLine;
    //第5行以降(迷路データ)
    for(int i = 0; i < mzHeight; i++)
    {
        int j = 0;
        while(j < mzWidth - 1)
        {
            data += MazeData[i, j].ToString() + ",";
            j++;
        }
        data += MazeData[i, j].ToString() + Environment.NewLine;
    }
    //ファイル(UTF-8:Encoding.UTF8)を書く
    using(StreamWriter file = new StreamWriter(mzFileName, false, Encoding.UTF8))
    {
        //ファイルを書く
        file.Write(data);
        // ファイルを閉じる
        file.Close();
    }
}
/////////////////////////////
//Maze.tmpファイルを削除する
/////////////////////////////

public void DeNovo()
{
    //ファイルの削除
    File.Delete(mzFileName);
}

//解説:この3つの中断ファイル操作メソッドは「中断ファイルを読む(IniMaze)」、「中断ファイルを作成する(Abort)」、「中断ファイルを削除する(DeNovo-注)」動作をします。やっている内容は、極めてプリミティブな「変数数値の文字列化/文字列の数値化とファイル読み込み/書き込み」だけで、(ファイルを扱う場合はエラーが生じることがあるのでusingやtry~catch~finallyを使うこと位が注意点で)使用されているメソッドも特殊なものはありませんので、コメントを読んでいただければわかると思います。

注:ラテン語で「新たに」「再び」という意味です。

なお、ファイルが書き込む内容は、
 第1行:迷路幅、迷路高さ
 第2行:UNIT値、表示方法(2D/3D)、進行方向
 第3行:迷路探索者の位置
 第4行:出口の位置
 第5行~:迷路配列(MazeData)のWall(1)とPath(0)のデータ
です。以下の例を参照してください。(勿論暗号化したり、バイナリーで見えないように書いてもよいのですが、遊びのサンプルなので、これをいじくってチートできるようにテキストにしていること、前に書いた通りです。)
【初期状態のMaze.tmpの例】
41,41
16,True,1
1,1
39,39
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1
1,0,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1
1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1
1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1
1,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1
1,0,1,1,1,0,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,0,1
1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1
1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1
1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,1
1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1
1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1
1,0,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1
1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,1
1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,1,1
1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1
1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1
1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,1
1,1,1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,0,1
1,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0,1
1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,0,1,0,1,0,1,0,1
1,0,1,0,0,0,0,0,1,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1
1,0,1,1,1,1,1,1,1,0,1,0,1,0,1,1,1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1
1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,1
1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1
1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,0,0,1
1,0,1,0,1,0,1,0,1,1,1,1,1,0,1,1,1,1,1,1,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1
1,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,1
1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,1,1,1,1,0,1
1,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,1
1,0,1,0,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,0,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1
1,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1
1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,0,1,1,1,0,1
1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1
1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,1
1,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1
1,0,1,1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1
1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,1,0,1
1,1,1,1,1,0,1,0,1,1,1,1,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1,0,1,0,1,0,1
1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1


【変更点-ファイル:TestMaze.cs】
Maze_DLLの変更により、アプリケーション(以下アプリ)も変更が必要です。

<追加>
//迷路の初期化

private void InitMaze()
{
    if(maze != null)
        maze.Dispose();
    maze = new Maze();                //初期値の41 x 41の迷路で、660 x 660のサイズとなる
    maze.GenerateMaze();            //迷路を作る
    maze.Display(Is3D, WhereToGo = maze.GetDir());    //Mazeコントロールの進行方向を継承して、迷路を表示する

//解説:Mazeコントロールから進行方向の値を承継します。Maze.tmpファイルがない場合は初期値(右-1)となります。
    maze.Location = new Point(10, 10);
    maze.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
    this.Controls.Add(maze);
}
//解説:先ず、オリジナルでは進行方向の初期値が右だったので、アプリの進行方向データも初期値を右にしていましたが、今回の変更により、毎回変わる可能性があるので、コントロールの初期値を取得する(WhereToGo = maze.GetDir();)仕様にしました。

<追加>
//終了処理

protected override void OnFormClosing(FormClosingEventArgs e)
{
    base.OnFormClosing(e);
    DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
    if(dr == DialogResult.Yes)
    {
        dr = MessageBox.Show("中断して、後で再開する予定ですか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.Yes)
            maze.Abort();    //中断ファイルを作成する
        else
            maze.DeNovo();    //中断ファイルを削除する
    }
    else
    {
        dr = MessageBox.Show("新しい迷路にしますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.Yes)
        {
            e.Cancel = true;
            maze.DeNovo();    //中断ファイルを削除する
            InitMaze();
        }
        else
            e.Cancel = true;
    }
}

//解説:最初は「はい(Yes)、いいえ(No)、中断(Abort)」のボタンを使って「三択」にしようと思っていたのですが、C#ではないのと、今までは「単純に今やっている迷路探索を続ける」選択肢が無かったので、「四択」にしました。
最初に「終了するか、否か」聞いてきますので、これに「はい」を押すと、次に「中断後再開するか、否か」着てきます。

 実行終了(1)-中断すると中断ファイルが作られ、次のプログラム実行で読み込まれる。
 実行終了(2)-完全終了すると存在する中断ファイルも削除され、次のプログラム実行では新規迷路となる。
終了しない(「いいえ」)を押すと、そのまま継続するか、新規迷路にするか聞いてきます。

 実行継続(1)-中断ファイルを削除して、Mazeコントロールを更新するので、新規迷路でプログラムを継続することなる。
 実行終了(2)-何もしないので現在の迷路を継続する。


以上でした。自分で動かしていると、41 x 41の迷路でも結構時間がかかるので、途中で中断せざるを得ない局面がままあったことから、この改造を行いました。まぁ、仕様を変更してこっちのプログラムを組みなおすと、あっちも手を入れなくてはならない、というように、DLL、アプリ共に見直しが必要で結構手がかかることが分かりました。(爆)

ではでは、

これにて一件落着。

前回、フォームに張り付けると一応簡単な迷路を表示するMazeコントロールを作りましたので、それを使って実際に「単に迷路の中をウロウロするアプリ」を作ってみましょう。題して、"TestMaze.cs"です。いつもの通り、解説はコメント//解説:を見てください。

 

【TestMaze.cs】

///////////////////////////////////////
//迷路コントロールテストプログラム
///////////////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Reflection;                    //Assemblyを使う為

using Maze_DLL;                                //Mazeクラスを使う為
//解説:Maze_DLLはより多くのdllを使いますが、ここで書く必要はありません。

namespace TestMaze
{
    /////////////////////////////////////////////////////////////
    //AppFormクラス(迷路コントロールのテスト用フォーム)
    /////////////////////////////////////////////////////////////

    public partial class AppForm : Form
    {
        //表示切替フラグ
        bool Is3D = true;
        //進行方向
        int WhereToGo = 1;    //上-0、右-1、下-2、左-3
        //迷路コントロール(解説:これが今回作った迷路コントロールです。)
        Maze maze;
        //ボタンコントロール
        Button btnDisp, btnUp, btnDown, btnLeft, btnRight, btnExit;

        [STAThread]    //解説:マルチスレッドアプリ以外はこれをつけましょう。
        public static void Main()    //解説:これが本アプリのエントリーポイントメソッドです。
        {
            AppForm ap = new AppForm();
            Application.Run(ap);
        }

        public AppForm()    //解説:本AppFormクラスのコンストラクターですね。
        {
            Assembly myOwn = Assembly.GetEntryAssembly();
            this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);    //プログラムアイコンをフォームにつける
            this.ClientSize = new Size(760, 682);                    //Mazeコントロールを660x660にする為
            this.MinimumSize = new Size(456, 401);                    //Mazeコントロールを340x340にする為
            this.Text = "C#による迷路サンプル";
            this.Load += AppForm_Load;
        }

        private void AppForm_Load(object sender, EventArgs e)
        {
            //表示切替ボタン
            btnDisp = new Button();
            btnDisp.Location = new Point(ClientSize.Width - btnDisp.Width - 6, 10);
            btnDisp.Text = "表示切替";
            btnDisp.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnDisp.Click += ButtonDisp_Click;
            this.Controls.Add(btnDisp);

            //↑ボタン
            btnUp = new Button();
            btnUp.Size = new Size(24, 24);
            btnUp.Location = new Point(ClientSize.Width - 56, btnDisp.Height + 20);
            btnUp.Text = "↑";
            btnUp.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnUp.Click += ButtonUp_Click;
            this.Controls.Add(btnUp);

            //↓ボタン
            btnDown = new Button();
            btnDown.Size = new Size(24, 24);
            btnDown.Location = new Point(ClientSize.Width - 56, btnDisp.Height + 68);
            btnDown.Text = "↓";
            btnDown.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnDown.Click += ButtonDown_Click;
            this.Controls.Add(btnDown);

            //←ボタン
            btnLeft = new Button();
            btnLeft.Size = new Size(24, 24);
            btnLeft.Location = new Point(ClientSize.Width - 80, btnDisp.Height + 44);
            btnLeft.Text = "←";
            btnLeft.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnLeft.Click += ButtonLeft_Click;
            this.Controls.Add(btnLeft);

            //→ボタン
            btnRight = new Button();
            btnRight.Size = new Size(24, 24);
            btnRight.Location = new Point(ClientSize.Width - 32, btnDisp.Height + 44);
            btnRight.Text = "→";
            btnRight.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            btnRight.Click += ButtonRight_Click;
            this.Controls.Add(btnRight);

            //終了ボタン
            btnExit = new Button();
            btnExit.Location = new Point(ClientSize.Width - btnExit.Width - 6, ClientSize.Height - btnExit.Height - 12);
            btnExit.Text = "終了";
            btnExit.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            btnExit.Click += ButtonExit_Click;
            this.Controls.Add(btnExit);

            //迷路の初期化
            InitMaze();
        }

        //終了処理
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            DialogResult dr = MessageBox.Show("終了しますか?\r\n「いいえ」を押すと新しい迷路が始まります。", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.No)
            {
                e.Cancel = true;
                InitMaze();    //解説:「「いいえ」を押すと新しい迷路が始まります。」に対応する迷路の再初期化です。
            }
        }

        //表示切替ボタン
        private void ButtonDisp_Click(object sender, EventArgs e)
        {
            Is3D = !Is3D;    //解説:トグル処理です。
            maze.Display(Is3D, WhereToGo);
        }

        //↑ボタン
        private void ButtonUp_Click(object sender, EventArgs e)
        {
            MoveInMaze(0);
        }

        //→ボタン
        private void ButtonRight_Click(object sender, EventArgs e)
        {
            MoveInMaze(1);
        }

        //↓ボタン
        private void ButtonDown_Click(object sender, EventArgs e)
        {
            MoveInMaze(2);
        }

        //←ボタン
        private void ButtonLeft_Click(object sender, EventArgs e)
        {
            MoveInMaze(3);
        }

        //終了ボタン
        private void ButtonExit_Click(object sender, EventArgs e)
        {
            //終了処理
            Close();
        }

        //迷路の初期化
        private void InitMaze()
        {
            if(maze != null)
                maze.Dispose();                  //解説:新しい迷路を作る際に一旦旧いMazeコントロールを廃棄します。
            maze = new Maze();               //初期値の41 x 41の迷路で、660 x 660のサイズとなる
            maze.GenerateMaze();            //迷路を作る
            maze.Display(Is3D, WhereToGo);    //迷路を表示する(表示は3D、右方向に進行します。)
            maze.Location = new Point(10, 10);
            maze.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
            this.Controls.Add(maze);
        }

        //↑、↓、←、→ボタンを押した際に向いている方向に合わせて迷路を移動する
        private void MoveInMaze(int dir)    //dir:上-0、右-1、下-2、左-3
        {
            if(Is3D)    //解説:3D表示
                WhereToGo = (WhereToGo + dir) % 4;    //相対方向
            else         //解説:2D表示
                WhereToGo = dir;                    //絶対方向
//解説:BCCSkeltonのMazeの時にはコンピューターの探索者が動くので「総てが絶対方向でプログラミング」でしたが、今回は「2D表示の場合は常に絶対方向ですが、3D表示の場合は人間が矢印ボタンで操作する場合、『右を向いて前進ボタンを押す("相対的前進"をする)と、絶対方向は右』」になります。その為、現在の進行方向に対して相対的な0~3を加算して、その剰余を取る形で絶対方向に変換するようにしました。

            switch(WhereToGo)    //解説:Mazeクラスの”Go~"メソッドは壁や迷路外にはゆけないようになっています。
            {
                case 0:    //Up
                    maze.GoUp();
                    break;
                case 1:    //Right
                    maze.GoRight();
                    break;
                case 2:    //Down
                    maze.GoDown();
                    break;
                case 3:    //Left
                    maze.GoLeft();
                    break;
            }
            maze.Display(Is3D, WhereToGo);    //解説:移動後の現在地、方向に基づいて再表示します。

            if(IsExit())                                     //解説:その際に迷路出口に達していた場合の処理です。
                Close();                                    //解説:必ずしもClose()にしなくてもよく、独立した処理をつけても結構です。
            //解説:IsExit()メソッド以外の「罠にはまった」「落ちている宝を見つけた」などのイベントを追加しましょう。
        }

        //出口に達した場合の処理
        private bool IsExit()
        {
            if(maze.IsExit())
            {
                MessageBox.Show("Congratulations! You've got out of the maze!", "You Made It!!!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                return true;
            }
            return false;
        }
    }
}

 

どうでしょう?

Mazeクラスコントロールに機能を詰め込むと、それを使うアプリの方の負担は可也小さくなりますね。

//解説:」でも書きましたが、この「単にウロウロする」アプリに、

「罠」、「宝」、「敵」、迷路も「多層階」になる

とそれだけでRPGっぽくなってきますね。この場合、探索者(プレイヤー)の記録情報も多くなるので

クラス化して多人数探索

のスタート地点にするのもよいかもしれません。アイデアは色々とありますが、私はもう余り「RPGゲーマー」ではないので、示唆だけにとどめます。

 

ということで、

【迷路】シリーズはこれで終わり

と思っていたのですが、「単にウロウロするだけでももう少し便利に」とプログラミング感性が夜中に働き、この爺でさえ次のようなアイデアが浮かびました。(夜中にスマホからpcのメアドへメールを送る爺でした。)

現実にどこまでやるか、は別とし、「このMaze_DLL.dllとTestMaze.exeを機能改良する例」としてもう一回程度お付き合い下さい。

 

【夜中のメールメモ】

1.(忙しくて探索を完了できない人の為に、中断機能として)Mazeコントロールに自動保存、読み込み機能を入れる。
2.コンストラクターに読み込み機能を入れて、自動読み込みファイルがあれば、初期設定をこれで行う。
3.迷路プログラムの終了時の確認を3択にし、「はい、中断、いいえ(新規迷路)」を選ぶ形にし、はいの時は自動読み込みファイルを削除し、中断の時に現在のデータを書き込む。

ファイルのデータは簡単に、

 

Unit

幅、高さ

探索者の方向

迷路データ(幅x高さの形で、0ー通路、1ー壁、2ー探索者、3ー出口のデータを出力する。)

とし、これにより将来4や5で(宝や敵を表す)他のデータを書き込むこともできるし、エディターで(壁を抜く等)チートも出来る。


なお、このファイルは Streamwriter、StreamReaderで文字列の読み書きをし、データはString.Splitを使って切り出す。
 

この程度は追加しましょうか?

途中からDiceに嵌りかけたので、先ずはMazeを完全に終わらせるため、最後にプログラムの解説を(調理っぽく)行います。

 

1.準備

先ずは、リソース画像

を整え、"Maze.resources"ファイルを作ります。(注)

注:私はVisual Studioを使っていないので、自作のResWriter

でリソースファイルを作ります。(なお、参考までにResWriter.csはBCCForm and BCCSkeltonのSampleBCCSkelton\MSCompass\Debug\Sample内に入っています。)

 

2.ソース

ソースファイルは長いので末尾に載せました。コメントの他、所々解説を付けましたので、参考にしてください。

 

3.コンパイル

MSCompAssのコンパイルオプションを開いてください。

ターゲットをライブラリーファイル(*.dll)にして、リソースファイルを"Maze.resources"に指定し、プログラムアイコン(注)

をつけます。参照ファイルにAtelier.dllを指定することをお忘れなく。なお、私は警告レベルは最高、最適化も指定しています。ご参考までにMSCompAssのoptファイルを載せます。

【Maze_DLL.optファイル】

[Compile Option]
Target=3
Resource=1
RscFile=C:\Users\(略)\Maze\Maze.resources
IconFile=
DbgOpt=2
WarnErr=5
RefFile=C:\Users\(略)\Maze\Atelier.dll
Others=

 

これでMaze_DLL.dllが完成です。

 

【末尾資料:Maze_DLL.cs】

/////////////////////////////////////////////////////////
//迷路ゲームプログラム
//作成:棒倒し法、穴開け法、壁伸ばし法
//解法:https://ja.wikipedia.org/wiki/%E8%BF%B7%E8%B7%AF
/////////////////////////////////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Resources;               //ResourceManagerを使う為
using System.Collections.Generic;  //Listを使う為
using Atelier;                                //Atelier.dllを使う為

namespace Maze_DLL
{
    //////////////////////////////////////////////////////////////
    //        迷路クラス(迷路コントロールをDLL化したもの)
    //使い方:インスタンスをコントロールとしてフォームに付加する。
    //////////////////////////////////////////////////////////////

    public class Maze : Panel    //PictureBoxの枠となるPanelから派生
    {
        //////////////////////////
        //クラス、変数、プロパティ
        //////////////////////////
        //通路・壁情報

        const int Path = 0;
        const int Wall = 1;
        //セル情報
        private class Cell
        {
            public int X {get; set;}
            public int Y {get; set;}
        }
        //方向
        const int dirUp = 0;
        const int dirRight = 1;
        const int dirDown = 2;
        const int dirLeft = 3;
        /////////////////
        //迷路関連データ
        /////////////////
        //迷路の基準規模

        private int UNIT = 16;                //壁、通路一単位のピクセル数(Bitmapオリジナルは16 x 16)
        private int mzWidth, mzHeight;   //迷路のサイズ
        //2次元配列の迷路情報
        private int[,] MazeData;
        //穴掘り開始候補座標
        private List<Cell> StartCells;
        //迷路表示切替フラグ
        bool mzIs3D = true;
        //迷路出口の位置
        Cell mzExit;
        //迷路探索者の位置
        Cell mzWhereIam;
        //迷路探索者の進行方向
        int mzWhereToGo = 1;    //上-0、右-1、下-2、左-3
        ///////////////////////
        //迷路関連コントロール
        ///////////////////////

        private PictureBox picBox;        //ピクチャーボックスコントロール
        private Easel easel;                 //描画を行うEaselオブジェクト
        ///////////////////////////
        //迷路関連ビットマップ画像
        ///////////////////////////

        private Bitmap bmpBack, bmpBall, bmpBrick, bmpExit;
        ////////////////////////////////////////////////////
        //コンストラクタ(引数がない場合の初期値は41 x 41)
        ////////////////////////////////////////////////////

        public Maze(int width = 41, int height = 41)
        {
            //幅、高さが5未満のサイズはエラーで中断させる
            if(width < 5 || height < 5)
            {
                throw new ArgumentOutOfRangeException();
            }
            //幅、高さが偶数の場合は強制的に+1で奇数に変更する
            if(width % 2 == 0)
                width++;
            if(height % 2 == 0)
                height++;
            //迷路情報を初期化
            mzWidth = width;
            mzHeight = height;
            MazeData = new int[mzWidth, mzHeight];
            StartCells = new List<Cell>();
            //迷路探索者の初期位置
            mzWhereIam = new Cell();
            mzWhereIam.X = 1;
            mzWhereIam.Y = 1;
            //迷路出口の初期位置
            mzExit = new Cell();
            mzExit.X = mzWidth - 2;
            mzExit.Y = mzHeight - 2;
            if(mzWidth != 41 || mzHeight != 41)
                mzSetUNIT();                    //デフォルト初期値でない場合、UNIT値を変更する必要がある
            //コントロール本体(好みで変更可)
            this.BorderStyle = BorderStyle.Fixed3D;
            this.AutoScroll = true;                //スクロールバー付
            this.Width = mzWidth * UNIT + 4;    //デフォルト初期値は660(656 + 4)
            this.Height = mzHeight * UNIT + 4;    //デフォルト初期値は660(656 + 4)
            //描画先とするEaselオブジェクトを作成する
            easel = new Easel(this.ClientSize.Width, this.ClientSize.Height);
            //ピクチャーボックスコントロール
            picBox = new PictureBox();            //枠の中のPictureオブジェクトの作成
            picBox.Location = new Point(0, 0);    //枠内の位置
            picBox.SizeMode = PictureBoxSizeMode.AutoSize;    //画像サイズでPictureBoxの大きさが変更される
            picBox.Image = easel.Canvas;        //PictureBoxにEaselオブジェクトを貼り付ける
            //Maze(Panel)にPictureBoxを追加
            this.Controls.Add(picBox);            //picBoxをpanelの子にする
            //Mazeサイズ変更時の処理
            this.SizeChanged += mzSizeChanged;
            //このプログラムのアッセンブリーを取得する
            System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
            //ResourceManagerインスタンスの作成
            ResourceManager rm = new ResourceManager("Maze", asm);
            //Bitmapを読み込む
            bmpBack = (Bitmap)rm.GetObject("Back");
            bmpBall = (Bitmap)rm.GetObject("Ball");
            bmpBall.MakeTransparent(Color.Black);
            bmpBrick = (Bitmap)rm.GetObject("Brick");
            bmpExit = (Bitmap)rm.GetObject("Exit");
            bmpExit.MakeTransparent(Color.Black);
        }
        ///////////////
        //デストラクタ
        ///////////////

        ~Maze()
        {
            bmpBack.Dispose();
            bmpBall.Dispose();
            bmpBrick.Dispose();
            bmpExit.Dispose();
        }
        /////////////////////////////
        //Mazeサイズが変更された場合
        /////////////////////////////

        private void mzSizeChanged(object sender, EventArgs e)
        {
            mzSetUNIT();
            easel.Resize(this.ClientSize.Width, this.ClientSize.Height);    //解説:Mazeの画像ビットマップのリサイズ
            Display(mzIs3D, mzWhereToGo);    //解説:Mazeの画像をリサイズして再描画
            picBox.Image = easel.Canvas;
        }
        /////////////////////////////////
        //Mazeのクライアントエリアサイズ
        //に基づきUNITを決定する
        /////////////////////////////////

        private void mzSetUNIT()
        {
            int w = this.ClientSize.Width / mzWidth;
            int h = this.ClientSize.Height / mzHeight;
            UNIT = (w < h) ? w : h;        //Maze内に表示が収まるように小さい方を選択
        }
        /////////////////////////
        //Mazeのサイズを取得する
        /////////////////////////

        public Size mzGetSize()
        {
            return new Size(mzWidth, mzHeight);
        }
        ///////////////
        //迷路生成処理
        ///////////////

        public int[,] GenerateMaze()
        {
            //全てを壁で埋める
            //穴掘り開始候補(x,yともに偶数)座標を保持しておく

            for(int y = 0; y < this.mzHeight; y++)
            {
                for(int x = 0; x < this.mzWidth; x++)
                {
                    if(x == 0 || y == 0 || x == this.mzWidth - 1 || y == this.mzHeight - 1)
                    {
                        MazeData[x, y] = Path;  // 外壁は判定の為通路にしておく(最後に戻す)
                    }
                    else
                    {
                        MazeData[x, y] = Wall;
                    }
                }
            }
            //穴掘り開始
            Dig(1, 1);
            //外壁を戻す
            for(int y = 0; y < this.mzHeight; y++)
            {
                for(int x = 0; x < this.mzWidth; x++)
                {
                    if(x == 0 || y == 0 || x == this.mzWidth - 1 || y == this.mzHeight - 1)
                    {
                        MazeData[x, y] = Wall;
                    }
                }
            }
            return MazeData;
        }
        /////////////////////////////
        //迷路の座標(x, y)に穴を掘る
        /////////////////////////////

        private void Dig(int x, int y)
        {
            //指定座標から掘れなくなるまで堀り続ける
            Random rnd = new Random();
            while(true)
            {
                //掘り進めることができる方向のリストを作成
                List<int> directions = new List<int>();
                if(this.MazeData[x, y - 1] == Wall && this.MazeData[x, y - 2] == Wall)
                    directions.Add(dirUp);
                if(this.MazeData[x + 1, y] == Wall && this.MazeData[x + 2, y] == Wall)
                    directions.Add(dirRight);
                if(this.MazeData[x, y + 1] == Wall && this.MazeData[x, y + 2] == Wall)
                    directions.Add(dirDown);
                if(this.MazeData[x - 1, y] == Wall && this.MazeData[x - 2, y] == Wall)
                    directions.Add(dirLeft);
                //掘り進められない場合、ループを抜ける
                if(directions.Count == 0) break;
                //指定座標を通路とし穴掘り候補座標から削除
                SetPath(x, y);
                //掘り進められる場合はランダムに方向を決めて掘り進める
                int dirIndex = rnd.Next(directions.Count);
                //決まった方向に先2マス分を通路とする
                switch(directions[dirIndex])
                {
                    case dirUp:
                        SetPath(x, --y);
                        SetPath(x, --y);
                        break;
                    case dirRight:
                        SetPath(++x, y);
                        SetPath(++x, y);
                        break;
                    case dirDown:
                        SetPath(x, ++y);
                        SetPath(x, ++y);
                        break;
                    case dirLeft:
                        SetPath(--x, y);
                        SetPath(--x, y);
                        break;
                }
            }
            //どこにも掘り進められない場合、穴掘り開始候補座標から掘りなおし
            //候補座標が存在しないとき、穴掘り完了

            Cell cell = GetStartCell();
            if(cell != null)
            {
                Dig(cell.X, cell.Y);
            }
        }
        ///////////////////////////////////////////////////
        //座標を通路とする(穴掘り開始座標候補の場合は保持)
        ///////////////////////////////////////////////////

        private void SetPath(int x, int y)
        {
            this.MazeData[x, y] = Path;
            if(x % 2 == 1 && y % 2 == 1)
            {
                //穴掘り候補座標
                StartCells.Add(new Cell() { X = x, Y = y });
            }
        }
        /////////////////////////////////////
        //穴掘り開始位置をランダムに取得する
        /////////////////////////////////////

        private Cell GetStartCell()
        {
            if(StartCells.Count == 0) return null;

            //ランダムに開始座標を取得する
            Random rnd = new Random();
            int index = rnd.Next(StartCells.Count);
            Cell cell = StartCells[index];
            StartCells.RemoveAt(index);

            return cell;
        }
        /////////////////
        //迷路を表示する
        /////////////////

        public void Display(bool flag, int dir)
        {
            mzIs3D = flag;        //直近の表示フラグを記録
            mzWhereToGo = dir;    //直近の方向を記録
            if(flag)
                Display3D(mzWhereIam.X, mzWhereIam.Y, dir, 0, 0, this.ClientSize.Width, this.ClientSize.Height, 6);
            else
                Display2D();
        }
        /////////////////////
        //迷路を2Dで表示する
        /////////////////////

        public void Display2D()
        {
            easel.Clear();    //一旦白紙にする
            for(int y = 0; y < mzHeight; y++)        // 高さ
            {
                for(int x = 0; x < mzWidth; x++)    // 幅
                {
                    if(MazeData[x, y] == Wall)
                    {
                        easel.PutBMP(bmpBrick, x * UNIT, y * UNIT, x * UNIT + UNIT - 1, y * UNIT + UNIT - 1);
                    }
                }
            }
            easel.PutBMP(bmpExit, mzExit.X * UNIT, mzExit.Y * UNIT, mzExit.X * UNIT + UNIT - 1, mzExit.Y * UNIT + UNIT - 1);
            easel.PutBMP(bmpBall, mzWhereIam.X * UNIT, mzWhereIam.Y * UNIT, mzWhereIam.X * UNIT + UNIT - 1, mzWhereIam.Y * UNIT + UNIT - 1);
            picBox.Image = easel.Canvas;
        }
        ////////////////////////////////////////////////////
        //迷路を3Dで表示する(本関数は自らを再帰処理する)
        //迷路内の探索者位置(x, y)、進行方向(dir)、
        //ウィンドウ表示画面サイズ((x1, y1) - (x2, y2))、
        //サイズ縮小用分母(Denominator)
        ////////////////////////////////////////////////////

        public bool Display3D(int x, int y, int dir, int x1, int y1, int x2, int y2, int den)
        {
            //再帰処理で外周壁に達したら何もしないで戻る
            if(x == 0 || x == mzWidth - 1)    return false;
            if(y == 0 || y == mzHeight - 1)    return false;
            //表示領域を正方形を保つために表示サイズを調整
            if((x1 + y1) == 0)        //再帰中は適用外
                x2 = y2;    //常に高さに合わせる
            //進行方向先の光景を縮小率(1/3)で小さくする(diffはx、yの差(減少)分)
            int diff = (x2 - x1) / den / 2;        //x, y共に同じ
            //進行方向一つ先のセルのサイズ(x、y共に差分x2づつ減少する)
            int nextx1 = x1 + diff;
            int nextx2 = x2 - diff;
            int nexty1 = y1 + diff;
            int nexty2 = y2 - diff;
            //一旦黒で塗潰す
            easel.Clear(Color.Black);
            //白色で描画
            easel.gFtColor = Color.White;
            //正面、左、右が壁か否かのフラグ
            bool FWall = false, LWall = false, RWall = false;
            switch(dir)
            {
                case dirUp:    //上へ進む場合
                    if(MazeData[x, y - 1] == Wall)
                        FWall = true;
                    else
                        FWall = false;
                    if(MazeData[x - 1, y] == Wall)
                        LWall = true;
                    else
                        LWall = false;
                    if(MazeData[x + 1, y] == Wall)
                        RWall = true;
                    else
                        RWall = false;
                    y--;
                    break;
                case dirRight:    //右へ進む場合
                    if(MazeData[x + 1, y] == Wall)
                        FWall = true;
                    else
                        FWall = false;
                    if(MazeData[x, y - 1] == Wall)
                        LWall = true;
                    else
                        LWall = false;
                    if(MazeData[x, y + 1] == Wall)
                        RWall = true;
                    else
                        RWall = false;
                    x++;
                    break;
                case dirDown:    //下へ進む場合
                    if(MazeData[x, y + 1] == Wall)
                        FWall = true;
                    else
                        FWall = false;
                    if(MazeData[x + 1, y] == Wall)
                        LWall = true;
                    else
                        LWall = false;
                    if(MazeData[x - 1, y] == Wall)
                        RWall = true;
                    else
                        RWall = false;
                    y++;
                    break;
                case dirLeft:    //左へ進む場合
                    if(MazeData[x - 1, y] == Wall)
                        FWall = true;
                    else
                        FWall = false;
                    if(MazeData[x, y + 1] == Wall)
                        LWall = true;
                    else
                        LWall = false;
                    if(MazeData[x, y - 1] == Wall)
                        RWall = true;
                    else
                        RWall = false;
                    x--;
                    break;
            }
            den = 3;    //現在地はセルを半分表示して1/6にしているが、その先はセル毎に1/3づつ減少
            if(!FWall)    //再帰で最後の突き当りから奥を描画させる
                Display3D(x, y, dir, nextx1, nexty1, nextx2, nexty2, den);
            //壁、通路の描画
            if(FWall)    //正面が壁なら四角を描く
                easel.Box(nextx1, nexty1, nextx2, nexty2);
            if(LWall)    //左側の描画処理
            {
                easel.Line(x1, y1, nextx1, nexty1);
                easel.Line(x1, y2, nextx1, nexty2);
            }
            else
            {
                easel.Line(x1, nexty1, nextx1, nexty1);
                easel.Line(x1, y1, x1, y2);
                easel.Line(x1, nexty2, nextx1, nexty2);
                easel.Line(nextx1, nexty1, nextx1, nexty2);
            }
            if(RWall)    //右側の描画処理
            {
                easel.Line(x2, y1, nextx2, nexty1);
                easel.Line(x2, y2, nextx2, nexty2);
            }
            else {
                easel.Line(x2, nexty1, nextx2, nexty1);
                easel.Line(x2, y1, x2, y2);
                easel.Line(x2, nexty2, nextx2, nexty2);
                easel.Line(nextx2, nexty1, nextx2, nexty2);
            }
            picBox.Image = easel.Canvas;
            return true;
        }
        ///////////
        //移動-上
        ///////////

        public bool GoUp()
        {
            bool CanGo = (MazeData[mzWhereIam.X, mzWhereIam.Y - 1] == Path);
            if(CanGo)
                mzWhereIam.Y--;
            else
                Console.Beep();
            return CanGo;
        }
        ///////////
        //移動-下
        ///////////

        public bool GoDown()
        {
            bool CanGo = (MazeData[mzWhereIam.X, mzWhereIam.Y + 1] == Path);
            if(CanGo)
                mzWhereIam.Y++;
            else
                Console.Beep();
            return CanGo;
        }
        ///////////
        //移動-左
        ///////////
 
       public bool GoLeft()
        {
            bool CanGo = (MazeData[mzWhereIam.X - 1, mzWhereIam.Y] == Path);
            if(CanGo)
                mzWhereIam.X--;
            else
                Console.Beep();
            return CanGo;
        }
        ///////////
        //移動-右
        ///////////

        public bool GoRight()
        {
            bool CanGo = (MazeData[mzWhereIam.X + 1, mzWhereIam.Y] == Path);
            if(CanGo)
                mzWhereIam.X++;
            else
                Console.Beep();
            return CanGo;
        }
        ////////////////
        //出口到着判定
        ////////////////

        public bool IsExit()
        {
            return (mzWhereIam.X == mzExit.X && mzWhereIam.Y == mzExit.Y);
        }
    }
}