前回はオリジナルの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);
}
アップしようとしたら、
「大きすぎる」
としかられたので、今回は此処迄。悪しからず。