前回はオリジナルのVisual Studioベースの(即ちInitializeComponent()を使った)コードの解説をしましたので、今回は完全コードベースのMSCompAssベースの改造オセロプログラムをやります。(今もちょっと対戦し、1勝4敗でした。残念無念。)

改造のポイントは以下の通りです。

(1)(フィールド(変数)やメソッド(関数)の名称を含め)プログラムの可読性を高めた。

(2)不要、または効率性の点からコードを修正。

(3)機能的には最初に先攻、後攻を選択させる形にして、都度メッセージボックスでユーザー指示を与え、簡単に対戦結果を保存する。

 

いつも通り、コメントで"解説:"します。

 

【Othello.cs】

/////////////////////////////////////////////////////////////////
//オセロゲーム(Othello.cs)
//原典:http://www.souzousha.iinaa.net/Source/Othello.txt
//オセロ(またはReversi):https://en.wikipedia.org/wiki/Reversi
//【用語】(解説:余計なようですが、一応こういう用語で統一しました。)
//Board - 盤
//Disks - 石
//Square - 升目
/////////////////////////////////////////////////////////////////

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Reflection;        //Assemblyを使う為
using System.IO;                 //StreamReader/Writerを使用するのに必要
using System.Text;                 //Encoding.GetEncodingを使用するのに必要

namespace Othello
{
    //アプリケーションクラス(解説:プログラムのエントリーポイント用のクラスです。)
    public class App
    {
        [STAThread]    //解説:これをいつも忘れないように。(SingleThreadApartment)
        public static void Main()
        {
            OthelloForm of = new OthelloForm();
            Application.Run(of);
        }
    }

    //Othelloクラス
    public partial class OthelloForm : Form
    {
        //ウィンドウコントロール
        Label label;                                    //ラベルコントロール
        RadioButton radioButton1, radioButton2;            //ラジオボタンコントロール
        Button startButton, passButton, exitButton, datButton;    //ボタンコントロール
        //表示関係
        public Image image2disp = new Bitmap(412, 412);    //表示用ビットマップ(幅412、高さ412)

        //解説:オリジナルはでかかったのですが、マージンを入れてもこれだけで十分なので。又名称も変更しました。
        public Graphics gr_image;                        //表示用ビットマップのグラフィックス
        //解説:これも名称変更しました。

        Brush bD = new SolidBrush(Color.DarkGreen);        //ブラシ暗い緑色
        Brush bW = new SolidBrush(Color.White);            //        白色
        Brush bB = new SolidBrush(Color.Black);            //        黒色
        Brush bG = new SolidBrush(Color.Gray);            //        灰色
        Pen pB = new Pen(Color.Black, 1);                //ペン    黒色
        Pen pW = new Pen(Color.White, 1);                //        白色
        Pen pG = new Pen(Color.DarkGray, 1);            //        暗い灰色
        Pen p1 = new Pen(Color.White, 2);                //        白色(太さ2)
        Pen p2 = new Pen(Color.White, 4);                //        白色(太さ4)
        //解説:変更なしです。

        string pmpt = "先攻(黒石)、後攻(白石)を選択して、開始ボタンをクリックして下さい。";
        //解説:2か所、プロンプト用の文字列を使っていたので、この文字列に統一しました。

        //オセロデータ関係
        byte[,] dataTable = new byte[8, 8];                //オセロ盤配列[y, x](0-空、1-黒、2-白)

        //解説:略称のdTabが分かりにくかったので変更しました。また添字が[y, x]になる点明記しました。
        int[,] EvalTable =                                //評価用テーブル[y, x](8 x 8の盤に対応)
        //解説:オリジナルのEvalだけだと分かりづらいので変更しました。また添字が[y, x]になる点明記しました。

                {{200,  6, 70, 30, 30, 70,  6, 200},    //戦略的重要度を数値で表している。
                {  6,  5,  7,  6,  6,  7,  5,   6},        //四隅が200で最も高く、四隅の一つ手前等
                 { 70,  7, 40, 30, 30, 40,  7,  70},        //要所が高く設定されている。
                { 30,  6, 30,  1,  1, 30,  6,  30},        //本プログラムの最も重要なノウハウ部分。
                { 30,  6, 30,  1,  1, 30,  6,  30},
                { 70,  7, 40, 30, 30, 40,  7,  70},
                {  6,  5,  7,  6,  6,  7,  5,   6},
                {200,  6, 70, 30, 30, 70,  6, 200}};
        //オセロ盤の特定位置の周囲8方向を表すテーブルで第1引数は↓の方向(0 - 7)を表し、
        //第2引数(0 - 1)はy座標、x座標の差分を表している。
        int[,] vectorTable                                //指定点から伸ばす方向表

        //解説:オリジナルのprocTableでは意味不明なので、名称変更しました。
            = {    {-1, -1 }, {-1, 0 }, {-1, 1},            //上(左、中、右)
                { 0, -1 }, { 0, 1 },                    //左、右
                { 1, -1 }, { 1, 0 }, { 1, 1}};            //下(左、中、右)
        //戦績データ(Log.datを作成する為、追加)・・・解説:戦績データを今回、累計で持ちます。
        int Games, Wins, Loses;                            //今回の勝負数、人間の勝数、人間の負数
        int GT_Games, GT_Wins, GT_Loses;                //その累積数

        //コンストラクター
        public OthelloForm()
        {
            //プログラムアイコンをフォームにつける
            Assembly myOwn = Assembly.GetEntryAssembly();
            this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);

            //解説:因みにこういうアイコンを作りました。(BCCSkeltonで作ったIconViewerの画像)


            this.Size = new Size(560, 528);            //初期サイズ
            this.MinimumSize = new Size(530, 508);    //最小サイズ
            this.Text = "Othello";
            //解説:以下がメインフォームのイベントです。

            this.Load += Form_Load;
            this.MouseClick += MainForm_MouseClick;
        }

        /////////////////////////
        //ウィンドウイベント処理
        /////////////////////////

        //解説:オリジナルではウィンドウイベントメソッド、コントロールイベントメソッド、ユーザーメソッドが混在していたので、このような表示でまとめています。
        //WM_CREATE処理
        private void Form_Load(object sender, EventArgs e)
        {
            //Labelの設定
            label = new Label();
            label.Location = new Point(10, 10);
            label.Width = 400;    //Othello盤面幅(400)
            label.Text = pmpt;    //プロンプト表示
            this.Controls.Add(label);
            //RadioButtonsの設定
            radioButton1 = new RadioButton();
            radioButton1.Location = new Point(10, label.Height + 10);
            radioButton1.Text = "先攻(黒)";
            this.Controls.Add(radioButton1);
            radioButton2 = new RadioButton();
            radioButton2.Location = new Point(radioButton1.Width + 10, label.Height + 10);
            radioButton2.Text = "後攻(白)";
            this.Controls.Add(radioButton2);
            //Buttons(開始、パス、終了、戦績)の設定
            startButton = new Button();
            startButton.Location = new Point(ClientSize.Width - startButton.Width - 10, 20);
            startButton.Text = "開始";
            startButton.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            startButton.Click += startButton_Click;
            this.Controls.Add(startButton);
            passButton = new Button();
            passButton.Location = new Point(ClientSize.Width - passButton.Width - 10, startButton.Height + 30);
            passButton.Text = "パス";
            passButton.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            passButton.Click += passButton_Click;
            passButton.Enabled = false;
            this.Controls.Add(passButton);
            exitButton = new Button();
            exitButton.Location = new Point(ClientSize.Width - exitButton.Width - 10, startButton.Height + passButton.Height + 40);
            exitButton.Text = "終了";
            exitButton.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
            exitButton.Click += exitButton_Click;
            this.Controls.Add(exitButton);
            datButton = new Button();
            datButton.Location = new Point(ClientSize.Width - datButton.Width - 10, this.ClientSize.Height - datButton.Height - 10);
            datButton.Text = "戦績";
            datButton.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            datButton.Click += datButton_Click;
            this.Controls.Add(datButton);
            //ビットマップimage2dispのGraphicsを取得
            gr_image = Graphics.FromImage(image2disp);
            gr_image.Clear(this.BackColor);

            //解説:ビットマップ(image2disp)のグラフィックハンドル(gr_image)でimage2dispの背景を描画します。
            this.Invalidate();    //フォームの再描画(解説:このメソッドでOnPaintイベントが発生します。)
            //ログファイルの読み込み(解説:Log.datファイルから↑の戦績データを読み込みます。)
            LogRead();
        }

        //WM_L/RBUTTONDOWN処理(解説:右でも左でもマウスボタンが押されると呼ばれます。)
        private void MainForm_MouseClick(object sender, MouseEventArgs e)
        {
            //座標の指定範囲内か否かを判別し、外なら何もしない
            Rectangle rect = new Rectangle(20, 60, 400, 400);
            if(!rect.Contains(new Point(e.X, e.Y)))
                return;
            //解説:オリジナルのコードは不完全な条件式を使ったので、オセロ盤の左外で条件が成就するのでこのように書き換えました。

/*            解説:以下は前回のコメントの引用です。

            int x = (e.X - 20) / 50, y=(e.Y-60)/50; if(x>7 || y<0) return;
            // 解説:マウス座標を外れていた場合は何もしない、という意味でしょう。

            //          マウス座標(e.Xとe.Y→共にフォームのクライアント領域の座標)

            //          から、オセロ盤の描画始点(20, 60)をオフセットし、オセロ盤の

            //          枠のサイズ(50 x 50)で(整数)除算したものをオセロ盤の枠座

            //          標(0 - 7, 0 - 7)として使うつもりだったんだと思います。
            //          しかし、クライアント座標がマイナス(例:e.X==10でe.Y==60)

            //          の場合、x(-0.2)が0となり、条件式("if(x(0)>7 || y(0)<0)")

            //          を充足してしまいます。また何故正確に

            //              "if((x < 0 || x > 7) || (y < 0 || y > 7))"

            //          としなかったのかも不明です。

*/

            //解説:このRectangleクラスのContainsメソッドはゲームなどで結構役に立ちそうです。

            else if(!radioButton1.Checked && !radioButton2.Checked)    //ゲームが開始されていない
            {
                //ユーザーインストラクションを追加(解説:ラジオボタンにチェックが入っていないとエラー表示します。)
                MessageBox.Show("ゲームは開始していません。\r\n先攻、後攻を指定して「開始」ボタンを押してください。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            else
            {
                //マウスポインター座標をオセロ盤座標(0 - 7 x 0 - 7)に変換(解説:↑の赤字部分を直接引数としています。)
                person((e.X - 20) / 50, (e.Y - 60) / 50);
            }
        }

        //WM_PAINT処理
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);            //解説:このbase.~は標準のOnPaint処理を行います。
            displayBoard();              //グリッドと盤面をimage2dispに描画(解説:まだウインドウには描画されません。)
            e.Graphics.DrawImage(image2disp, 10, 50);    //ビットマップをPoint(10, 50)に表示(解説:ここで初めてオセロ盤が描画されます。)
        }

        //WM_CLOSE処理(解説:オリジナルになかったので追加しました。)
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.No)
            {
                e.Cancel = true;            //解説:キャンセル処理です。
            }
            else
            {
                LogWrite();                //勝敗データの更新(解説:戦績データをLog.datファイルに書き込みます。)
                //ビットマップimage2dispとGraphicsの開放(元は無かったので追加)

                //解説:コメント通りです。アンマネージドリソースの未開放はメモリーリークの原因となります。
                gr_image.Dispose();        //image由来のGraphicsを開放
                image2disp.Dispose();    //imageを開放
            }
        }

        ///////////////////////////
        //コントロールイベント処理
        ///////////////////////////
        //「開始」ボタン処理

        private void startButton_Click(object sender, EventArgs e)
        {
            if(!radioButton1.Checked && !radioButton2.Checked)    //ゲームが開始されていない
            {
                //ユーザーインストラクションを追加(解説:ラジオボタンがチェックされない限り始められません。)
                MessageBox.Show("先攻、後攻の指定がありません。\r\n指定してください。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            InitTable();                    //盤の初期化と石の初期配置(解説:Initialize()だと何をするのか不明だったので。)
            this.Invalidate();                //フォームの再描画(解説:これで中央に二つづつ石が置かれます。)
            label.Text = "";                //ラベルの初期表示(pmpt)を消す
            startButton.Enabled = false;    //「開始」ボタンを無効化する
            passButton.Enabled = true;        //「パス」ボタンを有効化する
            radioButton1.Enabled = false;    //「先攻」を無効化する
            radioButton2.Enabled = false;    //「後攻」を無効化する
            //解説:オリジナルで「ゲーム中か否か」を表すBooleanフィールド gameStartが使われていなかったので削除し、その役割をstartButton.Enabledに持たせ、同時にゲーム中に変更されては困る先攻、後攻ラジオボタンや押してほしくないボタンを無効にしました。

            //ユーザーインストラクションを追加(解説:一応、どうやって遊ぶのかの指示は必要ですよね?)
            string str =    "オセロゲームを開始します。\r\n" + 
                            "あなたが手を打つには、オセロ盤上で石を置く位置をマウスでクリックしてください。\r\n" + 
                            "あなたの手の後、PCは自動的に石を打ちます。\r\n" +
                            "あなたのご健闘を祈ります。";
            MessageBox.Show(str, "遊び方", MessageBoxButtons.OK, MessageBoxIcon.Information);
            if(radioButton2.Checked)        //後攻ならPCに先手を行わせる
                computer();
        }

        //パスボタン処理
        private void passButton_Click(object sender, EventArgs e)
        {
            //石を置ける場所があるかどうか判定し、置ける場合はパス解消。ない場合はパス。
            int diskColor = radioButton1.Checked ? 1 : 2;  //解説:オリジナルのTBを名称変更し、簡潔なコードにしました。
            if(canPlace(diskColor))
                MessageBox.Show("置ける場所がありますのでパスできません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            else
                computer();
        }

        //終了ボタン処理(解説:オリジナルに終了ボタンがなかったので追加しました。)
        private void exitButton_Click(object sender, EventArgs e)
        {
            Close();
        }

        //戦績ボタン処理(解説:これは機能追加の為に追加したボタンです。)
        private void datButton_Click(object sender, EventArgs e)
        {
            //ユーザーインストラクション
            string str =    "【今回の戦績】\r\n" + 
                            "ゲーム数\t\t:" + Games.ToString() + 
                            "\r\nプレーヤー勝数\t:" + Wins.ToString() + 
                            "\r\nPC勝数\t\t:" + Loses.ToString() + 
                            "\r\n引分数\t\t:" + (Games - Wins - Loses).ToString() + 
                            "\r\n\r\n【前回迄の累計】\r\n" + 
                            "ゲーム数\t\t:" + GT_Games.ToString() + 
                            "\r\nプレーヤー勝数\t:" + GT_Wins.ToString() + 
                            "\r\nPC勝数\t\t:" + GT_Loses.ToString() +
                            "\r\n引分数\t\t:" + (GT_Games - GT_Wins - GT_Loses).ToString();
            MessageBox.Show(str, "戦績の表示", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

アップしようとしたら、

「大きすぎる」

としかられたので、今回は此処迄。悪しからず。