今回は、コンピュータと対戦する仕組みを作ってみます。
コンピュータと対戦するといっても、コンピュータはでたらめに
石を打っていき、偶然3が出来たときに、次にこれを4にしようとします。
相手が止めないときは5を作って勝ちます。
今までに作った盤面に4がないか、3がないかを調べる関数を利用すれば
簡単に作れそうに思いますが、タイミングが結構面倒です。
では、プログラムを見てみましょう。
// gomoku06.rcの一部
/////////////////////////////////////////////////////////////////////////////
//
// Menu
//
MYMENU MENU DISCARDABLE
BEGIN
POPUP "ファイル(F)"
BEGIN
MENUITEM "ゲーム開始(&S)", IDM_START
MENUITEM "コンピュータと対戦する(&C)", IDM_COMP
MENUITEM "対戦を読み込む(&R)", IDM_READ
MENUITEM SEPARATOR
MENUITEM "終了(&X)", IDM_END
END
END
リソース・スクリプトのうち、メニュー部分に「コンピュータと対戦する」が増えています。他の部分は同じです。
// gomoku06.cpp
#ifndef STRICT
#define STRICT
#endif
#include <windows.h>
#include <windowsx.h>
#include <time.h>
#include "resource.h"
#define SHUI 30 //碁盤の周囲の幅
#define KANKAKU 20 //碁盤のマス目の間隔
#define STONESIZE 10 //碁石の半径
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK MyNameProc(HWND, UINT, WPARAM, LPARAM);
ATOM InitApp(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
BOOL MyMakeBan(HDC);
BOOL MyCircle(HDC, int, int, int); //盤(X,Y)に半径Rの円を描画
BOOL SetStone(HWND, int, int);
BOOL MyStoneDraw(HDC);
int Is5(HWND, int, int);
int Is4(HWND); //4があるかどうか調べその数を返す
int Is3(HWND); //3があるかどうか調べその数を返す
BOOL MyRecord(HWND, char *, char *); //対戦を記録する
BOOL MyGetTime(char *);
BOOL MyRead(HWND); //対戦をファイルから読み込む
int SetRecFromFile(char *); //ファイルからの情報を元にRec配列をセットする
BOOL Replay(HWND, int); //対戦を再現する
BOOL CompPlay(HWND); // コンピュータと対戦する時、先手かどうか決める
BOOL CompPut(HWND); //コンピュータが打つ
BOOL SearchPos(HWND, int *, int *); //コンピュータが打つべき位置を探す
int Comp3, Comp4; //コンピュータの3,4の数
char szClassName[] = "gomoku06"; //ウィンドウクラス
HINSTANCE hInst;
char szBuf[128];
BOOL bSente = TRUE; //現在の差し手 先手:TRUE 後手:FALSE
BOOL bStart = FALSE; //対戦中かどうか
BOOL bShoHai = FALSE; //勝敗がついているか
int ban[15][15]; //0:石無し 1:先手 2:後手
int nTe; //何手目か
BOOL bName = FALSE; //対戦者氏名を記録するかどうか
BOOL bComp = FALSE; //コンピュータと対戦するかどうか
BOOL bCompSente; //コンピュータが先手かどうか
char szSenteName[32], szGoteName[32]; //対戦者氏名
char szStartTime[32], szEndTime[32]; //対戦開始、終了日時
struct _tagRec{
int row;
int col;
} Rec[15 * 15];
乱数を使う関係でtime.hをインクルードしました。いくつかの関数、変数が追加となりました。
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst,
LPSTR lpsCmdLine, int nCmdShow)
{
MSG msg;
BOOL bRet;
hInst = hCurInst;
if (!InitApp(hCurInst))
return FALSE;
if (!InitInstance(hCurInst, nCmdShow))
return FALSE;
while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) {
if (bRet == -1) {
MessageBox(NULL, "GetMessage Error!", "Error", MB_OK);
break;
} else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
いつものところですが、メッセージループがちょっと変更になっています。
GetMessage関数が-1を返したときループを抜けてプログラムが終了するようにしてあります。
(*)
InitApp, InitInstanceの各関数に変更はありません。
//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
int id, x, y;
PAINTSTRUCT ps;
HDC hdc;
char szSashite[32];
static HMENU hMenu;
switch (msg) {
case WM_CREATE:
hMenu = GetMenu(hWnd);
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
MyMakeBan(hdc);
MyStoneDraw(hdc);
if (!bShoHai) {
if (bSente) {
if (!bName) {
strcpy(szSashite, "先手●");
} else {
wsprintf(szSashite, "先手[%s]●", szSenteName);
}
} else {
if (!bName) {
strcpy(szSashite, "後手○");
} else {
wsprintf(szSashite, "後手[%s]○", szGoteName);
}
}
wsprintf(szBuf, "差し手 = %s", szSashite);
TextOut(hdc, 30, SHUI + KANKAKU * 14 + 30, szBuf, strlen(szBuf));
wsprintf(szBuf, "第 %02d 手終了 現在 %02d 手目待ち", nTe, nTe + 1);
TextOut(hdc, 30, SHUI + KANKAKU * 14 + 50, szBuf, strlen(szBuf));
} else{
TextOut(hdc, 30, SHUI + KANKAKU * 14 + 30, szBuf, strlen(szBuf));
}
EndPaint(hWnd, &ps);
break;
case WM_LBUTTONDOWN:
if (bStart == FALSE)
break;
x = LOWORD(lp);
y = HIWORD(lp);
if (x >= SHUI && y >= SHUI &&
x <= KANKAKU * 14 + SHUI &&
y <= KANKAKU * 14 + SHUI) {
if (SetStone(hWnd, x, y) == FALSE) {
MyGetTime(szEndTime);
bShoHai = TRUE;
if (bComp)
bComp = FALSE;
InvalidateRect(hWnd, NULL, TRUE);
if (MessageBox(hWnd, "対戦を記録しますか", "記録の確認", MB_ICONQUESTION | MB_YESNO) == IDYES) {
MyRecord(hWnd, szStartTime, szEndTime);
} else {
break;
}
} else {
nTe++;
}
}
if (bComp && bCompSente == bSente) {
CompPut(hWnd);
}
break;
case WM_MENUSELECT:
if (bStart) {
EnableMenuItem(hMenu, IDM_START, MF_BYCOMMAND | MF_GRAYED);
EnableMenuItem(hMenu, IDM_READ, MF_BYCOMMAND | MF_GRAYED);
EnableMenuItem(hMenu, IDM_COMP, MF_BYCOMMAND | MF_GRAYED);
DrawMenuBar(hWnd);
} else {
EnableMenuItem(hMenu, IDM_START, MF_BYCOMMAND | MF_ENABLED);
EnableMenuItem(hMenu, IDM_READ, MF_BYCOMMAND | MF_ENABLED);
EnableMenuItem(hMenu, IDM_COMP, MF_BYCOMMAND | MF_ENABLED);
DrawMenuBar(hWnd);
}
break;
case WM_COMMAND:
switch (LOWORD(wp)) {
case IDM_START:
if (DialogBox(hInst, "MYNAME", hWnd, (DLGPROC)MyNameProc) == IDCANCEL)
break;
bStart = TRUE;
bShoHai = FALSE;
MyGetTime(szStartTime);
nTe = 0;
memset(ban, 0, sizeof(ban));
bSente = TRUE;
InvalidateRect(hWnd, NULL, TRUE);
break;
case IDM_READ:
bStart = TRUE;
bShoHai = FALSE;
nTe = 0;
memset(ban, 0, sizeof(ban));
bSente = TRUE;
InvalidateRect(hWnd, NULL, TRUE);
MyRead(hWnd);
bStart = FALSE;
break;
case IDM_COMP:
bStart = TRUE;
bShoHai = FALSE;
nTe = 0;
memset(ban, 0, sizeof(ban));
bSente = TRUE;
InvalidateRect(hWnd, NULL, TRUE);
CompPlay(hWnd);
break;
case IDM_END:
SendMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
break;
case WM_CLOSE:
id = MessageBox(hWnd,
"終了してもよいですか",
"終了確認",
MB_YESNO | MB_ICONQUESTION);
if (id == IDYES) {
DestroyWindow(hWnd);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return (DefWindowProc(hWnd, msg, wp, lp));
}
return 0;
}
左ボタンが押されて、人間が石を置いた後、コンピュータと対戦中(bCompがTRUE)
で、なおかつコンピュータの打つ順番であるとき(bSenteとbCompSenteがともにTRUEまたは、FALSEの時)は、
CompPut関数を呼んで、コンピュータが石を打ちます。人間が石を置いた後はコンピュータの番なので、bSenteとbCompSenteの比較は 不要のように思われるかもしれませんが、これがないといろいろ不都合が起こります。 実験してみてください。
WM_MENUSELECTメッセージがきたときの処理が少し増えています。
メニューからIDM_COMPが選択されたら、いろいろな変数を初期化してCompPlay関数を呼んでいます。
MyMakeBan, MyCircle, SetStone, MyStoneDraw, Is5, Is4, Is3, MyRecord, MyGetTime, MyNameProc, MyRead, SetRecFromFile, Replayの各関数に変更はありません。
BOOL CompPlay(HWND hWnd)
{
int nId;
Comp3 = 0;
Comp4 = 0;
nId = MessageBox(hWnd, "あなたが先手になりますか", "先手の確認", MB_YESNO | MB_ICONQUESTION);
if (nId == IDYES)
bCompSente = FALSE;
else
bCompSente = TRUE;
bComp = TRUE;
if (bCompSente) {
strcpy(szSenteName, "Computor");
strcpy(szGoteName, "You");
} else {
strcpy(szSenteName, "You");
strcpy(szGoteName, "Computor");
}
bName = TRUE;
MyGetTime(szStartTime);
if (bCompSente)
CompPut(hWnd);
return FALSE;
}
メニューからIDM_COMPが選択されたとき、1度だけ実行される関数です。コンピュータが先手になるか、後手になるかを決めます。
そして、対戦記録用の氏名の変数に書き込んでおきます。(注:コンピュータのスペルは computer,computorどちらでもよいのですが、現在は殆ど前者が使われています)
コンピュータが先手の場合は、直ちにCompPut関数を呼んで、コンピュータが石を打ちます。 後手の場合は、人間が左クリックをした後コンピュータが石を打ちます。この辺は 簡単そうで、いざプログラムを書くと予想外の事態に遭遇します。「同期オブジェクトと待機関数を 使って・・」などと考えるとかなり複雑なプログラムとなってしまいます。
BOOL CompPut(HWND hWnd)
{
int x, y;
srand((unsigned)time( NULL ));
while (1) {
if (SearchPos(hWnd, &x, &y) == FALSE) {
x = rand() % 15;
y = rand() % 15;
}
if (ban[x][y] == 0) {
if (SetStone(hWnd, x * KANKAKU + SHUI, y * KANKAKU + SHUI) == FALSE) {
bComp = FALSE;
bStart = FALSE;
bShoHai = FALSE;
bName = FALSE;
return FALSE;
}
break;
}
}
nTe++;
return TRUE;
}
コンピュータが石を打つ関数です。まず、SearchPos関数を呼んで、コンピュータが3または4または5になるところがないかどうか 調べます。 ちょうどよいところがない場合は、乱数で適当なところに石を置いてしまいます。
SetStone関数で石を置きます。このとき勝敗が決まってしまったらFALSEを返して戻ります。
SetStone関数を実行して勝敗が決まらないときはnTeを1増やして戻ります。
BOOL SearchPos(HWND hWnd, int *x, int *y)
{
int i, j;
int Jibun, Aite;
if (bCompSente) {
Jibun = 1;
Aite = 2;
} else {
Jibun = 2;
Aite = 1;
}
if ((nTe == 0 || nTe == 1) && ban[7][7] == 0) {
{
char szBuf[256];
wsprintf(szBuf, "nTe = %d", nTe);
MessageBox(hWnd, szBuf, "OK", MB_OK);
}
*x = 7;
*y = 7;
return TRUE;
}
for (j = 0; j < 15; j++) {
for (i = 0; i < 15; i++) {
if (ban[i][j] == 0) {
ban[i][j] = Jibun;
if (Is5(hWnd, i, j) == 1) {
*x = i;
*y = j;
ban[i][j] = 0;
return TRUE;
} else if (Is4(hWnd) >= 1 && Comp4 == 0){
*x = i;
*y = j;
ban[i][j] = 0;
Comp4++;
return TRUE;
} else if (Is3(hWnd) >= 1 && Comp3 == 0) {
*x = i;
*y = j;
ban[i][j] = 0;
Comp3++;
return TRUE;
} else {
ban[i][j] = 0;
}
}
}
}
Comp4 = 0;
Comp3 = 0;
return FALSE;
}
コンピュータが石を置いて、5,4,3になるところがないか調べる関数です。
たとえば3になるところが見つかったらComp3を1にして戻ります。次にこの関数が呼ばれたとき、別な場所で3になるところを調べても仕方がないので 3になる場所は探しません。4になる場所を探します。
コンピュータが3を作った後、人間がこれを止めた場合、この関数は最後まで 行くので、Comp3は0に戻しておきます。
これは、まずコンピュータがでたらめに石を置いて、偶然3が出来ないと いつまでたってもでたらめな所に起き続けてしまいます。また、人間が3を作っても これを止めようとはしません。人間が3を作ったときこれを止めるようにしてみてください。
Update 06/Mar/2002 By Y.Kumei