初めまして。新人?アルバイトのzeosuttです。
秋です。
皆でゲームAIを持ち寄って対戦したい季節ですね。
何のゲームにしましょうか。
そうですね、簡単に実装できるので、〇×ゲームにしましょう。
実は、両者が最善を尽くすと必ず引き分けになる
さて、どんなに強いAIを作っても、それを対戦させる仕組みがなくては意味がありませんね。
この記事では、AI同士を対戦させるための仕組み を作ってみます。
AIの作成自体は主題ではありません。
人対人
まずは、人間同士で対戦ができるようにしましょう。
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
const char *sign = "ox" ;
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] == '.' ) {
*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 );
}
}
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 ) {
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 );
fprintf(stderr , " %s : no such file \n " , path_to_ai[i]);
exit(EXIT_FAILURE );
}
close(pipefd_to_ai[0 ]);
close(pipefd_from_ai[1 ]);
fp_to_ai[i] = fdopen(pipefd_to_ai[1 ], "w" );
fp_from_ai[i] = fdopen(pipefd_from_ai[0 ], "r" );
}
}
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としましょう。
お疲れ様でした。