第331章 コンピュータと対戦する


今回は、コンピュータと対戦する仕組みを作ってみます。 コンピュータと対戦するといっても、コンピュータはでたらめに 石を打っていき、偶然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を作ったときこれを止めるようにしてみてください。


[SDK第4部 Index] [総合Index] [Previous Chapter] [Next Chapter]

Update 06/Mar/2002 By Y.Kumei
当ホーム・ページの一部または全部を無断で複写、複製、 転載あるいはコンピュータ等のファイルに保存することを禁じます。