昨日は色々と悩みがあって、疲れました。
悩みというのは「メナスの盤データにアクセスをさせるべきか、否か」であり、今頃になって根幹仕様がぐらついてしまいました。
その理由はメナスの盤データを表示するメンバー関数はありますが、メナスの盤の「値渡し」で処理(注)した盤データを表示する関数が無いことです。といってメナスの盤データ(原本)を勝手に処理することもできず、どうしようかと悩んだのでした。
注:盤面を回転させたり、反転させたり、先手、後手を交換したりする処理です。詳しくは後日説明します。
結局、セキュリティ上メナスの盤データはprotectedにし、手を打つ関数(一度打ったところには打てない為、盤データの書き換えは絶対的にできない)のみしかアクセスできないようにしました。
とうことで、データ形とCBOARDクラスの説明に入ります。(背景、経緯、仕様等は既に【アルゴリズム】のブログで述べているので割愛します。)
1.データ形
盤のデータと付帯データを構造体として定義しています。
//定数定義(先手(1)、後手(2)、未打(0))
#define FIRST 1
#define SECOND 2 //-1は、評価で1 x -1 x 1 = -1や-1 x 1 x -1 = 1となるので止めた
#define VACANT 0
////////////////////////////////////
//BOARD Structure for use of CBOARD
////////////////////////////////////
typedef struct _board {
char m_Pos[9];
// ------- 盤面はm_Pos[0]-[8]のchar配列を使う
// |0|1|2| これがMENACEのビーズに相当
// -------
// |3|4|5|
// -------
// |6|7|8|
// -------
int m_LastMove; //試合中は最終手(0 - 8)、試合後DB登録時は
} BOARD, *PBOARD; //評価の為に勝者データ(先手(1)、後手(2))
盤のデータはm_Posという,9Bのchar配列変数です。当初は〇×のデータを2bitで18ビット、付帯データを入れても3バイト以下で押さえようかと思いましたが、それは悲しい8bit、主記憶64KBの時代のトラウマで、現在の32bitコンピューター(最小データ単位はchar)では却って速度低下要因となることから止めました。
m_LastMoveという変数は対戦中は何手目を打ったか、を記録(注)しますが、対戦の勝敗が付いた後は勝者データを入れます。(本来は勝者データを別に持っていたのですが、両データは重なり合わないので、サイズ縮小目的から共用にしました。
注:何手目かを記録するのは、盤面のデータは現在のデータでなく、「一手前のデータ」なので元に戻す(PushBack関数に担当させています)ためです。
////////////////////////
//CMBOARDクラスヘッダー
//(盤面データのクラス)
////////////////////////
class CBOARD {
protected: //CBOARDと承継したCMENACEは利用できる
//変数
BOARD m_Board; //試合用盤
//(解説:これが↑で述べていた、クラス外部からのアクセスをどうするか、問題の部分です。)
public:
//関数
CBOARD(); //コンストラクター(初期化のみ)
void Init(); //初期化(再ゲーム時に必要)
BOARD GetBoard() {return m_Board;} //盤面データ(m_Board)のデータを渡す
//(解説:この関数は盤データを渡しますが、ポインターや参照渡しではないので、値(データのコピー)を渡しているだけです。
bool MakeMove(int, char); //手を打つ
//(解説:この関数は盤データにアクセスできます。しかし、一度打った場所に打ち直すことはエラーとなり、禁じられています。)
int* CheckBoard(); //盤面を調べる(戻り値は空き位置の配列とその数)
//(解説:この関数は「打つことができる場所を配列で渡します。配列の要素の個数は配列の最後に入っています。)
bool RandomMove(char); //乱数で手を打つ
//(解説:この関数も盤データにアクセスできます。しかし、一度打った場所に打ち直すことはエラーとなり、禁じられています。)
void RecMove(int m) {m_Board.m_LastMove = m;} //手を記録する
//(解説:この関数がm_LastMoveに対戦中の打った場所を記録させます。)
int IsOver(); //未了-0、縦横斜めに3つ並んだ-1|2、引き分け-3
//(解説:勝敗評価関数です。なお、CMENACEになるとこの関数の他、MENACEが「投了」して負けることがあります。)
void PushBack(int); //最終手ひとつ前に戻してm_Board.m_LastMoveに勝者を記録する
//(解説:対戦後、一手戻して「どういう打ち手が原因で負けたかを記録します。)
int WhoWon() {return m_Board.m_LastMove;} //PushBack()後の勝者確認用
virtual void ShowBoard(); //盤面表示の仮想関数
//(解説:この関数はCUI用で、ウィンドウズプログラムになった時には使いません。)
};
//コンストラクター(初期化のみ)
CBOARD::CBOARD() {
srand(time(NULL)); //乱数の初期化
Init();
}
//(解説:乱数を初期化し、次のInit()関数でメンバー変数を初期化します。)
//初期化(再ゲームに必要)
void CBOARD::Init() {
for(int i = 0; i < 9; i++)
m_Board.m_Pos[i] = 0; //m_Board.m_Pos配列を0初期化
m_Board.m_LastMove = 0; //m_Winの初期化
}
//(解説:コメントにある通り、以前はm_Winという変数に勝者データを入れていました。)
//手を打つ
bool CBOARD::MakeMove(int i, char hand) {
if(m_Board.m_Pos[i] != VACANT)
return false;
else {
m_Board.m_Pos[i] = hand; //手を打つ
RecMove(i); //打手を記録
return true;
}
}
//(解説:既に打たれている場所には打つことができません。)
//盤面を調べる(戻り値は空き位置の配列とその数)
int* CBOARD::CheckBoard() {
static int pos[10];
pos[9] = 0; //pos[9]に残空き位置数を入れる
for(int j = 0; j < 9; j++) {
if(m_Board.m_Pos[j] == VACANT) {
pos[pos[9]] = j;
pos[9]++;
}
}
if(!pos[9])
return NULL; //打てる場所が無い(pos「9」== 0)
else
return pos; //打てる場所の配列
}
//(解説:最大9の場所データを渡します。10個目の配列にデータ個数を入れています。)
//乱数で手を打つ(打てない場合、不可フラグをたて、falseを返す)
bool CBOARD::RandomMove(char hand) {
int* cp = CheckBoard();
if(!cp[9]) //cp[0] - cp[8]迄が空き位置の添字、cp[9]が残空き位置数
return false;
else {
int i = cp[rand() % cp[9]];
MakeMove(i, hand);
RecMove(i); //打手を記録
return true;
}
}
//(解説:この関数も盤データを変えますが、矢張り打った場所には打てないので修正は不能です。)
int CBOARD::IsOver() {
int res; //勝負の進行状態を表す戻り値
if(!CheckBoard()) //残空き位置無し(全て埋まって勝敗が決した時は書き直される)
res = 3; //引き分け状態が初期値
else
res = 0; //空き位置があれば未了状態が初期値
int chk; //縦横斜め3つの手の積(先手なら1、後手なら8になる)
if((chk = m_Board.m_Pos[0] * m_Board.m_Pos[1] * m_Board.m_Pos[2]) == 1) //横第1行
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[3] * m_Board.m_Pos[4] * m_Board.m_Pos[5]) == 1) //横第2行
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[6] * m_Board.m_Pos[7] * m_Board.m_Pos[8]) == 1) //横第3行
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[0] * m_Board.m_Pos[3] * m_Board.m_Pos[6]) == 1) //縦第1列
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[1] * m_Board.m_Pos[4] * m_Board.m_Pos[7]) == 1) //縦第2列
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[2] * m_Board.m_Pos[5] * m_Board.m_Pos[8]) == 1) //縦第3列
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[0] * m_Board.m_Pos[4] * m_Board.m_Pos[8]) == 1) //斜め第1列
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
if((chk = m_Board.m_Pos[2] * m_Board.m_Pos[4] * m_Board.m_Pos[6]) == 1) //斜め第2列
res = FIRST; //先手勝利
if(chk == 8)
res = SECOND; //後手勝利
//未了、引き分けでなければ一手戻して勝者を記録する(DB登録用)
if(res == FIRST || res == SECOND)
PushBack(res);
return res;
}
//(解説:長たらしく書かれていますが、空き位置があるか、縦横斜めに1または2が3つ並んでいるかを判断しています。)
//最終手ひとつ前に戻してm_Board.m_LastMoveに勝者を記録する(DB登録用)
void CBOARD::PushBack(int winner) {
m_Board.m_Pos[m_Board.m_LastMove] = 0;
m_Board.m_LastMove = winner;
}
//(解説:これが勝敗決定後の処理です。最後の手を「空き」に戻し、勝者を記録します。)
//盤面表示関数(CUI版-仮想関数で承継時オーバーライド可能)
void CBOARD::ShowBoard() {
char c;
cout << "-------" << endl;
for(int i = 0; i < 9; i++) {
switch(m_Board.m_Pos[i]) {
case FIRST:
c = 'O';
break;
case SECOND:
c = 'X';
break;
case VACANT:
c = ' ';
break;
}
cout << "|" << c;
if((i % 3) == 2)
cout << "|" << endl << "-------" << endl;
}
}
この段階では、コンピューターは単に空いている場所にランダム(乱数)に手を打つだけです。とはいえ、キチンと動作を確認するためのテストプログラムを載せておきます。コンパイルは将来vectorを利用するのでBCC55ではエラーが出るため、BCC102で大なってください。そのためのバッチプログラムも載せます。
【テストプログラム】
///////////////////////////////
// Console program for CBOARD
///////////////////////////////
#include <conio.h> //getch()使用の為
#include <stdlib> //srand()、rand()使用の為
#include <iostream>
using namespace std;
#include "..\CBOARD.h"
int main(int argc, char **argv) {
CBOARD board;
cout << "CBOARDクラスのデータ(char x 9 + int)サイズ = " << sizeof(board) << "バイト" << endl;
cout << "BD構造体のデータ(char x 9 + int)サイズ = " << sizeof(BOARD) << "バイト" << endl << endl;
cout << "先手(1)、後手(2)または機械対戦(3)を選択してください" <<endl;
int you;
while(you < 1 || you > 3) {
cin >> you;
}
cout << "<<盤面座標>>\r\n-------\r\n|0|1|2|\r\n-------\r\n|3|4|5|\r\n-------\r\n|6|7|8|\r\n-------\r\n>>>>>><<<<<<" << endl;
int times = 0;
int res = 0; //未了0、先手勝利1、後手勝利2、引き分け3
board.ShowBoard();
do {
switch(you) {
case 1: //先手
if(!(times % 2)) { //先手打
cout << "先手は打つ場所(0-8)を選んでください" << endl;
int move = -1;
while(move < 0 || move > 8) {
cin >> move;
if(!board.MakeMove(move, you)) {
cout << "そこは既に打たれています" << endl;
move = -1;
}
}
}
else //後手打
board.RandomMove(2);
board.ShowBoard();
break;
case 2: //後手
if(!(times % 2)) //先手打
board.RandomMove(1);
else { //後手打
board.ShowBoard();
cout << "後手は打つ場所(0-8)を選んでください" << endl;
int move = -1;
while(move < 0 || move > 8) {
cin >> move;
if(!board.MakeMove(move, you)) {
cout << "そこは既に打たれています" << endl;
move = -1;
}
}
}
board.ShowBoard();
break;
case 3: //機械対戦
if(!(times % 2)) //先手打
board.RandomMove(1);
else { //後手打
board.RandomMove(2);
}
board.ShowBoard();
break;
}
times++;
if(times > 4)
res = board.IsOver();
} while(!res);
switch(res) {
case 1:
cout << ">>>先手の勝ち-終了一手前盤面<<<" << endl;
break;
case 2:
cout << ">>>後手の勝ち-終了一手前盤面<<<" << endl;
break;
case 3:
cout << ">>>引き分け-終了時盤面<<<" << endl;
break;
}
board.ShowBoard();
cout << "(m_LastMove = " << board.WhoWon() << ")" << endl << endl;
//Pause
getch();
return 0;
}
【バッチプログラム例】
"C:\Borland\BCC102\bin\bcc32c.exe" "C:\Users\(パス)\MENACE\CBOARD-Test\BOARD.cpp" -tC -w
del "C:\Users\(パス)\MENACE\CBOARD-Test\BOARD.tds"
pause







