第326章 五目並べを作る


さて、今回から「五目並べ」ゲームを作ることにします。

今回は、碁盤目の描画と石の描画をします。



ゲームを始めるには、メニューの「ファイル」「ゲーム開始」を選択します。

碁盤以外の所でクリックしても無視されます。

碁盤の所でクリックするとその場所に黒または白石が描画されます。

すでに石の置いてあるところをクリックすると「そこは置けません」と注意されます。

勝敗の判定や、禁じ手などを注意する機能はありません。



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

まず、このプログラムのアイコン(gomoku.ico)を作っておきます。

VC++の「アイコンの新規作成」でもいいし、適当なアイコン・ソフトを使ってもよいでしょう。

// gomoku01.rcの一部 ///////////////////////////////////////////////////////////////////////////// // // Menu // MYMENU MENU DISCARDABLE BEGIN POPUP "ファイル(F)" BEGIN MENUITEM "ゲーム開始(&S)", IDM_START MENUITEM SEPARATOR MENUITEM "終了(&X)", IDM_END END END ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. MYICON ICON DISCARDABLE "gomoku.ico" #endif // 日本語 resources /////////////////////////////////////////////////////////////////////////////

リソース・スクリプトの一部です。特に、説明は不要でしょう。

// gomoku01.cpp #ifndef STRICT #define STRICT #endif #include <windows.h> #include "resource.h" #define SHUI 30 //碁盤の周囲の幅 #define KANKAKU 20 //碁盤のマス目の間隔 #define STONESIZE 10 //碁石の半径 LRESULT CALLBACK WndProc(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); char szClassName[] = "gomoku01"; //ウィンドウクラス HINSTANCE hInst; BOOL bSente = TRUE; //現在の差し手 先手:TRUE 後手:FALSE BOOL bStart = FALSE; //対戦中かどうか int ban[15][15]; //0:石無し 1:先手 2:後手

これを見ただけで、これから何をしようとしているのか察しがつきますね。

SHUIは、碁盤の周囲にどれだけ空白を取るかを指定します。

KANKAKUは、碁盤目の線の間隔です。碁石の半径は間隔の半分程度が良いようです。

int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) { MSG msg; hInst = hCurInst; if (!InitApp(hCurInst)) return FALSE; if (!InitInstance(hCurInst, nCmdShow)) return FALSE; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } //ウィンドウ・クラスの登録 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, "猫でもわかる五目並べ", //タイトルバーにこの名前が表示されます WS_OVERLAPPEDWINDOW, //ウィンドウの種類 CW_USEDEFAULT, //X座標 CW_USEDEFAULT, //Y座標 CW_USEDEFAULT, //幅 CW_USEDEFAULT, //高さ 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, x, y; static int nTe = 0; PAINTSTRUCT ps; HDC hdc; char szBuf[64], szSashite[16]; static HMENU hMenu; switch (msg) { case WM_CREATE: hMenu = GetMenu(hWnd); break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); MyMakeBan(hdc); MyStoneDraw(hdc); if (bSente) strcpy(szSashite, "先手●"); else strcpy(szSashite, "後手○"); 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)); 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) { SetStone(hWnd, x, y); nTe++; } break; case WM_COMMAND: switch (LOWORD(wp)) { case IDM_START: bStart = TRUE; EnableMenuItem(hMenu, IDM_START, MF_BYCOMMAND | MF_GRAYED); DrawMenuBar(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; }

メイン・ウィンドウのプロシージャです。

WM_CREATEメッセージが来たら、メニューハンドルを取得しておきます。

さて、一番肝心なのはWM_PAINTメッセージが来たときの処理です。

まず、MyMakeBan関数を呼んで、碁盤目を描画させます。

次に、MyStoneDraw関数を呼んで、石を描画します。これは、ban配列を 見て、石を描画しています。ban配列には先手なら1, 後手なら2, 空いているなら0 が格納されています。

次に碁盤の下方に表示する文字列を作成して表示します。

これで、今回のプログラムは終わったも同然です。(本当かいな!?)

bStartがFALSEなら(まだ、対戦が始まっていないなら)WM_LBUTTONDOWNメッセージが来ても 何もしません。

対戦が始まっていたら、クリックされた座標を検査します。碁盤の中なら SetStone関数を呼んで、ban配列に数値をセットします。ここでは、座標の値をそのまま SetStone関数に渡している点に注意してください。

メニューからIDM_STRAT(「ゲーム開始」)が選択されたら、bStartをTRUEにします。 そして、メニューの「ゲーム開始」をグレー表示にします。

BOOL MyMakeBan(HDC hdc) { int i; HBRUSH hBrush, hOldBrush; for (i = 0; i < 15; i++) { MoveToEx(hdc, SHUI, i * KANKAKU + SHUI, NULL); LineTo(hdc, KANKAKU * (15 - 1) + SHUI, i * KANKAKU + SHUI); } for (i = 0; i < 15; i++) { MoveToEx(hdc, KANKAKU * i + SHUI, SHUI, NULL); LineTo(hdc, KANKAKU * i + SHUI, KANKAKU * (15 - 1) + SHUI); } hBrush = (HBRUSH)GetStockObject(BLACK_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); MyCircle(hdc, 3, 3, 3); MyCircle(hdc, 11, 3, 3); MyCircle(hdc, 3, 11, 3); MyCircle(hdc, 11, 11, 3); MyCircle(hdc, 7, 7, 3); SelectObject(hdc, hOldBrush); return TRUE; }

碁盤を描画する関数です。

縦、横の線を15本ずつ引きます。間隔はKANKAKUにします。

さて、便宜上碁盤の左上の位置を(0,0)とします。右下は(14,14)とします。 これは、ban[x][y]に対応しています。 線を引き終えたら本物の碁盤に見せるために、小さい黒点を描画します。 この時呼び出すMyCircle関数にはこの碁盤座標(?)を使っています。 小さい黒点は(3,3)(11,3)(3,11)(11,11)(7,7)に打ちます。

BOOL MyCircle(HDC hdc, int x, int y, int r) { int x1, x2, y1, y2; x1 = x * KANKAKU + SHUI - r; x2 = x * KANKAKU + SHUI + r + 1; y1 = y * KANKAKU + SHUI - r; y2 = y * KANKAKU + SHUI + r + 1; Ellipse(hdc, x1, y1, x2, y2); return TRUE; }

碁盤に円を描画する関数です。引数のx,yは碁盤座標です。 円を描画するには中心点は

中心点のx座標 = x * KANKAKU + SHUI
中心点のy座標 = y * KANKAKU + SHUI

となります。さて、ちょっとした注意ですが

Ellipse(hdc, x1, y1, x2, y2);

とするとこの楕円は(x1,y1,x2-1,y2-1)の矩形に収まる楕円となります。 (x1,y1,x2,y2)ではない点に注意してください。 (x2,y2)は矩形の外になります。他の描画関数についても同様です。

BOOL SetStone(HWND hWnd, int x, int y) { int banx, bany; banx = (x - SHUI + KANKAKU / 2) / KANKAKU; bany = (y - SHUI + KANKAKU / 2) / KANKAKU; if (ban[banx][bany] != 0) { MessageBox(hWnd, "そこは置けません", "注意", MB_OK); return TRUE; } if (bSente) { ban[banx][bany] = 1; } else { ban[banx][bany] = 2; } InvalidateRect(hWnd, NULL, FALSE); bSente = !bSente; return TRUE; }

クリックされた場所から、ban配列に値をセットする関数です。

クリックする場所は必ずしも正確に碁盤目の交点とは限りません。 多少ずれてクリックしてしまうのが普通でしょう。

クリックされた座標から碁盤座標に変換する方法を考えてみましょう。

たとえば、碁盤座標の(a, b)に石を置きたいとして、正確にこの位置をクリックした場合 クライアント座標(x, y)は(SHUI + a * KANKAKU, SHUI + b * KANKAKU)となります。 従って

a = (x - SHUI) / KANKAKU
b = (y - SHUI) / KANKAKU

となります。

さて、整数のわり算は小数点が出ると切り捨てられます。そこで、割られる数に割る数の半分を 足しておくと、四捨五入したようなことになります。実際いろいろやってみるとわかります。

さて、碁盤座標がわかったらban配列の値を調べます。すでに1か2が入っていれば、そこには 置けませんので、注意を促します。

置ける場合は先手なら1,後手なら2を書き込みます。

その後InvalidateRect関数を呼んで、再描画させます。この時InvalidateRect関数の 最後の引数をTRUEにしておくと、背景が消去され画面がちらつくことがあるのでここでは、 FALSEにしてあります。そして、bSenteの値を反対にしておきます。

BOOL MyStoneDraw(HDC hdc) { int i, j; HBRUSH hBrush, hOldBrush; for (i = 0; i < 15; i++) { for (j = 0; j < 15; j++) { if (ban[i][j] != 0) { if (ban[i][j] == 1) { hBrush = (HBRUSH)GetStockObject(BLACK_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); } if (ban[i][j] == 2) { hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); } MyCircle(hdc, i, j, STONESIZE); SelectObject(hdc, hOldBrush); } } } return TRUE; }

ban配列を順番に見ていって1なら黒丸、2なら白丸を描画する関数です。 GetStockObjectで取得したハンドルはDeleteObject関数で破棄する必要はありません。

石の描画で碁盤目すべてを調べていますが、ちょっと無駄ですね。もう少し効率の良い 方法を考えてみてください。


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

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