第330章 記録された対戦を再現する


今回は前章で作ったプログラムで、対戦を記録した ファイルから、対戦を再現します。



対戦を記録したファイルは、2種類あることに注意してください。

対戦者の氏名が記録されているものと、記録されていないものがあります。 まず、この2つを判別する必要があります。

五目並べ対戦記録
先手: 粂井康孝
後手: 田中太郎
開始:2002年01月28日21時10分21秒
終了:2002年01月28日21時11分04秒
第001手--07,07
第002手--03,03
第003手--08,06
第004手--03,04
第005手--06,08
第006手--01,02
第007手--09,05
第008手--05,09
第009手--10,04
これは、対戦者氏名が記録されている例です。実際の対戦は 6行目から書かれています。

対戦者氏名が記録されていない場合は4行目から記録が書かれています。

2行目の先頭4バイトを調べて「先手」と書かれていたら氏名の記載のある ファイルです。

2行目の先頭4バイトが「開始」なら氏名の記載のないファイルということになります。

氏名付きのファイルであれば2行目を6バイトとばすと先手の氏名がわかります。

以下同様....

このように考えるとプログラムは容易です。



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

// gomoku05.rcの一部

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

MYMENU MENU DISCARDABLE 
BEGIN
    POPUP "ファイル(F)"
    BEGIN
        MENUITEM "ゲーム開始(&S)",              IDM_START
        MENUITEM "対戦を読み込む(&R)",          IDM_READ
        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"

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

MYNAME DIALOG DISCARDABLE  0, 0, 135, 93
STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "対戦者氏名登録"
FONT 9, "MS Pゴシック"
BEGIN
    EDITTEXT        IDC_SENTE,34,7,92,15,ES_AUTOHSCROLL
    EDITTEXT        IDC_GOTE,34,31,92,15,ES_AUTOHSCROLL
    DEFPUSHBUTTON   "OK",IDOK,7,72,50,14
    PUSHBUTTON      "キャンセル",IDCANCEL,78,72,50,14
    LTEXT           "先手",IDC_STATIC,7,7,24,11
    LTEXT           "後手",IDC_STATIC,7,33,24,11
    CONTROL         "氏名を記録しない",IDC_NONAME,"Button",BS_AUTOCHECKBOX | 
                    WS_TABSTOP,7,51,77,12
END
メニュー項目「対戦を読み込む」が増えました。
//        gomoku05.cpp

#ifndef STRICT
    #define STRICT
#endif
#include <windows.h>
#include <windowsx.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); //対戦を再現する

char szClassName[] = "gomoku05";    //ウィンドウクラス
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; //対戦者氏名を記録するかどうか
char szSenteName[32], szGoteName[32]; //対戦者氏名
char szStartTime[32], szEndTime[32]; //対戦開始、終了日時
struct _tagRec{
    int row;
    int col;
} Rec[15 * 15];
SetStone関数の引数を変更しました。最後の引数をなくしました。

その代わり、szBufをグローバル変数にして若干プログラムを簡素化しました。

MyRead, SetRecFromFile, Replayの各関数が新規に追加されました。

また、対戦開始日時と終了日時を収納する変数をグローバル変数に変更しました。

WinMain, 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;
                    InvalidateRect(hWnd, NULL, TRUE);
                    if (MessageBox(hWnd, "対戦を記録しますか", "記録の確認", MB_ICONQUESTION | MB_YESNO) == IDYES)
                        MyRecord(hWnd, szStartTime, szEndTime);
                } else {
                        nTe++;
                }
            }
            break;
        case WM_MENUSELECT:
            if (bStart) {
                EnableMenuItem(hMenu, IDM_START, MF_BYCOMMAND | MF_GRAYED);
                EnableMenuItem(hMenu, IDM_READ, MF_BYCOMMAND | MF_GRAYED);
                DrawMenuBar(hWnd);
            } else {
                EnableMenuItem(hMenu, IDM_START, MF_BYCOMMAND | MF_ENABLED);
                EnableMenuItem(hMenu, IDM_READ, 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);
                    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;
}
static char szBuf[128]は、取りやめてグローバル変数になりました。

WM_LBUTTONDOWNメッセージが来た時の応答で、SetStone関数の引数が 変更となりました。

WM_MENUSELECTメッセージが来た時に、対戦中の時は IDM_STARTメニュー項目が選択できないようにしていたのを 再現中にも選択できないように追加しました。

メニューからIDM_READが選択された時、いろいろな初期化をして MyRead関数を呼ぶようにしました。この部分が今回のメインです。

MyMakeBan, MyCircleの各関数に変更はありません。
BOOL SetStone(HWND hWnd, int x, int y)
{
    int banx, bany, n4, n3, n5;
    char szSashite[64], szStr[128];

    if (bSente) {
        if (!bName) {
            strcpy(szSashite, "先手");
        } else {
            wsprintf(szSashite, "先手(%s)", szSenteName);
        }
    } else {
        if (!bName) {
            strcpy(szSashite, "後手");
        } else {
            wsprintf(szSashite, "後手(%s)", szGoteName);
        }
    }

    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;
    }
    //記録
    Rec[nTe].row = banx;
    Rec[nTe].col = bany;

    InvalidateRect(hWnd, NULL, FALSE);
    
    n5 = Is5(hWnd, banx, bany);
    if (n5 == 1) {
        bStart = FALSE;
        wsprintf(szStr, "%d手目で%sが勝ちました", nTe + 1, szSashite);
        MessageBox(hWnd, szStr, "5連", MB_OK);
        wsprintf(szBuf, "%d手目で%sが勝ちました", nTe + 1, szSashite);
        return FALSE;
    }
    if (n5 == -1) {
        bStart = FALSE;
        wsprintf(szStr, "%d手目で%sが多連で負けました", nTe + 1, szSashite);
        MessageBox(hWnd, szStr, "多連", MB_OK);
        wsprintf(szBuf, "%d手目で%sが多連で負けました", nTe + 1, szSashite);
        return FALSE;
    }
    n4 = Is4(hWnd);
    if (n4 != 0) {
        if (n4 > 1) {
            wsprintf(szStr, "%sに44が出来ました。\nまだ対戦を続けますか", szSashite);
            if (MessageBox(hWnd, szStr, "確認", MB_YESNO | MB_ICONQUESTION) == IDNO) {
                bStart = FALSE;
                wsprintf(szBuf, "%d手目で%sの44勝ちです", nTe + 1, szSashite);
                return FALSE;
            }
        }
        wsprintf(szStr, "%sに4が出来ました", szSashite);
        MessageBox(hWnd, szStr, "警告", MB_OK);

    }
    n3 = Is3(hWnd);
    if (n3 == 1 && n4 == 1) {
        wsprintf(szStr, "%sに43が出来ました\nまだ対戦を続けますか", szSashite);
        if (MessageBox(hWnd, szStr, "確認", MB_YESNO | MB_ICONQUESTION) == IDNO) {
            bStart = FALSE;
            wsprintf(szBuf, "%d手目で%sが43で勝ちました", nTe + 1, szSashite);
            return FALSE;
        }
    }
    if (n3 != 0) {
        if (n3 > 1) {
            wsprintf(szStr, "%sの反則負けです", szSashite);
            MessageBox(hWnd, szStr, "33", MB_OK);
            bStart = FALSE;
            wsprintf(szBuf, "%d手目で%sが33の反則負けです", nTe + 1, szSashite);
            return FALSE;
        }
        wsprintf(szStr, "%sに3が出来ました", szSashite);
        MessageBox(hWnd, szStr, "警告", MB_OK);
    }
    
    bSente = !bSente;
    InvalidateRect(hWnd, NULL, TRUE);
    return TRUE;
}
引数が変更となりました。中身は同じです。
MyStoneDraw, Is5, Is4, Is3, MyRecord, MyGetTimeの各関数に変更はありません。
LRESULT CALLBACK MyNameProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp)
{
    static HWND hSenteName, hGoteName, hNameCheck;

    switch (msg) {
        case WM_INITDIALOG:
            hSenteName = GetDlgItem(hDlg, IDC_SENTE);
            hGoteName = GetDlgItem(hDlg, IDC_GOTE);
            hNameCheck = GetDlgItem(hDlg, IDC_NONAME);
            Edit_LimitText(hSenteName, 31);
            Edit_LimitText(hGoteName, 31);
            Edit_SetText(hSenteName, szSenteName);
            Edit_SetText(hGoteName, szGoteName);
            return TRUE;
        case WM_COMMAND:
            switch (LOWORD(wp)) {
                case IDOK:
                    if (Button_GetCheck(hNameCheck) == BST_CHECKED) {
                        bName = FALSE;
                        strcpy(szSenteName, "");
                        strcpy(szGoteName, "");
                        EndDialog(hDlg, IDOK);
                        return TRUE;
                    } else {
                        bName = TRUE;
                        Edit_GetText(hSenteName, szSenteName, sizeof(szSenteName));
                        Edit_GetText(hGoteName, szGoteName, sizeof(szGoteName));
                        if (strcmp(szSenteName, "") == 0 || strcmp(szGoteName, "") == 0) {
                            MessageBox(hDlg, "氏名が記入されていません", "記入漏れ", MB_OK);
                            return FALSE;
                        }
                    }
                    EndDialog(hDlg, IDOK);
                    return TRUE;
                case IDCANCEL:
                    EndDialog(hDlg, IDCANCEL);
                    return TRUE;
                case IDC_NONAME:
                    if (Button_GetCheck(hNameCheck) == BST_CHECKED) {
                        EnableWindow(hSenteName, FALSE);
                        EnableWindow(hGoteName, FALSE);
                    } else {
                        EnableWindow(hSenteName, TRUE);
                        EnableWindow(hGoteName, TRUE);
                    }
                    return TRUE;
            }
            return FALSE;
    }
    return FALSE;
}
対戦を始める時に出てくるダイアログのプロシージャですが、 「氏名を記録しない」にチェックが付いている時は、氏名の 変数を空にするようにしました。また、チェックが付いていないにもかかわらず、 氏名を記入せずにOKボタンを押すと注意を促すようにしました。
//対戦をファイルから読み込む
BOOL MyRead(HWND hWnd)
{
    OPENFILENAME of;
    char szFile[MAX_PATH], szFileTitle[MAX_PATH];
    HANDLE hFile;
    HGLOBAL hMem;
    char *lpBuf, *lpToken, szTxt[64], *szSeps = "\r\n";
    DWORD dwRead;
    int nShohai; // nShohai:第何手で勝負がついたか
    DWORD dwFileSize;

    strcpy(szFile, "");
    strcpy(szFileTitle, "");

    memset(&of, 0, sizeof(OPENFILENAME));
    of.lStructSize = sizeof(OPENFILENAME);
    of.hwndOwner = hWnd;
    of.lpstrTitle = "対戦記録の読み込み";
    of.lpstrFilter = "五目ファイル(*.gom)\0*.gom\0All Files\0*.*\0\0";
    of.lpstrFile = szFile;
    of.lpstrFileTitle = szFileTitle;
    of.nMaxFile = MAX_PATH;
    of.nMaxFileTitle = MAX_PATH;
    of.Flags = OFN_FILEMUSTEXIST;
    of.lpstrDefExt = "gom";

    if (!GetOpenFileName(&of))
        return FALSE;
    hFile = CreateFile(szFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        MessageBox(hWnd, "CreateFile 関数が失敗しました", "Error", MB_OK);
        return FALSE;
    }
    dwFileSize = GetFileSize(hFile, NULL);
    hMem = GlobalAlloc(GHND, dwFileSize + 1);
    if (hMem == NULL) {
        MessageBox(hWnd, "メモリの確保に失敗しました", "Error", MB_OK);
        CloseHandle(hFile);
        return FALSE;
    }
    lpBuf = (char *)GlobalLock(hMem);
    if (lpBuf == NULL) {
        MessageBox(hWnd, "GlobalLock 関数が失敗しました。", "Error", MB_OK);
        CloseHandle(hFile);
        GlobalFree(hMem);
        return FALSE;
    }
    ReadFile(hFile, lpBuf, dwFileSize, &dwRead, NULL);
    CloseHandle(hFile);
    
    lpToken = strtok(lpBuf, szSeps);//1行目

    lpToken = strtok(NULL, szSeps);//2行目
    memset(szTxt, '\0', sizeof(szTxt));
    strncpy(szTxt, lpToken, 4);
    if (strcmp(szTxt, "先手") == 0) {
        bName = TRUE;
        strcpy(szSenteName, lpToken + 6);
        lpToken = strtok(NULL, szSeps); //3行目
        strcpy(szGoteName, lpToken + 6);
        lpToken = strtok(NULL, szSeps); //4行目
        strcpy(szStartTime, lpToken + 5);
        lpToken = strtok(NULL, szSeps); //5行目
        strcpy(szEndTime, lpToken + 5);
    } else if (strcmp(szTxt, "開始") == 0) {
        bName = FALSE;
        strcpy(szStartTime, lpToken + 5);
        lpToken = strtok(NULL, szSeps); //3行目
        strcpy(szEndTime, lpToken + 5);
    } else {
        MessageBox(hWnd, "正規のファイルではありません", "Error", MB_OK);
        return FALSE;
    }
    lpToken = strtok(NULL, szSeps); //対戦の1行目
    nShohai = SetRecFromFile(lpToken);

    GlobalUnlock(hMem);
    GlobalFree(hMem);
    
    Replay(hWnd, nShohai);

    return TRUE;
}
対戦をファイルから読み出して、再現します。

まず、ファイルのサイズを調べて、必要なメモリを確保します。

確保したメモリにファイルの内容を一気に読み込みます。

これを、改行(\r\n)で切り分けていきます。これでファイルを 行単位で調べることが出来ます。行を調べてあれば、対戦者氏名、 対戦日時などをグローバル変数に格納します。

対戦内容をSetRecFromFile関数に渡して、Rec配列に書き込みます。

Replay関数はRec配列を見ながら、対戦を再現します。

ざっとこんな流れになります。

さて、行の先頭に「先手」と書かれているかどうかを調べるのに ここでは、行から4バイトだけstrncpy関数でszTxtに書き込んで、 これが、「先手」と一致しているかを調べています。このstrncpy関数は 末尾にヌル文字を付けてくれないので注意してください。

char *strncpy( char *strDest, const char *strSource, size_t count );
int SetRecFromFile(char *lpToken)
{
    char szBuf[4], szSeps[] = "\r\n";
    int i = 0;

    memset(Rec, -1, sizeof(Rec));
    while (1) {
        szBuf[0] = lpToken[9];
        szBuf[1] = lpToken[10];
        szBuf[2] = '\0';
        Rec[i].row = atoi(szBuf);
        szBuf[0] = lpToken[12];
        szBuf[1] = lpToken[13];
        szBuf[2] = '\0';
        Rec[i].col = atoi(szBuf);
        lpToken = strtok(NULL, szSeps);
        if (lpToken == NULL)
            break;
        i++;
    }
        
    return i + 1;
}
対戦記録からRec配列に書き込む関数です。

「第001手--07,07」などのように書き込まれているので 最初の9バイトは読み飛ばします。そこから2バイトが第1手目の X座標です。コンマがあるので1バイトとばして、その次からの2バイトが Y座標です。これを整数に変換して配列に書き込みます。

strtokがNULLを返した時は、もう切り分けられない状態なので 無限ループを抜けます。勝敗がつくまでの手数を返して関数は 終了します。

BOOL Replay(HWND hWnd, int n)
{
    int x, y, i, id;
    char szBuf[256], szSashite[32];

    if (bName) {
        wsprintf(szBuf, "開始 %s\n終了 %s\n対戦者 %s VS %s",
            szStartTime, szEndTime, szSenteName, szGoteName);
    } else {
        wsprintf(szBuf, "開始 %s\n終了 %s", szStartTime, szEndTime);
    }
    MessageBox(hWnd, szBuf, "対戦の再現", MB_OK);
    for (i = 0; i < n; i++) {
        if (bSente) {
            if (!bName)
                strcpy(szSashite, "先手");
            else
                wsprintf(szSashite, "先手(%s)", szSenteName);
        } else {
            if (!bName)
                strcpy(szSashite, "後手");
            else
                wsprintf(szSashite, "後手(%s)", szGoteName);
        }
        x = Rec[i].row;
        y = Rec[i].col;
        wsprintf(szBuf, "%d手目に%sは(%02d, %02d) に置きました", nTe + 1, szSashite, Rec[i].row, Rec[i].col);
        id = MessageBox(hWnd, szBuf, "OK", MB_OKCANCEL);
        if (id == IDCANCEL) {
            id = MessageBox(hWnd, "再現を中止しますか", "中止", MB_ICONQUESTION | MB_YESNO);
            if (id == IDYES) {
                bStart = FALSE;
                bShoHai = FALSE;
                return FALSE;
            }
        }
        SetStone(hWnd, x * KANKAKU + SHUI, y * KANKAKU + SHUI);
        
        nTe++;
        
    }
    bShoHai = TRUE;
    bStart = FALSE;
    InvalidateRect(hWnd, NULL, TRUE);
    return TRUE;
}
Rec配列から1手ずつ座標を読み出して、SetStone関数に渡します。 この時SetStone関数の引数の座標はクライアント座標なので そのまま渡してはまずいです。

さて、SetStone座標は石を描画してその位置をまたRec配列に書いているので 無駄な動作をすることになります。これがいやな人は、再現中はSetStone関数内で Rec配列に書き込まないように書きかえてください。

また、再現中は1手ずつメッセージボックスが出ます。あまりスマートでは ないので、たとえばクライアント領域をクリックすると次の手に進むようにするなどの 工夫をしてみてください。


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

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