前回(注)、アップロードサイズオーバーでアップできなかった残りの部分を解説します。
注:前回はプログラムの「アプリケーションクラス」、「Othelloクラス」の内「メンバーフィールド(C++でいうメンバー変数)」、「コンストラクター」、「ウィンドウイベント処理」と「コントロールイベント処理」までをカバーしました。
今回カバーするのは「オセロゲーム関連処理」、「グラフィック表示」と「ログファイル処理」になります。
///////////////////////
//オセロゲーム関連処理
///////////////////////
//コンピュータ側判定処理
public void computer()
{
if(isOver())
return; //ゲーム終了なら何もしない
int num; //裏返すことが出来る石の数
byte diskColor = radioButton1.Checked ? (byte)2 : (byte)1; //先攻後攻判断(解説:これも簡略化表記です。)
int MaxEval = -1, Bestx = -1, Besty = -1, MaxNum = 0; //MaxEval-最大戦略的評価値
//解説:オリジナルよりも分かり易くMaxPr→MaxEval、MaxI→Besty、MaxJ→Bestx変更しています。
for(int y = 0; y < 8; y++)
{ //オセロ盤の全桝目をチェック
for(int x = 0; x < 8; x++)
{
int numTotal = 0; //裏返すことが出来る石の数の合計
for(int dir = 0; dir < 8; dir++) //iは桝目(x, y)の周囲8方向(解説:意味が分かるように'i'から変更)
{
if((num = numberCount(x, y, diskColor, dir)) > 0)
numTotal += num; //裏返せる石の合計
}
if(numTotal > 0 && //裏返せる石があり、且つ
(MaxEval < EvalTable[y, x] || //戦略評価が以前の最大値を超えるか(EvalTableは1から200)
//解説:オリジナルの「評価の最大値が0未満」は「その枠の評価値」が必ず1以上(評価テーブル参照)なので不要としています。
(MaxEval == EvalTable[y, x] && MaxNum < numTotal))) //同じでも裏返せる石の数が多い場合
{
MaxEval = EvalTable[y, x]; //戦略評価を更新
MaxNum = numTotal; //裏返せる石の最大数更新
Bestx = x; Besty = y; //その升目の座標を記録
}
}
}
if (MaxEval < 0)
MessageBox.Show("打つ場所がありません。パスします", "確認", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
else
flipDisks(Bestx, Besty, diskColor); //石を裏返す(解説:メソッドの名称を変更しています。)
}
//人間側判定処理
public void person(int posx, int posy)
{
if(dataTable[posy, posx] != 0) //空でない場合は何もしない
return;
byte diskColor = radioButton1.Checked ? (byte)1 : (byte)2; //先攻後攻判断(解説:簡略表記にしています。)
if(canPlace(posx, posy, diskColor)) //石を置くことができる場合
{
flipDisks(posx, posy, diskColor); //裏返し処理
Thread.Sleep(200); //waitの代わり(解説:無くてもよいのですが、一応ウェイトを置きました。)
computer(); //コンピュータ側処理
}
else
MessageBox.Show("そこには打てません。", "確認", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
isOver(); //ゲーム終了チェック(解説:終了判定メソッドを名称変更し、結果表示も移行しました。)
}
//石の配置初期設定
private void InitTable() //解説:Initialize()から名称変更
{
//データテーブルを空き(0)で初期化
for(int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
dataTable[y, x] = 0;
}
}
//中央に黒白石の初期配置
dataTable[3, 3] = dataTable[4, 4] = 1;
dataTable[4, 3] = dataTable[3, 4] = 2;
}
//空の桝目(x, y)から指定方向に裏返せる石の数を返す。置けない場合は0を返す。(解説:オリジナルでは置けない場合、-1を返していましたが、このメソッドの条件判定がすべて"> 0"であったので、falseらしく0を返すようにしました。)
//引数:座標(x, y)、自石色(黒-1、白-2)、指定座標周囲の方向(0-7)
public int numberCount(int x, int y, int diskColor, int dir)
{
if(dataTable[y, x] != 0)
return 0; //既に石が置かれている場合
int oponentColor = 3 - diskColor; //敵石の色(黒 - 1、白 - 2)
int dx = vectorTable[dir, 1], dy = vectorTable[dir, 0]; //設定方向のx、y座標差分(vectorTable参照)
int nextX = x + dx, nextY = y + dy; //設定方向1個先のx、y座標
if(nextX < 0 || nextX > 7 || nextY < 0 || nextY > 7)
return 0; //指定方向1個先が盤外の場合(一次チェック-無いと範囲外エラーとなる)
int numStone = 0; //(nextX, nextY)から指定方向へ続く敵石の数
while(dataTable[nextY, nextX] == oponentColor)
{
numStone++; //裏返すことが出来る敵石の数を増やす
nextX += dx; //指定方向に沿って(nextX, nextY)の桝目の先を
nextY += dy; //x、y座標差分を繰り返してチェック
if(nextX < 0 || nextX > 7 || nextY < 0 || nextY > 7)
return 0; //盤外の場合
}
if(dataTable[nextY, nextX] == diskColor) //敵石の連続が終わる座標をチェック
return numStone; //自石であれば裏返せる
else
return 0; //それ以外(空-0)の場合
}
//桝目(x, y)に石を置くことができるかを判定
public bool canPlace(int x, int y, int diskColor)
{
for(int dir = 0; dir < 8; dir++)
{
if(numberCount(x, y, diskColor, dir) > 0)
return true;
}
return false;
}
//盤上に石を置く場所があるか判定
public bool canPlace(int diskColor)
{
for(int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
if(canPlace(x, y, diskColor))
return true;
}
}
return false;
}
//終了判定(解説:終了-true、未終了-false)
public bool isOver()
{
//既に試合終了しておれば何もしない(解説:これを入れておかないと、personの中のcomputerが呼ばれるので。)
if(startButton.Enabled == true)
return true;
for(int diskColor = 1; diskColor < 3; diskColor++)
{
if(canPlace(diskColor))
return false; //敵味方一つでも石を置ける場合
}
//両者の石をカウントする
int diskColor1 = 0, diskColor2 = 0; //diskColor1が先攻(黒)、diskColor2が後攻(白)
for(int y = 0; y < 8; y++)
{
for(int x = 0; x < 8; x++)
{
if(dataTable[y, x] == 1)
diskColor1++;
else if(dataTable[y, x] == 2)
diskColor2++;
}
}
//石の数で勝負判定
if(diskColor1 == diskColor2)
MessageBox.Show("引き分けです。", "結果", MessageBoxButtons.OK, MessageBoxIcon.Information);
else if((diskColor1 > diskColor2 && radioButton1.Checked) || (diskColor2 > diskColor1 && radioButton2.Checked))
{
Wins++; //勝数を更新
MessageBox.Show("貴方の勝ちです。黒 = " + diskColor1 + ", 白 = " + diskColor2, "結果", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
Loses++; //負数を更新
MessageBox.Show("PCの勝ちです。黒 = " + diskColor1 + ", 白 = " + diskColor2, "結果", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
//解説:「貴方」「私」がはっきりしなかったので、「貴方(人間)」「PC」にしました。
}
Games++; //ゲーム数を更新
label.Text = pmpt; //プロンプト表示
radioButton1.Enabled = true; //「先攻」を有効化する
radioButton2.Enabled = true; //「後攻」を有効化する
radioButton1.Checked = radioButton2.Checked = false; //先攻、後攻のチェックを外す
startButton.Enabled = true; //「開始」ボタンを有効化する
passButton.Enabled = false; //「パス」ボタンを無効化する
return true;
}
//石を裏返す
public void flipDisks(int x, int y, byte diskColor) //解説:分かり易いように、名称変更しています。
{
byte [, ] Temp = new byte[8, 8]; //盤の状態を作業用盤コピー(注)
//注:8方向でチェックはオリジナルで行うので、オリジナルの盤面を変えないように
for(int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
Temp[i, j] = dataTable[i, j];
}
}
for(int dir = 0; dir < 8; dir++) //作業用盤を8方向でチェック
{
int num = numberCount(x, y, diskColor, dir);
if(num > 0) //裏返すことが出来る石があるなら
{
int flipx = x, flipy = y;
int dx = vectorTable[dir, 1], dy = vectorTable[dir, 0]; //指定方向(dir)のx、y差分
for(int i = 0; i <= num; i++, flipx += dx, flipy += dy) //裏返す桝目+現在の位置
//解説:裏返す数が「num + 1」なのは、敵の石と現在の位置(空)を自分の石にする為です。
{
Temp[flipy, flipx] = diskColor;
}
}
}
for(int i = 0; i < 8; i++) //解説:作業用盤面をデータテーブルにコピーする
{
for(int j = 0; j < 8; j++)
{
dataTable[i, j] = Temp[i, j];
}
}
this.Invalidate(); //フォームの再描画
}
///////////////////
//グラフィック表示
///////////////////
//石の表示
private void displayStone(int x, int y, Brush b, Pen p, float R)
{
//クライアントエリアオフセット(10, 10)と盤面のオフセット(10, 10)で(20, 20)
float X = x * 50 + 20, X1 = X + 1;
float Y = y * 50 + 20, Y1 = Y + 1;
gr_image.FillEllipse(bG, X1, Y1, R, R); //石に厚さがあるような形で表示
gr_image.FillEllipse(b , X, Y, R, R);
gr_image.DrawEllipse(p , X, Y, R, R);
}
//盤面、桝目と石の表示
public void displayBoard()
{
//(10,10)から背景(400 x 400)を暗い緑色で塗潰す
gr_image.FillRectangle(bD, 10, 10, 400, 400);
//太さ2の白色で内側に縦横線(格子)を引く
for(float X = 60; X < 390; X += 50)
gr_image.DrawLine(p1, X, 10, X, 410);
for(float Y = 60; Y < 390; Y += 50)
gr_image.DrawLine(p1, 10, Y, 410, Y);
//太さ4の白色で外枠を描く
gr_image.DrawRectangle(p2, 10, 10, 400, 400);
//dataTable(0-空、1-黒石、2-白石)を描く
for(int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{ //白い石は大きく見えるので若干小さく表示
if(dataTable[y, x] == 1) //黒石
displayStone(x, y, bB, pG, 30);
else if(dataTable[y, x] == 2) //白石
displayStone(x, y, bW, pB, 29);
}
}
}
///////////////////
//ログファイル処理
///////////////////
//解説:以下が戦績データをLog.datファイルに読み書きするメソッドです。
private bool LogRead()
{
string data = "";
/* 出力UTF-8 :Encoding.UTF8;
出力UTF-16LE:Encoding.Unicode;
出力S-JIS :GetEncoding("shift-jis") */
using (StreamReader file = new StreamReader(".\\Log.dat", Encoding.UTF8, false))
{
//ファイルを読む
data = file.ReadToEnd();
if(data == "") //ファイルが読み込めない場合
return false;
//data文のデータ化(GT_Games、GT_Winns、GT_Loses)
string[] words = data.Split(',');
if(!Int32.TryParse(words[0], out GT_Games))
return false;
if(!Int32.TryParse(words[1], out GT_Wins))
return false;
if(!Int32.TryParse(words[2], out GT_Loses))
return false;
// ファイルを閉じる
file.Close();
}
return true;
}
private void LogWrite()
{
//累計を今回のデータで更新する
GT_Games += Games;
GT_Wins += Wins;;
GT_Loses += Loses;
string data = GT_Games.ToString() + "," + GT_Wins.ToString() + "," + GT_Loses.ToString();
/* 出力UTF-8 :Encoding.UTF8;
出力UTF-16LE:Encoding.Unicode;
出力S-JIS :GetEncoding("shift-jis") */
using (StreamWriter file = new StreamWriter(".\\Log.dat", false, Encoding.UTF8))
{
//ファイルを書く
file.Write(data);
// ファイルを閉じる
file.Close();
}
}
}
}
以上です。フィールドやメソッドの名称をかえ、関連するメソッドをまとめて集めたりすることで、大分分かり易くなったのではないでしょうか?
なお、このオセロゲームは単純なゲームですが、結構入れ込んで遊べます。特に盤や石のグラフィック表示や、評価テーブルが素晴らしいですね。
wikiによれば、完全勝利手順はまだ分かっていないそうですが、定石があるようで、それに基づけば結構な戦績を残せられそうです。(私はオセロの初心者で、PCの評価テーブルを見ているのですが、↓のような有様です。)
ps. 尚、このプログラムのソースファイルとコンパイルオプションファイルは、BCCForm_and_BCCSkeltonパッケージの".\SampleBCCSkelton\MSCompAss\Debug\Samples\Othello"フォールダーにアイコンファイルと共に入っています。