第333章 357ゲーム その1


今回から「357ゲーム」を作ります。と、言ってもゲームの中身そのものは C言語編第76章と同じです。これのGUI版と言うわけです。ゲームのルール等については C言語編を参照してください。



プログラムを実行すると左の図のようなものが出てきます。猫のアイコンとか 「猫でもわかる357ゲーム」のロゴは適当に作ってください。あるいは無くてもかまいません。

メニューの「ファイル」「ゲーム開始」でゲームを始めます。



先手になるか、後手になるかを聞かれます。



自分の番になったら左の図のようなダイアログが出るので、どの山から いくつ取るかを指定します。

もちろん、山の石がマイナスになったり、すべての山の石が0になるような取り方は できません。今回はコンピュータは乱数を発生してでたらめに石を取るので弱いです。



では、プログラムを見てみましょう。

// game35701.rcの一部

/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

MYMENU MENU 
BEGIN
    POPUP "ファイル(&F)"
    BEGIN
        MENUITEM "ゲーム開始(&S)",              IDM_START
        MENUITEM SEPARATOR
        MENUITEM "終了(&X)",                    IDM_END
    END
END


/////////////////////////////////////////////////////////////////////////////
//
// Bitmap
//

MYBMP                   BITMAP                  "bitmap1.bmp"

/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

MYDLG DIALOGEX 0, 200, 131, 78
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "石を取ってください"
FONT 10, "MS ゴシック", 400, 0, 0x80
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,44,51,50,14
    LTEXT           "取る山",IDC_STATIC,15,14,25,8
    LTEXT           "石の個数",IDC_STATIC,15,33,33,8
    COMBOBOX        IDC_COMBO1,73,14,51,57,CBS_DROPDOWN | CBS_SORT | 
                    WS_VSCROLL | WS_TABSTOP
    COMBOBOX        IDC_COMBO2,73,33,51,49,CBS_DROPDOWN | CBS_SORT | 
                    WS_VSCROLL | WS_TABSTOP
END

/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
MYICON                  ICON                    "icon1.ico"


普通のリソース・スクリプトです。ビットマップやアイコンは適当に作ってください。
//        35701.cpp

#ifndef STRICT
    #define STRICT
#endif
#include <windows.h>
#include <windowsx.h>
#include <stdlib.h>
#include <time.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);
ATOM InitApp(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
BOOL StartGame(HWND);
BOOL ShowStone(HWND);
int comp_take(HWND);
int human_take(HWND);
int judge(HWND);

char szClassName[] = "game35701";    //ウィンドウクラス
HINSTANCE hInst;
int nStone[3]; //各山の石の数
char szTxt[3][8], szStone[3][32];
BOOL bSente, bOrder;//コンピュータが先手かどうか, 現在の差し手
int nMtOrder, nTakeStone;

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 (int)msg.wParam;
}
bSenteがTRUEならコンピュータが先手、FALSEなら人間が先手です。

bOrderは誰の番かを示します。TRUEなら先手の番、FALSEなら後手の番です。

いつも通りのWinMain関数ですが、.netになってからはreturn msg.wParam;とすると 文句を言われるのでintに型キャストしました。(文句を言われて当然といえば当然ですが・・)

さて、メッセージループのところはGetMessage関数が-1を返すこともあり得ることを考慮して 書きましたが、.netに自動的にひな形を作らせるとやはり従来通りのループを作ります。

//ウィンドウ・クラスの登録

ATOM InitApp(HINSTANCE hInst)
{
    WNDCLASSEX wc;
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;    //プロシージャ名
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInst;//インスタンス
    wc.hIcon = LoadIcon(hInst, "MYICON");
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = "MYMENU";    //メニュー名
    wc.lpszClassName = (LPCSTR)szClassName;
    wc.hIconSm = LoadIcon(hInst, "MYICON");
    return (RegisterClassEx(&wc));
}

//ウィンドウの生成

BOOL InitInstance(HINSTANCE hInst, int nCmdShow)
{
    HWND hWnd;

    hWnd = CreateWindow(szClassName,
            "猫でもわかる357ゲーム", //タイトルバーにこの名前が表示されます
            WS_OVERLAPPEDWINDOW, //ウィンドウの種類
            CW_USEDEFAULT,    //X座標
            CW_USEDEFAULT,    //Y座標
            300,    //幅
            230,    //高さ
            NULL, //親ウィンドウのハンドル、親を作るときはNULL
            NULL, //メニューハンドル、クラスメニューを使うときはNULL
            hInst, //インスタンスハンドル
            NULL);
    if (!hWnd)
        return FALSE;
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);
    return TRUE;
}
初期のウィンドウサイズを適当な大きさにしておきました。
//ウィンドウプロシージャ

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
    int id;
    
    HDC hdc, hdc_mem;
    PAINTSTRUCT ps;
    HBITMAP hBmp;
    BITMAP bmp_info;
    int nW, nH, nX, nY;
    RECT rc;

    switch (msg) {
        case WM_CREATE:
            nStone[0] = 3;
            nStone[1] = 5;
            nStone[2] = 7;
            ShowStone(hWnd);
            break;
        case WM_COMMAND:
            switch (LOWORD(wp)) {
                case IDM_END:
                    SendMessage(hWnd, WM_CLOSE, 0, 0);
                    break;
                case IDM_START:
                    nStone[0] = 3;
                    nStone[1] = 5;
                    nStone[2] = 7;
                    bOrder = TRUE;
                    ShowStone(hWnd);
                    StartGame(hWnd);
                    break;
            }
            break;
        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            hBmp = LoadBitmap(hInst, "MYBMP");
            GetObject(hBmp, sizeof(BITMAP), &bmp_info);
            nW = bmp_info.bmWidth;
            nH = bmp_info.bmHeight;
            GetClientRect(hWnd, &rc);
            hdc_mem = CreateCompatibleDC(hdc);
            SelectObject(hdc_mem, hBmp);
            nX = (rc.right - rc.left - nW) / 2;
            nY = 10;
            BitBlt(hdc, nX, nY, nW, nH, hdc_mem, 0, 0, SRCCOPY);
            DeleteDC(hdc_mem);
            DeleteObject(hBmp);
            TextOut(hdc, 10, 80, szTxt[0], (int)strlen(szTxt[0]));
            TextOut(hdc, 30, 80, szStone[0], (int)strlen(szStone[0]));
            TextOut(hdc, 10, 100, szTxt[1], (int)strlen(szTxt[1]));
            TextOut(hdc, 30, 100, szStone[1], (int)strlen(szStone[1]));
            TextOut(hdc, 10, 120, szTxt[2], (int)strlen(szTxt[2]));
            TextOut(hdc, 30, 120, szStone[2], (int)strlen(szStone[2]));
            EndPaint(hWnd, &ps);
            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;
}
WM_CREATEメッセージが来たら、石山の石を初期化してShowStone関数を 呼んで、石山を表示します。

メニューから「ゲーム開始」(IDM_START)が選択されたらゲームの初期化をします。 そしてStartGame関数を呼んでゲームをスタートさせます。

WM_PAINTメッセージが来たら、画面に表示するいろいろなものを描画します。

まずはビットマップのロゴを表示させます。LoadBitmap関数はちょっと古いので 気にくわない人はLoadImage関数を使ってください。

テキストの表示は[A][B][C]と石(●)を別々に表示しています。 別に深い意味はありませんが、フォントによってはA,B,Cの幅が違うため 石が縦にそろわず、みっともなくなるからです。

WM_PAINTメッセージが来たときに何をするかがわからないと、SDKのプログラム 全体がわからなくなるので十分復習しておいてください。

BOOL StartGame(HWND hWnd)
{
    int nID;

    nID = MessageBox(hWnd, "あなたが先手にりますか", "先手/後手", MB_ICONQUESTION | MB_YESNO);
    if (nID == IDYES)
        bSente = FALSE;
    else
        bSente = TRUE;
    while (1) {
        if (bSente == bOrder) {
            if (comp_take(hWnd) != 0)
                break;
        } else {
            if (human_take(hWnd) != 0)
                break;
        }
    }
    return TRUE;
}
ゲームを進行する関数です。

最初に人間が先手になるかどうかを聞きます。

後は勝敗がつくまで無限ループです。 bSenteとbOrderが等しいときはコンピュータの番なのでcomp_take関数を呼びます。 そうでないときは人間の番なのでhuman_take関数を呼びます。どちらの関数も 勝負がつかないときは0を返します。勝負がついたときは-1を返します。

BOOL ShowStone(HWND hWnd)
{
    int i, j;
    memset(szTxt, '\0', sizeof(szTxt));
    memset(szStone, '\0', sizeof(szStone));

    for (i = 0; i < 3; i++)
        wsprintf(szTxt[i], "[%c] ", 'A' + i);
    for (i = 0; i < 3; i++) {
        for (j = 0; j < nStone[i]; j++)
            strcat(szStone[i], "●");
    }
    InvalidateRect(hWnd, NULL, TRUE);    
    return TRUE;
}
クライアント領域に石を表示させる関数です。各山の個数は nStone[x]を調べます。その数だけバッファに黒丸を書き込んでいます。 最後にInvalidateRect関数を呼んで無効領域を発生させて、WM_PAINTメッセージを 呼び出すようにしています。
LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp)
{
    static HWND hMt, hNo;
    char szMt[8], szNo[8];

    switch (msg) {
        case WM_INITDIALOG:
            hMt = GetDlgItem(hDlg, IDC_COMBO1);
            hNo = GetDlgItem(hDlg, IDC_COMBO2);
            ComboBox_InsertString(hMt, 0, "A");
            ComboBox_InsertString(hMt, 1, "B");
            ComboBox_InsertString(hMt, 2, "C");
            ComboBox_InsertString(hNo, 0, "1");
            ComboBox_InsertString(hNo, 1, "2");
            ComboBox_InsertString(hNo, 2, "3");
            ComboBox_InsertString(hNo, 3, "4");
            ComboBox_InsertString(hNo, 4, "5");
            ComboBox_InsertString(hNo, 5, "6");
            ComboBox_InsertString(hNo, 6, "7");
            break;
        case WM_COMMAND:
            switch (LOWORD(wp)) {
                case IDOK:
                    ComboBox_GetText(hMt, szMt, sizeof(szMt));
                    ComboBox_GetText(hNo, szNo, sizeof(szNo));
                    if (strcmp(szMt, "") == 0 || strcmp(szNo, "") == 0) {
                        MessageBox(hDlg, "山および取る個数を指定してください", "注意", MB_OK);
                        return FALSE;
                    }
                    nMtOrder = szMt[0] - 'A';
                    nTakeStone = atoi(szNo);
                    nStone[nMtOrder] -= nTakeStone;
                    if (nStone[nMtOrder] < 0) {
                        MessageBox(hDlg, "石の取り方が不正です", "警告", MB_OK);
                        nStone[nMtOrder] += nTakeStone;
                        return FALSE;
                    }
                    if (nStone[0] + nStone[1] + nStone[2] == 0) {
                        MessageBox(hDlg, "その取り方では山が全部0になってしまいます", "警告", MB_OK);
                        nStone[nMtOrder] += nTakeStone;
                        return FALSE;
                    }
                    EndDialog(hDlg, IDOK);
                    return TRUE;
                case IDCANCEL:
                    MessageBox(hDlg, "キャンセルはできません", "注意", MB_OK);
                    return FALSE;
            }
            return FALSE;
    }
    return FALSE;
}
人間が取るときのダイアログのプロシージャです。

ComboBox_InsertStringなどのマクロはwindowsx.hをインクルードしていないと 使えないので注意してください。気にくわない人はCB_INSERTSTRING をSendMessageしてください。

int comp_take(HWND hWnd)
{
    int mtorder, stone;
    char szBuf[128];

    srand((unsigned)time(NULL));
    
    while (1) {
        mtorder = rand() % 3;
        if (nStone[mtorder] == 0)
            continue;
        else
            break;
    }
    while (1) {
        stone = rand() % (nStone[mtorder]) + 1;
        nStone[mtorder] -= stone;
        if (nStone[0] + nStone[1] + nStone[2] == 0) {
            nStone[mtorder] += stone;
            continue;
        } else {
            break;
        }
    }

    wsprintf(szBuf, "コンピュータは%cから%d個取りました\n", mtorder + 'A', stone);
    MessageBox(hWnd, szBuf, "コンピュータ", MB_OK);
    ShowStone(hWnd);
    if (judge(hWnd) == 0) {
        MessageBox(hWnd, "コンピュータの勝ちです", "判定", MB_OK);
        return -1;
    }
    bOrder = !bOrder;
    return 0;
}
コンピュータが石を取る関数です。

最初にどの山から取るかを決めますが、その山の石がすでに0の時は 再度乱数を発生させて山を決めます。

山が決まったら乱数で取る石の数を決めます。 1からその山の石の個数までとなります。しかし、場合によっては すべての山が0になってしまうこともあり得ます。この場合は 再度やり直します。

取る石の数が決まったら、メッセージボックスで表示します。 そして、ShowStone関数を呼んで石を表示します。その後judge関数を 呼んで勝負がついたか(勝ったかどうか)を調べます。

勝敗がつかなかったときはbOrderを反転させます。

int human_take(HWND hWnd)
{
    DialogBox(hInst, "MYDLG", hWnd, (DLGPROC)MyDlgProc);
    ShowStone(hWnd);
    if (judge(hWnd) == 0) {
        MessageBox(hWnd, "あなたのの勝ちです", "判定", MB_OK);
        return -1;
    }
    bOrder = !bOrder;
    return 0;
}
人間が石を取る関数です。大部分はダイアログボックスでやっているので簡単ですね。

取る石が決まったらjudge関数で勝ったかどうかを判定します。

勝敗がつかないときはbOrderを反転します。

int judge(HWND hWnd)
{
    if (nStone[0] + nStone[1] + nStone[2] == 1)
        return 0;
    else
        return 1;
}
勝ったかどうかを判定する関数です。

これは簡単です。すべての山の石の個数を合計して1になれば勝ちですね。

今回はコンピュータがランダムに石を取るため、人間が勝つことが多いでしょう。 C言語編第77章を参照して「必勝もどき」アルゴリズムを導入して コンピュータを強くしてみてください。


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

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