未だMazeのプログラム解説もしていないのに、前回

今回作ったMaze_DLL.dllはマネージドdllなので.NET環境では"using"キーワードで簡単にロードし、コードの中で簡単に呼び出すことが出来ますが、C++で作ったDLL、例えばBCCSkeltonサンプルの中の「Diceコントロール」等、("CreateWindowEx関数"などを使わないで)Win32リソースとしてコンパイルし、プログラムの冒頭でRegisterClassEx関数でウィンドウクラス登録を行わなければならないものだと、CILの実行タイミング上ちょっと難しいかな、と考えています。んんん、じゃ、C#で同様のコントロールを作成し、もっとまじめに「チンチロリン」でもゲームとして開発しようかな???

と書いたために、興味はいっぺんにそっちにぶっ飛んでしまいました。

 

ということで、

今日は朝からBCCSkeltonのDiceのリソースを使ってC#のダイスコントロール作り。

BCCSkeltonの時とは異なり、単純にButtonコントロールから派生させることが出来るので簡単。しかし、矢張りボタンniBitmapを貼り付ける際に乱数を10回廻し、その数のサイコロの絵を表示するのがCILだと難しく、BCCSkeltonのサンプルのように目まぐるしくサイコロの目を表示することが出来ません。

ウ~ン...

とうなったところで、「じゃ、ご本家Microsoft Bingに聞いてみるか?」ということで、

Microsoft Bing様、C#にWin32のInvalidaterect()とUpdateWindow()の働きをする命令はありますか?

と聞いてみた所、

なーーーーんと「答え一発、カシオミニ!」(分かる人は60歳以上)

FormクラスにはInvalidate()とUpdate()というメソッドがあり、更に「なお、InvalidateメソッドとUpdateメソッドを組み合わせて、Refreshメソッドとして呼び出すこともできます。」という有難ーいお言葉。

早速「おもちゃ箱」の中をひっくり返し、過去Work Productを改造して、きちんと".wav"音付きで賽の目がくるくる変化する、

というところまでこぎつけました。

 

しかし、これは次のプロジェクト(注)迄お預けです。

注:BCCSkeltonサンプルでは、単一回のチンチロリン勝負でしたが、今度は複数(胴元+子方3-4人程度)の「賭場」を↓のような形で

 

(1)初めに「何人の勝負にしますか?(1-4)」のように訊いてくる。

(2)元手は乱数で決定する。

(3)大きな資金が必要な親はリスクが高いので「最初に親を受けますか?」「親をパスしますか?」などと選択できるようにする。(これもゲーム判断のうち。)

(4)破産したり、「ハイ無し(無一文)」になると蹴っ飛ばされて、外につまみ出され、ゲームオーバーとなる。

(5)通常「受かっている状態で抜ける」(勝ち逃げ)のは難しいが、これを認めないと終わらないので認め、ユーザー都合の「ゲームの中断」や「勝敗の統計」等も検討する。

(6)なお、基本的なルールは総て阿佐田哲也著「麻雀放浪記第一巻」に依拠する。

 

シミュレートしてみようかな、と考えています。(これだと、酒を啜り乍ら、まだ一人遊びできる範囲じゃないでしょうか?)

 

開発途上のテストランの途中で、迷路の3D表示中変な画像が見えたので、大事をとってスタンドアローン版を実際に使っていましたが、再現しないので、DLL化してみました。(といっても、単にプログラムを分割して、Mazeコントロール部分をDLLにしただけですが。以下にその構成を示します。)

 

【スタンドアローン版(Maze.exe 20KB)の構成】

(省略)

using Atelier;                                //Atelier.dll(10KB)を使う

namespace Maze_DLL
{
    public partial class AppForm : Form
    {

(省略)

    }

    public class Maze : Panel    //PictureBoxの枠となるPanelから派生
    {

(省略)

    }

 

【DLL版の構成】

<TestMaze.exe(8KB)

(省略)

using Maze_DLL;                                //Maze_DLL.dll(15KB)を使う為

namespace TestMaze

{
    public partial class AppForm : Form
    {

(省略)

    }

 

<Maze_DLL>

(省略)

using Atelier;                                //Atelier.dll(10KB)を使う

namespace Maze_DLL
{
    public class Maze : Panel    //PictureBoxの枠となるPanelから派生
    {

(省略)

    }

 

と、いうことで最終形の実行ファイル(TestMaze.exe)は8KBとコンパクトですが、実行する為には同じフォールダーかパスが通ったフォールダーにMaze_DLL.dllとAtelier.dllが入っていないと動きません。実はC#で開発された実行ファイルは皆このように「見た目はコンパクトだけど、実行時には有象無象のDLLがアップロードされて総計では結構な量になる」様に設計されています。(スタンドアローン版だと総計30KB、DLL版だと33KB)

いずれにしてもDLL版でMazeがコントロールとして簡単に作成でき、フォームに張り付けることが出来るようになりました。

 

【余談】

この「見た目はコンパクトだけど、実行時には有象無象のDLLがアップロードされて総計では結構な量になる」ですが、しかし、これは開発したソフトウェアの再利用による効率化が奏功するわけで、とても開発効率が良いことを意味します。一方、C#のDLLは.NET上のマネージドDLL(注1)であり、Embarcadero C++で作られたアンマネージドDLL(注2)とは異なります。

注1:「マネージドDLLとは、.NET Framework上で実行されるマネージドコードのライブラリです。マネージドコードとは、.NET Frameworkの共通言語ランタイム環境で実行されるコードのことであり、その実行がランタイムによって管理されます1. マネージドDLLは、C#やVisual Basicなどの高水準言語で記述されたコードをコンパイルして生成されます。マシンコードではなく、中間言語と呼ばれるバイナリ形式で提供されます。この中間言語は、共通中間言語 (CIL) または Microsoft Intermediate Language (MSIL) と呼ばれます。CLRは、マネージドDLLを取得し、それをマシンコードにコンパイルしてから実行します。これにより、CLRは自動メモリ管理、セキュリティ境界、タイプセーフなどのいくつかの重要なサービスを提供します。」(By Bing)

注2:「マネージドDLLとアンマネージドDLLは、.NET FrameworkにおけるDLLの2つの種類です。マネージドDLLは、.NET Framework上で実行されるマネージドコードのライブラリであり、C#やVisual Basicなどの高水準言語で記述されたコードをコンパイルして生成されます。一方、アンマネージドDLLは、.NET Framework外部で実行されるアンマネージドコードのライブラリであり、C++などの低水準言語で記述されたコードをコンパイルして生成されます。
アンマネージドDLLとマネージドDLLの主な違いは、実行環境です。アンマネージドDLLは、.NET Framework外部で実行されるため、CLR(共通言語ランタイム)によって管理されません。そのため、メモリ管理やセキュリティ管理などの機能が提供されません。一方、マネージドDLLは、.NET Framework上で実行されるため、CLRによって管理されます。CLRは自動メモリ管理、セキュリティ境界、タイプセーフなどのいくつかの重要なサービスを提供します
。」(By Bing)

 

しかし、C#でもアンマネージドDLLを使用することが出来ます。(注3)

注3:例えば次のような宣言を行って"なんちゃら.dll"をロードし、その中の「関数名」を呼び出すことが出来る。

   [DllImport("なんちゃら.dll")]
   extern static <戻り値型> 関数名(引数型 引数名, ...);    //ご参考

   なお、C++の引数の形、長さには注意を要するので、C#の変数系との対応関係に注意されたい。

 

今回作ったMaze_DLL.dllはマネージドdllなので.NET環境では"using"キーワードで簡単にロードし、コードの中で簡単に呼び出すことが出来ますが、C++で作ったDLL、例えばBCCSkeltonサンプルの中の「Diceコントロール」等、("CreateWindowEx関数"などを使わないで)Win32リソースとしてコンパイルし、プログラムの冒頭でRegisterClassEx関数でウィンドウクラス登録を行わなければならないものだと、CILの実行タイミング上ちょっと難しいかな、と考えています。んんん、じゃ、C#で同様のコントロールを作成し、もっとまじめに「チンチロリン」でもゲームとして開発しようかな???

 

BCCSkeltonを使ってC++でMazeを書いていた時は、まだまだ脳みそが柔軟で動きも早かったんだなぁ、と感じる今日この頃。

 

とはいえ、何とかオリジナルをC#に移植してDLLにする前のデスクトップ版プロトタイプが出来上がったように感じます。

実際に動かしてみて、出口まで行ってみました。

一応動いていますが、まだ未発生のバグ等もあるかもしれず、当分モニターしてみましょう。

 

簡単便利なC#でプログラミングしていると、(ネイティブな機械語に近い)C++との違いをよく感じますが、今回PanelクラスからMazeクラスを派生させているところで次のようなコードを書いた所、想定と異なる結果が出て面喰い、(以下のコードのように)MessageBoxを色々と挟んでデバッグしてみた所、面白いことが分かりました。

 

【Program codes at issue】

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

    public class Maze : Panel    //ピクチャー(ボックス)の枠となるPanelから派生
    {
(省略)
        //コンストラクタ(引数がない場合の初期値は41 x 41)
        public Maze(int width = 41, int height = 41)    //引数がない場合の初期値
        {
(省略)
            //迷路情報を初期化
            mzWidth = width;    
//迷路の幅
            mzHeight = height;  //迷路の高さ
            MazeData = new int[mzWidth, mzHeight];
            StartCells = new List<Cell>();
            if(mzWidth != 41 || mzHeight != 41)
                mzSetUNIT();    
//デフォルト初期値でない場合、UNIT値(注:オリジナルのビットマップサイズ)を変更
            //コントロール本体(好みで変更可)
            this.BorderStyle = BorderStyle.Fixed3D;    //「枠」のスタイルはClientEdgeExタイプを採用(サイズは2 x 2本分)
            this.AutoScroll = true;            //スクロールバー付

    
MessageBox.Show(String.Format("Width = {0}\r\nHeight = {1}", this.Width, this.Height), "Before", MessageBoxButtons.OK, MessageBoxIcon.Hand);
    MessageBox.Show(String.Format("Width DIff = {0}\r\nHeight Diff = {1}", this.Width - this.ClientSize.Width, this.Height - this.ClientSize.Height), "Diff", MessageBoxButtons.OK, MessageBoxIcon.Hand);
            this.Width = mzWidth * UNIT + (this.Width - this.ClientSize.Width);        //デフォルト初期値は660(656 + 4)
    MessageBox.Show(String.Format("Width = {0}\r\nHeight = {1}", this.Width, this.Height), "After W", MessageBoxButtons.OK, MessageBoxIcon.Hand);
            this.Height = mzHeight * UNIT + (this.Height - this.ClientSize.Height);    //デフォルト初期値は660(656 + 4)
    MessageBox.Show(String.Format("Width = {0}\r\nHeight = {1}", this.Width, this.Height), "After", MessageBoxButtons.OK, MessageBoxIcon.Hand);
    MessageBox.Show(String.Format("Width DIff = {0}\r\nHeight Diff = {1}", this.Width - this.ClientSize.Width, this.Height - this.ClientSize.Height), "Diff", MessageBoxButtons.OK, MessageBoxIcon.Hand);

 

(省略)
        }

    }

 

上記はMazeクラスのコンストラクターの一部で、デフォルト初期値の迷路幅と高さ(共に41)にオリジナルの壁ビットマップのサイズ(UNIT)を乗じ、更にコントロール幅とそのクライアント領域幅の差(メッセージボックスで"Diff"と書いてあるもの)を加算してMazeコントロールのサイズを決定しています。従ってその結果は本来、コメント

//デフォルト初期値は660(656 + 4)

と書いてある通り(41 x 16 = 656+「枠」2 x 2)になるはずでした。

が、

結果は...(メッセージボックスを順に表示します。)

最初に作られるMaze(Panel)コントロールのサイズです。

その時の「枠(ウィンドウサイズとクライアント領域の差)」はないようです。

ウィンドウ(コントロール)幅を設定した段階です。なんと、まだ「枠」は"0"になっています。

ウィンドウ(コントロール)高さを設定した段階です。今度は「枠」が"2 x 2"になっています。従って、

となります。

 

この結果、Mazeコントロールのサイズは、

いびつな "656 x 660"

になってしまいます。「枠」のスタイルは先に設定しているので、これってC#と共通中間言語(CIL)のタイミング差、ということかと思い、他の処理を入れたりして時間差を設けてみましたが、結果は変わらず。諦めて、

"this.Width - this.ClientSize.Width"

を(無粋だし、OSによっては異なってしまうかもしれない)

"4"

に変えようかと思っています。

残念、無念。

 

前回アプリネタがないので、取り敢えず迷路をコントロールにしてみてはどうかな(「C#で迷路のWindows版コントロール(迷路の生成、迷路の表示、その他関連メソッドは都度追加???)」)と考え、手を付けました。

 

あんまり強い動機があるわけではないので、「過去の成果物の再利用で」いーんじゃない?」と考えましたが...

 

(1)BitmapとIcon

BCCSkelton版

を使おうかと考えましたが、矢張りC#はちょっと変えようと、壁は「ハノイの塔」の石盤から拝借しました。

 

(2)Mazeクラスコントロール

ウィンドウ描画が不可欠なので、(後でどうするかはまた考えるとして)取り急ぎは前に作った"Atelier.dll"にある"Easel"クラスを利用することにし、そのサンプルのコードを最大限利用すること年、描画先はPictureBoxになるので「MazeクラスはPictureBoxを入れるPanelコントロール(Panelクラス)から派生」させることにしました。

 

(3)テストフォーム

まあ、2Dと3Dで迷路が表示でき(そのためのボタンが必要ですね)、「自分で動いている感」(Mazeクラスコントロールで表示)があればよいかと上下左右ボタンで移動させようかと考えています。(左右は向きを変える仕様です。)そういうことで作ったプロトタイプフォームの画像がこちら(↓)。

 

なんか、それっぽくはなってきましたね。

あ”~、アプリのイメージがわかない。

 

こういう時はクールダウンする為に事務的な作業をするに限る。

 

ということで、先ずはC#で迷路のWindows版コントロール(迷路の生成、迷路の表示、その他関連メソッドは都度追加???)を作ることを「暇つぶし目標」としてみました。

 

進捗を「つなぎ」として報告しましょう。

 

C# 5でも迷路データが作れるようになったので、これを使って何か作りたいな、と思うのは当然の成り行きでしょう。

 

定年退職した(直後にコロナに席巻されましたが)2020年の6月には、もうBCCSkeltonでMazeプログラムを、ここで紹介した「穴掘り法」のアルゴリズムをC++で再現してMazeというプログラムを書きました。矢張り迷路を作成するC++のクラスを作り、迷路データが使えるようになったので作ったのは「『穴掘り法』で迷路を作り、『さ迷い歩く人間(Wanderer)』を迷路に送って、「左手法」(注)で出口迄さ迷わせるのをつらつらと眺める環境ソフト」でした。

(解説:↑の赤丸●がWanderer、出口は「黄色いドア」で、2D、3Dで表示(切り替え)可能)

 

何故「コンピューター(Wanderer)が解(出口)をもとめる仕様」にしたのかというと、プログラマーが元々解を知っていたのでは面白くないからです。しかしこれも失敗で、矢張り「何度もこのプログラムを走らせるような暇人は居ない」ことをあたらめて思い知らされました。

 

では、迷路データをどのように使うと何度でも遊びたくなるようなプログラムになるのか?

 

まぁ、それが分かったら「私は大金持ち」になるんでしょう。

 

所で、「迷路の解法」について話が出たので、これについて少し書きましょう。(きっといつか役に立つと思います。)

上で書いたように、BCCSkeltonのプログラムでは「左手法」(注)を使っています。(今考えるとWandererクラス作成時に「左手法」(注)か「右手法」(注)か選択できるとよかったかな、と思います。)この方法は通常定番で(定番の迷路作成法だと)無敵と考えられていますが、迷路に「浮島」が存在し、迷路の中の浮島に出口があると到達できずに出発点に戻ってしまいます。

【浮島の例(棒倒し法の迷路から壁を少し間引いたもの)

S」が入口、「E」が出口

 

このような場合でも、「トレモー・アルゴリズム」という解法であれば必ず解けるといわれますが、実際の洞窟等の迷路では実行が難しいでしょう。(注)

 

注:ここを参照しました。伝統的な「右手法」「左手法」の他、いくつかの解法が紹介されていますが、私的にはこの迷路の解法が最強のように感じます。要すれば、↑の迷路を例にとると、

(1)自分の周りを確認し、行ったことがない場所に移動し、「往路→」マークを付ける。(「行ったことがない」=マークがない)

(2)「出口(成功)」または「入口(失敗-不可=出口なし)」で終了

(3)「突き当り」の場合等、(1)のチェックでマーク無しの場所がない場合、「往路→」マークの場所(の一つ)に進み、「復路←」マークを付ける。(「復路←」マークがついた道は今後一切入らないことになります。)

 

以上のプロセスを行うと、「袋小路」に「復路←」マークが付き、「往路→」マークは出口までの道のみにつくことになり、最悪総ての迷路を歩く(「入口(失敗-不可=出口なし)」に戻る)ことになります。(

:「左手法」、「右手法」で残された「マーク無し」の浮島地帯(例:上記図の赤四角■部分)もこれでExploreされます。また、プログラミングするなら、「迷路のセル(Maze(、)配列)」を今までは「0」=通路、「1」=壁としていたのを、「2」=往路、「3」=復路とすることで簡単にWandererExクラスが出来そうです。が、これも(BCCSkeltonの例と同じく)「直ぐに飽きるリスク」が濃厚なのでやりませんが...。なお、「トレモー・アルゴリズム」は「実際の洞窟等の迷路では実行が難しいでしょう」と書いたのは、迷路のすべてに「往路→」マーク、「復路←」マークをつけることが困難だ、という意味です。

 

さてさて、「迷路の作成」「迷路の解法」について一通りカバーしてきましたが、前回書いた

迷路作ってどーすんのさ?

という問にはまだまだ答えを見出していない私なのでした。

 

棒倒し法、穴掘り法とC# 5でコンパイルできるように改造してきましたが、最後は「壁伸ばし法」です。

 

壁伸ばし法」のサンプルソースプログラムをC# 5でコンパイルすると忽ち、

error CS1056: 文字 '$' は予期されていません。

という(もう見慣れた)エラーメッセージが出るので、既に修正している「穴掘り法」と同様DebugPrintメソッドの修正とMainメソッドの追加を行います。(「穴掘り法」はこれだけでコンパイル、実行できました。)

 

【DebugPrintメソッド】

        //デバッグ用メソッド(旧いコンパイラーなので$を使えない)
        public void DebugPrint(int[,] maze)
        {
            Console.WriteLine("Width: {0}\nHeight: {1}", maze.GetLength(0), maze.GetLength(1));
            for (int y = 0; y < maze.GetLength(1); y++)
            {
                for (int x = 0; x < maze.GetLength(0); x++)
                {
                    Console.Write(maze[x, y] == Wall ? "〇" : " ");
    //オリジナルは"■" : " "で、半角になってしまう
                }
                Console.WriteLine();
            }
        }

 

【Mainメソッド】

//////////////////////////////////////////////////////////
//迷路-壁伸ばし法
//参照URL:https://algoful.com/Archive/Algorithm/MazeExtend
//////////////////////////////////////////////////////////

    public class Application
    {
        public static void Main()
        { 
//実際にはCreate、CreatorをGenerate、Generatorに変えています。
            MazeCreator_Extend mzExt = new MazeCreator_Extend(20, 20);
            mzExt.DebugPrint(mzExt.CreateMaze());
            Console.ReadKey();
        }
    }

 

この修正、追加を行ったソースをコンパイルすると、あれれ、今度は、

error CS0840: 'ConsoleApplication1.Maze.MazeCreator_Extend.Width.get' は abstract または extern に指定されていないため、本体を宣言する必要があります。自動的に実装されたプロパティは、get および set アクセサーの両方を定義する必要があります。

いうエラーがMazeCreator_Extendクラスの

        public int Width { get; }
        public int Height { get; }

で出ます。

C# 5では、「まだ"{ get; }"だけではだめなのかな?」と思いながら、

       //これらプロパティがpublicで宣言されているため、

       //({ get; }と同じように)クラス外からsetが使えない

       //ようにprivateで宣言する。

        public int Width { get; private set; }
       
public int Height { get; private set; }

のように修正します。

 

この修正版でコンパイルすると、今度はソースコードの

        // セル情報
        private struct Cell
        {
            public int X { get; set; }
            public int Y { get; set; }
            public Cell(int x, int y)    
//解説:Cell構造体のコンストラクターです
            {
                this.X = x;
                this.Y = y;

            }
        }

の所で、

 

error CS0188: すべてのフィールドが割り当てられる(注1)までは、'this' オブジェクトは使用できません。
 

 

error CS0843: 自動的に実装されたプロパティ 'ConsoleApplication1.Maze.MazeCreator_Extend.Cell.X' のバッキング フィールド(注2)は、コントロールが呼び出し元に返される前に完全に割り当てられている必要があります。コンストラクター初期化子から既定のコンストラクターを呼び出すことを検討してください。

 

という新しいエラーが出ます。

注1:「割り当てられる」とは何かデータが代入されている状態、即ちデータを収容できるメモリーが「割り当てられた(確保された)」ことを意味します。これは通常C#では変数(フィールド)の宣言の他に値を代入したり、"new"ステートメントを使ってメモリーを確保する必要があります。

注2:「バッキング(Backing?)フィールド(↑のXYというプロパティで使う変数)」という意味が「黒子で使う変数」と読めるのですが、本当に正しいか調べてみると、ここ

"Generally, you should declare private or protected accessibility for fields. Data that your type exposes to client code should be provided through methods, properties, and indexers. By using these constructs for indirect access to internal fields, you can guard against invalid input values. A private field that stores the data exposed by a public property is called a backing store or backing field. You can declare public fields, but then you can't prevent code that uses your type from setting that field to an invalid value or otherwise changing an object's data."

とでており、矢張りアクセサーによりアクセスすることが出来るprivate or protected の「黒子となる変数(記憶領域)やフィールド」という「裏打ち、後ろ盾」を意味するようです。

 

ということで、プロパティの自動実装が使えないようなので上記オリジナルのコードを手動実装に分解して書き直します。

【修正したCell構造体】

        //セル情報
        private struct Cell
        {

/* 旧いコンパイラーは自動実装でエラーが出る
            public int X {get; set;}
            public int Y {get; set;}
            public Cell(int x, int y)
            {
                this.X = x;
                this.Y = y;
            }
*/

            private int _x;  //解説:プロパティXの「黒子」変数
            private int _y;  //解説:プロパティYの「黒子」変数
            public int X
            {

                //解説:get、setを実装する
                get{return _x;}
                set{
_x = value;}
            }
            public int Y
            {
                
//解説:get、setを実装する
                get{return _y;}
                set{
_y = value;}
            }
            public Cell(int x, int y)  
//解説:Cell構造体のコンストラクター
            {
                //解説:Cellインスタンスが生成された段階で記憶領域が割り当てられる。
                _x = x;
                
_y = y;
            }
        }

 

この追加の「プロパティ自動実装の補完コード」を書いてコンパイルすると、

きちんとコンパイル、実行できるようになりました。

 

以上で参照サイトの迷路作成定番方法のC# 5用修正は終わります。で、この後の問題は、

 

迷路作ってどーすんのさ?

 

ということになり、また頭が痛くなります。

 

本ブログは元々Vectorにアップしている、旧Borland(現Embarcadero)C++用のリソースエディターであるBCCFormとライブラリー(Ascii用BCCSkeltonとUTF-16用ECCSkelton)のユーザーに対するサポートを行う目的であったのですが、世はBorland→Embarcadero、32bit→64bit、Visual Studioもロハ等々の環境変化があり、最近は本来の目的に関連するブログが乏しくなってきております。

 

脱線の方向は、64bitプログラムが書けるC#のプログラミング関連、8bitから始めた私のコンピューティング関連(【昔話】)、プログラミングに関わる雑多なお話(【無駄話】)等ですが、「どうせ脱線するなら、とことん脱線しよう」ということで、私の数少ない趣味の中から、プログラミングに加えて「食べ物の話」を時々入れるようにしますのでご容赦ください。

 

「食べ物の話」といっても、齢69歳の年金暮らし、「どこぞの○○は絶品」等のグルメ話はありません。より、ずーっと、ずーっとずーっと、ずーーーーーーっと庶民的な料理とかレシピとか(注)、疑問に思うこととかの話です。また、ここでの「美味しい」は飽くまで「私の美味しい(とか、美味しくなくても懐かしいとか)」という主観だけの話ですので、そこは読者の方が適宜ご評価されてお読みいただければ、と存じます。

注:私は(「(1年356日-病気の日)×晩酌」という酒飲みなので、主に酒の肴を中心として)料理もします。作ったものはインスタなんて今どきのメディアではなく、自作ソフトのAlbumで管理しています。

 

いずれにせよ、(特にネタに詰まった時に...汗;)時々このジャンルで話をさせてください。

 

さて、第一回

として何を書こうか、と考え、丁度今日昼食に自作「東京ラーメン」(私はなんでも「自作」なんですが)を作ったので、

この「東京ラーメン」について書いてみます。

 

先ず「東京ラーメン」と言った時、それが私に何を意味するのか、というと「子供の時に母親が不在等の時にとる、志那そば」という醤油ラーメンです。「志那そば」という言葉は徐々に消え(ご参考)、「中華そば」、「ラーメン」という言葉に代わりましたが、そのコンセプトは東京ラーメンに収斂されるでしょう。

大体この東京ラーメンという言葉自体1980-90年代のラーメンブームで浮上したもので、今や「東京ラーメン」でググっても、これをラーメンのジャンルとして取り上げているのはこのWikiの記事位になったのは寂しい限りです。(今やこってりスープ、背油、豚骨等の「ふとりそ」ラーメンが主流なんでしょう。)

 

それでも矢張りこういう「懐かしい」「レトロな志那そば」っぽいものが時々食べたい、という方に私の作り方を紹介します。

(1)麺

まぁ、拘りはないので細~中細麺の生麵なら(茹ですぎない限り)なんでもよいでしょう。

(2)スープ

私の場合、色々と試行錯誤して、「①李錦記の鶏ガラスープだし、②濃縮そばつゆ大匙二杯、③醤油大匙一杯」を基準として、だしのバリエーションは①に色々と調整を行い、甘辛調整は②③の量とその他調味料を調整して作ります。

(3)トッピング

私の好みで絶対に外せないのは「味付け志那竹」、「長ネギ」と「海苔」、野菜は「ほうれん草煮びたし」以外にも少量の野菜を志那っと調理させたものなら何でもOKじゃないでしょうか?写真はモヤシとキャベツを痛めたものですが、今日はモヤシと白髪(といっても、緑の部分も多い)ねぎを炒めたものを使いました。私はSo-so-meat-eaterなので、チャーシューにはこだわらず、(面倒くさいので)ロースハムで代用することが多いです。

 

結果はとてもあっさりとした「志那そば」が出来ます。若い方は物足りない味かもしれませんが、「東京ラーメン」が絶滅危惧種になろうとしている今、家庭でも「それっぽい味」を簡単に作れることを覚えておいてください。

 

一番簡単な棒倒し法の次は「素直な(注)」穴掘り法を解説します。

注:今まで行ったC# 5用の変更のみでコンパイルが可能なので...

 

今回は面倒なので私のMainメソッドを付加し、改造したDebugPrintメソッドにしたものを紹介します。なお、オリジナルは参照サイトでサンプルのウェブ版テストプログラムと共にご覧ください。

 

【穴掘り法サンプル】

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication1.Maze
{
///////////////////////////////////////////////////////
//迷路-穴掘り法
//参照URL:https://algoful.com/Archive/Algorithm/MazeDig
///////////////////////////////////////////////////////

    //解説:Mainメソッドの付加
    public class Application
    {
        public static void Main()
        {
            
//MazeGenerator_Dig mzDig = new MazeGenerator_Dig(20, 20);    //他のサンプルではGeneratorなので統一したが...何か?
            //mzDig.DebugPrint(mzDig.GenerateMaze());

            MazeCreator_Dig mzDig = new MazeCreator_Dig(20, 20);
            mzDig.DebugPrint(mzDig.CreateMaze());
            Console.ReadKey();
        }
    }


    public class MazeCreator_Dig    //解説:"MazeCreateor_Dig"はMazeCreator_Digに変更
    {
        // 2次元配列の迷路情報
        private int[,] Maze;
        private int Width { get; set; }
        private int Height { get; set; }

        // 穴掘り開始候補座標
        private List<Cell> StartCells;

        // コンストラクタ
        public MazeCreator_Dig(int width, int height)
        {
            // 5未満のサイズや偶数では生成できない
            if (width < 5 || height < 5) throw new ArgumentOutOfRangeException();
            if (width % 2 == 0) width++;
            if (height % 2 == 0) height++;

            // 迷路情報を初期化
            this.Width = width;
            this.Height = height;
            Maze = new int[width, height];
            StartCells = new List<Cell>();
        }

        // 生成処理
        public int[,] CreateMaze()
        {
            // 全てを壁で埋める
            // 穴掘り開始候補(x,yともに偶数)座標を保持しておく
            for (int y = 0; y < this.Height; y++)
            {
                for (int x = 0; x < this.Width; x++)
                {
                    if (x == 0 || y == 0 || x == this.Width - 1 || y == this.Height - 1)
                    {
                        Maze[x, y] = Path;  // 外壁は判定の為通路にしておく(最後に戻す)
                    }
                    else
                    {
                        Maze[x, y] = Wall;
                    }
                }
            }

            // 穴掘り開始
            Dig(1, 1);

            // 外壁を戻す
            for (int y = 0; y < this.Height; y++)
            {
                for (int x = 0; x < this.Width; x++)
                {
                    if (x == 0 || y == 0 || x == this.Width - 1 || y == this.Height - 1)
                    {
                        Maze[x, y] = Wall;
                    }
                }
            }
            return Maze;
        }

        // 座標(x, y)に穴を掘る
        private void Dig(int x, int y)
        {
            // 指定座標から掘れなくなるまで堀り続ける
            var rnd = new Random();
            while (true)
            {
                // 掘り進めることができる方向のリストを作成
                var directions = new List<Direction>();
                if (this.Maze[x, y - 1] == Wall && this.Maze[x, y - 2] == Wall)
                    directions.Add(Direction.Up);
                if (this.Maze[x + 1, y] == Wall && this.Maze[x + 2, y] == Wall)
                    directions.Add(Direction.Right);
                if (this.Maze[x, y + 1] == Wall && this.Maze[x, y + 2] == Wall)
                    directions.Add(Direction.Down);
                if (this.Maze[x - 1, y] == Wall && this.Maze[x - 2, y] == Wall)
                    directions.Add(Direction.Left);

                // 掘り進められない場合、ループを抜ける
                if (directions.Count == 0) break;

                // 指定座標を通路とし穴掘り候補座標から削除
                SetPath(x, y);
                // 掘り進められる場合はランダムに方向を決めて掘り進める
                var dirIndex = rnd.Next(directions.Count);
                // 決まった方向に先2マス分を通路とする
                switch (directions[dirIndex])
                {
                    case Direction.Up:
                        SetPath(x, --y);
                        SetPath(x, --y);
                        break;
                    case Direction.Right:
                        SetPath(++x, y);
                        SetPath(++x, y);
                        break;
                    case Direction.Down:
                        SetPath(x, ++y);
                        SetPath(x, ++y);
                        break;
                    case Direction.Left:
                        SetPath(--x, y);
                        SetPath(--x, y);
                        break;
                }
            }

            // どこにも掘り進められない場合、穴掘り開始候補座標から掘りなおし
            // 候補座標が存在しないとき、穴掘り完了
            var cell = GetStartCell();
            if (cell != null)
            {
                Dig(cell.X, cell.Y);
            }
        }

        // 座標を通路とする(穴掘り開始座標候補の場合は保持)
        private void SetPath(int x, int y)
        {
            this.Maze[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;

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

            return cell;
        }

        //解説:定番のC# 5用DebugPrintメソッド
        //デバッグ用メソッド(旧いコンパイラーなので$を使えない)

        public void DebugPrint(int[,] maze)
        {
            Console.WriteLine("Width: {0}\nHeight: {1}", maze.GetLength(0), maze.GetLength(1));
            for (int y = 0; y < maze.GetLength(1); y++)
            {
                for (int x = 0; x < maze.GetLength(0); x++)
                {
                    Console.Write(maze[x, y] == Wall ? "〇" : " ");    
//オリジナルは"■" : " "で、半角になってしまう
                }
                Console.WriteLine();
            }
        }


        //解説:オリジナルでは既に使われている定数、クラス、enum変数が末尾で定義されている
        // 通路・壁情報
        const int Path = 0;
        const int Wall = 1;

        // セル情報
        private class Cell
        {
            public int X { get; set; }
            public int Y { get; set; }
        }

        // 方向
        private enum Direction
        {
            Up = 0,
            Right = 1,
            Down = 2,
            Left = 3
        }
    }
}
 

今回は棒倒し法の経験から簡単に対処できましたね。出力は棒倒し法(↓)と同じように表示されます。