初めまして。新人?アルバイトのzeosuttです。
秋です。
皆でゲームAIを持ち寄って対戦したい季節ですね。
何のゲームにしましょうか。
そうですね、簡単に実装できるので、〇×ゲームにしましょう。
さて、どんなに強い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()
がこの記事の肝です。
やっていることは単純で、
pipe()
でAIとやり取りするためのパイプを作成し、後ほど子プロセスをAIに置き換える、という意図を持って
fork()
し、子プロセスの標準入出力を
dup2()
で先ほどのパイプに書き換え、最後に子プロセスを
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としましょう。
お疲れ様でした。