初めまして。新人?アルバイトの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としましょう。
お疲れ様でした。