腰痛の中、セットして寝ていればよいので女房殿に頼まれたレガシーラップトップのHDDデータ消去を始め、一台目は懐かしのWindowsXPでノスタルジーに浸りましたと書きました。

【腰痛リハビリ】HDDデータ消去

 

一台目は用意したCDではうまくゆかず、補助用に作成したブータブルUSBで処理しましたが、二台目(これも懐かしのWindows Vista)ではCDブートで問題なくデータ消去できました。

 

ところがっ!

 

一番新しいWin10搭載の三台目Jソウルブラザーズ(LEN〇V〇のCore i5 ラップトップ)になった途端、問題発生!

【問題】

BIOSセットアップのブートメニューでCD/DVDブート、USBブートを選択してもHDDのWin10が立ち上がってしまう。

 

これは本当に参りました。BIOS設定でブートメニューから明確にCD/DVDやUSBを選択しても、すぐに返ってきてしまい、エラーメッセージも出ないためbに何が原因なのか全く分からないからです。WEBで同じような問題を抱えている記事や、LEN〇V〇のBIOSの問題等を調べましたが、これという情報はありません。いい加減頭にきて、女房殿には「ヤ〇ダ電機でHDDを破砕してくれるので大丈夫だから」と話しましたが、矢張りしっくりきません。

そんなこんなで、またベッドでスマホ検索をしていると聞きなれない「セキュアブート」とか「UEFI USB」とかの語に気が付きました。確かにBIOSメニューで「セキュリティ」というタブはあるのですが、ブートとは関係ないだろうし「大体BIOSメニューで簡単に解除出来たらセキュリティに並んだろうが」という突っ込みも込めて更に調査すると、次のことが分かりました。

 

(1)64bit CPUの時代になって旧来のIBM PC互換機のメーカー別BIOSではハードの進化に耐えられなくなり、外部記憶装置の2.2Tバイトの上限も届いてしまっているので、安全性を兼ねて統一規格が求められていたようです。

(2)そこで登場したのがUEFI(Unified Extensible Firmware Interface)という仕様のようで、詳細は←のWikiをご覧いただくとして、確かに、セキュアブートについて

UEFIセキュアブートは、起動対象のオペレーティングシステムの電子署名を検証して正当なソフトウェアであることが確認できた場合にのみブート処理を継続する。WindowsマークのあるマシンではセキュアブートにMicrosoftの電子署名が使われており、Windows 8以降はセキュアブート電子署名が付与されている。一方で、Windows 7以前のオペレーティングシステムやほとんどのLinuxディストリビューションは電子署名が付与されていないため、セキュアブートが有効なUEFIブートローダーでは起動できない。」

と書かれています。

 

「これかっ!」ということで、「セキュリティ」タブのUEFIブート(セキュアブート)を「レガシーブート」に変更し、再度試してみたところ何とかUSBのFreeDOSを立ち上げることができました。

 

私は1998年から2004年までPC自作にはまり、結構詳しいつもりでいたのですが、矢張りこの20年近くの間のハード進化は大きく、全くついてきていなかったことが原因でした。

 

いずれにせよ、これにてHDDデータ消去一件落着!

 

しかし、この経験をもとにひとこと言わせていただければ、

(1)PCメーカーは「製品寿命の概念から、廃棄までのスパンで製品設計を行うべきであり、HDDデータ消去処理を想定した消費者対応を行うべきではないか?」

(2)OS供給者はそれが走るPCの製品寿命を予見できることから、OSにてユーザー情報を(消去のみならず、移転を含め)安全に処理するメソッドを提供すべきではないか?

じゃないかしら?如何?

発症から一週間がたち、寝返り、上体を起こすことができて、コルセットがあれば短時間は歩行可能になりました。(立ち姿勢が長くなるとまだ痛みがぶり返します。丁度、ウルトラマンのカラータイマーのようです。って、知らないか?)

 

今日はリハビリの一環として、女房殿から頼まれていた旧いラップトップPCの処分の為、HDDデータを消去する作業を行いました。

 

最近女房殿が買い替えために現在4年前に嫁いだ娘の物と併せて3台のラップトップがあり、これを処分したいのですが、矢張りデータ消去が気になります。女房殿が調べたところ「専門業者は無料で消去、処分」、「ヤ〇ダ電機も無料で消去、処分」、「ビ〇クカ〇ラは有料」とのことでしたが、確認を依頼された私がWEBの案内をよく読むと無料といっても「専門業者は、消去ソフトを渡すので自己責任で消去」、「ヤ〇ダ電機はハードを破砕処分するのdだけど、到着までの運送中の漏洩リスクは依頼者負担」ということで、矢張り「自己責任で消去」することが不可避であることが判明。

 

私自身もデータ消去は自分で確実に行うことが賢明と考えたので、適切な消去ツールと方法をwebで確認してゆきました。その結果、

・ソフトはフリーの"Destroy.com"が最適で、

・方法はブートアップCD(DVD)またはブートアップUSBを作成し、実行

することがベストと考えられました。

 

ということで、本日Destroyをダウンロード(および、CD/DVDの失敗を見込んで、ブートアップUSBを作成するrufusというフリーソフトもダウンロード)し、CD焼き付けソフト(およびその後の動作不良からWindowsの右クリック)でisoイメージファイルをCDに焼き付け、一番旧いラップトップで試してみました。

 

まず、内臓HDD以外のデバイスによるブートアップを確認する為に通常起動を行います。それで"F12"キーでBIOSの選択ブートアップが可能であることを確認。(余談ですが、十数年ぶりにWindowsXPの起動画面を見てノスタルジーに浸りまくりでした!)

次にF12で"CD/DVD"からのブートアップを試すのですが、autorun.iniの指定ファイルがXPにないのか、結局WindowsXPがHDDから立ち上がってしまいます。

ということで、次は(こんなこともあろうかと用意していた)ブートアップUSBメモリースティックを作成して、Destroy.comをコピーしてトライし、今度はめでたく成功!!

矢張り最初の認識ドライブはメモリースティックになっていたので、ドライブをHDDに変更し、NSA(National Security Agency)標準のデータ消去方法(セクターランダム書き込みx2+ゼロ書き込みx1の計3回)を実行。

完了までの所要時間は5時間!

でした。

後2台あるから気長にやります。

 

ps. もう一つベッドに寝ながらスマホで色々とweb検索の上に学習した成果に本日目途ができたので、次はこれをネタにしようかと思っています。学習を開始してから私も何度か躓いて悩みましたが、皆さんも混乱することもあろうかとそのプロセスを書いてみようと思っています。

先週金曜にブログをアップした後、夕方から突然の腰痛でダウン。

土、日は寝返りもできず、一週間たった今日もまだよちよち移動の状態。

すみません、という訳で更新できませんでした。

寝ている間も次のネタをスマホで調べているので、またフリーのC++コンパイラーで遊べるネタを紹介しますね。

 

ということで、しばし待たれよ。

 

ではNimの最終回です。今回はNimのダイアログ関連を解説します。

 

【NimProc.hの続き】

///////////////////////////////
//ユーザーダイアログの関数定義
//コントロール関数
///////////////////////////////
////////////////////////
//TAKECOINSDLGダイアログ
////////////////////////
bool TAKECOINSDLG::OnInit(WPARAM wParam, LPARAM lParam) {
//(解説:これは人間が手を打つときに呼び出して列番号と取得するコイン数を入力するダイアログです。)
    char num[2] = {'0', NULL};
    int* p = Nim.m_Nim.GetCoins();    //Nimのm_Coins[]のコピー配列を取得する
    for(int i = 5; i > 0; i--) {
        if(p[i - 1] > 0) {
            num[0] = '0' + i;
            SendItemMsg(IDC_COL, CB_INSERTSTRING, 0, (LPARAM)num);
        }
    }
    return TRUE;
}
//(解説:m_Nimのm_Coins配列のコピーを呼び出し、コイン数が0ではない列番号をコンボボックスに登録します。)

bool TAKECOINSDLG::OnCol(WPARAM wParam, LPARAM lParam) {

    if(HIWORD(wParam) != CBN_SELCHANGE)    //列の選択以外の場合
        return FALSE;
    //列が選択された場合
    int no = SendItemMsg(IDC_COINS, CB_GETCOUNT, 0, 0);
    //IDC_COINSの文字列をクリアー
    for(int i = 0; i < no; i++)
        SendItemMsg(IDC_COINS, CB_DELETESTRING, 0, 0);
    //数字登録用変数
    char num[2] = {'0', NULL};
    //Nimのm_Coins[]のコピー配列を取得
    int* p = Nim.m_Nim.GetCoins();
    //列コンボボックスから選択列の番号を取得
    no = SendItemMsg(IDC_COL, CB_GETCURSEL, 0, 0);
    //選択列の数字文字列を取得
    SendItemMsg(IDC_COL, CB_GETLBTEXT, no, (LPARAM)num);
    //列配列添字に変換
    no = num[0] - '1';
    //列配列の個数を文字列に変換し、IDC_COINSに登録
    for(int i = 0; i < p[no]; i++) {
        num[0] = '0' + (p[no] - i);
        SendItemMsg(IDC_COINS, CB_INSERTSTRING, 0, (LPARAM)num);
    }
    return TRUE;
}
//(解説:列番号のコンボボックス(IDC_COL)がフォーカスを得たときに呼ばれますが、その際の動作が列番号の選択(CBN_SELCHANGE)の場合以外はFALSEで戻ります。列番号が選択されたら、(既に一回処理を行っていた場合はデータが残っているので)先ずコンボボックスをクリア<行削除>し、選択された番号からm_Nimのm_Coins配列のコピーを取ってコインの個数を調べます。コインの個数が分かれば、選択肢として1からコイン数までをコイン数のコンボボックス(IDC_COINS)に登録します。なお、ここで「文字列」←→「整数」変換は、データが1-5迄なので、一桁(+終端NULL)の文字列配列numを設け、数字はnum[0]から'0'を引き(「- '1'」としているのは、「- '0' - 1」の意味です)、文字列の場合はnum[0] += '0'とすることで変換ができます。よく使われる定番処理なので覚えておくとよいでしょう。なお、この処理はchar変数の範囲(0 - 255)までの数値しか使えないことに注意してくださいね。)

bool TAKECOINSDLG::OnIdok() {

    //数字登録用変数
    char num[2] = {'0', NULL};
    //列コンボボックスの値を取得
    int no = SendItemMsg(IDC_COL, CB_GETCURSEL, 0, 0);
    if(no == CB_ERR) {
        MessageBox(m_hWnd, "列が選択されていません", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    SendItemMsg(IDC_COL, CB_GETLBTEXT, no, (LPARAM)num);
    int col = num[0] - '0';
    //コインコンボボックスの値を取得
    no = SendItemMsg(IDC_COINS, CB_GETCURSEL, 0, 0);
    if(no == CB_ERR) {
        MessageBox(m_hWnd, "コイン数が選択されていません", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    SendItemMsg(IDC_COINS, CB_GETLBTEXT, no, (LPARAM)num);
    no = num[0] - '0';
    //下2バイトで列、コイン個数を返す
    no = col * 0x100 + no;
    EndModal(no);
    return TRUE;
}
//(解説:ここでもchar num[2]を使った数値←→文字列変換を使っています。またここでの注意点はコンボボックスの選択忘れのまま、OKボタンで戻らないようにすることです。(不定の値や異常値を返すことになる。)また、コンボボックスのエラーには通常CB_ERR(-1)という定数が使われます。また踊り値は赤字で書いたように、整数変数colに列数、noにコイン数をいれ、下から2バイトにcolを、最下位バイトにnoを再度noに入れ直しています。

bool TAKECOINSDLG::OnIdcancel() {

    int col = SendItemMsg(IDC_COL, CB_GETCURSEL, 0, 0);
    if(col == CB_ERR) {
        MessageBox(m_hWnd, "列が選択されていません", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    int num = SendItemMsg(IDC_COINS, CB_GETCURSEL, 0, 0);
    if(num == CB_ERR) {
        MessageBox(m_hWnd, "コイン数が選択されていません", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    EndModal(FALSE);
    return TRUE;
}
//(解説:ここでもESCでキャンセルが効きますが、未選択状態では不定の値や異常値を返すことになるのでしつこくエラー処理をします。列数、コイン数を入力しないとキャンセルできないようにしています。)

////////////////////
//INPUTDLGダイアログ
////////////////////
bool INPUTDLG::OnInit(WPARAM wParam, LPARAM lParam) {

    SendItemMsg(IDC_GAMES, CB_INSERTSTRING, 0, (LPARAM)"100");
    SendItemMsg(IDC_GAMES, CB_INSERTSTRING, 1, (LPARAM)"500");
    SendItemMsg(IDC_GAMES, CB_INSERTSTRING, 2, (LPARAM)"1000");
    SendItemMsg(IDC_GAMES, CB_INSERTSTRING, 3, (LPARAM)"5000");
    return TRUE;
}
//(解説:PC対PC対戦の対戦回数入力です.MENACEでは100、500、1000でしたが、処理が速いので5000も追加しました。)

bool INPUTDLG::OnIdok() {

    int n = SendItemMsg(IDC_GAMES, CB_GETCURSEL, 0, 0);
    if(n == CB_ERR) {
        MessageBox(m_hWnd, "回数が選択されていません", "エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    switch(n) {
    case 0:
        n = 100;
        break;
    case 1:
        n = 500;
        break;
    case 2:
        n = 1000;
        break;
    case 3:
        n = 5000;
        break;
    }
    EndModal(n);
    return TRUE;
}

//(解説:選択文字列に応じてその数値を返します。)

bool INPUTDLG::OnIdcancel() {

    EndModal(FALSE);
    return TRUE;
}

//(解説:キャンセルすると0(FALSE)を返します。)

///////////////////
//INFODLGダイアログ
///////////////////
bool INFODLG::OnInit(WPARAM wParam, LPARAM lParam) {

    //Nimについての情報開示
    CSTR num(Nim.m_Nim.DBSize());
    SendItemMsg(IDC_EDIT1, WM_SETTEXT, 0, (LPARAM)num.ToChar());
    num = Nim.m_Games;
    SendItemMsg(IDC_EDIT2, WM_SETTEXT, 0, (LPARAM)num.ToChar());
    num = Nim.m_1st;
    SendItemMsg(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)num.ToChar());
    num = Nim.m_2nd;
    SendItemMsg(IDC_EDIT4, WM_SETTEXT, 0, (LPARAM)num.ToChar());
    return TRUE;
}
//(解説:MENACEと同じく、対戦情報メンバー変数の表示を行うだけです。数値→文字列変換処理はCSTRクラス変数を使っています。)

bool INFODLG::OnIdok() {

    EndModal(TRUE);
    return TRUE;
}

bool INFODLG::OnIdcancel() {

    EndModal(FALSE);
    return TRUE;
}

//////////////////////
//VERSIONDLGダイアログ
//////////////////////
bool VERSIONDLG::OnIdok() {

    EndModal(TRUE);
    return TRUE;
}

bool VERSIONDLG::OnIdcancel() {

    EndModal(FALSE);
    return TRUE;
}

//(解説:定番のバージョンダイアログです。表示文字列はリソースの中に入れているのでこれだけです。)

これでWindows版のNimができました。後はビルドですが、今回もSTLのvectorをts勝っているのでbcc32cを使うことになり、BatchGoodでビルドします。以下はその出力内容です。(速度最適化(O2)でコンパイルしています。)

【Nim.bat】

@ECHO OFF
ECHO ----------------------------------
ECHO  BatchGood - Batch File Generator
ECHO  for Embarcadero "bcc32c.exe"
ECHO  Copyright (c) 2021 by Ysama
ECHO ----------------------------------

cd "C:\Users\ysama\Programing\Borland C++\Nim\Debug"
"C:\Borland\BCC102\bin\bcc32c.exe" "C:\Users\ysama\Programing\Borland C++\Nim\Nim.cpp" -tW -O2 -w- -I"C:\Borland\BCCForm"
del "C:\Users\ysama\Programing\Borland C++\Nim\Debug\Nim.tds"
"C:\Borland\BCC102\bin\brc32.exe" "C:\Users\ysama\Programing\Borland C++\Nim\Nim.rc" "C:\Users\ysama\Programing\Borland C++\Nim\Debug\Nim.exe"
del "C:\Users\ysama\Programing\Borland C++\Nim\Nim.res"
pause

 

Nim.exeをダブルクリックすると、きちんと動作しているようです。

 

最後はNimProc.hの関数定義となります。いつもこのパートは量が多いので、今回も2回に分けて解説しようと思います。

 

【NimProc.h】

//////////////////////////////////////////
// NimProc.h
// Copyright (c) 01/10/2022 by BCCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnCreate(WPARAM wParam, LPARAM lParam) {

    //コモンコントロールの初期化
    InitCommonControls();

    //ツールバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    TBar.Init(m_hWnd, m_hInstance, TOOLBAR);
    //ツールバーボタン用カスタムビットマップ追加
    TBar.AddBmp(m_hInstance, MAKEINTRESOURCE(IDI_TOOLBAR), 10);
    //ツールバーボタン追加
    TBBUTTON tbb[14];
    ZeroMemory(tbb, sizeof(tbb));
    tbb[0].iBitmap = TBar.m_id + 0;
    tbb[0].fsState = TBSTATE_ENABLED;
    tbb[0].fsStyle = TBSTYLE_BUTTON;
    tbb[0].idCommand = IDM_RANDOM;
    tbb[1].iBitmap = TBar.m_id + 1;
    tbb[1].fsState = TBSTATE_ENABLED;
    tbb[1].fsStyle = TBSTYLE_BUTTON;
    tbb[1].idCommand = IDM_EVAL;
    tbb[2].iBitmap = TBar.m_id + 2;
    tbb[2].fsState = TBSTATE_ENABLED;
    tbb[2].fsStyle = TBSTYLE_BUTTON;
    tbb[2].idCommand = IDM_EXP;
    tbb[3].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[4].iBitmap = TBar.m_id + 3;
    tbb[4].fsState = TBSTATE_ENABLED;
    tbb[4].fsStyle = TBSTYLE_BUTTON;
    tbb[4].idCommand = IDM_RVF;
    tbb[5].iBitmap = TBar.m_id + 4;
    tbb[5].fsState = TBSTATE_ENABLED;
    tbb[5].fsStyle = TBSTYLE_BUTTON;
    tbb[5].idCommand = IDM_RVE;
    tbb[6].iBitmap = TBar.m_id + 5;
    tbb[6].fsState = TBSTATE_ENABLED;
    tbb[6].fsStyle = TBSTYLE_BUTTON;
    tbb[6].idCommand = IDM_FVE;
    tbb[7].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[8].iBitmap = TBar.m_id + 6;
    tbb[8].fsState = TBSTATE_ENABLED;
    tbb[8].fsStyle = TBSTYLE_BUTTON;
    tbb[8].idCommand = IDM_EXIT;
    tbb[9].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[10].iBitmap = TBar.m_id + 7;
    tbb[10].fsState = TBSTATE_ENABLED;
    tbb[10].fsStyle = TBSTYLE_BUTTON;
    tbb[10].idCommand = IDM_INFO;
    tbb[11].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[12].iBitmap = TBar.m_id + 8;
    tbb[12].fsState = TBSTATE_ENABLED;
    tbb[12].fsStyle = TBSTYLE_BUTTON;
    tbb[12].idCommand = IDM_ABOUT;
    tbb[13].iBitmap = TBar.m_id + 9;
    tbb[13].fsState = TBSTATE_ENABLED;
    tbb[13].fsStyle = TBSTYLE_BUTTON;
    tbb[13].idCommand = IDM_VERSION;
    TBar.AddButtons(14, tbb);

    //ステータスバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    SBar.Init(m_hWnd, m_hInstance, STATUSBAR);
    //ステータスバー区画設定
    int sec[2] = {70, -1};
    SBar.SetSection(2, sec);
    //ステータスバー文字列設定
    SBar.SetText(0, "Nim Ver 1,.0");
    SBar.SetText(1, "最初にヘルプを読んでください");
//(解説:ここまではいつも通りSkeltonWiardのコードのままです。なお、NimはMENACE程ポピュラーではないので、ステータスバーの初期値に「最初にヘルプを読んでください」とメッセージを入れました。)
 

    //仮想ウィンドウの初期化
    m_cvs.SetCanvas(m_hWnd);
    //背景を明るい緑で塗潰し描画
    m_cvs.Color(4);
    m_cvs.BrSelection(1);
    m_cvs.Clear();
//(解説:ウィンドウへの描画はWM_PAINT時に書き込む必要があり、いつでも描画できるBasicなどをやられた方には煩わしいと思われるようです。この為CANVASという「描画用仮想ウィンドウクラス」を作りました。ここに書き込むと後はBCCSkelonが勝手にWM_PAINT時にウィンドウへ描画してくれるというクラスです。背景をにしたのは、10円玉の銅色が映えると思ったからです。)
 

    //描画用ビットマップの読み込み
    g_hCoin = (HBITMAP)LoadImage(m_hInstance, MAKEINTRESOURCE(IDI_COIN), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE);
    g_hBlanc = (HBITMAP)LoadImage(m_hInstance, MAKEINTRESOURCE(IDI_BLANC), IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE);
//(解説:作成してリソースに入れておいた10円玉とその背景のビットマップを取り込みます。(今回はビットマップの破棄を忘れないようにします。))
 
    //ヘルプファイルパス、名(""で囲む)
    g_HelpFile = "\"";
    g_HelpFile = g_HelpFile + g_arg.Path();
    g_HelpFile = g_HelpFile + "\\NimHelp.chm\"";
//(解説:BatchGoodで活躍したCARGクラスです。ここでも最初にヘルプファイルパス、名の設定に役立ちます。)
 
    //メンバー変数の初期化
    m_Games = 0;        //通算ゲーム数
    m_1st = 0;            //先手の勝数
    m_2nd = 0;            //後手の勝数
    GameInit();            //m_Nim、m_You、、m_Type、m_Times、m_Result、m_Noの初期化
//(解説:最後にゲーム関係変数の初期化をします。)
 
    return TRUE;
}

bool CMyWnd::OnLButtonDown(WPARAM wParam, LPARAM lParam) {

    //PC対人間対戦の場合は試合を進め、PC対PCの場合は一時中止する
    if(m_Type > 0) {
        if(m_Type < 4)
            PCvsMan();
        else
            MessageBox(m_hWnd, "一時中止します", "一時中止", MB_OK | MB_ICONEXCLAMATION);
    }
    return TRUE;
}

bool CMyWnd::OnRButtonDown(WPARAM wParam, LPARAM lParam) {

    //PC対人間対戦の場合は試合を進め、PC対PCの場合は一時中止する
    if(m_Type > 0) {
        if(m_Type < 4)
            PCvsMan();
        else
            MessageBox(m_hWnd, "一時中止します", "一時中止", MB_OK | MB_ICONEXCLAMATION);
    }
    return TRUE;
}
//(解説:今回久しぶりに左右マウスボタン押し下げ処理を行いました。といっても「ゲーム中なら(m_Type > 0)」、「人間との対戦のみ(m_Type < 4)」ゲームを続ける(PCvsMan関数を呼ぶ)というものです。(一応PCvsPC対戦の場合(m_Type >= 4)、メッセージボックスを出して止められるように考えました。やって見ると分かりますが、5000対戦でもすぐに終わるので止められません。))
 
bool CMyWnd::OnNotify(WPARAM wParam, LPARAM lParam) {

    //ツールバーからのツールチップ情報取得
    switch (((LPNMHDR)lParam)->code) {
        case TTN_NEEDTEXT:
            static LPTOOLTIPTEXT lptip;
            lptip = (LPTOOLTIPTEXT)lParam;
            switch (lptip->hdr.idFrom) {
                case IDM_RANDOM:
                    lptip->lpszText = "対「乱 数男";
                    break;
                case IDM_EVAL:
                    lptip->lpszText = "対「関 数子」";
                    break;
                case IDM_EXP:
                    lptip->lpszText = "対 「経 験太」";
                    break;
                case IDM_RVF:
                    lptip->lpszText = "乱vs関";
                    break;
                case IDM_RVE:
                    lptip->lpszText = "乱vs経";
                    break;
                case IDM_FVE:
                    lptip->lpszText = "関vs経";
                    break;
                case IDM_EXIT:
                    lptip->lpszText = "終了";
                    break;
                case IDM_INFO:
                    lptip->lpszText = "対戦情報";
                    break;
                case IDM_ABOUT:
                    lptip->lpszText = "Nimについて";
                    break;
                case IDM_VERSION:
                    lptip->lpszText = "バージョン情報";
                    break;
            }
    }
    return TRUE;
}
//(解説:これは定番のツールバーボタンのツールチップ処理です。)
 
bool CMyWnd::OnSize(WPARAM wParam, LPARAM lParam) {

    //クライアントエリアサイズの保存と1スパンの計算
    m_Width = LOWORD(lParam);        //ウインドウ幅
    m_Height = HIWORD(lParam);        //ウインドウ高さ
    if(m_Width > m_Height)
        m_Span = m_Height / 7;        //1コマ幅、高さ
    else
        m_Span = m_Width / 7;        //1コマ幅、高さ
    //ツールバーとステータスバーを調整
    TBar.AutoSize();
    SBar.AutoSize();
    //コインの描画
    ShowCoins();
    return TRUE;
}
//(解説:ウィンドウサイズが変更された場合の処理です。私のような高齢者もいようかと、大きくすると10円玉が拡大されます。なお、↓の処理で初期値32 x 32(オリジナルのビットマップは64 x 64)以下の描画にならないようにしました。)
 
bool CMyWnd::OnMinMax(WPARAM wParam, LPARAM lParam) {

    //典型的なウィンドウのサイズ制限処理
    MINMAXINFO *pmmi;
    pmmi = (MINMAXINFO*)lParam;
    pmmi->ptMinTrackSize.x = 380;    //クライアントエリア364 + 16
    pmmi->ptMinTrackSize.y = 425;    //クライアントエリア364 + 61
    return FALSE;                    //処理はDefWndProcに任す
}
//(解説:定番の最小サイズ確保処理です。32 x 32 x (5<コイン数> + 2<左右余白>)で設定しました。)
 
bool CMyWnd::OnPaint(WPARAM wParam, LPARAM lParam) {

    PAINTSTRUCT paint;
    m_cvs.OnPaint(BeginPaint(m_hWnd, &paint));
    EndPaint(m_hWnd, &paint);

    return TRUE;
}
//(解説:「SkeltonWizardで「仮想ウィンドウ(CANVASクラス)を使う」を選ぶと、その後のWM_PAINTメッセージを選択しなくてもこの関数定義が生成されます。(実際に生成される第2行は、グローバル変数cvsの「cvs.OnPaint」になります。今回CMyWndのメンバー関数にしたので宣言部、定義部共に"m_"を追加しました。)逆にCANVASクラスをマニュアルで使うときにはWM_CREATEで初期化をし、WM_PAINTでこの赤字3行を入れるようにしてください。)
 
bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    if(MessageBox(m_hWnd, "終了しますか", "終了確認",
                    MB_YESNO | MB_ICONINFORMATION) == IDYES)  {
        //ビットマップの解放
        DeleteObject((HGDIOBJ)g_hCoin);
        DeleteObject((HGDIOBJ)g_hBlanc);

        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    }
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
        return FALSE;
}
//(解説:今回は忘れずビットマップを解放しています。)
 
/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////
bool CMyWnd::OnRandom() {

    m_Type = 1;        //人間対乱数
    if(MessageBox(m_hWnd, "先手にしますか?", "先手・後手確認", MB_YESNO | MB_ICONQUESTION) == IDYES)
        m_You = FIRST;
    else
        m_You = SECOND;
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(FALSE);
    //手の打ち方を表示
    MessageBox(m_hWnd, "手を打つにはマウスをクリックしてください", "手の打ち方", MB_OK | MB_ICONINFORMATION);
    //ステータスバーの表示
    SBar.SetText(1, "人間対乱数");
    SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"人間対乱数");    //ToolTipをつける
    //PC対人間対戦
    PCvsMan();
    return TRUE;
}

bool CMyWnd::OnEval() {

    m_Type = 2;        //人間対関数
    if(MessageBox(m_hWnd, "先手にしますか?", "先手・後手確認", MB_YESNO | MB_ICONQUESTION) == IDYES)
        m_You = FIRST;
    else
        m_You = SECOND;
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(FALSE);
    //手の打ち方を表示
    MessageBox(m_hWnd, "手を打つにはマウスをクリックしてください", "手の打ち方", MB_OK | MB_ICONINFORMATION);
    //ステータスバーの表示
    SBar.SetText(1, "人間対関数");
    SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"人間対関数");    //ToolTipをつける
    //PC対人間対戦
    PCvsMan();
    return TRUE;
}

bool CMyWnd::OnExp() {

    m_Type = 3;        //人間対経験値
    if(MessageBox(m_hWnd, "先手にしますか?", "先手・後手確認", MB_YESNO | MB_ICONQUESTION) == IDYES)
        m_You = FIRST;
    else
        m_You = SECOND;
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(FALSE);
    //手の打ち方を表示
    MessageBox(m_hWnd, "手を打つにはマウスをクリックしてください", "手の打ち方", MB_OK | MB_ICONINFORMATION);
    //ステータスバーの表示
    SBar.SetText(1, "人間対経験値");
    SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"人間対経験値");    //ToolTipをつける
    //PC対人間対戦
    PCvsMan();
    return TRUE;
}
//(解説:PC対人間のメニューアイテムとツールボタンの処理ですが、対戦区分フラグのm_Type以外は全て同じ処理です。先手後手を定め、ゲームが始まったらメニューっツールバーボタンを無効化(有効化関数のFALSEです...汗)し、次から先に進むにはマウスクリックすることを表示し、ステータスバーに情報を書き込みます。)
 
bool CMyWnd::OnRvf() {

    m_Type = 4;        //乱数対関数
    m_No = inputdlg.DoModal(m_hWnd, "IDD_INPUT", inputdlgProc);
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(FALSE);
    //ステータスバーの表示
    SBar.SetText(1, "乱数対関数");
    SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"乱数対関数");    //ToolTipをつける
    //PC対PC対戦
    PCvsPC();
    return TRUE;
}

bool CMyWnd::OnRve() {

    m_Type = 5;        //乱数対経験値
    m_No = inputdlg.DoModal(m_hWnd, "IDD_INPUT", inputdlgProc);
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(FALSE);
    //ステータスバーの表示
    SBar.SetText(1, "乱数対経験値");
    SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"乱数対経験値");    //ToolTipをつける
    //PC対PC対戦
    PCvsPC();
    return TRUE;
}

bool CMyWnd::OnFve() {

    m_Type = 6;        //関数対経験値
    m_No = inputdlg.DoModal(m_hWnd, "IDD_INPUT", inputdlgProc);
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(FALSE);
    //ステータスバーの表示
    SBar.SetText(1, "関数対経験値");
    SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"関数対経験値");    //ToolTipをつける
    //PC対PC対戦
    PCvsPC();
    return TRUE;
}
//(解説:PC対PC対戦も、対戦区分のm_Typeだけ違うだけで同様です。対戦回数を求めるダイアログを表示し、メニューアイテムとツールバーボタンを無効化し、ステータスバーに情報表示します。)
 
bool CMyWnd::OnExit() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
}
//(解説:定番の終了処理です。
 
bool CMyWnd::OnInfo() {

    infodlg.DoModal(m_hWnd, "IDD_INFO", infodlgProc);
    return TRUE;
}
//(解説:対戦履歴情報をIDD_INFOダイアログで表示します。)
 
bool CMyWnd::OnAbout() {

    CSTR cmd = "hh ";
    cmd = cmd + g_HelpFile;
    WinExec(cmd.ToChar(), SW_SHOWNORMAL);

    return TRUE;
}
//(解説:。"NimHelp.chm"というヘルプファイルをHTML Help Workshopのhh.exeで開きます。)
 
bool CMyWnd::OnVersion() {

    versiondlg.DoModal(m_hWnd, "IDD_VERSION", versiondlgProc);
    return TRUE;
}
//(解説:バージョン情報ダイアログを開きます。
 
///////////////////
//ユーザー定義関数
///////////////////
//ウィンドウにコインを表示する
void CMyWnd::ShowCoins() {

    //m_Nimのm_Coins[]のコピー配列を取得する
    int* p = m_Nim.GetCoins();
    //コインとブランクのビットマップをコンパチDCに読み込む
    HDC hDC1 = CreateCompatibleDC(m_cvs.hDC());
    SelectObject(hDC1 , g_hCoin);    //コイン
    HDC hDC2 = CreateCompatibleDC(m_cvs.hDC());
    SelectObject(hDC2 , g_hBlanc);    //ブランク
    //一旦ウィンドウ画面を消す
    m_cvs.Clear();
    //ビットマップの表示(原寸64 x 64をm_Span x m_Spanにする)
    for(int i = 0; i < 5; i++) {
        for(int j = 0; j < 5; j++) {
            if(p[j] >= (5 - i)) 
                StretchBlt(m_cvs.hDC(),
                            m_Span + m_Span * j,
                            m_Span + m_Span * i,
                            m_Span, m_Span, 
                            hDC1,
                            0, 0, 64, 64, SRCCOPY);
            else
                StretchBlt(m_cvs.hDC(),
                            m_Span + m_Span * j,
                            m_Span + m_Span * i,
                            m_Span, m_Span, 
                            hDC2,
                            0, 0, 64, 64, SRCCOPY);
        }
    }
    DeleteDC(hDC1);    
    DeleteDC(hDC2);
    InvalidateRect(m_hWnd, NULL, TRUE);    
}
//(解説:ウィンドウにコインを表示する関数です。最初色々と考えましたが、結局CUIと同じくm_Coins配列に基づいて一括で表示することにしました。m_Spanを使うことでウィンドウサイズに合わせてビットマップの大きさや位置を変更することができます。ご注意いただくのはビットマップは仮想ウィンドウのデバイスコンテキスト(m_cvs.hDC())のコンパチDCに取り込み、仮想ウィンドウへ書き込んでいることです。)
 
//ゲーム初期化関数
void CMyWnd::GameInit() {

    m_Nim.Init();            //CNIM mNimの初期化
    m_You = 0;                //m_You(先手-1、後手-2)の初期化
    m_Type = 0;                //各種対戦のID
    m_No = 0;                //連続対戦のゲーム数
}
//(解説:総ゲーム数(m_Games)、先手(m_First)、後手(m_Second)の勝利数以外の、通算対戦情報以外の変数を初期化します。)
 
//PC対人間対戦
void CMyWnd::PCvsMan() {

    bool Surrender = FALSE;
    for(int i = 0; i < 2; i++) {
        int *p = m_Nim.GetCoins();        //pにコイン数の配列へのポインターを渡す
        if(m_Nim.GetNextPlayer() == m_You) {
            int no, col;
            no = takecoinsdlg.DoModal(m_hWnd, "IDD_TAKECOINS", takecoinsdlgProc);
            col = (no & 0xF00) >> 8;
            no &= 0xF;

            m_Nim.TakeCoin(col - 1, no);//先手の人間の手
            ShowCoins();                //コイン状況の表示
        }
        else {
            if(m_Type == 1) {
                if(m_Nim.RandomTake()) {//乱数を使ったPCの手
                    ShowCoins();        //コイン状況の表示
                }
                else {                    //投了(打てない)
                    MessageBox(m_hWnd, "投了です", "PCの負け", MB_OK);
                    Surrender = TRUE;
                }
            }
            else if(m_Type == 2) {
                if(m_Nim.EvalTake()) {    //評価関数を使ったPCの手
                    ShowCoins();        //コイン状況の表示
                }
                else {                    //投了(打てない)
                    MessageBox(m_hWnd, "投了です", "PCの負け", MB_OK);
                    Surrender = TRUE;
                }
            }
            else if(m_Type == 3) {
                if(m_Nim.ExpTake()) {    //経験DBを使ったPCの手
                    ShowCoins();        //コイン状況の表示
                }
                else {                    //投了(打てない)
                    MessageBox(m_hWnd, "投了です", "PCの負け", MB_OK);
                    Surrender = TRUE;
                }
            }
        }
        //ゲーム終了か?(終了-TRUE、継続-FALSE)
        if(Surrender || m_Nim.IsGameOver())
            WhenOver();                    //ゲーム終了処理
    }
}
//(解説:CUIのNim.cppからのPC対人間部分の移植です。「手を打つ」→「打った後のコインを表示する」→「勝敗が付いたかチェックする」の繰り返しです。なお、↑の赤字部分が人間の手を打つ部分でIDD_TAKECOINSダイアログで列番号と取得コイン数を受け取りますが、整数(4バイト)の下位第2バイトを列番号、最下位バイトを取得コイン数としてダイアログから値を受け取っています。ゲームが終了したらWhenOver関数で処理します。)

//PC対PC対戦
void CMyWnd::PCvsPC() {

    while(m_No > 0) {
        m_Nim.Init();            //CNIM mNimの初期化
        while(!m_Nim.IsGameOver()) {
            if(m_Type == 4) {
                if(!m_Nim.RandomTake()) {    //投了(打てない)
                    SBar.SetText(1, "投了-先手PCの負け");
                    break;
                }
                if(!m_Nim.EvalTake()) {        //投了(打てない)
                    SBar.SetText(1, "投了-後手PCの負け");
                    break;
                }
            }
            else if(m_Type == 5) {
                if(!m_Nim.RandomTake()) {    //投了(打てない)
                    SBar.SetText(1, "投了-先手PCの負け");
                    break;
                }
                if(!m_Nim.ExpTake()) {        //投了(打てない)
                    SBar.SetText(1, "投了-後手PCの負け");
                    break;
                }
            }
            else if(m_Type == 6) {
                if(!m_Nim.EvalTake()) {        //投了(打てない)
                    SBar.SetText(1, "投了-先手PCの負け");
                    break;
                }
                if(!m_Nim.ExpTake()) {        //投了(打てない)
                    SBar.SetText(1, "投了-後手PCの負け");
                    break;
                }
            }
        }
        WhenOver();        //ゲーム終了処理
        --m_No;
    }
    MessageBox(m_hWnd, "対戦が終了しました。\r\n今回の勝敗通算結果は「対戦情報」"\
                        "でご確認ください。", "対戦が終了しました",
                        MB_OK | MB_ICONEXCLAMATION);
    //ゲーム初期化
    GameInit();
    ShowCoins();
    //メニュー、ツールバーの有効化
    ChangeMenuStatus(TRUE);

}
//(解説:PC対PC対戦の処理です。PC対人間対戦と同じにしようと思いましたが、PC対PCでは最大5000回対戦するので、毎回メッセージボックスを消すのも大変です。ということで、PC対PC対戦は速すぎてコイン表示の変化も見えないので表示も止め、基本人間の関与を最小にして(人間はいつ終わったのかわからないので...汗)最後に「終了したよ」メッセージボックスを出すようにしました。なお、最後にWhenOverでもやっている「ゲーム初期化」と「メニュー、ツールバーの有効化」を行っているのは、最後の対戦(m_No == 1)でWhenOverを行った後、一番外のループを抜けるのでこの処理が行われないからです。(PC対人間対戦の場合はm_Noが0になったことを確認してWhenOverを読んでいる点が異なります。)

//ゲーム終了処理
void CMyWnd::WhenOver() {
    
    m_Games++;
    switch(m_Nim.GetNextPlayer()) {        //先手勝利(2)、後手勝利(1)
    case 1:
        m_2nd++;
        SBar.SetText(1, "後手の勝ち");
        SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"後手の勝ち");    //ToolTipをつける
        if(!m_No)
            MessageBox(m_hWnd, "後手の勝ち", "勝敗結果", MB_OK | MB_ICONEXCLAMATION);
        break;
    case 2:
        m_1st++;
        SBar.SetText(1, "先手の勝ち");
        SBar.SendMsg(SB_SETTIPTEXT, 1, (LPARAM)"先手の勝ち");    //ToolTipをつける
        if(!m_No)
            MessageBox(m_hWnd, "先手の勝ち", "勝敗結果", MB_OK | MB_ICONEXCLAMATION);
        break;
    }          
    if(!m_No) {
        //ゲーム初期化
        GameInit();
        ShowCoins();
        //メニュー、ツールバーの有効化
        ChangeMenuStatus(TRUE);
    }
}
//(解説:終了時処理です。PC対人間の時はメッセージボックスで結果を表示しますが、PC対PCの場合はスキップします。また、対戦回数がゼロになるとゲームの初期化をします。(PC対PC対戦の場合との違いは↑のピンク部分参照。))

 

//メニュー、ツールバーの有無効化
void CMyWnd::ChangeMenuStatus(bool EnableFlag) {

    //メニューハンドルの取得
    HMENU hMenu = GetSubMenu(GetMenu(m_hWnd), 0);
    //メニューバー
    EnableMenuItem(hMenu, 0, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hMenu, 1, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hMenu, 2, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hMenu, 4, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hMenu, 5, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hMenu, 6, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    DrawMenuBar(GetHandle());        //Re-draw
    //ツールバー(MAKELONGマクロは16bit整数2つをunsigned 32bit整数にする)
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_RANDOM, MAKELONG(EnableFlag, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_EVAL, MAKELONG(EnableFlag, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_EXP, MAKELONG(EnableFlag, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_RVF, MAKELONG(EnableFlag, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_RVE, MAKELONG(EnableFlag, 0));
    SendMessage(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_FVE, MAKELONG(EnableFlag, 0));
}
//(解説:対戦が開始されると(始まった対戦の攪乱要因となる)対戦関連メニューアイテムとツールバーボタンは触ってほしくないので、それらを無効化(FALSE)します。再度有効化するにはTRUEを引数に与えます。)

以上でメインウィンドウのウィンドウメッセージ、メニュー関連とユーザー定義関数の定義部分を解説しました。後はダイアログ関連とバッチファイルについて次回解説します。

 

リソースはできたので、後はCUIで確認したCCOINSクラスとCNIMクラスをスケルトンに実装します。

 

【Nim.cpp】

//////////////////////////////////////////
// Nim.cpp
//Copyright (c) 01/10/2022 by BCCSkelton
//////////////////////////////////////////
#include    "Nim.h"
#include    "NimProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!Nim.IsOnlyOne()) {
        //ここに2重起動時の処理を書く(下記は1例)
        HWND hWnd = FindWindow("MainWnd", "Nim");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }

    //ウィンドウ登録 - Init(ClassName, hInstance, WndProc, "IDM_MAIN",
    //                        (以下省略可)MAKEINTRESOURCE(IDI_ICON), IDC_ARROW, Brush)
    Nim.Init("MainWnd", hInstance, SDIPROC, "IDM_MAIN", MAKEINTRESOURCE(IDI_ICON));

    //ウィンドウ作成と表示-Create(WindowTitle, (以下省略可)Style,
    //                        ExStyle, hParent, hMenu, x, y, w, h)
    if(!Nim.Create("Nim", WS_OVERLAPPEDWINDOW, NULL, NULL, NULL,
                    CW_USEDEFAULT, CW_USEDEFAULT, 336, 381))
        return 0L;

    //メッセージループに入る
    return Nim.Loop();
}
//(解説:メインプログラムなんですけど、いつも地味です。ほぼSkeltonWizard通りで二重起動防止のところだけコメントをはずしました。)

 

【Nim.h】

//////////////////////////////////////////
// Nim.h
// Copyright (c) 01/10/2022 by BCCSkelton
//////////////////////////////////////////
//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "BCCSkelton.h"
//リソースIDのヘッダー
#include    "ResNim.h"
//CNIMクラスのヘッダー
#include    "CNIMW.h"

//(解説:CNIMクラスのヘッダーですが、これを入れるとbcc32ではコンパイルできなくなるので、スケルトンの動きを確認するまではコメントアウト(//)しておくことが必要です。)


/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCSDIクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CSDI
{
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CSDI(UName) {}

    //メンバー変数(システム)
    int m_Width;        //ウインドウ幅
    int m_Height;        //ウインドウ高さ
    int m_Span;            //1コマ幅、高さ
    CANVAS m_cvs;        //仮想ウィンドウ
    //メンバー変数(ゲーム)
    CNIM m_Nim;            //Nim本体
    int m_You;            //Player(先手-1、後手-2)
    int m_Type;            //各種対戦のID(1-人間対乱数、2-人間対評価関数、3-人間対経験値、4-乱数対関数、5-乱数対経験値、6-関数対経験値)
    int m_No;            //連続対戦のゲーム数
    int m_Games;        //対戦の通算ゲーム数
    int m_1st;            //先手の勝数
    int m_2nd;            //後手の勝数

//(解説:ここはマニュアルで追加した部分です。最初の「(システム)」の幅、高さ、一コマ幅(高さ-正方形なので)は最小サイズ確保と描画の為の変数です。CANVASクラスも勿論描画の為です。次の「(ゲーム)」がCUIのcppプログラムにもあったゲーム用の変数ですね。)

    //メニュー項目、ダイアログコントロール関連
    bool OnRandom();
    bool OnEval();
    bool OnExp();
    bool OnRvf();
    bool OnRve();
    bool OnFve();
    bool OnExit();
    bool OnInfo();
    bool OnAbout();
    bool OnVersion();

//(解説:ここまではメニュー項目です。)

    //ウィンドウメッセージ関連
    bool OnCreate(WPARAM, LPARAM);
    bool OnLButtonDown(WPARAM, LPARAM);

    bool OnRButtonDown(WPARAM, LPARAM);
    bool OnNotify(WPARAM, LPARAM);

    bool OnSize(WPARAM, LPARAM);
    bool OnPaint(WPARAM, LPARAM);

    bool OnMinMax(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
//(解説:いつもと違うのはマウスのボタンダウンメッセージをSkeltonWizardで追加したのと、マニュアルで最小サイズメッセージ(OnMiniMax関数)を追加したことです。)

    //ユーザー定義関数
    void ShowCoins();        //ウィンドウにコインを表示する
    void GameInit();        //ゲーム初期化関数
    void PCvsMan();            //PC対人間対戦
    void PCvsPC();            //PC対PC対戦
    void WhenOver();        //ゲーム終了処理
    void ChangeMenuStatus(bool);    //メニュー、ツールバーの有無効化

//(解説:これらはげーん処理の関数です。上からGUIのコイン表示関数、ゲーム初期化関数、PC対人間対戦の関数、PC対PC対戦の関数、終了時処理関数と最後が対戦中のメニュー、ツールバーボタンの無効化関数です。)

};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////
CMyWnd Nim("Nim");    //ウィンドウクラスインスタンスの生成

BEGIN_SDIMSG(Nim)    //ダイアログと違い、コールバック関数名を特定しない
    //メニュー項目、ダイアログコントロール関連
    ON_COMMAND(Nim, IDM_RANDOM, OnRandom())
    ON_COMMAND(Nim, IDM_EVAL, OnEval())
    ON_COMMAND(Nim, IDM_EXP, OnExp())
    ON_COMMAND(Nim, IDM_RVF, OnRvf())
    ON_COMMAND(Nim, IDM_RVE, OnRve())
    ON_COMMAND(Nim, IDM_FVE, OnFve())
    ON_COMMAND(Nim, IDM_EXIT, OnExit())
    ON_COMMAND(Nim, IDM_INFO, OnInfo())
    ON_COMMAND(Nim, IDM_ABOUT, OnAbout())
    ON_COMMAND(Nim, IDM_VERSION, OnVersion())
    //ウィンドウメッセージ関連
    ON_CREATE(Nim)
    ON_LBUTTONDOWN(Nim)
    ON_RBUTTONDOWN(Nim)
    ON_NOTIFY(Nim)
    ON_SIZE(Nim)
    ON_PAINT(Nim)

    ON_(Nim, WM_GETMINMAXINFO, OnMinMax(wParam, lParam))
    ON_CLOSE(Nim)
END_WNDMSG

//(解説:いつものマクロテーブルです。しつこいですが、WM_GETMINMAXINFOは手書きです。赤字で書いたBCCSkeltonで対応していないウィンドウメッセージの取り込み方(ON_(インスタンス名, メッセージ定数, 呼び先の関数と引数))も覚えてください。マクロテーブルでのウィンドウメッセージ関連の関数呼び出しには通常wParamとlParam(実体のある変数名)を忘れずに入れてください。また、クラスのメンバー関数宣言部はWPARAM、LPARAMという変数型式名で全て大文字になることに注意してください。)


///////////////////
//ツールバーの作成
///////////////////
CTBAR TBar;

///////////////////////
//ステータスバーの作成
///////////////////////
CSBAR SBar;

///////////////////////
//ビットマップハンドル
///////////////////////
HBITMAP g_hCoin;
HBITMAP g_hBlanc;

//(解説:これらは十円玉と背景のビットマップ用ハンドルです。)


////////////////////////
//CARGクラスインスタンス
////////////////////////
CARG g_arg;

//(解説:データ保存、読み込み用に使います。)


//////////////////////
//Helpファイルパス、名
//////////////////////
CSTR g_HelpFile;

//(解説:ヘルプはMicrosoft HTML Help Workshopを使っています。)


///////////////////////////////////////////
// CDLGクラスからTAKECOINSDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class TAKECOINSDLG : public CDLG {
public:
    bool OnInit(WPARAM, LPARAM);
    bool OnCol(WPARAM, LPARAM);
    bool OnIdok();
    bool OnIdcancel();
};

//(解説:いつもと違うのはOnColです。これはOnInitで列番号を入れたドロップダウンリスト(コンボボックス)が選択された時に、取るコイン数のドロップダウンリストに個数を設定する為に必要です。)


////////////////////////////////////////////////////////////////////////////
// TAKECOINSDLGクラスダイアログ変数の生成とコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
TAKECOINSDLG takecoinsdlg;

BEGIN_MODALDLGMSG(takecoinsdlgProc, takecoinsdlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(takecoinsdlg, IDC_COL, OnCol(wParam, lParam))

//(解説:このIDC_COLが列番号用のドロップダウンリストです。)

    ON_COMMAND(takecoinsdlg, IDOK, OnIdok())
    ON_COMMAND(takecoinsdlg, IDCANCEL, OnIdok())
END_DLGMSG

///////////////////////////////////////////
// CDLGクラスからINPUTDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class INPUTDLG : public CDLG {
public:
    bool OnInit(WPARAM, LPARAM);
    bool OnIdok();
    bool OnIdcancel();
};

//(解説:このダイアログはMENACEからの流用です。)


////////////////////////////////////////////////////////////////////////////
// INPUTDLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
INPUTDLG inputdlg;

BEGIN_MODALDLGMSG(inputdlgProc, inputdlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(inputdlg, IDOK, OnIdok())
    ON_COMMAND(inputdlg, IDCANCEL, OnIdok())
END_DLGMSG

///////////////////////////////////////////
// CDLGクラスからINFODLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class INFODLG : public CDLG {
public:
    bool OnInit(WPARAM, LPARAM);
    bool OnIdok();
    bool OnIdcancel();
};

//(解説:このダイアログもMENACEからの流用です。)


////////////////////////////////////////////////////////////////////////////
// INFODLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
INFODLG infodlg;

BEGIN_MODALDLGMSG(infodlgProc, infodlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(infodlg, IDOK, OnIdok())
    ON_COMMAND(infodlg, IDCANCEL, OnIdcancel())
END_DLGMSG

///////////////////////////////////////////
// CDLGクラスからVERSIONDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class VERSIONDLG : public CDLG {
public:
    bool OnIdok();
    bool OnIdcancel();
};

//(解説:このダイアログもMENACE以前からの使い廻しです。)


////////////////////////////////////////////////////////////////////////////
// VERSIONDLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
VERSIONDLG versiondlg;

BEGIN_MODALDLGMSG(versiondlgProc, versiondlg)    //第1引数がコールバック関数の名前
    ON_COMMAND(versiondlg, IDOK, OnIdok())
    ON_COMMAND(versiondlg, IDCANCEL, OnIdcancel())
END_DLGMSG

 

後はNimProc.hの宣言した関数の実装とゲームの運営の説明だけですね。次回にBatchGoodの作ったバッチファイルも載せましょう。

 

「隗(かい)より始めよ」とは中国の故事で元々は「大事をなすには手近なことから着手せよ」との意らしい。(それが転じて言い出しっぺが始める、という用法もあるそうです。)

 

ということであれば、BCCForm and BCCSkelton派はまずリソースから始めます。

 

Nimで必要なリソースは、

(1)コイン、ツールバーボタン等のイメージリソース

(2)ウィンドウ操作とゲーム運行の為のメニュー、ツールバーボタン

(3)ユーザーインターフェースとしてのステータスバー

(4)手の入力(どの列から何個コインを取るか)と結果表示の為のダイアログと関連コントロール

になるかと思います。

 

実は最初(1)で少々迷いました。MENACEの時はプッシュボタンにビットマップを貼って表示したのですが、Nimの場合、5個から1個までの5列で元々ないスペースがあることと、コインを取った後どう見せるか、があるからです。また、コインの取り方でアニメ的に見せたりすることもできるでしょうが、その為には絵心のない私がフリップするコインを描く等の課題(ハードル?)が残ります。そんなことから、「コイン=(私のオリジナルストーリーでは)10円玉」「CANVASを使ったビットマップ表示」「(CUI版と同じく、m_Coins配列内容に沿って)一挙に表示」で決定しました。

 

また、ウィンドウ上の10円玉の表示に似せてシステムアイコンを作成し、今回は通常のウィンドウソフトのようなメニュー構成でない為「対戦に関わるにツールバービットマップ」も自作としました。

最初の3つが人間とPCプレーヤー(注)との対戦、後の3つがAutoBattleのABでタイプの番号が付いています。

注:乱 数男(ミダレ カズオ-乱数を使って手を打ってくる)
  関 数子(セキ カズコ-排他的論理和を使った関数を判断根拠として手を打ってくる)
  経 験太(オサメ ケンタ-MENACEの様に、対戦経験を知見として蓄積して手を打ってくる)

 

(2)は以下にNim.rcの内容を示します。(ResNim.hは単に番号だけだから今回は省略)

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResNim.h"

//----------------------------------
// ダイアログ (IDD_TAKECOINS) (解説:これは人間が列とコイン数を指定して手を打つ為のダイアログです。)
//----------------------------------
IDD_TAKECOINS DIALOG DISCARDABLE 0, 0, 168, 92
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "コインを取る"
FONT 9, "MS 明朝"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 2, 2, 32, 32
 CONTROL "列番号を入力してください", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 36, 2, 126, 12
 CONTROL "", IDC_COL, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | CBS_DROPDOWNLIST, 36, 18, 102, 72
 CONTROL "取るコイン数を入力してください", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 36, 36, 126, 12
 CONTROL "", IDC_COINS, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | CBS_DROPDOWNLIST, 36, 52, 102, 72
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 128, 72, 32, 16
}

//----------------------------------
// ダイアログ (IDD_INPUT)  (解説:これはMENACEと同じく自動対戦の回数を入力する為ダイアログです。)
//----------------------------------
IDD_INPUT DIALOG DISCARDABLE 0, 0, 168, 56
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "対戦回数入力"
FONT 9, "MS 明朝"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 2, 2, 32, 32
 CONTROL "対戦回数を入力してください", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 36, 2, 126, 12
 CONTROL "", IDC_GAMES, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | CBS_DROPDOWNLIST | CBS_SORT, 36, 18, 102, 60
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 128, 36, 32, 16
}

//----------------------------------
// ダイアログ (IDD_INFO) (解説:これもMENACEからの転用で、対戦成績表示用のダイアログです。)
//----------------------------------
IDD_INFO DIALOG DISCARDABLE 0, 0, 140, 92
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "Nim対戦成績"
FONT 9, "MS 明朝"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 2, 2, 32, 32
 CONTROL "登録手の数", 0, "STATIC", SS_LEFT | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 26, 8, 68, 12
 CONTROL "今回の対戦数", 0, "STATIC", SS_LEFT | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 2, 26, 92, 12
 CONTROL "今回の先手勝利数", 0, "STATIC", SS_LEFT | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 2, 42, 92, 12
 CONTROL "今回の後手勝利数", 0, "STATIC", SS_LEFT | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 2, 58, 92, 12
 CONTROL "", IDC_EDIT1, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_READONLY | ES_RIGHT, 98, 8, 40, 12, WS_EX_CLIENTEDGE
 CONTROL "", IDC_EDIT2, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_READONLY | ES_RIGHT, 98, 26, 40, 12, WS_EX_CLIENTEDGE
 CONTROL "", IDC_EDIT3, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_READONLY | ES_RIGHT, 98, 42, 40, 12, WS_EX_CLIENTEDGE
 CONTROL "", IDC_EDIT4, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_READONLY | ES_RIGHT, 98, 58, 40, 12, WS_EX_CLIENTEDGE
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 98, 74, 40, 16
}

//----------------------------------
// ダイアログ (IDD_VERSION)
//----------------------------------
IDD_VERSION DIALOG DISCARDABLE 0, 0, 160, 40
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION "バージョン情報"
FONT 9, "Times New Roman"
{
 CONTROL IDI_ICON, 0, "STATIC", SS_SUNKEN | SS_ICON | WS_CHILD | WS_VISIBLE, 12, 10, 32, 32
 CONTROL "Nim Version 1.0\nCopyright 2022 by Ysama", 0, "STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 42, 8, 80, 24
 CONTROL "OK", IDOK, "BUTTON", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 130, 14, 20, 12
}

//-------------------------
// メニュー(IDM_MAIN)
//-------------------------
IDM_MAIN MENU DISCARDABLE
{
    POPUP "対戦(&G)"
    {
        MENUITEM "対「乱 数男」(&R)", IDM_RANDOM
        MENUITEM "対「関 数子」(&F)", IDM_EVAL
        MENUITEM "対 「経 験太」(&E)", IDM_EXP
        MENUITEM SEPARATOR
        MENUITEM "乱vs関", IDM_RVF
        MENUITEM "乱vs経", IDM_RVE
        MENUITEM "関vs経", IDM_FVE
        MENUITEM SEPARATOR
        MENUITEM "終了(&X)", IDM_EXIT
    }

// (解説:通常の「ファイル」のところに即「対戦」を持ってきました。)
    POPUP "対戦情報(&I)"
    {
        MENUITEM "対戦情報(&I)", IDM_INFO
    }
// (解説:「対戦」結果の表示用です。)
    POPUP "ヘルプ(&H)"
    {
        MENUITEM "Nimについて(&A)", IDM_ABOUT
        MENUITEM "バージョン情報(&V)", IDM_VERSION
    }
}

//--------------------------
// イメージ(IDI_ICO)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "C:\Users\(パス)\Nim\Nim.ico"
// (解説:システムアイコンです。)
//--------------------------
// イメージ(IDI_TOOLBAR)
//--------------------------
IDI_TOOLBAR    BITMAP    DISCARDABLE    "C:\Users\(パス)\Nim\ToolBar.bmp"
// (解説:↑に示したツールバーです。)
//--------------------------
// イメージ(IDI_COIN)
//--------------------------
IDI_COIN    BITMAP    DISCARDABLE    "C:\Users\(パス)\Nim\Coin.bmp"
// (解説:10円玉です。写真を撮ってそれを加工しようか、と思ったのですが、フリー画材のものを使いました。)
//--------------------------
// イメージ(IDI_BLANC)
//--------------------------
IDI_BLANC    BITMAP    DISCARDABLE    "C:\Users\(パス)\Nim\Blanc.bmp"

// (解説:10円玉が無い場所の為の空白ビットマップです。)

 

リソースは以上です。これをSkeltonWizardにかけてスケルトンを作ります。特殊な処理は特にありませんが、おなじみのWM_CREATE、WM_NOTIFY、WM_SIZE、WM_CLOSEの他に、人間の手を打つダイアログを表示する為に"WM_LBUTTONDOWN"と"WM_RBUTTONDOWN"を追加しています。(最小サイズを維持するためにWM_GETMINMAXINFOを入れますが、これはSkeltonWIzardでは対応していないので、マニュアルで追加します。)

 

今回私は、出来上がったファイルをBCC Developer(「プロジェクト設定」の「リソーススクリプトファイル」につけられる最初の'"'を取ってくださいね)とbcc32で取り敢えずスケルトンを動作確認し、次いでコイン表示系、ダイアログ系の動作確認を行ってから、最後にCCOINSW.hとCNIMW.h(共にCUIのコイン表示関数を取り去ったもの)を導入して完成させました。次回以降は単にファイルだけズラズラ説明するのではなく、そういったマニュアルでの処理に重点を置いて説明しましょう。(なお、NimのWindows版は新しくアップしたBCCForm and BCCSkelotnの最新版に入れておきました。(CUI版は入っていませんので、CUI版-これもなかなか楽しい-はブログからコピペしてください。)

 

Nimにも少し飽きたので、ブログで勝手なことを書きましょう。

 

前に改めて↑を書きました。(皆さんがいつでも見れるように最初のメッセージに載せています。)

その後、既に知っているbcc32(c)やBCBは別とし、他の開発環境の味見をしています。

 

(1)Dev-C++

前にこれってBCCDeveloper+bcb32っぽい、でも英語以外はトラブるかも、と書きましたが、その評価は今も変わりません。しかし、より突っ込んだ味見をすべく、「File」(ファイル)メニューの「New」(新規)で「Project」を選ぶと、Windows、Console、DLL等のテンプレート(スケルトン)がおまけで付くことになります。加えて「MultiMedia(「マルチメディァ」って言葉自体がレトロですね)」でDirect3D、OpenGLのでもサンプルをコードでいただけます。(まぁ、色鮮やかな三角形が動くだけの矢張りレトロな奴ですが。)で、不図、bcc32cでコンパイルした実行ファイルと比較してみようということで、最近Nimの開発で様々な変数のサイズを調べる単純なプログラムをコンパイルしたのですが、先ず、

#include    <stdlib>

で「そんなファイルはありませんエラーが出ちゃいました。(#include    <stdlib.h>と#include    <iostream>は大丈夫なので、やはり2000年代規格で止まっているのかもしれません。)

まぁ、そこはあわてず騒がず、エラーが出た行に伝家の宝刀"//"を追加して何を逃れるのですが、#include    <windows.h>を入れていても、BOOLとBYTEで「定義が無いエラーが出たのはちょっとショックですね。(DWORDはOK)

気を取り直して、これらの行を消してコンパイルすると実行ファイルができ、走らせるとint 4バイト、ショート2バイト、ブール1バイトとbcc32と同じ変数サイズでした。(まぁ、当たり前か。)

気になったのは実行型ファイルのサイズで、bcc32cで(コメントアウトした行を含めて)コンパイルしても175KBだったものが、Dev-C++では2,307KBとRTLがまとめられて巨大になっている感じがします。

まぁ、前回の総括の通りで変わらないですね。(といいつつ、おもちゃとして取っておきます。)

 

(2)BCC Developerでbcc32cを使えるか?

これも「できるんじゃないか?」と書きましたので、試してみました。確かに環境設定でbcc32c.exeをコンパイラーに選べるのでコンパイルはできます。が、他のオプションは警告で済みますが、出力形式オプションが旧い"-W"になっているのでこれはエラーとなり、ビルドできません。マニュアルでそこを"-tW"と書き直しても、BCCDeveloperのボタンを押すと書き直されるのでやはりエラーになりました。

と、いうことでBCC Developerの使いやすいエディター環境は利用できてもmakeはBCCMakerを使うか、手っ取り早くBatchGoodを使わないとならないようです。

 

(3)Visual Studio CommunityEdition

最後が大御所のこれですね。起動に時間がかかるとか、メニュー、オプションの類が膨大でそれだけで心が折れるとか、プロジェクトやソリューションの管理が厳格で、自分で勝手にいじれないとか、「本人は分かっているけど、学習途上の初心者にはわかりにくいユ-ザーインターフェース」とか色々と難点がありますが、C++どころか、C#やXAML、Visual Basic(.NET Core)とか最先端の言語で遊べるというのは確かにありがたいことです。(しかし、プロトタイプをビルドできても、それをどうやって自家薬籠中の物にするのかが五里霧中で結局そこまでになりそうですが。)

 

また、新しいものをかじって食レポしてみたいと思います。

 

ということで、CNIMクラスができたので、CUIで走らせてみます。

 

【NIM.cpp】

/////////////////////////////
// Test program for CNIM.h
/////////////////////////////
#include    <conio.h>    //getch()使用の為
#include    <stdlib>    //srand()、rand()使用の為
#include    <iostream>    //cout、cin使用の為
#include    <windows.h>    //READFILE、WRITEFILE、DWORD等使用の為
//(解説:最後のwindows.hにDWORDとありますが、最初unsigned intの代わりにDWORD表記を使ったからです。後、TRUE、FALSEもそうですね。)

using namespace std;

#include    "CNIM.h"
//(解説:CNIMクラスを飲み込みます。)

 

//メイン関数
int main(int argc, char **argv) {

    //変数宣言
    CNIM nim;
    int  type = 0, you = 0, no, no_of_games, first = 0, second = 0;
    int l, n, *p;

//(解説:nimはCNIMクラスのインスタンス、typeは対戦タイプで↓参照。youは先手、後手フラグ、noは何回ゲームをするか、no_of_gamesは通算ゲーム数、first、secondは先手、後手の勝利数です。l、n、*pは作業用変数。)

    //対戦方法を決める
    cout << "☆対戦方法を選んでください。" << endl;
    while(type < 1 || type > 7) {
        cout << "1.人間対乱数\r\n2.人間対評価関数\r\n3.人間対経験値\r\n4.乱数対関数\r\n5.乱数対経験値\r\n6.関数対経験値\r\n7.中止\r\n>";
        cin >> type;
        if(type == 7)
            return 0;    //中止
    }
    if(type < 4) {
        cout << "☆先手、後手を選んでください。" << endl;
        while(you < 1 || you > 3) {
            cout << "1.先手\r\n2.後手\r\n3.中止\r\n>";
            cin >> you;
            if(you == 3)
                return 0;    //中止
        }
        no_of_games = no = 1;    //人間の試合の時は1回だけ
    }
    else {
        cout << "☆試合数を入力してください。" << endl;
        no = -1;
        while(no < 0 || no > 1000) {
            cout << "(1000回迄:'0'-中止)>";
            cin >> no;
            if(no == 0)
                return 0;    //中止
        }
        no_of_games = no;
    }

//(解説:人間対PCの時は人間の先手、後手を選択させ、PC対PCの時は何試合連続対戦するか決めさせます。)

   nim.ShowCoins();        //コイン状況の表示
    cout << "1  2  3  4  5 (列番号)" << endl;
//(解説:入力案内としての初期コイン状況の表示です。)

    //Game開始
    while(no) {

//(解説:人間対PCの時は1回だけで、PC対PCの時は連続対戦ができます。)

        nim.InitGame();
        while(!nim.IsGameOver()) {

//(解説:ゲーム終了判定です。)

            p = nim.GetCoins();        //pにコイン数の配列へのポインターを渡す
            if(type < 4) {            //人間との対戦
                if(nim.GetNextPlayer() == you) {
                    l = n = 0;
                    while(l < 1 || l > 5) {
                        cout << "コインを取る列を入力してください:";
                        cin >> l;
                        if(l < 1 || l > 5)    //必ず1 - 5にしないと次が危うい
                            continue;
                        if(!p[l - 1]) {            //コインが無い列を選択した場合
                            cout << "エラー:その列はコインが無く、選択できません" << endl;
                            l = 0;
                            continue;
                        }
                    }
                    l--;                        //1 - 5 → 0 - 4へ

//(解説:「1-5列」の指定ですが、配列添字は0-4ですよ、という意味です。)

                    while(n < 1 || n > p[l]) {
                        cout << "取るコインの数を入力してください:";
                        cin >> n;
                    }

//(解説:その列のコイン数以上は取れません。)

                    cout << endl;
                    nim.TakeCoin(l, n);            //先手の人間の手
                    nim.ShowCoins();            //コイン状況の表示
                    cout << "1  2  3  4  5 (列番号)" << endl;

//(解説:結果表示です。)

                }
                else {
                    if(type == 1) {
                        if(nim.RandomTake()) {    //乱数を使ったPCの手
                            nim.ShowCoins();            //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();            //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;

//(解説:投了の場合、whileループを抜け出します。)

                        }
                    }
                    else if(type == 2) {
                        if(nim.EvalTake()) {    //評価関数を使ったPCの手
                            nim.ShowCoins();            //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();            //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }
                    else if(type == 3) {
                        if(nim.ExpTake()) {        //経験DBを使ったPCの手
                            nim.ShowCoins();            //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();            //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }

//(解説:関数や経験値の場合も、考え方は乱数と同じです手を打つ関数が異なるだけです。)

                    }
                }
            }
            else {                    //機械対戦
                you = FIRST;
                if(nim.GetNextPlayer() == you) {
                    if(type == 4) {
                        if(nim.RandomTake()) {    //乱数を使ったPCの手
                            cout << ".";
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();    //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }
                    else if(type == 5) {
                        if(nim.RandomTake()) {    //乱数を使ったPCの手
                            cout << ".";
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();    //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }
                    else if(type == 6) {
                        if(nim.EvalTake()) {    //評価関数を使ったPCの手
                            cout << ".";
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();    //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }
                }
                else {
                    if(type == 4) {
                        if(nim.EvalTake()) {    //評価関数を使ったPCの手
                            cout << ".";
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();    //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }
                    else if(type == 5) {
                        if(nim.ExpTake()) {        //経験DBを使ったPCの手
                            cout << ".";
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();    //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }
                    else if(type == 6) {
                        if(nim.ExpTake()) {        //経験DBを使ったPCの手
                            cout << ".";
                        }
                        else {                    //投了(打てない)
                            cout << "投了です" << endl;
                            nim.ShowCoins();    //コイン状況の表示
                            cout << "1  2  3  4  5 (列番号)" << endl;
                            break;
                        }
                    }

//(解説:自動対戦(機械対戦)でも考え方は同じです。但し、速度が速いので途中表示は"."だけにしています。)

                }
            }

//(解説:対戦一回毎のループです。)

        }

        if(nim.GetNextPlayer() == SECOND) {
            cout << "先手の勝ち" << endl;
            first ++;
        }
        else {
            cout << "後手の勝ち" << endl;
            second++;
        }
        no--;

//(解説:連続対戦のループです。)

    }
    //勝敗統計の表示
    cout << "対戦数: " << no_of_games << endl;
    cout << "先手の勝数: " << first << endl;
    cout << "後手の勝数: " << second << endl;
    //m_CoinsDBの内容を表示する
    cout << endl << ">> m_CoinsDBの内容 <<";
    nim.DBShow();
    //Pause
    getch();

    return 0;
}

 

出力が長くなります(特にCoinsDBの履歴表示)が、人間対PC、PC対PC共に思ったような結果が出ました。

それではこれをWindows版に移植してみましょう。

注:CUIのヘッダーファイルはCUI表示の関数等を取り除き、CCOINSW.h、CNIMW.hとし、テストファイルもBCCSkeltonのWindows版のサンプルには入れていませんので悪しからず。

 

では、作成したCCOINSクラスをCNIMクラスに発展させてゆきましょう。これをC++ではCCOINSクラスをCNIMに承継させて新たなメンバー変数(プロパティ)とメンバー関数(メソッド)を追加するというそうです。

今回も開発用のCUIベースのファイルです。長いので要点部分のみ(解説:)で説明します。

 

【CNIM.h】

///////////////////////////////////
// Class of CNIM for playing "nim"
// Copyright (c) by Ysama, 2021
///////////////////////////////////
/*
【ルール】
(1)5列の5個、4個、3個、2個、1個のコイン群をテーブルに置き、
(2)先手、後手が一回に、一つの列から、1個以上のコインを取ってゆき、
(3)最後のコインを取ることになった方が負け
nimの双対ゲーム(https://ja.wikipedia.org/wiki/%E3%83%8B%E3%83%A0)
*/

//(解説:ルールのおさらいです。私のNimは正統派Nimの双対ゲームになっています。)

#include    <vector>
using namespace std;
//(解説:またまたvectorを使うのでそのヘッダーファイルの取り込みと、その名前空間を使用する宣言を行います。)


//CCOINSクラスヘッダー
#include    "CCOINS.h"
//(解説:CCOINSのヘッダーも取り込みます。)

////////////////////////
//CNIMクラスヘッダー
////////////////////////
class CNIM : public CCOINS {
//(解説:クラス宣言があるクラスを承継する際に':'の後、承継方式と共にクラス名を記載します。)
private:
    //メンバー変数
    vector <unsigned int> m_GameProcess;    //対戦毎の手の推移を記録
    vector <unsigned int> m_CoinsDB;        //対戦状態の記録用DB
    vector <unsigned int>::iterator m_Itr;    //m_CoinsDB用イテレーター
    unsigned int m_Record;                //対戦状態の記録用変数
//(解説:新たに追加したメンバー変数です。m_GameProcessはすべての手の推移を記録します。m_CoinsDBは(MENACEの時のm_MENACEDBと同じように)過去の参照手用の記録用です。m_Itrはvectorへのポインターのようなものです。また、データ形式を符号なし4バイト整数としたので、毎回の手(m_Coins[5]でしたね)を1整数へ落とし込むためのm_Record変数を追加しました。)
    //m_Record = 0000:0000(16進数4バイト)
    //              ①② ③
    //①評価フラグ(1バイト)0-未定、1-採用、2-忌避
    //②列数(上位4bits)→0000:0000←コイン5個の列数(下位4bits、①②合計2バイト-16bits)
    //③コイン数区分による列数内訳    0000:0000:0000:0000(2進数2バイト-16bits)
    //(下位2バイト)                    4個 3個 2個 1個

//(解説:これが4バイト符号なし整数m_Recordに、どのようにしてm_Coins[5]と列数、評価フラグを入れ込んでいるかの説明です。)

public:
    //メンバー関数
    CNIM();                        //コンストラクター
    ~CNIM();                    //デストラクター
    void InitGame();            //ゲームの初期化
    bool IsGameOver();            //勝敗の評価および記録
    bool CheckCoins(unsigned int);    //unsigned intデータのチェック(TRUE-必勝形、FALSE-必敗形)
    bool EvalTake();            //評価して手を打つ
    bool Eval();                //コイン状況に基づく勝敗評価(TRUE-必勝形、FALSE-必敗形))
    bool ExpTake();                //経験から手を打つ
    void SetRecord(int);        //m_Coinsの状態をm_Recordに記録する
    void Register(unsigned int, bool);    //手の登録(引数:登録データ、TRUE-忌避、FALSE-採用)
    bool Search(unsigned int, int&);    //手の検索(あればTRUEで引数に評価が入る、なければFALSE)
    int CoinsCmp();                //手の比較
    int DBSize();                //DBのデータ数を返す
    void DBShow();                //DBのデータを表示する
    bool CoinsWrite(char*);        //DBデータの書き込み
    bool CoinsRead(char*);        //DBデータの読み込み
    void ShowGameProcess();        //ゲームプロセス配列の表示(デバッグ用)
    void ShowData(unsigned int);//m_Recordと同じ形式のデータを表示
//(解説:追加メンバー関数は以降の定義で説明します。)

};

//コンストラクター
CNIM::CNIM() {

    //m_CoinsDBの読み込み(ファイル名(固定):"Nim.dat")
    //CoinsRead("Nim.dat");
    CoinsRead("Nim.dat");
}
//(解説:MENACEではアプリ側でデータの読み込み、書き出しをしていましたが、Nimではコンストラクター、デストラクターでそれを大なう用にしました。また、ファイル名しか記述していないので、Nim.exeと同じカレントディレクトリーから読み書きすることになります。)
//デストラクター
CNIM::~CNIM() {

    //m_CoinsDBの書き込み(ファイル名(固定):"Nim.dat")
    //CoinsWrite("Nim.dat");
    CoinsWrite("Nim.dat");
}
//(解説:上記の通り。何故コメントで同じことを書いているのか?私にも不明(笑)ですが、どうするか迷っていたからでしょう。)
 

//ゲームの初期化
void CNIM::InitGame() {

    //CCOINSの初期化
    Init();
    //Vector変数を初期化
    m_GameProcess.clear();
}
//(解説:CCOINSのInit()のみならず、手の記録用vectorであるm_GameProcessを初期化する必要があったのでこれを作りました。)
 

//勝敗の評価および記録
bool CNIM::IsGameOver() {

    int result = IsOver();                    //勝敗判定関数IsOver(勝負中-0または敗者1、2)
    if(result) {                            //勝敗がついたならば
    //1列だけの場合や2列で一方が1の必勝形の場合は登録不要なので
    //その確認を行い、その一つ前の手(必勝形に導くので採用(1))を取得する
        int dmy;
        m_Itr = m_GameProcess.end() - 1;
        auto last = *m_Itr;
        //1行だけなら
        while((last & 0x00F00000) == 0x00100000) {
            //更にひとつ前の登録記録へ遡る
            last = *(--m_Itr);
        }
        //2行で
        while((last & 0x00F00000) == 0x00200000) {
            //少なくとも一方が1なら
            if((last & 0x0000000F) > 0) {
                //ひとつ前の登録記録へ遡る
                last = *(--m_Itr);
            }
            else
                break;                        //1が無ければ先に進む
        }
        if(!Search(last, dmy)) {            //同じコイン状況が無ければ
            Register(last, CheckCoins(last));    //一手前のコイン状況を登録
        }
        return TRUE;
    }
    else {                                    //未勝敗ならば、勝負中(0)で記録
        SetRecord(0);                        //m_Recordに最新手を更新
        m_GameProcess.push_back(m_Record);    //m_GameProcessへ記録する
        return FALSE;
    }
}
//(解説:本ゲームがNimの双対ゲームなので、評価関数が最後で反転することを説明しました。この為、評価関数で「最後の1列の場合」と「2列で一方が1の場合」はプログラマーが介入しますので、これらは記録する必要が無く、それら以前の状態までm_GameProcessを戻してから、その内容をm_CoinsDBで検索し、その結果に基づいて追加登録するか、そのまま終了(TRUE)で戻ります。勝敗が付いていなければm_GameProcessに登録して、継続中(FALSE)で戻ります。)
 

//手の評価(必勝形-TRUE、必敗形-FALSE)
//(Eval関数と平仄を合わせる)
bool CNIM::CheckCoins(unsigned int data) {

    //5列のコイン数の排他的論理和
    //但し、コイン数がすべて1の場合は逆の結果となる
    int d = ((data & 0x000F0000) >> 16) * 5;                //5個の列数
    int flag = (data & 0x000F0000) >> 16;
    for(int i = 0; i < ((data & 0x0000F000) >> 12); i++)    //4個の列数
        d ^= 4;
    flag += (data & 0x0000F000) >> 12;
    for(int i = 0; i < ((data & 0x00000F00) >> 8); i++)        //3個の列数
        d ^= 3;
    flag += (data & 0x00000F00) >> 8;
    for(int i = 0; i < ((data & 0x000000F0) >> 4); i++)        //2個の列数
        d ^= 2;
    flag += (data & 0x000000F0) >> 4;
    for(int i = 0; i < (data & 0x0000000F); i++)            //1個の列数
        d ^= 1;
    if(flag > 0)    //2個以上の列数が一つでもあれば
        return d;    //必勝形-TRUE、必敗形-FALSE
    else            //データがすべて1または0であれば
        return !d;    //必勝形-FALSE、必敗形-TRUE
}
//(解説:まず整数dには各列のコイン数の排他的論理和を入れます。従って最初に5個の列(最大でも一つしかない)x5(コイン個数だから)を入れ、4個の列、3個の列と繰り返します。この際、ゲーム途中では1-4個までは複数の列がある可能性があるので、排他的論理和計算は列数だけ繰り返します。そしてその結果を返すのですが、本ゲームが双対ゲームなので、残った列が全て1個の場合、結果が反転します。それを検出するのがflagの役目で、排他的論理和計算の際に複数個の列をチェックしているのです。)

//評価して手を打つ
bool CNIM::EvalTake() {

    int col[5], i, j;
    for(i = 0, j = 0; i < 5; i++) {        //列配列に0以外の列を入れる
        if(m_Coins[i])
            col[j++] = i;
    }
    //以下のcase 1とcase 2の明らかな勝利手は評価以前に確定
    switch(j) {
    case 1:                                //1列だけの場合
        if(m_Coins[col[0]] == 1)        //1なら既に負け(手を打てない)
            break;
        else {
            m_Coins[col[0]] = 1;        //1を残してすべて取る
            m_Player = 3 - m_Player;    //先手、後手の交代
            return TRUE;                //手を打てた
        }
    case 2:                                //2列の場合
        if(m_Coins[col[0]] == 1) {        //一方が1ならば、
            m_Coins[col[1]] = 0;        //他方のすべてのコインを取る
            m_Player = 3 - m_Player;    //先手、後手の交代
            return TRUE;                //手を打てた
        }
        else if(m_Coins[col[1]] == 1) {    //他方が1ならば、
            m_Coins[col[0]] = 0;        //一方のすベてのコインを取る
            m_Player = 3 - m_Player;    //先手、後手の交代
            return TRUE;                //手を打てた
        }                                //1でない場合はdefault処理へ
    default:                            //それ以外の場合
        int coins;                        //データ一時保存用変数
        for(i = 0; i < j; i++) {
            coins = m_Coins[col[i]];    //m_Coins[col[i]の値を保存する
            for(int k = 1; k <= coins; k++) {    //1から自分のコイン数迄
                m_Coins[col[i]] = coins - k;    //コインを取った想定を行い、
                if(!Eval()) {                    //相手に不利であれば確定する
                    m_Player = 3 - m_Player;    //先手、後手の交代
                    return TRUE;        //手を打てた
                }
            }
            m_Coins[col[i]] = coins;    //元に戻す
        }
        break;
    }
    return FALSE;                        //m_Player(自分)が手を打てない(投了)
}
//(解説:これが排他的論理和で手を打つ関数です。1列の場合と2列で一方が1個の場合の必勝形のみプログラムで処理し、その他(default)は次に述べるEval関数で手を打ちます。打てる場合、関数を終了する前に次のプレーヤーを設定します。↑に青地で示したのが先手、後手のトグルの部分です。)

//コイン状況に基づく勝敗評価(TRUE-必勝形、FALSE-必敗形)
//(CheckCoins関数と平仄を合わせる)
//すべての列について排他的論理和を取る
//例:0, 0, 2, 2, 0 → 排他的論理和は0(必敗形)
//但し、0でない列が1だけの場合は例外となる
//例:0, 0, 1, 1, 0 → 排他的論理和は0だが、必勝形となっている

bool CNIM::Eval() {

    int d;
    bool flag = FALSE;
    for(int i = 0; i < 5; i++) {
        if(!i)                    //i == 0でコイン数を代入
            d = m_Coins[i];
        else                    //以降は排他的論理和を取る
            d ^= m_Coins[i];
        if(m_Coins[i] > 1)        //コイン数が1以上であれば
            flag = TRUE;        //flagは真
    }
    if(flag)                    //いずれかの列のコイン数が1以上だったら
        return d;                //排他的論理和
    else                        //列のコイン数すべてが1または0だったら
        return !d;                //排他的論理和の逆
}
//(解説:上に赤字で示した通りです。CheckCoins関数はm_Recordと同じ、符号なし整数4バイトに書かれたデータをチェックしますが、この関数は直接m_Coins配列をチェックします。)

//経験から手を打つ
bool CNIM::ExpTake() {

    //列配列に0以外の列を入れる
    int col[5], i, j;
    for(i = 0, j = 0; i < 5; i++) {
        if(m_Coins[i])
            col[j++] = i;
    }
    //CoinsDBの'2'(禁忌)に該当した手を入れるテーブル
    char TabooTable[5][5] = {0};        //[列][個数]ゼロ初期化
    int TTCounter = 0;                    //TabooTable登録数
    //以下のcase 1とcase 2の明らかな勝利手は評価以前に確定
    switch(j) {
    case 1:                                //1列だけの場合
        if(m_Coins[col[0]] == 1)        //1なら既に負け(手を打てない)
            break;
        else {
            m_Coins[col[0]] = 1;        //1を残してすべて取る
            m_Player = 3 - m_Player;    //先手、後手の交代
            return TRUE;                //手を打てた
        }
    case 2:                                //2列の場合
        if(m_Coins[col[0]] == 1) {        //一方が1ならば、
            m_Coins[col[1]] = 0;        //他方のすべてのコインを取る
            m_Player = 3 - m_Player;    //先手、後手の交代
            return TRUE;                //手を打てた
        }
        else if(m_Coins[col[1]] == 1) {    //他方が1ならば、
            m_Coins[col[0]] = 0;        //一方のすベてのコインを取る
            m_Player = 3 - m_Player;    //先手、後手の交代
            return TRUE;                //手を打てた
        }                                //1でない場合はdefault処理へ
    default:                            //それ以外の場合
        int coins, v;                    //データ一時保存および評価用変数
        for(i = 0; i < j; i++) {
            coins = m_Coins[col[i]];        //m_Coins[col[i]の値を保存する
            for(int k = 1; k <= coins; k++) {    //1から自分のコイン数迄
                m_Coins[col[i]]= coins - k;        //コインを取った想定を行い、
                SetRecord(0);                    //Searchの為、m_Recordにコイン状況を記録
                if(Search(m_Record, v)) {        //同じ手があり、その評価が「採用(1)」であれば
                    if(v == 1) {                //その評価が「採用(1)」であれば
                        m_Player = 3 - m_Player;    //先手、後手の交代
                        return TRUE;            //手を打てた
                    }
                    else if(v == 2) {            //その評価が「忌避(2)」であれば
                        TabooTable[col[i]][coins - k] = 1;    //禁忌テーブルに登録する
                        TTCounter++;                        //禁忌テーブル登録数
                    }
                }                        //見つからなければそのままにする
            }
            m_Coins[col[i]] = coins;    //元に戻す
        }
        /* 注意:すべての候補がTabooTableに該当すると無限ループになるので、
           安全の為、乱数による手決めが忌避テーブル登録回数 + 1以上になった場合、
           投了させます */
        do {
            i = rand() % j;        //値を持つ列を乱数で抽出
            //m_Coins[col[i]]のコイン数から、乱数で定めた値(1~そのコイン数)を差し引く
            v = m_Coins[col[i]] - ((rand() % m_Coins[col[i]]) + 1);
            --TTCounter;
            if(TTCounter < 0)        //(登録回数 + 1)なのでこれ以上やっても全て忌避の筈
                break;

        } while(TabooTable[i][v]);    //禁忌テーブルに引っかかれば乱数をやり直す
        m_Coins[col[i]] = v;
        m_Player = 3 - m_Player;    //先手、後手の交代
        return TRUE;                //手を打てた
    }
    return FALSE;                    //打つ手がなく、「投了」
}
//(解説:これが過去の手の経験から手を打つ関数で、基本的な構造はその前のEvalTakeと変わりませんが、排他的論理和での判断の代わりに、m_Recordと同じ形式のデータにしてCoinsDBを検索し、無ければそのまま進んで最後に乱数で手を決め、あった場合は「推奨」の場合その手に決定し、「忌避」の場合「忌避テーブル」に書き込んで乱数による手の選択をする際にこの忌避テーブルをチェックしてから最終的に手を決定します。なお、稀に残ったすべての手が「忌避」となった場合、無限ループになる可能性があるので、安全の為「忌避テーブルに書き込んだ数よりも多く忌避テーブルに引っかかったら投了する」処理としています。)

void CNIM::SetRecord(int result) {    //m_Coinsの状態をm_Recordに記録する

    int ln = 0, coins5 = 0, coins4 = 0, coins3 = 0, coins2 = 0, coin1 = 0;
    for(int i = 0; i < 5; i++) {
        if(m_Coins[i]) {
            ln++;
            switch(m_Coins[i]) {
            case 5:
                coins5++;
                break;
            case 4:
                coins4++;
                break;
            case 3:
                coins3++;
                break;
            case 2:
                coins2++;
                break;
            case 1:
                coin1++;
                break;
            default:
                break;
            }
        }
    }
    m_Record = result;            //先頭は0-未定、1-採用、2-忌避
    m_Record <<= 4;                //4 bits上位へシフト
    m_Record |= ln;                //0以外の数値を持つ列数
    m_Record <<= 4;                //8 bits上位へシフト
    m_Record |= (char)coins5;    //5つのコインの列数
    m_Record <<= 4;                //8 bits上位へシフト
    m_Record |= (char)coins4;    //4つのコインの列数
    m_Record <<= 4;                //4 bits上位へシフト
    m_Record |= (char)coins3;    //3つのコインの列数
    m_Record <<= 4;                //4 bits上位へシフト
    m_Record |= (char)coins2;    //2つのコインの列数
    m_Record <<= 4;                //4 bits上位へシフト
    m_Record |= (char)coin1;    //1つのコインの列数
}
//(解説:これが現在のm__Coinsの状況をm_Recordの4バイト整数に空き込む処理です。今回は整数7つ分のデータを1つに押し込むためにシフトとビット演算を多くやりました。)
 

//手の登録
void CNIM::Register(unsigned int data, bool flag) {

    //相手に渡す手の評価なので
    if(flag)        //「忌避」(flagがTRUE)
        data |= 0x02000000;
    else            //「採用」(flagがFALSE)
        data |= 0x01000000;
    m_CoinsDB.push_back(data);
}
//(解説:手の登録をvectorのpush_back関数で行いますが、その前に「推奨」「忌避」のデータをビット演算で追加しています。)

//手の検索
bool CNIM::Search(unsigned int data, int& value) {

    unsigned int cmp;    //比較用変数(data & 0x0000FFFFと比較する)
    for(m_Itr = m_CoinsDB.begin(); m_Itr != m_CoinsDB.end(); m_Itr++) {
        cmp = *m_Itr & 0x000FFFFF;                    //下位2バイト半のコイン状況で
        if(cmp == (data & 0x000FFFFF)) {            //dataと比較する
            value = (*m_Itr & 0xFF000000) >> 24;    //同一であればその評価を取得する
            return TRUE;
        }
    }
    return FALSE;
}
//(解説:これは符号なし整数データの下位2バイト半(1-5個の列数)のコイン状況でCoinsDBを検索してあればTRUE、無ければFALSEを返しますが、見つかったデータの評価フラグも返したかったので、引数のvalueを参照形にして値を返しています。)

//DBのデータ数を返す
int CNIM::DBSize() {

    return m_CoinsDB.size();
}
//(解説:これはMENACEの時と全く同じものでvectorのsize関数の値を返しています。)

//DBのデータを表示する
void CNIM::DBShow() {

    int i = 1;
    for(m_Itr = m_CoinsDB.begin(); m_Itr != m_CoinsDB.end();m_Itr++,  i++) {
        cout << endl << "<< 第" << i << "番目のレコード >>" << endl;
        ShowData(*m_Itr);
    }
}
//(解説:CoinsDBの登録データを表示する関数ですが、表示部分はShowData関数にまとめています。)


bool CNIM::CoinsWrite(char* fn) {

    int num = DBSize();
    unsigned int data;
    HANDLE hFile = CreateFile(fn, GENERIC_WRITE,
                FILE_SHARE_WRITE, NULL,
                OPEN_ALWAYS, NULL, NULL);    //ファイルを書込でオープン
    if(hFile != INVALID_HANDLE_VALUE) {        //オープンできたか
        DWORD dwWrite;
        //先ずintでデータ数を書き込む
        WriteFile(hFile, &num, sizeof(int), &dwWrite, NULL);
        m_Itr = m_CoinsDB.begin();
        //次にm_Coinsの配列を書き込む
        for(int i = 0; i < num; i++, m_Itr++) {
            data = *m_Itr;
            WriteFile(hFile, &data, sizeof(data), &dwWrite, NULL);
        }
        CloseHandle(hFile);                    //ファイルのクローズ
        return TRUE;
    }
    else
        return FALSE;
}

//(解説:Win32APIを使ったデータ(符号なし整数)の書き込み関数です。)


bool CNIM::CoinsRead(char* fn) {

    int num;
    unsigned int data;
    HANDLE hFile = CreateFile(fn, GENERIC_READ,
                FILE_SHARE_READ, NULL,
                OPEN_EXISTING, NULL, NULL);    //ファイルを読込でオープン
    if(hFile != INVALID_HANDLE_VALUE) {        //オープンできたか
        m_CoinsDB.clear();                    //m_CoinsDBのデータを空にする
        DWORD dwRead;
        //先ずint numにデータ数を読み込む
        ReadFile(hFile, &num, sizeof(int), &dwRead, NULL);
        for(int i = 0; i < num; i++) {
            ReadFile(hFile, &data, sizeof(data), &dwRead, NULL);
            m_CoinsDB.push_back(data);
        }
        CloseHandle(hFile);                //ファイルのクローズ
        return TRUE;
    }
    else
        return FALSE;
}

//(解説:同じく読込関数です。)


void CNIM::ShowGameProcess() {

    int i = 1;
    for(m_Itr = m_GameProcess.begin(); m_Itr != m_GameProcess.end(); m_Itr++, i++) {
        cout << endl << "<< 第" << i << "番目のレコード >>" << endl;
        ShowData(*m_Itr);
    }
}

//(解説:手の推移を記録するm_GameProcessを表示する関数です。これはデバッグ目的です。)


void CNIM::ShowData(unsigned int data) {

    cout << "評価フラグ:" << ((data & 0xFF000000) >> 24) << endl;
    cout << "列数   :" << ((data & 0x00F00000) >> 20) << endl;
    cout << "5 つの列数:" << ((data & 0x000F0000) >> 16) << endl;
    cout << "4 つの列数:" << ((data & 0x0000F000) >> 12) << endl;
    cout << "3 つの列数:" << ((data & 0x00000F00) >> 8) << endl;
    cout << "2 つの列数:" << ((data & 0x000000F0) >> 4) << endl;
    cout << "1 つの列数:" << (data & 0x0000000F) << endl;
}

//(解説:符号なし整数のビットデータを表示する共通関数です。)

 

如何でしたでしょうか?次回はこのCNIMクラスの動作試験の為に作ったNIM.cppというテストランプログラムを紹介します。