スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

ゲームAI同士を対戦させてみる

初めまして。新人?アルバイトのzeosuttです。


秋です。

皆でゲームAIを持ち寄って対戦したい季節ですね。

何のゲームにしましょうか。

そうですね、簡単に実装できるので、〇×ゲームにしましょう。

f:id:spookies_yano:20170919125328p:plain
実は、両者が最善を尽くすと必ず引き分けになる

さて、どんなに強いAIを作っても、それを対戦させる仕組みがなくては意味がありませんね。

この記事では、AI同士を対戦させるための仕組みを作ってみます。

AIの作成自体は主題ではありません。

人対人

まずは、人間同士で対戦ができるようにしましょう。

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

const char *sign = "ox";   // 〇/× は o/x で表現

// 手を表現する構造体
typedef struct {
    int i, j;
} Choice;

// 盤面を初期化する
void init_field(char field[3][4]) {
    for (int i = 0; i < 3; i++) {
        strcpy(field[i], "...");
    }
}

// 指定されたストリームに盤面を送る
void send_field(const char field[3][4], FILE *fp) {
    for (int i = 0; i < 3; i++) {
        fprintf(fp, "%s\n", field[i]);
    }
}

// 指定されたストリームから手を受け取る
void receive_from_ai(Choice *pchoice, FILE *fp) {
    fscanf(fp, "%d %d", &pchoice->i, &pchoice->j);
}

// 手が有効か?
bool is_valid(const Choice *pchoice, const char field[3][4]) {
    const int i = pchoice->i, j = pchoice->j;
    return i >= 0 && i < 3 && j >= 0 && j < 3 && field[i][j] == '.';
}

// 手をシミュレートする
void simulate(int player, const Choice *pchoice, char field[3][4]) {
    field[pchoice->i][pchoice->j] = sign[player];
}

// 指定されたプレイヤーが勝利しているか?
bool is_win(int player, const char field[3][4]) {
    // 手抜きだけど怒らないで
    return field[0][0] == field[0][1] && field[0][1] == field[0][2] && field[0][2] == sign[player]
        || field[1][0] == field[1][1] && field[1][1] == field[1][2] && field[1][2] == sign[player]
        || field[2][0] == field[2][1] && field[2][1] == field[2][2] && field[2][2] == sign[player]
        || field[0][0] == field[1][0] && field[1][0] == field[2][0] && field[2][0] == sign[player]
        || field[0][1] == field[1][1] && field[1][1] == field[2][1] && field[2][1] == sign[player]
        || field[0][2] == field[1][2] && field[1][2] == field[2][2] && field[2][2] == sign[player]
        || field[0][0] == field[1][1] && field[1][1] == field[2][2] && field[2][2] == sign[player]
        || field[0][2] == field[1][1] && field[1][1] == field[2][0] && field[2][0] == sign[player];
}

// 盤面を表示する
void print_field(const char field[3][4]) {
    send_field(field, stdout);
}

// 手を表示する
void print_choice(const Choice *pchoice) {
    printf("choice: %d %d\n", pchoice->i, pchoice->j);
}

int main(void) {
    char field[3][4];
    int turn;

    init_field(field);

    print_field(field);
    for (turn = 0; turn < 9; turn++) {
        const int player = turn & 1;
        Choice choice;

        printf("AI%d(%c)'s turn\n", player + 1, sign[player]);

        // 一時的に標準入力を指定
        receive_from_ai(&choice, stdin);

        print_choice(&choice);

        if (!is_valid(&choice, field)) {
            const int oppo = player ^ 1;
            puts("invalid input");
            printf("AI%d(%c) win!\n", oppo + 1, sign[oppo]);
            break;
        }

        simulate(player, &choice, field);

        print_field(field);

        if (is_win(player, field)) {
            printf("AI%d(%c) win!\n", player + 1, sign[player]);
            break;
        }
    }
    if (turn == 9) {
        puts("draw...");
    }

    return 0;
}

はい。できました。

座標はi jの形式で入力してください。

例えば、左上なら0 0、その下なら1 0となります。

AI対AI

〇×ゲーム自体のコードは書けました。

後は、AIと通信をするように修正すれば完成です。

ここで、AIには、次の例のようなフォーマットで入力を与えることとします。

o
...
.o.
..x

最初の行は、自分が〇と×のどちらであるかを表します。

2行目以降は盤面の状態を表します。

また、AI作成者の負担を軽減するためにも、AIからは標準入出力でやり取りできるようにしましょう。

AI実装例

先に、このプログラムの入出力仕様に沿って、簡単なAIを実装しますね。

このプログラムの理解の一助となるほか、AI作成未経験者にとっては、入力を受け取り、手を考え、出力するという、AIの基本的な流れの理解にも役立つかもしれません。

#include <stdbool.h>
#include <stdio.h>

typedef struct {
    int i, j;
} Choice;

// 入力を受け取る
bool input(char *pmark, char field[3][4]) {
    if (scanf(" %c", pmark) == EOF) {
        return false;
    }

    for (int i = 0; i < 3; i++) {
        scanf("%s", field[i]);
    }

    return true;
}

// 手を考える
bool think(Choice *pchoice, int mark, const char field[3][4]) {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (field[i][j] == '.') {
                // 普通は、各手を何らかの基準で評価し、最も評価値が高いものを選ぶ
                // しかし、今回の主題はAI作成ではないため、ここは手を抜く
                *pchoice = (Choice){i, j};
                return true;
            }
        }
    }

    return false;
}

// 手を出力する
void print_choice(const Choice *pchoice) {
    printf("%d %d\n", pchoice->i, pchoice->j);
}

int main(void) {
    char mark, field[3][4];

    while (input(&mark, field)) {
        Choice choice;

        if (think(&choice, mark, field)) {
            print_choice(&choice);
        } else {
            // 有効な手がない場合の処理(今回は起こり得ない)
            puts("-1 -1");
        }

        fflush(stdout);
    }

    return 0;
}

左上から順に走査し、〇も×も書かれていない座標があれば、それを出力するAIです。

雑魚です。

対戦プログラムの修正

では、〇×ゲームのコードをAIと通信するように修正しましょう。

まず、以下を追加します。

#include <stdlib.h>
#include <unistd.h>

// コマンドライン引数のバリデーション
void validate(int argc, const char * const *argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s AI1 AI2\n", argv[0]);
        exit(EXIT_FAILURE);
    }
}

// AIと通信をする準備をする
void prepare_ai(FILE *fp_to_ai[2], FILE *fp_from_ai[2], const char * const path_to_ai[2]) {
    for (int i = 0; i < 2; i++) {
        int pipefd_to_ai[2], pipefd_from_ai[2];

        pipe(pipefd_to_ai);
        pipe(pipefd_from_ai);

        if (fork() == 0) {
            // 標準入出力を繋ぎ変えた上で、プロセスをAIで置き換える

            close(pipefd_to_ai[1]);
            dup2(pipefd_to_ai[0], STDIN_FILENO);
            close(pipefd_to_ai[0]);

            close(pipefd_from_ai[0]);
            dup2(pipefd_from_ai[1], STDOUT_FILENO);
            close(pipefd_from_ai[1]);

            execlp(path_to_ai[i], path_to_ai[i], (char *)NULL);

            // AIの実行に失敗した場合はエラー
            fprintf(stderr, "%s: no such file\n", path_to_ai[i]);
            exit(EXIT_FAILURE);
        }

        close(pipefd_to_ai[0]);
        close(pipefd_from_ai[1]);

        // AIと高水準入出力関数でやりとりできるようにしておく
        fp_to_ai[i] = fdopen(pipefd_to_ai[1], "w");
        fp_from_ai[i] = fdopen(pipefd_from_ai[0], "r");
    }
}

// 指定されたストリームに、AIへの入力にあたるデータを送る
void send_to_ai(int player, const char field[3][4], FILE *fp) {
    fprintf(fp, "%c\n", sign[player]);
    send_field(field, fp);
    fflush(fp);
}

prepare_ai()がこの記事の肝です。

やっていることは単純で、

  1. pipe()でAIとやり取りするためのパイプを作成し、

  2. 後ほど子プロセスをAIに置き換える、という意図を持ってfork()し、

  3. 子プロセスの標準入出力をdup2()で先ほどのパイプに書き換え、

  4. 最後に子プロセスをexec()でAIに置き換えて

いるだけです。

各システムコール等の詳細はmanを読んでください。

なお、言うまでもなく、Windowsでは動作しません。

最後に、main()を次のように修正します。

65c117,120
< int main(void) {
---
> int main(int argc, char **argv) {
>    validate(argc, (const char **)argv);
> 
>    FILE *fp_to_ai[2], *fp_from_ai[2];
68a124,125
>    prepare_ai(fp_to_ai, fp_from_ai, (const char **)argv + 1);
> 
78,79c135,136
<       // 一時的に標準入力を指定
<       receive_from_ai(&choice, stdin);
---
>        send_to_ai(player, field, fp_to_ai[player]);
>        receive_from_ai(&choice, fp_from_ai[player]);

完成です。

$ ./tic_tac_toe ./ai1 ./ai2 のようにして実行してください。

公平性の問題やセキュリティの問題がありますが、とりあえずこれでおkとしましょう。

お疲れ様でした。