【C++】五目並べを作りました

C++

 先日作成したSLコマンドの要領で画面が下にスクロールしない五目並べが作れるのではないかと思い、調べていたところcurses.hを使った手法が手頃そうだったので試しに作ってみました。
 コンピュータと勝負できるようにミニマックス法を使ったアルゴリズムを導入しましたが、パソコンが重すぎで深層探索の3層までしか計算させることができませんので、余裕で勝てます。w
 コンピュータに半角カナで喋らせたところレトロゲーム感が出ました。

● 環境

  Windows11
  VisualStudio 2022
   vcpkg を使って pdcurses(curses.h) をインストールします

● 準備編

(1)vcpkgのインストール
   C++のパッケージ管理ソフトvcpkgをインストールします。
   インストールはこちらのページを参考にさせていただきました。すごく読みやすいです。
   (https://www.ktakedev.com/blog/90/
  インストールコマンドは次の通りです。

#gitkからリポジトリをクローンします

C:\ > git clone https://github.com/microsoft/vcpkg.git
Cloning into 'vcpkg'...
remote: Enumerating objects: 237501, done.
remote: Counting objects: 100% (269/269), done.
remote: Compressing objects: 100% (224/224), done.
remote: Total 237501 (delta 117), reused 152 (delta 45), pack-reused 237232Receiving objects: 100% (237501/237501), 69.7
Resolving deltas: 100% (157898/157898), done.
Updating files: 100% (11402/11402), done.

# カレントフォルダにvcpkgというフォルダができるので、移動します
C:\ > cd vcpkg
C:\vcpkg\ > bootstrap-vcpkg.bat  # インストール開始
Downloading https://github.com/microsoft/vcpkg-tool/releases/download/2024-06-10/vcpkg.exe -> C:\vcpkg\vcpkg.exe... done.
Validating signature... done.

vcpkg package management program version 2024-06-10-02590c430e4ed9215d27870138c2e579cc338772

See LICENSE.txt for license information.
Telemetry
---------
vcpkg collects usage data in order to help us improve your experience.
The data collected by Microsoft is anonymous.
You can opt-out of telemetry by re-running the bootstrap-vcpkg script with -disableMetrics,
passing --disable-metrics to vcpkg on the command line,
or by setting the VCPKG_DISABLE_METRICS environment variable.

Read more about vcpkg telemetry at docs/about/privacy.md

インストールはこれで完了です

(2)vcpkgの使い方
   利用可能なライブラリを検索
    vcpkg search [<部分文字列>]
   searchコマンドで利用可能なライブラリを検索します。

# パッケージの検索
C:\vcpkg>vcpkg search curses
ncurses                  6.4#2            Free software emulation of curses in System V Release 4.0, and more
pdcurses                 3.9#6            Public Domain Curses - a curses library for environments that don't fit th...
The result may be outdated. Run `git pull` to get the latest results.
If your port is not listed, please open an issue at and/or consider making a pull request.  -  https://github.com/Microsoft/vcpkg/issues

   ncurses はunix向けらしいです
   今回はWindowsなので、pdcursesを使いました。

# cursesパッケージのインストール
C:\vcpkg > vcpkg install pdcurses
Computing installation plan...
A suitable version of cmake was not found (required v3.29.2) Downloading portable cmake 3.29.2...
Downloading cmake...

https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-windows-i386.zip-
>C:\vcpkg\downloads\cmake-3.29.2-windows-i386.zip Downloading https://github.com/Kitware/CMake/releases/download/v3.29.2/cmake-3.29.2-windows-i386.zip Extracting cmake... The following packages will be built and installed: pdcurses:x64-windows@3.9#6 Detecting compiler hash for triplet x64-windows... A suitable version of powershell-core was not found (required v7.2.16) Downloading portable powershell-core 7.2.16... Downloading powershell-core...
https://github.com/PowerShell/PowerShell/releases/download/v7.2.16/PowerShell-7.2.16-win-x64.zip-
>C:\vcpkg\downloads\PowerShell-7.2.16-win-x64.zip Downloading https://github.com/PowerShell/PowerShell/releases/download/v7.2.16/PowerShell-7.2.16-win-x64.zip Extracting powershell-core... Compiler found: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe A suitable version of 7zip was not found (required v24.6.0) Downloading portable 7zip 24.6.0... Downloading 7zip...
https://github.com/ip7z/7zip/releases/download/24.06/7z2406-extra.7z-
>C:\vcpkg\downloads\7z2406-extra.7z Downloading https://github.com/ip7z/7zip/releases/download/24.06/7z2406-extra.7z Extracting 7zip... Restored 0 package(s) from C:\?????\AppData\Local\vcpkg\archives in 398 us. Use --debug to see more details. Installing 1/1 pdcurses:x64-windows@3.9#6... Building pdcurses:x64-windows@3.9#6... -- Downloading https://github.com/wmcbrine/PDCurses/archive/3.9.tar.gz -> wmcbrine-PDCurses-3.9.tar.gz... -- Extracting source C:/vcpkg/downloads/wmcbrine-PDCurses-3.9.tar.gz -- Applying patch nmake-install.patch -- Using source at C:/vcpkg/buildtrees/pdcurses/src/3.9-2f00932d3e.clean -- Found external ninja('1.11.0'). -- Getting CMake variables for x64-windows -- Building and installing x64-windows-dbg -- Building and installing x64-windows-rel CMake Warning at scripts/cmake/vcpkg_copy_pdbs.cmake:44 (message): Could not find a matching pdb file for: C:/vcpkg/packages/pdcurses_x64-windows/bin/pdcurses.dll C:/vcpkg/packages/pdcurses_x64-windows/debug/bin/pdcurses.dll Call Stack (most recent call first): ports/pdcurses/portfile.cmake:36 (vcpkg_copy_pdbs) scripts/ports.cmake:191 (include) -- Installing: C:/vcpkg/packages/pdcurses_x64-windows/share/unofficial-pdcurses/unofficial-pdcurses-config.cmake -- Installing: C:/vcpkg/packages/pdcurses_x64-windows/share/pdcurses/copyright -- Performing post-build validation Stored binaries in 1 destinations in 142 ms. Elapsed time to handle pdcurses:x64-windows: 9 s pdcurses:x64-windows package ABI: e667a39f440407bf7f4244d957f88d05d04fc202fba012ae78d7e9ad2ec3ac40 Total install time: 9 s pdcurses provides CMake targets: # this is heuristically generated, and may not be correct find_package(unofficial-pdcurses CONFIG REQUIRED) target_link_libraries(main PRIVATE unofficial::pdcurses::pdcurses) # 何もしなくても勝手にインストールされました

(3)Visual Studioからビルドできるようにする
   C++の開発では、VisualStudioを使っていますので、ビルドのリンカにライブラリを登録するというイメージです。
   コマンドプロンプトから次のコマンドを入力します。

C:\vcpkg > vcpkg integrate install
Applied user-wide integration for this vcpkg root.
CMake projects should use: "-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"

All MSBuild C++ projects can now #include any installed libraries. Linking will be handled automatically. Installing new libraries will make them instantly available.

 これだけで、VisualStudioの
   「ソリューションエクスプローラ」-「プロジェクト名を右クリック」-「プロパティ」
   プロジェクト名 プロパティページ - 構成プロパティ に
     [vcpkg]
が追加されていました。

(4)テストプロジェクトの実行

  curses.hがリンクされているか確認する為に、次のプログラムを実行しました。

#include <curses.h>
int main(int argc, char* argv[])
{
	// 初期化
	if (initscr() == NULL) {return 1;}
	int i = 0;
	while (true) {
		erase();
		// 文字列を描く
		mvaddstr(i, i, "Hello curses");
		i++;
		// 画面を更新
		refresh();
		// 1秒待機
		napms(1000);
	}
	return 0;
}

「Hello curses」という文字が動いていれば成功です。
こんな簡単に動的プログラムが作れるのかと驚きました。

● プログラム編

次のソースを作成しました。
minmax法のアルゴリズムはgpt先生から授かりました。
 探索深度を上げていけば、もう少し賢くなるのかもしれないですが、
   maxDepth
を4以上にすると、一手ごとに3分以上待たされ、実用的ではありませんでした。(スペックにも問題あり)

#include <curses.h> //vcpkgでインストール
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
#include <algorithm> // minmax法に使用
#include <random>\

#define BORD_WIDTH (10)
#define BORD_HEIGHT (10)

#define WIDTH (38)
#define HEIGHT (20)

char bord[HEIGHT][WIDTH] = {
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+",
"|   |   |   |   |   |   |   |   |   |",
"+---+---+---+---+---+---+---+---+---+"
};

// 配置した石を格納する配列
char BORD_ARRAY[BORD_HEIGHT][BORD_WIDTH] = { 0 };

typedef struct {
    int x;
    int y;
} INDEX;

INDEX index[BORD_HEIGHT][BORD_WIDTH] = {
    { {0, 0} ,{ 4, 0},{8, 0},{12, 0},{16, 0},{20, 0},{24, 0},{28, 0},{32, 0},{36, 0}},
    { {0, 2} ,{ 4, 2},{8, 2},{12, 2},{16, 2},{20, 2},{24, 2},{28, 2},{32, 2},{36, 2}},
    { {0, 4} ,{ 4, 4},{8, 4},{12, 4},{16, 4},{20, 4},{24, 4},{28, 4},{32, 4},{36, 4}},
    { {0, 6} ,{ 4, 6},{8, 6},{12, 6},{16, 6},{20, 6},{24, 6},{28, 6},{32, 6},{36, 6}},
    { {0, 8} ,{ 4, 8},{8, 8},{12, 8},{16, 8},{20, 8},{24, 8},{28, 8},{32, 8},{36, 8}},
    { {0,10} ,{ 4,10},{8,10},{12,10},{16,10},{20,10},{24,10},{28,10},{32,10},{36,10}},
    { {0,12} ,{ 4,12},{8,12},{12,12},{16,12},{20,12},{24,12},{28,12},{32,12},{36,12}},
    { {0,14} ,{ 4,14},{8,14},{12,14},{16,14},{20,14},{24,14},{28,14},{32,14},{36,14}},
    { {0,16} ,{ 4,16},{8,16},{12,16},{16,16},{20,16},{24,16},{28,16},{32,16},{36,16}},
    { {0,18} ,{ 4,18},{8,18},{12,18},{16,18},{20,18},{24,18},{28,18},{32,18},{36,18}}
};

//コンピュータのコメント カタカナは2バイト
char computer_comment[10][55] = {
    "フムフム ソウキマスカ        ",
    "ヤハリ ソコニオクノデスネ     ",
    "ソノテハヨンデイマシタ       ",
    "コレハチョットマズイデスネ    ",
    "スペースデイシヲオイテクダサイ",
    "....               ",
    "....               ",
    "....               ",
    "ソロソロ ホンキヲダシマスカ    ",
    "ナルホド              "
};
int random_comment;

// 石を格納するボード
int BORD_DATA[BORD_HEIGHT][BORD_WIDTH] = { 0 };

//プレーヤーのアイコン
#define PL_BR "O" // 黒石アイコン(色を変えたのでアイコンは一緒)
#define PL_WH "O" // 白石アイコン(色を変えたのでアイコンは一緒)
#define CUR_I "_" // カーソルアイコン

typedef struct {
    int x;
    int y;
} CUR;

CUR cur;
int currentPlayer = 1; // 1: 黒石, -1: 白石

//画面描画
void PictScreen() {
    clear(); // 画面をクリア
    
    for (int y = 0; y < HEIGHT - 1; y++) {
        for (int x = 0; x < WIDTH - 1; x++) {
            attron(COLOR_PAIR(1)); // 碁盤の色を設定
            mvprintw(y, x, &bord[y][x]); // 1文字づつ表示しないと画面がずれる
            attroff(COLOR_PAIR(1)); // 色設定を解除
        }
    }

    // ボード上の石を表示
    for (int y = 0; y < BORD_HEIGHT; y++) {
        for (int x = 0; x < BORD_WIDTH; x++) {
            if (BORD_DATA[y][x] == 1) {
                attron(COLOR_PAIR(2)); // 黒石の色を設定
                mvprintw(index[y][x].y, index[y][x].x, PL_BR); // 黒石
                attroff(COLOR_PAIR(2)); // 色設定を解除
            }
            else if (BORD_DATA[y][x] == -1) {
                attron(COLOR_PAIR(3)); // 白石の色を設定
                mvprintw(index[y][x].y, index[y][x].x, "O"); // 白石
                attroff(COLOR_PAIR(3)); // 色設定を解除
            }
        }
    }

    // 座標表示
    //mvprintw(19, 1, "y=%d x=%d index_y =%d  index_x = %d ", cur.y, cur.x, index[cur.y][cur.x].y, index[cur.y][cur.x].x);

    // カーソルの表示
    attron(COLOR_PAIR(4)); // カーソルの色を設定
    mvprintw(index[cur.y][cur.x].y, index[cur.y][cur.x].x, currentPlayer == 1 ? PL_BR : PL_WH);
    attroff(COLOR_PAIR(4)); // 色設定を解除

    //コメント
    mvprintw(20, 1, computer_comment[random_comment]);
    refresh();
}

// メッセージを表示する関数
void PrintMessage(const char* c) {
    mvprintw(20, 1, c);
    refresh(); // 画面を更新
}

// 初期値の設定
void init() {
    initscr();
    raw();
    keypad(stdscr, TRUE);
    noecho();

    //色の指定
    if (has_colors()) {
        start_color();
        init_pair(1, COLOR_BLACK, COLOR_YELLOW); // 碁盤の色
        init_pair(2, COLOR_WHITE, COLOR_BLACK);  // 黒石の色
        init_pair(3, COLOR_BLACK, COLOR_YELLOW);  // 白石の色
        init_pair(4, COLOR_RED, COLOR_BLACK);   // カーソルの色
    }

    cur.x = 0, cur.y = 0; // カーソルの初期位置を左上に
    PictScreen();
    srand(time(0)); // ランダムシードの初期化
    
    PrintMessage("ヨロシクオネガイシマス.      ");
}

//勝利判定 
bool isWin(int player) {
    // 検索方向のベクトル
    int dx[] = { 1, 0, 1, 1 };
    int dy[] = { 0, 1, 1, -1 };

    for (int y = 0; y < BORD_HEIGHT; y++) {
        for (int x = 0; x < BORD_WIDTH; x++) {
            if (BORD_DATA[y][x] == player) {
                for (int dir = 0; dir < 4; dir++) { //dir 0 右方向検索 1 下方向検索 2 右下方向検索 3 左下方向検索
                    int count = 1;
                    for (int step = 1; step < 5; step++) {
                        int nx = x + step * dx[dir];
                        int ny = y + step * dy[dir];
                        if (nx >= 0 && nx < BORD_WIDTH && ny >= 0 && ny < BORD_HEIGHT && BORD_DATA[ny][nx] == player) {
                            count++;
                        }
                        else {
                            break;
                        }
                    }
                    if (count == 5) {
                        //5つ並んだ方の勝利
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

//コンピュータアルゴリズム ミニマックス法 ここから
bool isFull() {
    for (int y = 0; y < BORD_HEIGHT; y++) {
        for (int x = 0; x < BORD_WIDTH; x++) {
            if (BORD_DATA[y][x] == 0) {
                return false;
            }
        }
    }
    return true;
}

int evaluate() {
    if (isWin(-1)) {
        return 1000;
    }
    else if (isWin(1)) {
        return -1000;
    }
    else {
        return 0;
    }
}

int minimax(int depth, bool isMax, int alpha, int beta, int maxDepth) {
    int score = evaluate();

    if (score == 1000 || score == -1000 || isFull()) {
        return score;
    }

    if (depth >= maxDepth) {
        return score;
    }

    if (isMax) {
        int best = INT_MIN;
        for (int y = 0; y < BORD_HEIGHT; y++) {
            for (int x = 0; x < BORD_WIDTH; x++) {
                if (BORD_DATA[y][x] == 0) {
                    BORD_DATA[y][x] = -1; // コンピュータの手
                    best = std::max(best, minimax(depth + 1, !isMax, alpha, beta, maxDepth));
                    BORD_DATA[y][x] = 0;
                    alpha = std::max(alpha, best);
                    if (beta <= alpha) {
                        break; // ベータカットオフ
                    }
                }
            }
        }
        return best;
    }
    else {
        int best = INT_MAX;
        for (int y = 0; y < BORD_HEIGHT; y++) {
            for (int x = 0; x < BORD_WIDTH; x++) {
                if (BORD_DATA[y][x] == 0) {
                    BORD_DATA[y][x] = 1; // プレイヤーの手
                    best = std::min(best, minimax(depth + 1, !isMax, alpha, beta, maxDepth));
                    BORD_DATA[y][x] = 0;
                    beta = std::min(beta, best);
                    if (beta <= alpha) {
                        break; // アルファカットオフ
                    }
                }
            }
        }
        return best;
    }
}

void computerMove() {
    int bestVal = INT_MIN;
    int bestMoveX = -1;
    int bestMoveY = -1;

    int maxDepth = 3; // 探索深さの制限 4にすると1分待ち

    for (int y = 0; y < BORD_HEIGHT; y++) {
        for (int x = 0; x < BORD_WIDTH; x++) {
            if (BORD_DATA[y][x] == 0) {
                BORD_DATA[y][x] = -1; // コンピュータの手
                int moveVal = minimax(0, false, INT_MIN, INT_MAX, maxDepth);
                BORD_DATA[y][x] = 0;

                if (moveVal > bestVal) {
                    bestMoveX = x;
                    bestMoveY = y;
                    bestVal = moveVal;
                }
            }
        }
    }

    BORD_DATA[bestMoveY][bestMoveX] = -1; // 最適な手を置く
}
//ミニマックス法 ここまで

//コンピュータアルゴリズム ランダム法
/*
void computerMove() {
    int x, y;
    do {
        x = rand() % BORD_WIDTH;
        y = rand() % BORD_HEIGHT;
    } while (BORD_DATA[y][x] != 0);
    BORD_DATA[y][x] = -1;
}*/
//ランダム法 ここまで

int main() {
    int ch;
    init();
    refresh();

    while (1) { 
        ch = getch();

        if (ch == KEY_UP) {
            cur.y = (cur.y - 1 + BORD_HEIGHT) % BORD_HEIGHT; // y座標を1減らす
        }
        else if (ch == KEY_DOWN) {
            cur.y = (cur.y + 1) % BORD_HEIGHT; // y座標を1増やす
        }
        else if (ch == KEY_LEFT) {
            cur.x = (cur.x - 1 + BORD_WIDTH) % BORD_WIDTH; // x座標を1減らす
        }
        else if (ch == KEY_RIGHT) {
            cur.x = (cur.x + 1) % BORD_WIDTH; // x座標を1増やす
        }
        else if (ch == ' ') {
            if (BORD_DATA[cur.y][cur.x] == 0) { // 空いている場所に石を置ける
                BORD_DATA[cur.y][cur.x] = 1; // プレイヤーの石を置く
                
                //ユーザの勝利判定
                if (isWin(1)) {
                    PrintMessage("アナタ ノショウリ                ");
                    refresh();
                    getch();
                    break;
                }
                
                //コンピュータ対ユーザモード
                PrintMessage("カンガエチュウ...              ");
                computerMove(); //コンピュータのターン
                
                //ユーザ対ユーザモード
                //currentPlayer = -currentPlayer; // プレイヤーを交代

                //コンピュータの勝利判定
                if (isWin(-1)) {
                    PictScreen(); //最後に置いた石を表示させる
                    mvprintw(20, 1, "コンピュータ ノショウリ!           ");
                    refresh();
                    getch();
                    break;
                }
                //次のコンピュータコメントを考える
                random_comment = rand() % 10;
            }
        }
        else if (ch == 'q') {
            PrintMessage("ゲームヲ シュウリョウシマス                ");
            break;
        }
        PictScreen();
    }
    endwin();
    return 0;
}

● 使い方

  実行すると、ユーザが黒石(先手)、コンピュータが白石(後手)でゲームが開始します。
  十字ボタンでカーソルを移動させて、石を置きたい場所でスペースキーを押してください。
  コンピュータが一手ごとに独り言をしゃべりますので、温かい気持ちで受け入れてください。

● 最後に

 今回はcursesを使った、プログラムが作れたのでとりあえずは満足です。
 見栄え的にはPC98当時の雰囲気出ていませんか?(アルゴリズムは容赦くださいw)
 コンピュータのセリフはランダムで表示されるので、その場にあった文字列を入れると笑えると思います。
 日本語等の全角文字を表示させると、表示が崩れるので、半角カナを使っています。
 Cursorが対応していないのかな?