いつもは体重を気にして夜はしっかりと食べない神さんが、バイトから夕刻に帰ってくるので、久々に夕食の提案をしたら「いいね!」ということで、昨晩神さんが作ったミートソースと私の摘み用にしこんでいた豚肉を野菜と添えて、(これも80gだけ残っていた)ペンネを合体して、ペンネボロネーゼ+豚の生姜焼き+サラダにしました。ペンネは(乾燥重量)40g、豚肉は50gづつ

 

夫婦で分かち合って

 

の夕食。確かに取り合わせに

 

 

は付きますが、でも、結構ハーモニーがあって大変美味しかったです。(「?」ついでに箸で食べちゃいました。こうするとペンネを一つづつ摘まむのでゆっくりと食べられ、満腹感もいっぱいで一種の「ダイエット食」になりました!)

 

 

前回、AvatarControllerをやりながら、このダイアログ

 

 

【拡大図】

 

について全く説明しなかったことに気が付きました!(ゴメンナサイ!)以下に概要を説明します。

 

(1)このダイアログがModal Dialogで、Worldクラスインスタンスを親(thisで呼ぶ)として呼んでいます。

 

(2)しかし、このダイアログは「3秒の命」しかなく、(何もしなくても)3秒経つと自動的に閉じられます

 

(3)↑ご覧のように多くのコントロールがありますが、「選択する動作は一つだけ」(動作はカラープッシュボタンを押すことにより実行しますが、ボタンは一つしか押せません)ので、その「3秒の命」の中で、ひとつだけ選択します。(カラープッシュボタンを押すとダイアログが閉じられます。)敢えて何もしない場合には最下段の「パス」ボタンを押してください。

 

(4)具体的に言うと、「遭遇したCellへの対応」では「親交」を結んだり、「威嚇」、「闘争」や「逃走」したりできます。又、食べ物があれば「食べる(への対応)」ことができ、野原があれば「移動(対応)」することができます。

 

(5)しかし、「ボタンを押しても、できないことがあるということを判断するには3秒間では難しい」だろうということで、「できないことはできない(グループボックスの.Enabledプロパティがfalse」ようにしていますので()、「押せるボタンが、今出来ること」になります。(さすがにそれはボタン状態からわかるでしょうと...)

:↑のイメージでいえば「遭遇したCellへの対応」と「食物への対応」がDisable状態です。

 

(6)また、、「遭遇したCellへの対応」では「異種族間婚姻が許可されているか」「自分と相手の種族、愛着性、敵対性」の数値が表示されるので、「親交を結ぶか、威嚇するか、即戦うか、逃げるか」を判断する素地になるでしょう。

 

(7)「食物への対応」動作も対象が複数ある場合は選択ができることも考えたのですが、余りに面倒にすると「特に反応が劣る高齢者」にはキツイのでPC判断と同じ対処にしました。

 

(8)「移動対応」も移動できる方向だけボタンを押せるよう、特に反応が劣る高齢者に対して「親切設計」にしています。

 

このように、従来のC++版のように単に「世界とCellの行く末を神の視座で眺める」というゲームから、「ユーザー自身がアバターを使って世界に入ってプレイする」というのが今回のC#版の売りなんですが、実際にプレイして思ったことは、

 

このゲームで、俺は主役にはなれない!

 

という

 

厳しい現実

 

でした。

 

Cell数を絞ってプレイする場合でも、採り得る戦略は「ひたすら食べ物を追っかけて、他のセルから逃れる」か、「なるべく戦わないで親交を深めるか」しかなく、「威勢よく他のセルを切り結ぶ」なんてことは考えない方がよい(直ぐに死ぬ)という現実です。

 

これがCellの数を多くすると

 

直ぐに大衆に飲み込まれてしまう...

 

という「しがない一個人の悲しい人生」を

 

嫌っていう程、味あわされます

 

正に「人生ゲーム」。期待した効果とは異なりましたが、それはそれで

 

大成功!!!

 

ps. 今回のプログラミングで初めて遭遇したエラーについて備忘として書いておきます。

 

最初AvatarControllerクラスからWorldクラスのフィールドやメソッドを使おうと思い、Worldクラス中の「Class in Class」にしようかと考えて進めたのですが、それでコンパイルすると、

 

CS0038: 入れ子になっている型 'Cell_World.World.AbatarController' を経由して、外の型 'Cell_World.World' の静的でないメンバーにアクセスすることはできません

というエラーが生じます。これはクラスインスタンスがスタックエリアに作られるために「アドレスが実際の実行まで確定できない」ことから来る問題()と考えました。

:言語設計により、相対距離でアクセスできる場合もあるようですが...一方、クラスの静的メンバーであれば、アドレスが確定しているためにアクセスが可能です。

【ご参考】

 

念のためにChat-GPTに確認すると矢張り、

 

C#でこのエラー(CS0038)が発生するのは、インナークラス(入れ子の型)から、外側のクラスの「静的ではない(non-static)メンバー」に直接アクセスしようとしたためです。C#の入れ子クラスは、Javaのインナークラスとは異なり、自動的に外側のクラスのインスタンス(this)への参照を持ちません。

(例)
csharpnamespace Cell_World
{
    public class World
    {
        public int worldSize = 100; // 静的ではないメンバー(インスタンス変数)

        public class AbatarController // 入れ子になっている型
        {
            public void Move()
            {
                // エラーCS0038:外側の非静的メンバーに直接アクセスできない
                int size = worldSize; 
            }
        }
    }
}

 

ということで、「Class in Class」は役に立たないので止め、コンストラクターの引数でWorldインスタンスを参照渡し(C++であれば「ポインター渡し」ですね)することにしました。ちょっとどんくさいですが...ご参考まで。

 

//////////////////////////////////////////////////

//  Cells for C#が長引いたので、閑話休題

//////////////////////////////////////////////////

using 赤ワイン

 

namespace 酒のつまみ

{

    赤ワインが少しあるので、これに合う摘みを考えていました。

    そんな時、手元に結構カリカリ感が半端ない、好みの生揚げが一切れあったので、

 

イタ風生揚げ

 

    にトライしてみました。赤ワインに合わせるように、

 

    (1) 揚げをカリカリに焼いて、

    (2) 火を通していた玉葱のみじん切りを載せて

    (3) ケチャップをかけ、

    (4) スライスチーズを載せて電子レンジで溶かします。

 

    思った以上に赤ワインに合いました。

 

    反省点は、

 

    (1) もぅ少し塩味を効かせたかった

    (2) レンジの時間が長すぎたのでチーズが流れ出した

 

    ことでしょうか?

 

    改善点としては、簡単に作るにはケチャップでよいのですが、しっかり作るには

 

    (1) 甘みを控えたトマトソースに

    (2) 少量のひき肉を入れたボロネーゼソース

 

    の方がいいかも、という感じです。

}

 

ps. この後、ハワイで買ったトリニダートスコーピオンのタバスコを使い()ましたが、効きまくり!危険なので「普通のタバスコ」をご使用ください。

:どの程度違うかというと、「普通のタバスコ:↑の摘みなら6振り程度かける」ところ、「スコーピオン:一滴をなすりつけるように6か所に伸ばしてゆく」くらいの差があります。最初普通にかけたら、頭が痛くなるような辛さでした。

 

文字数オーバー」エラーの為に"Worldクラス(3)"を更に分割しなければなりませんでしたが、前回の続きでアバター機能追加の為に新作した"AvatarController"ダイアログ

 

 

をやります。オリジナルのC++にはなかった機能ですので、適宜「解説:」を追加しておきます。

 

【Worldクラス後半】

    //////////////////////////////
    //AvatarController ダイアログ
    //(最長3秒で自動的に閉じる)

 

    //解説:最初は「長考可」でしたが、ゲームスピード、切迫感醸成の為に自動的に閉じるようにしました。
    //////////////////////////////

    class AvatarController : Form
    {
        ///////////////////////
        //定数定義(変更不可)
        ///////////////////////

        const int Wld_Width = 80;        //Worldの幅(セル数)
        const int Max_Dir = 8;            //↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
        const int Not_Found = -1;        //Find_***処理用
        //クラスフィールド
        private World wld;            //Worldクラスインスタンス
        private Cell cell;                //自分(Avatar)セル自身
        private Cell you;                //遭遇する他のCell
        private int yDir;                //遭遇する他のCellの方向
        private int fDir = Not_Found;    //食べ物の方向
        //ダイアログコントロール
        //グループボックス

        GroupBox gBox;
        GroupBox gBoxFood;
        GroupBox gBoxMove;
        //ラベル
        private Label lblMe;
        private Label lblYou;
        private Label lblTribe;
        private Label lblAffection;
        private Label lblHostility;
        private Label lblMeTribe;
        private Label lblMeAffection;
        private Label lblMeHostility;
        private Label lblYouTribe;
        private Label lblYouAffection;
        private Label lblYouHostility;
        //チェックボックス
        private CheckBox InterMarriage;
        //ボタン
        private Button btnGetClose;        //親交
        private Button btnIntimidate;    //威嚇
        private Button btnFight;        //闘争
        private Button btnFlee;            //逃走
        private Button btnEat;            //摂食
        private Button[] btnDir;        //移動
        private Button btnPass;            //パス(何もしない)
        //自動終了タイマー
        Timer dlgTimer;

        //worldはcのいる世界、dirがNot_Foundでなければ、遭遇した他のセルがy(nullでない)
        public AvatarController(World world, Cell c, Cell y, int dir)
        {    //解説:引数はWorldクラスインスタンス、自分のセル、遭遇したセルとそれがいる方向
            //引数の記録
            wld = world;
            cell = c;
            you = y;
            yDir = dir;
            //ダイアログの属性設定
            this.Text = "アバターコントローラー";
            this.ClientSize = new Size(320, 460);
            this.ControlBox = false;    //システムボタンを無効化
            this.ShowInTaskbar = true;    //タスクバー上表示
            this.FormBorderStyle = FormBorderStyle.FixedDialog;    //境界のスタイル
            this.StartPosition = FormStartPosition.CenterScreen;//画面中央(解説:親の右隣にされますが...)
            InitControls();                //コントロールの配置
            SetControls(cell);            //コントロールの初期設定
            //3秒で閉じる設定にする
            dlgTimer = new Timer();
            dlgTimer.Tick += new EventHandler(OnPass_Click);
            dlgTimer.Interval = 3000;    //解説:ミリ秒単位なので「3秒」
               dlgTimer.Start();
        }

        private void InitControls()
        {
            //フォームのレイアウトを一時停止
            this.SuspendLayout();
            //グループボックス(相手セルへの対応)
            gBox = new GroupBox();
            gBox.Location = new Point(10, 10);
            gBox.Size = new Size(ClientSize.Width - 20, 180);
            gBox.Anchor = (AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Left);
            gBox.Enabled = false;        //初期状態は使用不可
            gBox.Text = "遭遇したCellへの対応";
            //チェックボックスの属性設定
            InterMarriage = new CheckBox();
            InterMarriage.Location = new Point(10, 16);
            InterMarriage.Size = new Size(ClientSize.Width - 40, 18);
            InterMarriage.CheckAlign = ContentAlignment.MiddleRight;
            InterMarriage.TextAlign = ContentAlignment.MiddleLeft;
            InterMarriage.Text = "異種族婚許可";
            InterMarriage.AutoCheck = false;    //ユーザー変更不可
            //ラベルの属性設定
            lblTribe = new Label();
            lblTribe.Size = new Size(60, 18);
            lblTribe.Location = new Point(10, 68);
            lblTribe.BorderStyle = BorderStyle.Fixed3D;
            lblTribe.Text = "種族";
            lblTribe.TextAlign = ContentAlignment.MiddleCenter;
            lblTribe.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblAffection = new Label();
            lblAffection.Size = new Size(60, 18);
            lblAffection.Location = new Point(10, 94);
            lblAffection.BorderStyle = BorderStyle.Fixed3D;
            lblAffection.Text = "愛着性";
            lblAffection.TextAlign = ContentAlignment.MiddleCenter;
            lblAffection.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblHostility = new Label();
            lblHostility.Size = new Size(60, 18);
            lblHostility.Location = new Point(10, 120);
            lblHostility.BorderStyle = BorderStyle.Fixed3D;
            lblHostility.Text = "    敵対性";
            lblHostility.TextAlign = ContentAlignment.MiddleCenter;
            lblHostility.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblMe = new Label();
            lblMe.Size = new Size(100, 18);
            lblMe.Location = new Point(80, 42);
            lblMe.BorderStyle = BorderStyle.Fixed3D;
            lblMe.Text = "アバターセル";
            lblMe.TextAlign = ContentAlignment.MiddleCenter;
            lblMe.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblMe.BackColor = Color.SkyBlue;
            lblMeTribe = new Label();
            lblMeTribe.Size = new Size(100, 18);
            lblMeTribe.Location = new Point(80, 68);
            lblMeTribe.BorderStyle = BorderStyle.Fixed3D;
            lblMeTribe.Text = "";
            lblMeTribe.TextAlign = ContentAlignment.MiddleCenter;
            lblMeTribe.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblMeTribe.BackColor = Color.SkyBlue;
            lblMeAffection = new Label();
            lblMeAffection.Size = new Size(100, 18);
            lblMeAffection.Location = new Point(80, 92);
            lblMeAffection.BorderStyle = BorderStyle.Fixed3D;
            lblMeAffection.Text = "";
            lblMeAffection.TextAlign = ContentAlignment.MiddleCenter;
            lblMeAffection.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblMeAffection.BackColor = Color.SkyBlue;
            lblMeHostility = new Label();
            lblMeHostility.Size = new Size(100, 18);
            lblMeHostility.Location = new Point(80, 120);
            lblMeHostility.BorderStyle = BorderStyle.Fixed3D;
            lblMeHostility.Text = "";
            lblMeHostility.TextAlign = ContentAlignment.MiddleCenter;
            lblMeHostility.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblMeHostility.BackColor = Color.SkyBlue;
            lblYou = new Label();
            lblYou.Size = new Size(100, 18);
            lblYou.Location = new Point(190, 42);
            lblYou.BorderStyle = BorderStyle.Fixed3D;
            lblYou.Text = "相手のセル";
            lblYou.TextAlign = ContentAlignment.MiddleCenter;
            lblYou.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblYou.BackColor = Color.Violet;
            lblYouTribe = new Label();
            lblYouTribe.Size = new Size(100, 18);
            lblYouTribe.Location = new Point(190, 68);
            lblYouTribe.BorderStyle = BorderStyle.Fixed3D;
            lblYouTribe.Text = "";
            lblYouTribe.TextAlign = ContentAlignment.MiddleCenter;
            lblYouTribe.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblYouTribe.BackColor = Color.Violet;
            lblYouAffection = new Label();
            lblYouAffection.Size = new Size(100, 18);
            lblYouAffection.Location = new Point(190, 92);
            lblYouAffection.BorderStyle = BorderStyle.Fixed3D;
            lblYouAffection.Text = "";
            lblYouAffection.TextAlign = ContentAlignment.MiddleCenter;
            lblYouAffection.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblYouAffection.BackColor = Color.Violet;
            lblYouHostility = new Label();
            lblYouHostility.Size = new Size(100, 18);
            lblYouHostility.Location = new Point(190, 120);
            lblYouHostility.BorderStyle = BorderStyle.Fixed3D;
            lblYouHostility.Text = "";
            lblYouHostility.TextAlign = ContentAlignment.MiddleCenter;
            lblYouHostility.Font = new Font("MS明朝", 8, FontStyle.Regular);
            lblYouHostility.BackColor = Color.Violet;
            //ボタンの属性設定
            btnGetClose = new Button();                //親交
            btnGetClose.Size = new Size(60, 24);
            btnGetClose.Location = new Point(10, 148);
            btnGetClose.Text = "親交";
            btnGetClose.BackColor = Color.LimeGreen;
            btnGetClose.Click += new EventHandler(OnGetClose_Click);
            btnIntimidate = new Button();            //威嚇
            btnIntimidate.Size = new Size(60, 24);
            btnIntimidate.Location = new Point(82, 148);
            btnIntimidate.Text = "威嚇";
            btnIntimidate.BackColor = Color.Orange;
            btnIntimidate.Click += new EventHandler(OnIntimidate_Click);
            btnFight = new Button();                //闘争
            btnFight.Size = new Size(60, 24);
            btnFight.Location = new Point(154, 148);
            btnFight.Text = "闘争";
            btnFight.BackColor = Color.Red;
            btnFight.Click += new EventHandler(OnFight_Click);
            btnFlee = new Button();                    //逃走
            btnFlee.Size = new Size(60, 24);
            btnFlee.Location = new Point(226, 148);
            btnFlee.Text = "逃走";
            btnFlee.BackColor = Color.DeepSkyBlue;
            btnFlee.Click += new EventHandler(OnFlee_Click);
            //帰属関係設定
            gBox.Controls.Add(InterMarriage);
            gBox.Controls.Add(lblMe);
            gBox.Controls.Add(lblMeTribe);
            gBox.Controls.Add(lblMeAffection);
            gBox.Controls.Add(lblMeHostility);
            gBox.Controls.Add(lblYou);
            gBox.Controls.Add(lblYouTribe);
            gBox.Controls.Add(lblYouAffection);
            gBox.Controls.Add(lblYouHostility);
            gBox.Controls.Add(lblTribe);
            gBox.Controls.Add(lblAffection);
            gBox.Controls.Add(lblHostility);
            gBox.Controls.Add(btnGetClose);
            gBox.Controls.Add(btnIntimidate);
            gBox.Controls.Add(btnFight);
            gBox.Controls.Add(btnFlee);
            this.Controls.Add(gBox);
            //グループボックス(食物への対応)
            gBoxFood = new GroupBox();
            gBoxFood.Location = new Point(10, gBox.Height + 20);
            gBoxFood.Size = new Size(ClientSize.Width - 20, 48);
            gBoxFood.Anchor = (AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Left);
            gBoxFood.Text = "食物への対応";
            gBoxFood.Enabled = false;    //初期状態は使用不可
            //ボタンの属性設定
            btnEat = new Button();
            btnEat.Size = new Size(gBoxFood.Width - 40, 24);
            btnEat.Location = new Point(20, 16);
            btnEat.Text = "食べる";
            btnEat.BackColor = Color.LawnGreen;
            btnEat.Click += new EventHandler(OnEat_Click);
            //帰属関係設定
            gBoxFood.Controls.Add(btnEat);
            this.Controls.Add(gBoxFood);
            //グループボックス(移動対応)
            gBoxMove = new GroupBox();
            gBoxMove.Location = new Point(10, gBox.Height + gBoxFood.Height + 30);
            gBoxMove.Size = new Size(ClientSize.Width - 20, 160);
            gBoxMove.Anchor = (AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Left);
            gBoxMove.Text = "移動対応";
            //ボタンの属性設定
            btnDir  = new Button[8];    //↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
            int btnW = 40, btnH = 40;
            int posX = (gBoxMove.Width - btnW * 3 - 8) / 2;
            int posY = 20;
            //本プログラムのリソースマネージャー作成(解説:".dll"なので、実行ファイル".exe"の場合とちょっと違います。)
            ResourceManager rm = new ResourceManager("Cell_World", Assembly.GetAssembly(typeof(World)));
            Bitmap bmp;    //解説:貼り付け処理用ビットマップフィールド。Dispose()処理が必要か否か調べ、".resource"ファイルのビットマップへのポインターとしてしか機能していないので、「不要」という結論になりました。
            btnDir[0] = new Button();    //解説:ボタンインスタンスを作ります。以下同じ。
            btnDir[0].Size = new Size(btnW, btnH);    //解説:大きさを決定します。以下同じ。
            btnDir[0].Location = new Point(posX + btnW + 4, posY);    //解説:位置決めします。以下同じ。
            btnDir[0].BackColor = Color.LightCyan;    //解説:ボタンの背景色を指定します。以下同じ。
            btnDir[0].Click += new EventHandler(OnUp_Click);    //解説:ボタンが押された場合の割り込みメソッドを指定します。
            bmp = (Bitmap)rm.GetObject("U");    //リソース埋め込みビットマップの読み込み(アドレスをbmpポインターへ)
            bmp.MakeTransparent(Color.White);    //解説:".resource"ファイルのビットマップを透明化します。以下同じ。
            btnDir[0].Image = bmp;    //解説:ボタンにビットマップを貼り付けます。
            btnDir[0].Enabled = false;    //初期状態は使用不可
            btnDir[1] = new Button();
            btnDir[1].Size = new Size(btnW, btnH);
            btnDir[1].Location = new Point(posX + (btnW + 4) * 2, posY);
            btnDir[1].BackColor = Color.LightCyan;
            btnDir[1].Click += new EventHandler(OnUpRight_Click);
            bmp = (Bitmap)rm.GetObject("UR");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[1].Image = bmp;
            btnDir[1].Enabled = false;    //初期状態は使用不可
            btnDir[2] = new Button();
            btnDir[2].Size = new Size(btnW, btnH);
            btnDir[2].Location = new Point(posX + btnW * 2 + 8, posY + btnH + 4);
            btnDir[2].BackColor = Color.LightCyan;
            btnDir[2].Click += new EventHandler(OnRight_Click);
            bmp = (Bitmap)rm.GetObject("R");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[2].Image = bmp;
            btnDir[2].Enabled = false;    //初期状態は使用不可
            btnDir[3] = new Button();
            btnDir[3].Size = new Size(btnW, btnH);
            btnDir[3].Location = new Point(posX + (btnW + 4) * 2, posY + btnH * 2 + 8);
            btnDir[3].BackColor = Color.LightCyan;
            btnDir[3].Click += new EventHandler(OnDownRight_Click);
            bmp = (Bitmap)rm.GetObject("DR");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[3].Image = bmp;
            btnDir[3].Enabled = false;    //初期状態は使用不可
            btnDir[4] = new Button();
            btnDir[4].Size = new Size(btnW, btnH);
            btnDir[4].Location = new Point(posX + btnW + 4, posY + btnH * 2 + 8);
            btnDir[4].Click += new EventHandler(OnDown_Click);
            btnDir[4].BackColor = Color.LightCyan;
            bmp = (Bitmap)rm.GetObject("D");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[4].Image = bmp;
            btnDir[4].Enabled = false;    //初期状態は使用不可
            btnDir[5] = new Button();
            btnDir[5].Size = new Size(btnW, btnH);
            btnDir[5].Location = new Point(posX, posY + btnH * 2 + 8);
            btnDir[5].BackColor = Color.LightCyan;
            btnDir[5].Click += new EventHandler(OnDownLeft_Click);
            bmp = (Bitmap)rm.GetObject("DL");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[5].Image = bmp;
            btnDir[5].Enabled = false;    //初期状態は使用不可
            btnDir[6] = new Button();
            btnDir[6].Size = new Size(btnW, btnH);
            btnDir[6].Location = new Point(posX, posY + btnH + 4);
            btnDir[6].BackColor = Color.LightCyan;
            btnDir[6].Click += new EventHandler(OnLeft_Click);
            bmp = (Bitmap)rm.GetObject("L");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[6].Image = bmp;
            btnDir[6].Enabled = false;    //初期状態は使用不可
            btnDir[7] = new Button();
            btnDir[7].Size = new Size(btnW, btnH);
            btnDir[7].Location = new Point(posX, posY);
            btnDir[7].BackColor = Color.LightCyan;
            btnDir[7].Click += new EventHandler(OnUpLeft_Click);
            bmp = (Bitmap)rm.GetObject("UL");    //ビットマップの読み込み
            bmp.MakeTransparent(Color.White);
            btnDir[7].Image = bmp;
            btnDir[7].Enabled = false;    //初期状態は使用不可
            //帰属関係設定
            gBoxMove.Controls.Add(btnDir[0]);
            gBoxMove.Controls.Add(btnDir[1]);
            gBoxMove.Controls.Add(btnDir[2]);
            gBoxMove.Controls.Add(btnDir[3]);
            gBoxMove.Controls.Add(btnDir[4]);
            gBoxMove.Controls.Add(btnDir[5]);
            gBoxMove.Controls.Add(btnDir[6]);
            gBoxMove.Controls.Add(btnDir[7]);
            this.Controls.Add(gBoxMove);
            //「パス」(Pass)ボタンの属性設定(解説:内容的には"Cancel"です。)
            btnPass = new Button();
            btnPass.Size = new Size(60, 24);
            btnPass.Location = new Point(ClientSize.Width - btnPass.Width - 10, ClientSize.Height - btnPass.Height - 10);
            btnPass.Text = "パス";
            btnPass.Click += new EventHandler(OnPass_Click);
            this.Controls.Add(btnPass);
            //フォームのレイアウトを再開
            this.ResumeLayout(false);
            this.PerformLayout();
        }

        private void SetControls(Cell cell)    //解説:ダイアログへの引数から、コントロールの初期状態を設定します。
        {
            //異種族間婚許可の確認
            InterMarriage.Checked = cell.Intmarg;
            //Avatarの属性表示(自分(引数cell)から自分の属性をラベルに表示します。)
            string[] tribes = new string[] {"ピンク", "ブルー", "グリーン", "死亡"};
            lblMeTribe.Text = tribes[cell.Tribe];
            lblMeAffection.Text = cell.Affection.ToString();
            lblMeHostility.Text = cell.Hostility.ToString();
            //Avatarセルの周囲状況をダイアログに反映させる

            //解説:コンピューター処理では周囲状況への対応優先順位が

            //「1-セル」「2-食物」「3-野原」ですが、ダイアログでは

            //アバター(ユーザー)が自由に選択できます。
            for(int i = 0; i < Max_Dir; i++)
            {
                switch(cell.Around[i])    //ダイアログ呼び出し前にCheck_Around実行済
                {
                    case 0:                //野原
                    //「移動」処理の矢印の.Enabledを変更(進める)
                        btnDir[i].Enabled = true;
                        break;
                    case 1:                //山(進めない)
                    case 2:                //河川(進めない)
                        break;    //解説:処理なし、です。
                    case 3:                //食物
                        //「食べる 」処理のgBoxFood.Enabledを変更(食べられる)
                        fDir = i;        //食物のある方向
                        gBoxFood.Enabled = true;
                        break;
                    default:            //セル(cell.Around[i] >= 4)
                        break;    //解説:↓でセルyとyDirを使って行いますので、ここでは処理なし、です。
                }
                if(yDir != Not_Found)    //他のセルと遭遇したならば(解説:Cell yがnullではなく、その方向(yDir)が-1ではない)
                {
                    if(you.Tribe < 3)            //生存するセル(Cell-P、Cell-B、Cell-G)なら
                    {
                        gBox.Enabled = true;    //他のセルへの対応が可能となる
                    }
                    else if(you.Tribe == 3)        //死亡したセルなら食物扱いとなるので
                    {
                        fDir = yDir;
                        gBoxFood.Enabled = true;
                    }
                    //他のセルの属性表示(解説:二つ以上いたとしても、yとyDirについて処理します。)
                    lblYouTribe.Text = tribes[you.Tribe];
                    lblYouAffection.Text = you.Affection.ToString();
                    lblYouHostility.Text = you.Hostility.ToString();
                }
            }
        }

        //終了処理
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            //タイマーの停止と廃棄(解説:タイマーは開放<Dispose()>する必要があります。)
            dlgTimer.Stop();
            dlgTimer.Dispose();
        }
 

    //解説:以下ではWorldクラスインスタンスの「5つの"Inter-Cell relation/reaction(セル間関係/反応)」を処理します。
    //③-(セル間関係)(1) get_close()-親しくなり、愛着性が高まり、敵対性が低まります。
    //③-(セル間関係)(2) make_love()ー一定の愛着性を超えると交尾し、生殖力により出産します。
    //③-(セル間関係)(3) intimidate()ー敵対性と愛着性の度合いにより、相手を威嚇し、敵対性が高まり、愛着性が低まります。
    //③-(セル間関係)(4) fight()ー異種族婚許可、敵対性により、相互に攻撃、被害を受けます。
    //③-(セル間関係)(5) flee()ー敵対状態になっても、攻撃力、防御力の差が大きければ逃走します。」
    //その為にWorldクラスインスタンス(
wld)のアドレスが必要となり、本ダイアログの引数に入れています。

        private void OnGetClose_Click(object sender, EventArgs e)
        {
            wld.Get_Close(cell, you, yDir);
            Close();
        }

        private void OnIntimidate_Click(object sender, EventArgs e)
        {
            wld.Intimidate(cell, you, yDir);
            Close();
        }

        private void OnFight_Click(object sender, EventArgs e)
        {
            wld.Fight(cell, you, yDir);
            Close();
        }

        private void OnFlee_Click(object sender, EventArgs e)
        {
            wld.Flee(cell, you, yDir);
            Close();
        }

    //解説:以下は自分(Cellクラスインスタンスのcell)のメソッドで処理します。
        private void OnEat_Click(object sender, EventArgs e)
        {
            cell.Get_Food(fDir);
            Close();
        }

        private void OnUp_Click(object sender, EventArgs e)
        {
            cell.Get_Move(0);
            Close();
        }

        private void OnUpRight_Click(object sender, EventArgs e)
        {
            cell.Get_Move(1);
            Close();
        }

        private void OnRight_Click(object sender, EventArgs e)
        {
             cell.Get_Move(2);
            Close();
        }

        private void OnDownRight_Click(object sender, EventArgs e)
        {
            cell.Get_Move(3);
            Close();
        }

        private void OnDown_Click(object sender, EventArgs e)
        {
            cell.Get_Move(4);
            Close();
        }

        private void OnDownLeft_Click(object sender, EventArgs e)
        {
             cell.Get_Move(5);
            Close();
        }

        private void OnLeft_Click(object sender, EventArgs e)
        {
             cell.Get_Move(6);
            Close();
        }

        private void OnUpLeft_Click(object sender, EventArgs e)
        {
             cell.Get_Move(7);
            Close();
        }

//解説:お分かりの通り、全ての「5つの"Inter-Cell relation/reaction(セル間関係/反応)」、

//「摂食」、「移動」処理を実行すると本ダイアログを閉じます(Close())。

//従って、「一回の<3秒間の>ダイアログ表示でユーザーが選択した一つの行動」

//しかとれないことになります。
        private void OnPass_Click(object sender, EventArgs e)
        {
            Close();    //解説:「パス」はCancelと同じく何もしないでダイアログを閉じることです。
        }
    }
}    //解説:これはWorld_Cell(namespace)の閉じ括弧です。

 

以上で3回(実質的に4回ですが...)にわたって行ったWorldクラスの解説を終了します。

 

今後はこのCell_World.dll、Worldクラスインスタンスであるウィンドウコントロールの実際の使用例として、Cells.exeのソースを解説してゆきましょう。

 

前回に引き続き、今回はWorldクラス最後の解説として、PictureBoxから派生させてコントロールとなったWorldクラスの描画処理とアバター機能追加の為に新作した"AvatarController"ダイアログをやろうと思ったのですが、最後の最後で「文字数オーバーエラー」が出てしまいました。(エディターの「文字数:」はまだ16,140だったのですが...)

 

取り敢えず、PictureBoxから派生させてコントロールとなったWorldクラスの描画処理のみをやります。

 

【Worldクラス後半-その1】

        ///////////////////
        //描画関係メソッド
        ///////////////////
        //画面消去

        public void Clear()
        {
            gHandle.Clear(this.BackColor);    //解説:これは仮想画面Canvas(ビットマップ)の描画用ハンドルです。
        }

        //背景(Map)を表示する    //解説:Mapは文字ベースのオリジナル地形図です。
        public void ShowMap()
        {
            bool changed = false;                        //WorldのMapと論理マップ(PrevMap)との差異フラグ
            for(int j = 0; j < Wld_Height; j++)            //Y座標
            {
                for(int i = 0; i < Wld_Width; i++)        //X座標
                {
                    //ビットマップ表示
                    int n = Check_What(i, j);            //(i, j)の位置に何があるか
                    if(PrevMap[Wld_Width * j + i] != n)    //前回から変更があれば
                    {
                        PrevMap[Wld_Width * j + i] = n;    //記録して
                        changed = true;                    //差異フラグを立てる
                    }
                    Cell c;                                //Cell(解説:セルがいた場合の情報収集用)
                    switch(n)
                    {
                        case 0:    //野原
                        case 1:    //山
                        case 2:    //河川
                        case 3:    //食物
                            if(changed)                    //前回から変更があれば(解説:正確には「あった場合のみ」
                                gHandle.DrawImage(Terrain_Bmp[n], i * 16, j * 16);    //解説:nに応じた「地形ビットマップ配列」を表示
                            break;
                        default:
                            if(n > 3)                    //Cell(4 + ID)の場合
                            {
                                c = Get_Cell(i, j);        //(i, j)の位置あるCellを取得
                                if(c.ID == MyID && c.Alive)    //アバターの場合
                                    gHandle.DrawImage(Cell_Bmp[Max_Tribe], i * 16, j * 16);    //解説:アバタービットマップを表示
                                else                        //アバターではない場合
                                    gHandle.DrawImage(Cell_Bmp[c.Tribe], i * 16, j * 16);    //解説:死亡セルはTribeが"3"となり、ここで表示
                            }
                            else if(n == -1)
                                MessageBox.Show("地形ファイル(*.trn)に許容されない文字が含まれています。",
                                                "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        break;
                    }
                }
                if(changed)        //変化が一つでもあれば、World(描画領域)を更新
                    this.Image = (Image)Canvas;    //解説:Worldクラスインスタンスのコントロールの背景画面となる
            }
        }
    }
 

次回にAvatarControllerダイアログをすべてカバーできるとよいのですが...(心配)

 

前回単に移植しただけ前半」をやりましたので、今回は「アバターを入れた為に大きく変えたGoes_Aroundメソッド関連中盤」をやります。前回と同じく、オリジナルのC++の解説も適宜参考にしてください。

 

オリジナルのC++の解説で書いたように、ここでやっている機能は

 

③「世界が廻る(時が過ぎる)」(goes_around())-セルに日常活動を行わせるのですが、この内容はの通りです。
③-(セル間関係)(1) get_close()-親しくなり、愛着性が高まり、敵対性が低まります。
③-(セル間関係)(2) make_love()ー一定の愛着性を超えると交尾し、生殖力により出産します。
③-(セル間関係)(3) intimidate()ー敵対性と愛着性の度合いにより、相手を威嚇し、敵対性が高まり、愛着性が低まります。
③-(セル間関係)(4) fight()ー異種族婚許可、敵対性により、相互に攻撃、被害を受けます。
③-(セル間関係)(5) flee()ー敵対状態になっても、攻撃力、防御力の差が大きければ逃走します。」

 

という「5つの"Inter-Cell relation/reaction(セル間関係/反応)」です。今回もこの5つのメソッドは全く同じです。唯一の変更は

 

Avatar

 

の処理()だけです。コメントに加え、適宜解説:を付加しました。

:アバターはユーザーが世界の中のセルの一つになって生存活動をマニュアルで行うfeature(注の注)です。アバターの処理はユーザーが行いますが、3秒間何もしないと勝手に他のセルが(PCの判断により)行動を継続してゆきます。尚、今回のコードでは実際にアバターの行う処理は出てきません。それらは「アバターコントローラー」ダイアログで処理されます。

注の注:A software feature is "a prominent or distinctive user-visible aspect, quality, or characteristic of a software system or systems", as defined by Kang et al ... (wiki)

 

【Worldクラス中盤】

        public void Goes_Around()                        //「世界が廻る(時が過ぎる)」→セルに日常活動を行わせる
        {
            if(!Gameover)
            {
                //最大人口超の場合
                if(CellList.Count > Max_Population)
                {
                    Gameover = true;                    //ゲームオーバー
                    Message += "現在の人口過密が異常となりました!(人口爆発)" + 
                                Environment.NewLine + Get_Distribution() +
                                Environment.NewLine + "(詳細はログを参照ねがいます。)";
                    return;
                }
                bool extinction = true;                    //絶滅チェッカー
                int LastCell = CellList.Count;
                for(int i = 0; i < LastCell; i++)        //"foreach"は配列が変化するとエラーになる為
                {
                    Cell cell = CellList[i];            //IDから対象セルを求める
                    cell.VitalCheck();                    //対象セルの生死状態をチェック
                    if(cell.Alive)                        //生存している場合
                    {
                        Check_Around(cell);                //対象セルの周囲を確認する
                        Point loc = cell.Where();        //対象セルの位置を取得
                        Map[loc.Y * Wld_Width + loc.X] = ' ';    //対象セルの旧座標を' 'にする
                        //周囲のセルの有無を確認する
                        Cell y = null;                    //周囲のセル
                        int dir = Not_Found;            //周囲のセルのいる方向
                        int val = cell.Find_Cell();        //対象セルの周囲にセルがいるか確認
                        if(val != Not_Found)            //周囲にセルが見つかったならば
                        {
                            dir = val & 0x0F;            //dirは周囲のセルの絶対方向(0 - 7)
                            y = Get_Cell(val >> 4);        //周囲のセルのポインターを取得
                        }
                        //コンピューター処理とマニュアル処理へ分岐させる(解説:ここが最大の変更点)
                        if(i == MyID && cell.Alive)        //ユーザー判断による処理(アバターの場合)死んだcellは食物でしかない

                            AvatarProcess(this, cell, y, dir);    //解説:↓参照
                        else                            //コンピューター判断による処理
                            AutoProcess(cell, y, dir);    //解説:↓参照(PC判断による処理)
                        loc = cell.Where();                //対象セルの移動があり得るので再取得
                        Map[loc.Y * Wld_Width + loc.X] = 'C';    //マップのセルxの新座標を'C'にする
                        cell.Aging();                    //老化による減衰と生命力消耗
                        cell.Param_Change();            //状況変化から各種パラメーターを調整
                        //絶滅チェック(一つでもAliveがtrueであればfalse)
                        extinction = false;
                    }
                    cell.VitalCheck();                    //活動後の対象セルの生死状態をチェック(解説:既に↑で同じメソッドをやっているので、一見冗長処理のように感じますが、このメソッドの後描画処理になるので、このターンで死亡した場合に正常に表示されるように行っています。)
                }
                if(extinction)                            //全滅により終了
                {
                    Gameover = true;
                    Message += "全セルが死亡しました!(全滅)" + 
                                Environment.NewLine + Get_Distribution() +
                                Environment.NewLine + "(詳細はログを参照ねがいます。)";
                }
            }
        }

        private void AvatarProcess(World world, Cell cell, Cell y, int dir)
        {
            //解説:ここでは AvatarControlleダイアログを、メインウィンドウの右隣に呼び出す処理しかやっていません。

            AvatarController ac = new AvatarController(world, cell, y, dir);
            //Worldクラスインスタンスを持つ親フォームの右隣に表示する
            ac.StartPosition = FormStartPosition.Manual;//ダイアログ位置を「任意指定」にする
            Form parent = this.FindForm();                //親フォーム(C++なら「へのポインター」)
            ac.Location = new Point(parent.Location.X + parent.Width, parent.Location.Y);      //解説:Yは同じ、Xは右隣
            ac.ShowDialog(this);
            ac.Dispose();
            if(dir != Not_Found)                        //yが存在しない場合もあるので
                y.VitalCheck();                            //活動後の相手セルの生死状態をチェック(解説:yが死亡した場合の描画処理用)
        }

        private void AutoProcess(Cell cell, Cell y, int dir)
        {

            //解説:ここが従来の処理で、アバターがない場合、全てここで処理されます。

            if(dir != Not_Found)                        //周囲にセルが見つかったならば
            {
                y.VitalCheck();                            //相手セルの生死状態をチェック
                if(y.Alive)                                //相手セルが生きていれば
                {
                    if(cell.Intmarg)                    //異種族婚を許可しているか
                    {
                        //【調整項目】
                        if(cell.Affection < 30 &&        //愛着性が30未満で
                            cell.Hostility > 70)        //敵対性が70超であれば
                            Intimidate(cell, y, dir);    //相互に威嚇する
                        else if(cell.Affection > 40 &&    //愛着性が40超で
                            cell.Hostility < 50)        //敵対性が50未満であれば
                            Get_Close(cell, y, dir);    //親交を結ぶ
                        else                            //それ以外は
                            Flee(cell, y, dir);            //相手から離れる
                    }
                    else                                //異種族婚が認められなければ
                    {
                        if(cell.Tribe != y.Tribe)        //異種族なら
                            Intimidate(cell, y, dir);    //相互に威嚇する
                        else                            //同種族であっても
                        {
                            //【調整項目】
                            if(cell.Affection < 30 &&    //愛着性が30未満で
                                cell.Hostility > 70)    //敵対性が70超であれば
                                Intimidate(cell, y, dir);//相互に威嚇する
                            else if(cell.Affection > 40 &&    //愛着性が40超で
                                cell.Hostility < 50)    //敵対性が50未満であれば
                                Get_Close(cell, y, dir);//親交を結ぶ
                            else                        //敵対が強ければ
                                Flee(cell, y, dir);        //相手から離れる
                        }
                    }
                    y.VitalCheck();                        //活動後の相手セルの生死状態をチェック
                }
                else                                    //死んでいれば食物と同じ扱いとなる
                {
                    cell.Get_Food(dir);                    //移動して摂食する
                }
            }
            else                                        //周囲にセルがいないならば
            {
                dir = cell.Find_Food();                    //周囲に食物があるかどうか確認する
                if(dir != Not_Found)                    //あれば移動して、摂食する
                    cell.Get_Food(dir);
                else
                {
                    dir = cell.Find_Field();            //周囲に野原があるかどうか確認する
                    if(dir != Not_Found)                //あれば移動する
                        cell.Get_Move(dir);
                }
            }
        }

        public void Get_Close(Cell x, Cell y, int dir)    //親しくなり、愛着性が高まり、敵対性が低まる
        {
            //セルx, y両者の愛着性が50%を超える場合、交尾に進む
            if(x.Affection > 60 && y.Affection > 60)    //【調整項目】
                Make_Love(x, y, dir);
            //そうでなければセルx, y両者の愛着性が増え、敵対性が減る
            else {
                x.Affection += Increase;
                y.Affection += Increase;
                x.Hostility += Decrease;
                y.Hostility += Decrease;
            }
        }

        public void Make_Love(Cell x, Cell y, int dir)    //一定の愛着性を超えると交尾し、生殖力により出産する
        {
            //xと同種族のセルが何処か(!)に誕生する
            if(x.Fertility > 50)                        //【調整項目】50だとなかなか生まない
            {
                int n = CellList.Count;        //次のIDを控える
                Create_Cells(1, x.Intmarg);    //xと同じIntmargのセルを生成
                Cell z = Get_Cell(n);        //新生セルのポインターを取得
                z.Tribe = x.Tribe;            //新生セルをxと同じ種族に設定
                //【調整項目】出産後、相手yを殺し、摂食する
                //y.Killed();
                //x.Get_Food(dir);

            }
            else    //離別(逃走)する
                Flee(x, y, dir);
        }

        public void Intimidate(Cell x, Cell y, int dir)    //敵対性と愛着性の度合いにより、相手を威嚇し、敵対性が高まり、愛着性が低まる
        {
            //セルx, y両者の敵対性が50%を超える場合、闘争に進む
            if(x.Hostility > 50 && y.Hostility > 50)    //【調整項目】
                Fight(x, y, dir);
            //そうでなければセルx, y両者の敵対性が増え、愛着性が減る
            else
            {
                x.Affection += Decrease;
                y.Affection += Decrease;
                x.Hostility += Increase;
                y.Hostility += Increase;
            }
        }

        public void Fight(Cell x, Cell y, int dir)        //異種族婚許可、敵対性により、相互に攻撃、被害を受ける
        {
            //相手の攻撃力が自分の攻撃力よりMAX_PARAM / 4(25)以上、又は
            //相手の生命力が自分の生命力の倍以上あれば

            if((y.Power > x.Power + Max_Param / 4) ||
                (y.Power > x.Power * 2))
                Flee(x, y, dir);                                //逃走する
            else {
                //xの先制攻撃(xの攻撃力がyの防御力を上回る場合、被害を与える)
                y.Damaged(x.Power - y.Defense);
                //yの後攻攻撃
                x.Damaged(y.Power - x.Defense);
            }
        }

        public void Flee(Cell x, Cell y, int dir)        //敵対状態になっても、攻撃力、防御力の差が大きければ逃走する
        {
            int reverse = x.Find_Field(false, dir);        //相手yの反対方向の野原を探す
            x.Get_Move(reverse);                        //逃走する
        }

        public void Make_Log()                            //WorldのCell情報をログに記録
        {
            if(CellList.Count > 0)
            {
                //セルデータの出力ファイル名("CellOf(yyyy-mm-dd_hh_mm)from<Ini_Pop>.txt")
                string strNow = DateTime.Now.ToString("yyyy-MM-dd_HH_mm");
                 string path = ".\\Log\\CellsOf(" + strNow + ")from" + Ini_pop.ToString() + ".txt";
                //ファイル出力(解説:今回はファイルをすべてUnicode(UTF8)にしています。)
                using (StreamWriter file = new StreamWriter(path, false, Encoding.UTF8))
                {
                    file.Write(Show_Cell_Param());        //ファイルを書く
                    file.Close();                        // ファイルを閉じる
                }
            }
        }

 

さてさて、それでは次回は最後にAvatarControllerダイアログと描画処理について解説します。

 

前回Cellクラスをやったので、今回からWorldクラスをやります。

Worldクラスも以前にオリジナルのC++のプログラムのCworldクラスで解説しましたので、単に移植しただけ前半アバターを入れた為に大きく変えたGoes_Aroundメソッド関連中盤と、PictureBox派生コントロール化した描画部分後半ということで3回でやりましょうか?

 

正直「単に移植しただけ前半」は基本C++版のCworldクラスの構成や機能とあまり変わりはありません。以下はWorldクラスの基本的なデータ(フィールド)とメソッドになります。コメントを細かく書いているので内容は分かると思いますし、追加で「解説:」を入れておきました。

 

【Worldクラス前半】

    //////////////////
    //Worldクラス定義
    //////////////////

    public class World : PictureBox
    {
        ///////////////////////
        //定数定義(変更不可)
        ///////////////////////

        const int Wld_Width = 80;                //Worldの幅(セル数)
        const int Wld_Height = 40;                //Worldの高さ(セル数)
        const int Max_Population = Wld_Width * Wld_Height / 2;    //「世界」に棲めるセルの最大人口
        const int Max_Tribe = 4;                //現在はCell-P, Cell-B, Cell-G, Cell-D(死亡)
        const int Max_Dir = 8;                    //↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
        const int Not_Found = -1;                //Find_***処理用
        /////////////////////////////////
        //ゲーム関係フィールド
        /////////////////////////////////

        public static Random rand;                //乱数(World、Cell共通)-解説:例のプログラムで一本化した乱数発生器です。
        private bool Gameover = false;            //ゲーム終了サイン
        private string Message;                    //ゲームオーバーの際に、呼び出しユーザーに返すメッセージ
        private int Ini_pop = 0;                //開始時のセル数(0で初期化)
        private List<Cell> CellList = new List<Cell>();                //セルリスト
        private int[] Tribe_Status = new int[4];                    //Cell-P, Cell-B, Cell-G, Cell-Dの状況を記録(0で初期化)
        private char[] Map = new char[Wld_Width * Wld_Height];        //「文字」地形図(野原:' '、山:'^'、河川:'~'、食物:'O')
        private int[] PrevMap = new int[Wld_Width * Wld_Height];    //直前の「論理」地形図(野原:0、山:1、河川:2、食物:3、セル:4 + ID)
        public int MyID = Not_Found;            //アバターCellのID
        //【調整項目】定数定義(変更可)
        int Max_Param = 100;                    //Cellの属性の最大値
        int Increase = 1;                        //パラメーターの増加幅
        int Decrease = -1;                        //パラメーターの減少幅
        ///////////////////////
        //描画関係フィールド
        ///////////////////////
        //地形描画用Bitmap「野原:0」、「山:1」、「河川:2」、「食物:3」

        Bitmap[] Terrain_Bmp = new Bitmap[Max_Tribe];
        //Cell描画用Bitmap「ピンク:0」、「ブルー:1」、「グリーン:2」、「モノ(死亡):3」+ Cell-Me(Avatar)
        Bitmap[] Cell_Bmp = new Bitmap[Max_Tribe + 1];
        //PictrueBox描画関係プロパティ
        public Bitmap Canvas {set; get;}        //仮想画面ビットマップ
        public Graphics gHandle {set; get;}        //仮想画面のグラフィック

        //コンストラクター
        public World()
        {
            //乱数の初期化
            rand = new Random((int)DateTime.Now.Ticks);
            //World(PictureBoxから派生)コントロール
            this.Width = 16 * Wld_Width;        //16 x 16ビットマップ x 80
            this.Height = 16 * Wld_Height;        //16 x 16ビットマップ x 40
            //本プログラムのリソースマネージャー作成
            ResourceManager rm = new ResourceManager("Cell_World", Assembly.GetAssembly(typeof(World)));
            //ビットマップの読み込みー解説:埋め込みリソースに入っています。
            //地形は「野原:0」、「山:1」、「河川:2」、「食物:3」
            Terrain_Bmp[0] = (Bitmap)rm.GetObject("Field");
            Terrain_Bmp[1] = (Bitmap)rm.GetObject("Mountain");
            Terrain_Bmp[2] = (Bitmap)rm.GetObject("River");
            Terrain_Bmp[3] = (Bitmap)rm.GetObject("Food");
            //Cellは「ピンク:0」、「ブルー:1」、「グリーン:2」、「モノ(死亡):3」
            Cell_Bmp[0] = (Bitmap)rm.GetObject("Cell-P");
            Cell_Bmp[1] = (Bitmap)rm.GetObject("Cell-B");
            Cell_Bmp[2] = (Bitmap)rm.GetObject("Cell-G");
            Cell_Bmp[3] = (Bitmap)rm.GetObject("Cell-D");
            //アバターCell用-解説:アバターセルは濃いピンクのセルになります。
            Cell_Bmp[Max_Tribe] = (Bitmap)rm.GetObject("Cell-Me");
            //「世界」の初期化
            Init();
        }

        //デストラクター
        ~World()
        {
            //「世界」の初期化
            Init();                                    //「世界」の初期化
            //ビットマップの廃棄
            for(int i = 0; i < Max_Tribe; i++)
            {
                Terrain_Bmp[i].Dispose();
                Cell_Bmp[i].Dispose();
            }
            //アバターCell用
            Cell_Bmp[Max_Tribe].Dispose();
            //描画関係終了処理
            if(gHandle != null)
                gHandle.Dispose();                    //グラフィックリソースの開放
            if(Canvas != null)
                Canvas.Dispose();                    //ビットマップリソースの開放
        }

        /////////////////////
        //ゲーム関係メソッド
        /////////////////////

        public void Init()                            //「世界」の初期化
        {
            //MapとPrevMapの初期化(新しいC#ではArray.Fill)-Windows 11標準装備のC#はVer 5.0
            for(int i = 0; i < Map.Length; i++)
            {
                Map[i] = '\0';        //オリジナルの「文字」マップ
                PrevMap[i] = -1;    //「文字」マップに基づく論理マップ
            }
            //再ゲームも考えられるのでGCに任せる
            CellList.Clear();                        //Cellのリストを初期化
            Gameover = false;                        //ゲーム開始
            Message = String.Empty;                    //メッセージの初期化
            //既存の仮想画面、グラフィックハンドルがあれば廃棄する
            if(gHandle != null)
                gHandle.Dispose();
            if(Canvas != null)
                Canvas.Dispose();
            //指定サイズ(16 x 16)のビットマップ仮想画面(80 x 40)の作成
            Canvas = new Bitmap(16 * Wld_Width, 16 * Wld_Height);
            //Graphicsクラス(アンチエイリアス)
            gHandle = Graphics.FromImage(Canvas);
            gHandle.SmoothingMode = SmoothingMode.AntiAlias;
            gHandle.PixelOffsetMode = PixelOffsetMode.HighQuality;
            //背景表示
            if(Read_Map("world_terrain.trn"))    //解説:この"world_terrain.trn"がシステムマップです。
                ShowMap();
        }

        public void GameOver()                        //強制終了
        {
            //ゲーム終了処理
            Gameover = true;
        }

        public bool Is_GameOver()                    //ゲーム終了サインの取得
        {
            return Gameover;
        }

        public string Get_Message()                    //ゲーム終了時のメッセージを取得
        {
            return Message;
        }

        public bool Read_Map(string fn)                //「世界」の地図を読み込む
        {
            string data = "";
            try
            {
                using(StreamReader file = new StreamReader(fn, Encoding.UTF8))    //入力UTF-8
                {
                    data = file.ReadToEnd();        //ファイルを読む
                    file.Close();                    //ファイルを閉じる
                }
            }
            catch(FileNotFoundException e)            //FileNotFoundException用
            {
                string errMessage = "地形ファイル(" + fn + ")を読み込めませんでした" + Environment.NewLine;
                MessageBox.Show(errMessage + e.Message , "ファイル読込エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                Message += errMessage;
                return false;
            }
            catch(Exception e)                        //FileNotFoundException以外用
            {
                MessageBox.Show("その他エラーが発生しました。\r\n" + e.Message , "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            //文字列の行分割
            string[] lines = data.Split(new[]{"\r\n","\n","\r"}, StringSplitOptions.None);
            //エラーチェック
            if(lines.Length != Wld_Height)
            {
                MessageBox.Show("正しい地形ファイルではありません。" , "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            else
            {
                data = String.Empty;
                foreach(string line in lines)
                    data += line;
                Map = data.ToCharArray();
            }
            return true;
        }

        public int Check_What(int x, int y)            //Mapの桝目をチェックし、何があるかを返す(野原:0、山:1、河川:2、食物:3、セル:4 + m_id、それ以外:-1)
        {
            int val = Not_Found;    //戻り値
            //エラーチェック
            if(x >= Wld_Width || x < 0 || y >= Wld_Height || y < 0)
                return val;
            Cell c;                    //周囲にCellがいる場合のID取得用
            switch(Map[y * Wld_Width + x])
            {
                case '\0':            //「NULL(0X0)」 - 0
                case ' ':            //「野原(0X20)」- 0
                    val = 0;
                    break;
                case '^':            //「山(0X5E)」  - 1
                    val = 1;
                    break;
                case '~':            //「河川(0X7E)」- 2
                    val = 2;
                    break;
                case 'O':            //「食物(0X4F)」- 3
                    val = 3;
                    break;
                case 'C':            //「cell(0X43)」- 4 + ID
                    c = Get_Cell(x, y);    //その位置にいるセルのポインターを取得
                    val = 4 + c.ID;        //セルであることとIDを通知する
                    break;
                default:            //エラー処理
                    Message += "地形地図に規定外のもの(" + Map[y * Wld_Width + x].ToString() + ")が発見されました" + Environment.NewLine;
                    break;
            }
            return val;
        }

        public bool Create_Cells(int n, bool intmarg)    //指定個数のcellを作る
        {
            //開始時のセルの数を記録
            if(Ini_pop == 0)    Ini_pop = n;
            //Cellの生成
            for(int i = 0; i < n; i++)
            {
                Cell c = new Cell(intmarg);                //Cellインスタンスを生成
                //誕生位置決定
                while(true)
                {
                    //新たなCellのX、Y座標を乱数で求め、
                    Point loc = new Point(rand.Next() % Wld_Width, rand.Next() % Wld_Height);
                    //そこが「野原(0)」か否かチェック
                    if(Check_What(loc.X, loc.Y) == 0)
                    {
                        c.Born_At(CellList.Count, loc.X, loc.Y);    //空白であれば誕生位置として登録
                        Map[loc.Y * Wld_Width + loc.X] = 'C';        //Cellをマップに書き込む
                        Check_Around(c);                            //Cellの周囲情報を取得させる
                        break;
                    }
                }
                CellList.Add(c);                        //セルリストへ登録
            }
            return true;
        }

        public int How_Many_Cells()                        //m_celllistの登録されたcellの数を返す
        {
            return CellList.Count;
        }

        public void Check_Around(Cell c)                //周囲の状況をチェックし、指定cellのAround[8]を更新する
        {
            int[] around = c.Around;                    //Cell周囲の存在物(野原:0、山:1、河川:2、食物:3、セル:4 + ID)
            Point loc = c.Where();                        //CellのLocationを取得
            //エラーチェック(圏外は-1)
            if(loc.Y == 0)                                //上チェック不要
            {
                if(loc.X % Wld_Width == 0)                //左チェック不要
                {
                    //around[0, 1, 5, 6, 7]は不要
                    around[0] = around[1] = around[5] = around[6] = around[7] = Not_Found;
                    around[2] = Check_What(loc.X + 1, loc.Y);
                    around[3] = Check_What(loc.X + 1, loc.Y + 1);
                    around[4] = Check_What(loc.X, loc.Y + 1);
                }
                else if(loc.X % Wld_Width == Wld_Width - 1)    //右チェック不要
                {
                    //around[0, 1, 2, 3, 7]は不要
                    around[0] = around[1] = around[2] = around[3] = around[7] = Not_Found;
                    around[4] = Check_What(loc.X, loc.Y + 1);
                    around[5] = Check_What(loc.X - 1, loc.Y + 1);
                    around[6] = Check_What(loc.X - 1, loc.Y);
                }
                else                                    //左右チェック要
                {
                    //around[0, 1, 7]は不要
                    around[0] = around[1] = around[7] = Not_Found;
                    around[2] = Check_What(loc.X + 1, loc.Y);
                    around[3] = Check_What(loc.X + 1, loc.Y + 1);
                    around[4] = Check_What(loc.X, loc.Y + 1);
                    around[5] = Check_What(loc.X - 1, loc.Y + 1);
                    around[6] = Check_What(loc.X - 1, loc.Y);
                }
            }
            else if(loc.Y == Wld_Height - 1)            //下チェック不要
            {
                if(loc.X % Wld_Width == 0)                //左チェック不要
                {
                    //around[3, 4, 5, 6, 7]は不要
                    around[3] = around[4] = around[5] = around[6] = around[7] = Not_Found;
                    around[0] = Check_What(loc.X, loc.Y - 1);
                    around[1] = Check_What(loc.X + 1, loc.Y - 1);
                    around[2] = Check_What(loc.X + 1, loc.Y);
                }
                else if(loc.X % Wld_Width == Wld_Width - 1)    //右チェック不要
                {
                    //around[1, 2, 3, 4, 5]は不要
                    around[1] = around[2] = around[3] = around[4] = around[5] = Not_Found;
                    around[0] = Check_What(loc.X, loc.Y - 1);
                    around[6] = Check_What(loc.X - 1, loc.Y);
                    around[7] = Check_What(loc.X - 1, loc.Y - 1);
                }
                else                                    //左右チェック要
                {
                    //around[3, 4, 5]は不要
                    around[3] = around[4] = around[5] = Not_Found;
                    around[0] = Check_What(loc.X, loc.Y - 1);
                    around[1] = Check_What(loc.X + 1, loc.Y - 1);
                    around[2] = Check_What(loc.X + 1, loc.Y);
                    around[6] = Check_What(loc.X - 1, loc.Y);
                    around[7] = Check_What(loc.X - 1, loc.Y - 1);
                }
            }
            else                                        //上下チェック要
            {
                if(loc.X % Wld_Width == 0) {            //左チェック不要
                    //around[5, 6, 7]は不要
                    around[5] = around[6] = around[7] = Not_Found;
                    around[0] = Check_What(loc.X, loc.Y - 1);
                    around[1] = Check_What(loc.X + 1, loc.Y - 1);
                    around[2] = Check_What(loc.X + 1, loc.Y);
                    around[3] = Check_What(loc.X + 1, loc.Y + 1);
                    around[4] = Check_What(loc.X, loc.Y + 1);
                }
                else if(loc.X % Wld_Width == Wld_Width - 1) {    //右チェック不要
                    //around[1, 2, 3]は不要
                    around[1] = around[2] = around[3] = Not_Found;
                    around[0] = Check_What(loc.X, loc.Y - 1);
                    around[4] = Check_What(loc.X, loc.Y + 1);
                    around[5] = Check_What(loc.X - 1, loc.Y + 1);
                    around[6] = Check_What(loc.X - 1, loc.Y);
                    around[7] = Check_What(loc.X - 1, loc.Y - 1);
                }
                else {                                    //左右チェック要
                    //全て必要
                    around[0] = Check_What(loc.X, loc.Y - 1);
                    around[1] = Check_What(loc.X + 1, loc.Y - 1);
                    around[2] = Check_What(loc.X + 1, loc.Y);
                    around[3] = Check_What(loc.X + 1, loc.Y + 1);
                    around[4] = Check_What(loc.X, loc.Y + 1);
                    around[5] = Check_What(loc.X - 1, loc.Y + 1);
                    around[6] = Check_What(loc.X - 1, loc.Y);
                    around[7] = Check_What(loc.X - 1, loc.Y - 1);
                }
            }
        }

        public Cell Get_Cell(int n)                        //CellListの指定番目のセルを返す
        {
            return CellList[n];
        }

        public Cell Get_Cell(int x, int y)                //座標位置にあるセルを返す
        {
            int LastCell = CellList.Count;
            for(int i = 0; i < LastCell; i++)
            {
                Point loc = CellList[i].Where();
                if(loc.X == x && loc.Y == y)
                    return CellList[i];
            }
            return null;
        }

        public string Get_Distribution()                //ピンク、ブルー、グリーンの生死状況を返す
        {    //解説:C++の時はデータのみ整数配列で戻していましたが、C#では最初から文字列にしています。
            //Tribe_Statusの初期化
            for(int i = 0; i < Tribe_Status.Length; i++)
                Tribe_Status[i] = 0;
            int LastCell = CellList.Count;
            for(int i = 0; i < LastCell; i++)
            {
                switch(CellList[i].Tribe)
                {
                    case 0:        //ピンク
                        Tribe_Status[0]++;
                        break;
                    case 1:        //ブルー
                        Tribe_Status[1]++;
                        break;
                    case 2:        //グリーン
                        Tribe_Status[2]++;
                        break;
                    default:    //死亡(モノ)
                        Tribe_Status[3]++;
                        break;
                }
            }
            string Distribution = "----------" + Environment.NewLine
                                + "ピンク セル:" + Tribe_Status[0].ToString() + Environment.NewLine
                                + "ブルー セル:" + Tribe_Status[1].ToString() + Environment.NewLine
                                + "グリーンセル:" + Tribe_Status[2].ToString() + Environment.NewLine
                                + "死亡  セル:" + Tribe_Status[3].ToString();
            return     Distribution;
        }

        public string Show_Cell_Param()                    //総てのcell属性を表示する
        {
            string parameters = String.Empty;
            int LastCell = CellList.Count;
            for(int i = 0; i < LastCell; i++)
            {
                parameters += CellList[i].Show_Param();
                if(i == MyID)    //アバターを指定している場合にアバターのCellについて修正する
                {   //解説:以下は文字列末尾の改行コードを外し、" - My avatar"を付けてからまた改行コードを付けています。
                    parameters = parameters.TrimEnd('\r', '\n') + " - My avatar" + Environment.NewLine; 
                }
            }
            return parameters;
        }

 

では、次回はWorldクラスのGows_Around()メソッドとアバター処理を解説します。

 

前回からの続きです。今回はCell_World共通部分とCellクラスを取り上げます。

 

【World_Cellコード-共通部分とCellクラス】

//////////////////////////////////
// Cell_World.cs
// Class Definitions for Cells.cs
// Copyright (c) 2026 by Y-sama
//////////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;        //Graphicsクラス(アンチエイリアス)使用の為
using System.Collections.Generic;    //List使用の為
using System.IO;                    //FileStream使用の為
using System.Text;                    //Encording使用の為
using System.Reflection;            //Assemblyを使う為
using System.Resources;                //リソース使用の為

namespace Cell_World
{
    /////////////////
    //Cellクラス定義
    /////////////////

    public class Cell
    {
        ///////////////////////
        //ゲーム関係フィールド
        //     定数定義
        ///////////////////////

        int Max_Left = 17520;                    //セルの最長余命(単位1時間、2年で24hours x 365days x 2years = 17,520)
        int Max_Life = 4320;                    //セルの最大初期エネルギー(半年で24hours x 180days = 4320)
        const int Max_Tribe = 4;                //現在はCell-P, Cell-B, Cell-G, Cell-D(死亡)
        const int Max_Dir = 8;                    //↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
        const int Not_Found = -1;                //Find_***処理用
        int Living_Cost = 24;                    //【調整項目】余命(Left)、生命力(Life)の消耗時間
        /////////////////////////////////
        //メンバーフィールドとプロパティ
        /////////////////////////////////
        //生存フラグ

        public bool Alive {get; private set;}    //生存(true)、死亡(false)
        //繁殖フラグ
        public bool Intmarg {get; set;}            //異種族間婚の許可
        //位置
        private Point Location;                    //Cellの位置(World内の位置座標)
        private int[] m_Around = new int[8];    //環境情報フィールド(Aroundの実体)
        public int[] Around                        //Cell周囲の存在(野原:0、山:1、河川:2、食物:3、セル:4 + ID)
        {    get
            {
                return m_Around;
            }
            private set
            {
                m_Around = value;
            }
        }
        //性格系
        public int Affection {get; set;}        //愛着性
        public int Hostility {get; set;}        //敵対性
        //能力系
        public int Left {get; set;}                //余命(Max_Left: 単位1時間、2年で24hours x 365days x 2years = 17,520)
        public int Life {get; set;}                //生命力(Max_Life: 半年分で24hours x 180days = 4320)
        public int Power {get; private set;}    //攻撃力(Lifeによる)
        public int Defense {get; private set;}    //防御力(LifeとLeft<経験>による)
        public int Fertility {get; private set;}//生殖力(Leftによる)
        //識別系
        public int ID {get; set;}                //Cellのゼロベースの固有番号
        public int Tribe {get; set;}            //種族(ピンク:0、ブルー:1、グリーン:2、モノ(死亡):3の4色)
        //運動系
        public int Dir {get; set;}                //Cellの移動方向(↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7)

        ///////////////////
        //メンバーメソッド
        ///////////////////

        public Cell(bool intmarg)                //コンストラクター
        {
            //異種族婚許可、性格系、能力系、運動系パラメーターは生成時決定
            Init(intmarg);
        }

        public void Init(bool intmarg)            //Cellの初期化
        {
            //CellのIDはWorldクラスのCreate_Cellで与えられ、Born_Atで記録される
            ID = Not_Found;
            //生存フラグ
            Alive = true;
            //繁殖フラグ(原則異種族間婚はタブー)
            Intmarg = intmarg;
            //位置(周囲状況-WorldのCheck_Aroundで更新される)
            for(int i = 0; i < 8; i++)
                Around[i] = Not_Found;    //Not_Found(-1)はエラー値
            //性格系
            Affection = World.rand.Next(10, 101);        //愛着性(10から100まで)
            Hostility = World.rand.Next(10, 101);        //敵対性(10から100まで)
            //余命
            Left = (Max_Left * World.rand.Next(10, 101)) / 100;    //最長余命 X 10 - 100%
            //生命力(半年分)
            Life = (Max_Life * World.rand.Next(10, 101)) / 100;    //最大初期エネルギー X 10 - 100%
            //能力系
            Power = World.rand.Next(10, 101);            //攻撃力(LifeとLeftによる)
            Defense = World.rand.Next(5, 51)        ;    //防御力(Ditto)-一般に防御力は攻撃力より弱い
            Fertility = World.rand.Next(10, 101);        //生殖力(Leftによる)
            //識別系(種族表示色-ピンク、ブルー、グリーン、モノ(死亡)の4種-前3種を使用)
            Tribe = World.rand.Next(Max_Tribe - 1);
            //運動系
            Dir = World.rand.Next(8);                    //↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
        }

        public bool Born_At(int id, int x, int y)        //CellのID、座標設定(どこに、誰として生まれるのか)
        {
            if(ID != Not_Found)                            //既に生まれている場合、エラー
                return false;
            else
            {
                ID = id;
                Location.X = x;
                Location.Y = y;
                return true;
            }
        }

        public string Show_Param()                        //パラメーターの表示
        {
            string param = "No.";
            if(ID < 9)
                param += " " + (ID + 1).ToString() + " ";
            else
                param += (ID + 1).ToString() + " ";
            param += Alive ? "生存状態:存命" : "生存状態:死亡";
            param += "(余命:" + Left.ToString() + ")";
            string[] tribe = new string[4] {"ピンク", "ブルー", "グリーン", "モノトーン(死亡)"};
            param += "、種族:" + tribe[Tribe];
            param +=  "、愛着性:" + Affection.ToString() + "、敵対性:" + Hostility.ToString()
            + "、生命力:" + Life.ToString() + "、攻撃力:" + Power.ToString()
            + "、防御力:" + Defense.ToString() + "、生殖力:" + Fertility.ToString() + Environment.NewLine;
            return param;
        }

        public Point Where()                    //Cellの座標の取得
        {
            return Location;
        }

        public void Get_There(Point there)        //指定locationへ移動する
        {
            Location = there;
        }

        public void VitalCheck()                //死亡要因をチェックし、Aliveを決定する
        {
            if(Left > 0 && Life > 0)            //余命があり、且つ生命力がある
            {
                Alive = true;
            }
            else
            {
                Alive = false;                    //死亡
                Tribe = 3;                        //セルの色をモノ(死亡-3 <食物>)に変える
            }
        }

        public int Find_Cell()                    //周囲にセルがいるか否かチェックする(戻り値は複合)
        {                                        //戻り値(val):絶対方向(val & 0x0F)、周囲のセルのID(val >> 4)
            int dir = 0;                        //戻り値(全bitが0)
            int id = Not_Found;                    //戻り値(全bitが1)
            //方向は↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
            for(int i = 0; i < Max_Dir; i++)
            {
                dir = (Dir + 7 + i) % Max_Dir;    //進行方向斜め左から
                if(Around[dir] >= 4)            //セルがいる場合
                {
                    id = Around[dir] - 4;        //idはセルのID
                    return dir | (id << 4);        //idはdir(0 - 7)がかからないよう16倍にする
                }
            }
            return Not_Found;                    //セルがいない場合は-1を返す
        }

        public int Find_Food()                    //周囲に食物があるか否かチェックする
        {
            //方向は↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
            for(int i = 0; i < Max_Dir; i++)
            {
                int f_dir = (Dir + 7 + i) % Max_Dir;    //進行方向斜め左から
                if(Around[f_dir] == 3)
                    return f_dir;                //戻り値は絶対方向(0-7)
            }
            return Not_Found;                    //ない場合は-1を返す
        }

        public void Get_Food(int dir)            //周囲に食物があれば移動して摂食する
        {
            if(dir < 0 || dir > 7)
                return;                            //エラー(何もしない)
            Get_Move(dir)    ;                    //食物へ移動する
            Life += 168;                        //【調整項目】7日分(24h x 7d)のエネルギー回復
        }

        //周囲に(進むことが出来る)野原があるか否かチェックする |7|0|1|
        //進行方向(Dir)と前進、後退を指定できる(forward)         |6|C|2|
        //前進→Dir、後退→(Dir + 4) % 8;                         |5|4|3|

        public int Find_Field(bool forward = true, int d = -1)
        {
            //進行方向指示がある場合(エラーチェック付)
            if(d > -1 && d < 8)
                Dir = d;
            //野原の方角を記録(nは野原のある方向の数、dirは戻り値)
            int[] field = new int[Max_Dir] {-1, -1, -1, -1, -1, -1, -1, -1};
            int n = 0, dir = Not_Found;
            //方向は↑:0、↗:1、→:2、↘:3、↓:4、↙:5、←:6、↖:7
            for(int i = 0; i < Max_Dir; i++)
            {
                if(Around[i] == 0)                //Around[]に野原があれば
                {
                    field[n] = i;                //fieldにその方向を記録してゆく
                    if(forward)                    //前進可で
                    {
                        if(i == Dir)            //進行方向に野原があれば
                            dir = i;            //その方向
                    }
                    else                        //後退で
                    {
                        if(i == (Dir + 4) % 8)    //逆方向に野原があれば
                            dir = i;            //その方向
                    }
                    n++;
                }
            }
            if(n == 0)                            //ない場合は
                return Not_Found;                //-1を返す(エラー)
            else
            {
                if(dir == -1)                    //真前または後ろへ進めないなら
                    dir = field[World.rand.Next() % n];    //乱数による選択
                //dirがDirの場合でも「1/8(12.5%)」の確率で(パニック状態)
                else if(dir == Dir && World.rand.Next() % Max_Dir == 0)
                    dir = field[World.rand.Next() % n];    //乱数による選択
            }
            return dir;
        }

        public void Get_Move(int dir)            //野原があれば一つ移動する(Locationを変更する)
        {
            if(dir < 0 || dir > 7)
                return;                            //エラーチェック
            switch(dir)
            {
                case 0:        //↑
                    Location.Y--;
                    break;
                case 1:        //↗
                    Location.X++;
                    Location.Y--;
                    break;
                case 2:        //→
                    Location.X++;
                    break;
                case 3:        //↘
                    Location.X++;
                    Location.Y++;
                    break;
                case 4:        //↓
                    Location.Y++;
                    break;
                case 5:        //↙
                    Location.X--;
                    Location.Y++;
                    break;
                case 6:        //←
                    Location.X--;
                    break;
                case 7:        //↖
                    Location.X--;
                    Location.Y--;
                    break;
            }
            Dir = dir;
        }

        public void Aging()                            //加齢変化(老化)
        {
            Left -= Living_Cost;                    //寿命
            Life -= Living_Cost;                    //生命力
        }

        public void Param_Change()                    //【調整項目】状態変化により、各種パラメーターが変化する
        {
            //【余命とライフステージ】
            //余命(MAX_LEFT-17,520)の90%(15,768)が最大なので、人間の平均余命80歳として
            //幼年期0-15歳(20%)、青年期16-29歳(16%)、壮年期30-49歳(25%)、老年期50-80歳(37.5%)
            //の分布を考えれば、m_leftは、
            //幼年期(~12,951)、青年期(12,950~9,801)、壮年期(9,800~5,901)、老年期(5,900~0)
            //程度と考えられるが、余命の最小値が10%の1,752なので以下のように調整
            //幼年期(~8,001)、青年期(8,000~5,001)、壮年期(5,000~2,001)、老年期(2,000~0)
            //【攻撃力】
            //生命力の初期値は半年分のエネルギー(MAX_LIFE-4320)の10 - 90%であり、食物と死体を摂食
            //することでしか増加できない為、攻撃力(m_power)増加要因として幼年期は1000あれば1/2の
            //確率でm_powerが増加、青年期、壮年期は2000あれば1/2、1/3で増加
            //【防衛力】
            //これは「亀の甲より年の効」で「(見做し)経験を積んだ」として壮年期、老年期に1/3の確率
            //で増加
            //【生殖力】
            //これは若さだけなので、幼少期、青年期に1/2、壮年期に1/3の確率で増加、老年期は1/2の確率
            //で減少

            if(Left > 8000)            //幼年期
            {
                if(Life > 1000 && World.rand.Next() % 2 == 0)
                    Power++;
                if(World.rand.Next() % 2 == 0)
                    Fertility++;
            }
            else if(Left > 5000)    //青年期
            {
                if(Life > 2000 && World.rand.Next() % 2 == 0)
                    Power++;
                if(World.rand.Next() % 2 == 0)
                    Fertility++;
            }
            else if(Left > 2000)    //壮年期
            {
                if(Life > 2000 && World.rand.Next() % 3 == 0)
                    Power++;
                if(World.rand.Next() % 3 == 0)
                    Defense++;
                if(World.rand.Next() % 3 == 0)
                    Fertility++;
            }
            else                    //老年期
            {
                if(World.rand.Next() % 3 == 0)
                    Defense++;
            }
        }

        public void Damaged(int d)                    //加害を受け、生命力、余命が減少する
        {
            //ダメージ(d) = 相手攻撃力(Power) - 自分の防御力(Defense)とする
            if(d > 0)
            {
                Life -= d;
                Left -= d;
            }
        }

        public void Killed()                        //殺されて、生命力、余命が0になる
        {
            Left = 0;
            Life = 0;
            Tribe = 3;
            Alive = false;
        }
    }

 

Worldクラスはもっと長いので、これも適宜分割して解説します。

 

導入と前座を済ませたので、いよいよプログラム解説に入りましょうか。

 

1.今回のCell_World.csとオリジナルのCcell_world.hがどう違うのか?

初めからコードをドバっと書くのではなく、概要とコンセプトを先ず説明しましょう。又、この二つはそれ以外のところは大きく変わらないので、オリジナルの解説()が役に立つでしょう。

【Cell】最後に...Ccell_world.h(1)

  【Cell】最後に...Ccellクラス(2)

  【Cell】最後に...Cworldクラス(3)  

 

(1)一つは(Cell、Worldクラス共に)C++の「変数+隠蔽用関数」をC#のプロパティにしたことです。例えばC++のオリジナルで、

 

private:
    //生存フラグ
    bool m_alive;                //生存(true)、死亡(false)

...

public:

...

   bool is_alive() {return m_alive;};            //生死確認

 

などは、C#では

 

        //生存フラグ
        public bool Alive {get; private set;}    //生存(true)、死亡(false)
 

としています。逆に言えばそれ以外では「m_変数名」が「(大文字・小文字)プロパティ(またはフィールド)名」程度の違いしかありませんので、コードを比較すればよくわかると思います。

 

(2)二つ目は(これもCell、Worldクラス共に)乱数の使用方法を変え、前回も書きました通り)オリジナルではローカルで毎回乱数を生成していたのを、C#では「一つだけ」のWorldクラスの静的な乱数オブジェクトに変えたことです。

 

【C++】

    //ハードウェア乱数をシードにして初期化(uint32_t版)
    std::random_device seed;        //Windowsではrandom_deviceが有効でない可能性がある
    std::mt19937 engine(seed());
    std::uniform_int_distribution<> dist(10, 100);    //10から100までの整数乱数を生成

    ...

   m_affection = dist(engine);        //愛着性
 

【C#】

    Affection = World.rand.Next(10, 101);        //愛着性(10から100まで)
 

Cellクラスはこれだけで他の変更点はない、と思います。

 

(3)WorldクラスをPictureBox派生のコントロールにした

一番大きい変更点は、オリジナルではロジックだけであったWorldをPictureBoxから派生させたコントロールにして、描画機能を持ってきた点です。その為に、

 

        ///////////////////////
        //描画関係フィールド
        ///////////////////////

        //地形描画用Bitmap「野原:0」、「山:1」、「河川:2」、「食物:3」
        Bitmap[] Terrain_Bmp = new Bitmap[Max_Tribe];
        //Cell描画用Bitmap「ピンク:0」、「ブルー:1」、「グリーン:2」、「モノ(死亡):3」+ Cell-Me(Avatar)
        Bitmap[] Cell_Bmp = new Bitmap[Max_Tribe + 1];
        //PictrueBox描画関係プロパティ
        public Bitmap Canvas {set; get;}        //仮想画面ビットマップ
        public Graphics gHandle {set; get;}        //仮想画面のグラフィック
 

 

        ///////////////////
        //描画関係メソッド
        ///////////////////

        //画面消去
        public void Clear()
        {
            gHandle.Clear(this.BackColor);
        }

        //背景(m_map)を表示する
        public void ShowMap()
        {
            bool changed = false;                        //m_Worldのマップとm_PrevMapとの差異フラグ
            for(int j = 0; j < Wld_Height; j++)            //Y座標
            {
                for(int i = 0; i < Wld_Width; i++)        //X座標
                {
                    //ビットマップ表示
                    int n = Check_What(i, j);            //(i, j)の位置に何があるか
                    if(PrevMap[Wld_Width * j + i] != n)    //前回から変更があれば
                    {
                        PrevMap[Wld_Width * j + i] = n;    //記録して
                        changed = true;                    //差異フラグを立てる
                    }
                    Cell c;                                //Cell
                    switch(n)
                    {
                        case 0:    //野原
                        case 1:    //山
                        case 2:    //河川
                        case 3:    
//食物
                            if(changed)                    //前回から変更があれば
                                gHandle.DrawImage(Terrain_Bmp[n], i * 16, j * 16);
                            break;
                        default:
                            if(n > 3)                    //Cell(4 + ID)の場合
                            {
                                c = Get_Cell(i, j);        //(i, j)の位置あるCellを取得
                                if(c.ID == MyID && c.Alive)    //アバターの場合
                                    gHandle.DrawImage(Cell_Bmp[Max_Tribe], i * 16, j * 16);
                                else                        //アバターではない場合
                                    gHandle.DrawImage(Cell_Bmp[c.Tribe], i * 16, j * 16);
                            }
                            else if(n == -1)
                                MessageBox.Show("地形ファイル(*.trn)に許容されない文字が含まれています。",
                                                "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        break;
                    }
                }
                if(changed)        //変化が一つでもあれば、World(描画領域)を更新
                    this.Image = (Image)Canvas;
            }
        }
    }
 

を追加しています。

 

(4)Worldクラスにアバターを導入した

これも大きな変更点ですね。オリジナルは全てPCが判断して糧に動く「(Life Gameのような)正統派の環境ゲームソフト」でしたが、今回はユーザーがアバターでゲームに参加し、マニュアルでアバターを操作できるようにしました。(詳細のコードは長くなるので省きますが、)その為にWOrldクラスのGoes_Aroundメソッドにおいて、

 

        public void Goes_Around()                        //「世界が廻る(時が過ぎる)」→セルに日常活動を行わせる
        {
        ...

                int LastCell = CellList.Count;
                for(int i = 0; i < LastCell; i++)        //"foreach"は配列が変化するとエラーになる為
                {
                    Cell cell = CellList[i];            //IDから対象セルを求める
                    cell.VitalCheck();                    //対象セルの生死状態をチェック
                    if(cell.Alive)                        //生存している場合
                    {
                        Check_Around(cell);                //対象セルの周囲を確認する
                        Point loc = cell.Where();        //対象セルの位置を取得
                        Map[loc.Y * Wld_Width + loc.X] = ' ';    //対象セルの旧座標を' 'にする
                        //周囲のセルの有無を確認する
                        Cell y = null;                    //周囲のセル
                        int dir = Not_Found;            //周囲のセルのいる方向
                        int val = cell.Find_Cell();        //対象セルの周囲にセルがいるか確認
                        if(val != Not_Found)            //周囲にセルが見つかったならば
                        {
                            dir = val & 0x0F;            //dirは周囲のセルの絶対方向(0 - 7)
                            y = Get_Cell(val >> 4);        //周囲のセルのポインターを取得
                        }
                        //コンピューター処理とマニュアル処理へ分岐させる
                        if(i == MyID && cell.Alive)        //ユーザー判断による処理(アバターの場合)-死んだcellは食物でしかないので
                            AvatarProcess(this, cell, y, dir);
                        else                            //コンピューター判断による処理
                            AutoProcess(cell, y, dir);

        ...

        private void AvatarProcess(World world, Cell cell, Cell y, int dir)
        {

        ...

        }

 
        private void AutoProcess(Cell cell, Cell y, int dir)
        {

        ...

        }

というような振り分け処理を行っています。

 

(5)Worldクラスにアバター用コントロールダイアログを導入した

これもアバターを使うことになったので、そのコントロール用に作成しました。

 

    //////////////////////////////
    //AvatarController ダイアログ
    //(最長3秒で自動的に閉じる)
    //////////////////////////////

    class AvatarController : Form
    {

    ...

(長くなるので省略します。)

 

2.World_Cellコード-共通部分とCellクラス

1502行と分量が多いので、先ずは簡単なCellクラスのみ掲載します。...と書いて保存しようとしたら長すぎでエラーが出ましたので、次回に回します。

 

ご容赦!

 

前回【Cells for C#】その1-そもそも...の「セルの世界」)で書いた通り、今回のC++版 "Cell"のC#版 "Cells"への移植では、何度かトラブルがありました。

 

1.最初のトラブル-「System.ArgumentOutOfRangeException: インデックスが範囲を超えています。」

 

最初は「単純移植だけでいいや」と軽く考えていたのですが、C++とC#の「似て非なる」性格から、

 

【C++版"Cell"】

Cell.rc                ーリソースファイル(メニュー、ダイアログ、アイコン、ビットマップ)

Cell.h                 -ウィンドウとダイアログのクラス定義

Cell.cpp            -ウィンドウの作成とエントリーポイント

Ccell_world.h-cellクラスとworldクラスの定義とロジック処理

CellProc.h       -worldとcellのインスタンスを使ったウィンドウプログラム(UI)及び描画

      ↓

   

 

どうするか悩んだ末に、C++版の構成を変更し、

 

【C#版"Cells"】

Cell_World.cs     -CellクラスとWorldクラスの定義とロジックのPictureBox派生コントロール化(描画)

(Cell_World.dll   -↑のモジュール化)

Cell.cs                    ーCell_World.dllを使ったウィンドウプログラム(UI)

 

にしました。そして先ずC++のプログラムを適宜コピーしてC#用の構成とし、C++でよく使う"m_(小文字変数名)"や"(小文字関数名)()"などをC#作法に変えるとともに、C++ではカプシュール化の為のアクセス関数を廃止してC#のプロパティへ移行したり等一通りすべて終了してコンパイルし(勿論エラーなし)、実行すると、「インデックスが範囲を超えています」というエラーが出ます。

 

最初は例の「配列の添字が超過したエラー」だと思い、「配列」だからと、配列回りにトラップをかけてデバッグしてみたのですが、埒が明かないのでChat-GPTに相談すると、

 

「実は配列ではなく List<T> のインデクサ で発生することが非常に多いです。」

 

とのこと。また、通常配列の場合には

 

IndexOutOfRangeException

 

となるところ、今回は

 

ArgumentOutOfRangeException

 

であることから(成程!)「List廻りが怪しい」という有用な示唆を得て、エラー発生個所を

 

    int val = c.Find_Cell();     //対象セルxの周囲にセルがいるか確認

    int dir = val & 0x1111;     //dirは周囲のセルの絶対方向(0 - 7)-解説:"0b"が正しい。(注)

    int id = val >> 4;                 //idはそのセルのID(16倍されている)

注:ついでに言えば、C#はC# 6.0まで二進数表記("0b")が出来ず、最初C"0b"と書き直してまたエラーを出してしまいました。現在C# 5.0用に"0xF"としています。

 

というC++のオリジナルコードに潜んでいたバグ

 

    if(Around[Dir] >= 4)        //解説:本来"dir"となるところ、クラスフィールドで宣言された"Dir"に打ち間違えた。
    {
        id = Around[dir] - 4;
        return dir | (id << 4);
   }

 

が発見できました。(【無駄話】旧悪の暴露

 

2.次のトラブル("foreach"に潜む罠-System.InvalidOperationException)

私はもともとがC++だったので、"for(int i = 0; i < ..."というループを常用していましたが、C#に慣れるにつれ"foreach"を多用するようになりました。今回もC++のオリジナルコードをC#に移植する再、可能なところでは"foreach"に書き換えていたのですが、

 

「この例外は foreach 特有の仕組み によるものです。...foreachでは、内部的に Enumerator(列挙子)が作られ...作成時にそのバージョンを記録しています。...その後、Add(...)、Remove(...)、Clear()等が実行されると、列挙子のバージョンが変化し...次の要素へ進む際に"System.InvalidOperationException:"エラーが発生します。」

 

とのことであり(Listの要素はshiboushitaCellもデータで残すので一切削除はしませんが、交尾して追加することはあるので...)foreach文をすべて元のfor文に戻しました

 

3.乱数関連トラブル

乱数は「偶発的」に「異なる」ものを得たいときに使用し、今回のオリジナルC++コードでもセルを生成する際に種族位置性格能力方向の決定に以下の通り「ローカルにseedと乱数を使って

 

    //ハードウェア乱数をシードにして初期化(uint32_t版)
    std::random_device seed;        //Windowsではrandom_deviceが有効でない可能性がある
    std::mt19937 engine(seed());
    std::uniform_int_distribution<> dist(10, 100);  
 //10から100までの整数乱数を生成
 

いましたが、何故か同じ一つの種族、位置、性格、能力、方向に偏る現象が発生し、又Chat-GPTに相談したところ、

 

「それは非常に典型的な C# 移植時の問題です。」

 

ということで、ローカル

 

Random rand = new Random((int)DateTime.Now.Ticks & 0x0000FFFF);

 

等と毎回乱数発生器を生成するとほぼ同じような初発乱数が生成されると諭されました。その為、C#版ではWorldクラスにが代表してグローバルで使える静的(static)乱数生成器を「一つ」作り、その

 

World.rand.Next()

 

メソッドで異なる乱数を使用することにしました。(注)

注:その後も"(↑の対応を取りました)が、矢張りTribe(種族はピンク、ブルー、グリーンの3種および死亡で4種ですが、生成では前3種しか使わないのでMax_Tribe - 1にしています)がほぼ1種のみとなります。(時々別種が一つくらい)TribeプロパティにはWorld.rand.Next(0, 4)で0-3までの値が発生されるようにしていますが、うまくゆきません。 何か良い知恵がありますか? "

と書きながら、

"↑ と書いたのですが、Cellのコンストラクターに初期化コードが残っていたことを発見し、自己解決しました。foreachもすべてforに書き直し、何らエラーが出なくなりました。ありがとう。"

と恥ずかしいところを見せてしまいました。「デバッグ対応は慌てず、焦らず」ですね。

 

4.最後のダイアログ表示トラブル

これも既に【Chat-GPT】問答その1で書きましたが、メインのUIプログラムであるCells.csで、タイマーを設定してWorldを回す(Goes_Around()メソッドを呼ぶ)のですが、アバターコントロールダイアログを呼ぶとダイアログが大量発生します。

 

 

これもChat-GPTに相談してすぐに原因を特定

 

「今回の症状は WinForms の ShowDialog() が壊れているのではなく、「ダイアログ表示中も Timer の Tick が発生し続けている」ことが原因です。これは WinForms ではよく遭遇する挙動です。」

 

してくれたのですが、「じゃぁ、どうすべきなの?(と酔いのまわった頭では考えられず、直ぐに訊いてしまいました!)と返したら、Chat-GPTが(胸を張って、上から目線で...と感じました!)

 

「私なら UIは Cells.cs 側、World_Cell.dll 側には一切入れません。

 

と自信満々に言うので、(酔いのまわった頭では論議もまともにできないことから...)

 

「一寸試行錯誤して考えてみます。又相談させてください。」

 

と伝え、一晩寝てから翌日の早朝起の寝床の中で

 

「あっ、そうか!単純に呼ぶ際に一旦タイマーを止めて呼び、正常に帰ってきたらタイマーを再開すればいいじゃん!」

 

ということで自己解決しました。そして私が

 

「示唆された内容を考えたのですが、Goes_Aroundメソッドの呼び出しexe側でUI処理を専管する、と言う考えは同意するも、処理の内容は本来呼び出されたdll側で行うべきものであり、それをするためには、dll側にはない処理なので本来隠蔽されていたdll側のフィールド、メソッド等を呼び出しexe側に公開しないとなりませんし、exe側の処理が終わった後、dll側の中断されたforループの続きを再開させなければなりませんので、かなり無理があります。(forループの中でフラグを立てて処理を中断するのは、以降のセルの処理が全て未処理になるので、忌避したい所です。) 矢張り本来dll側でやる処理をユーザーにマニュアルでやらせるのであっても、処理はdllサイドでおこなうべきではないでしょうか? その為には、呼び出しexe側でGoes_Around呼び出し前にタイマーを切り(Enabledをfalse)、呼び出し後再スタートさせれば、dll側で処理することができるのではないでしょうか?」

 

というメッセージを翌朝返すと、Chat-GPTの反応は、

 

「潔さんの考え方にはかなり説得力があります。...(ループを途中)で抜ける案は確かに不自然です。なぜなら、

Cell 0 → 処理済み

Cell 1 → 処理済み

Cell 2 → 処理済み

Cell 3(MyID) → 中断

Cell 4 → 未処理

Cell 5 → 未処理 ...

となり、「1ターンの整合性」が崩れます。潔さんが嫌がる理由はよく分かります。...

 

むしろ潔さんが指摘された

呼び出しexe側でCellの処理をするとなると、
dllの処理対象や処理メソッド全てをexe側のダイアログに持たせる

こちらの方が設計的には不自然です。...

 

結論としては、今の説明を聞く限り、

 

「アバターの行動決定だけをダイアログで行い、実際のセル処理は従来どおりWorld.dll内で実行する」

 

という潔さんの方向性は十分合理的です。特に World が PictureBox 派生の WinForms コントロールであるなら、「DLLだからUIを持ってはいけない」という一般論は、あまり気にしなくてよいと思います。むしろ内部状態やセル処理の隠蔽を維持する方が、このプロジェクトでは重要に見えます。」

 

僕はChat-GPTは自民党衆議院議員になれると思う!