6.7 定石の登録 |
前節でコンピュータが定石を使用できるようになりました。
本節では定石ファイルを作成して、対局時にコンピュータがそのファイルを読み込むようにします。
"main.c"を修正します。
最初に必要な定数を定義しておきます。
#define OPENING_TRANSCRIPT_FILE "open_trans.txt" #define OPENING_FILE "open.dat" #define TRANSCRIPT_SIZE 128
OPENING_TRANSCRIPT_FILEは定石ファイルを作成するために使用します。
詳しいことは後で説明します。
OPENING_FILEは定石ファイルです。
このファイルを対局時に使用します。
TRANSCRIPT_SIZEはOPENING_TRANSCRIPT_FILEファイルを読み込むときに使用するバッファサイズです。
後で説明しますが、OPENING_TRANSCRIPT_FILEはテキストファイルです。
OPENING_TRANSCRIPT_FILEの1行1行が最大何バイトであるかをここで定義しています。
"main.c"で使用するパラメータを追加します。
最初にMainParam構造体にOpeningクラスを追加します。
struct _MainParam { Board *Board; Evaluator *Evaluator; Opening *Opening; Com *Com; }; typedef struct _MainParam MainParam;
次にMainParam初期化関数を修正します。
Openingクラスを生成して定石ファイルを読み込むようにします。
またComクラス生成時にOpeningクラスを渡すようにします。
static int main_param_initialize_each(MainParam *self) { self->Board = Board_New(); if (!self->Board) { return 0; } self->Evaluator = Evaluator_New(); if (!self->Evaluator) { return 0; } Evaluator_Load(self->Evaluator, EVALUATOR_FILE); self->Opening = Opening_New(); if (!self->Opening) { return 0; } Opening_Load(self->Opening, OPENING_FILE); self->Com = Com_New(self->Evaluator, self->Opening); if (!self->Com) { return 0; } return 1; }
MainParam破棄時にOpeningクラスも破棄するようにします。
static void main_param_finalize(MainParam *self) { if (self->Com) { Com_Delete(self->Com); } if (self->Opening) { Opening_Delete(self->Opening); } if (self->Evaluator) { Evaluator_Delete(self->Evaluator); } if (self->Board) { Board_Delete(self->Board); } }
定石を使用したい場面では定石を使用するように設定し、定石を使用したくない場面では定石を使用しないように設定します。
対局時には定石を使用します。
static void play(Board *board, Com *com) { (中略) while (1) { printf("コンピュータのレベルを選択してください (1-4)\n"); get_stream(buffer, BUFFER_SIZE, stdin); if (!strcmp(buffer, "1")) { Com_SetLevel(com, 2, 8, 10); break; } else if (!strcmp(buffer, "2")) { Com_SetLevel(com, 4, 10, 12); break; } else if (!strcmp(buffer, "3")) { Com_SetLevel(com, 6, 12, 14); break; } else if (!strcmp(buffer, "4")) { Com_SetLevel(com, 8, 14, 16); break; } } Com_SetOpening(com, 1); (中略) }
次に学習時には定石を使用しないようにします。
static void learn(Board *board, Evaluator *evaluator, Com *com) { (中略) Com_SetLevel(com, 4, 12, 12); Com_SetOpening(com, 0); (中略) }
定石登録関数opening_initialize()を記述します。
この関数では棋譜ファイルを読み込んで定石ファイルを出力します。
棋譜とは対局中に打たれた手の記録のことです。
棋譜ファイルは以下のようになっています。
F5D6C3D3C4F4C5B3C2E6C6B4 0 F5D6C3D3C4F4C5B3C2B4E3E6 0 (中略) F5F4E3F6D3C4F3 6.0 F5F4E3D6F3G4E6 6.0
1行目に棋譜、2行目にその評価値、以下同様に棋譜と評価値を記述しています。
評価値は黒から見た値で1石勝ちの見込みなら1.0とします。
サンプルコードには"open_trans.txt"が棋譜ファイルとして付属しています。
それではopening_initialize()の内部を見ていきましょう。
static void opening_initialize(Board *board, Opening *opening) { FILE *fp; char buffer[TRANSCRIPT_SIZE], value_buffer[BUFFER_SIZE]; PositionInfo info; int color, turn, value, min; int history_move[BOARD_SIZE * BOARD_SIZE * 2]; int i; fp = fopen(OPENING_TRANSCRIPT_FILE, "r"); if (!fp) { return; } while (1) {
最初に棋譜ファイルを開きます。
以降の処理はファイル読み込みが完了するまで続きます。
Board_Clear(board); color = BLACK; turn = 0; if (!get_stream(buffer, TRANSCRIPT_SIZE, fp)) { break; } if (!get_stream(value_buffer, BUFFER_SIZE, fp)) { break; } value = (int)(atof(value_buffer) * DISK_VALUE);
盤面を初期化した後、棋譜データと評価値を読み込みます。
for (i = 0; buffer[i] != '\0' && buffer[i + 1] != '\0'; i += 2) { if (!Board_CanPlay(board, color)) { history_move[turn] = PASS; } else { history_move[turn] = Board_Pos(tolower(buffer[i]) - 'a', buffer[i + 1] - '1'); if (!Board_Flip(board, color, history_move[turn])) { break; } } turn++; color = Board_OpponentColor(color); } history_move[turn] = NOMOVE;
棋譜にしたがって着手を行ないます。
また何手目にどこに着手したかも記録しておきます。
for (; turn >= 0; turn--) { if (color == BLACK) { min = -value; } else { min = value; } if (history_move[turn] == PASS) { if (Opening_Info(opening, board, Board_OpponentColor(color), &info)) { min = PositionInfo_Value(&info); } } else { for (i = A1; i <= H8; i++) { if (Board_Flip(board, color, i)) { if (Opening_Info(opening, board, Board_OpponentColor(color), &info)) { if (PositionInfo_Value(&info) < min) { min = PositionInfo_Value(&info); } } Board_Unflip(board); } } } PositionInfo_SetValue(&info, -min); Opening_SetInfo(opening, board, color, &info); Board_Unflip(board); color = Board_OpponentColor(color); }
1手ずつ戻しながら、各局面の局面データ登録を行ないます。
このとき注意しなければならないのは、局面の評価値の計算です。
ある局面(局面Aとします)の評価値は、定石データ内部でMinMax法を行なって決めます。
つまり、相手が定石データ内の最善手を打った場合の評価値をその局面の評価値とします。
具体的には以下のようになります。
(1)局面Aから1手進めた局面を列挙する。
(2)(1)の局面のうち、定石データに登録されている局面を列挙する。
(3)(2)の局面のうち、最善の評価となる局面を選ぶ。
(4)局面Aの評価値は(3)の手の評価値とする。
ただし、1手進めた局面が1つも登録されていない場合には、棋譜ファイルに記述されている評価値をその局面の評価値とします。
} fclose(fp); Opening_Save(opening, OPENING_FILE); printf("登録完了しました\n"); }
棋譜ファイルの読み込みが終わったら終了です。
定石登録モードを追加し、opening_initialize()を呼べるようにします。
int main(int argc, char **argv) { MainParam param; char buffer[BUFFER_SIZE]; srand((unsigned)time(NULL)); if (!main_param_initialize(¶m)) { printf("初期化に失敗しました\n"); return 0; } while (1) { printf("モードを選択してください (1:対戦 2:学習 3:定石登録 q:終了)\n"); get_stream(buffer, BUFFER_SIZE, stdin); if (!strcmp(buffer, "1")) { play(param.Board, param.Com); } else if (!strcmp(buffer, "2")) { learn(param.Board, param.Evaluator, param.Com); } else if (!strcmp(buffer, "3")) { opening_initialize(param.Board, param.Opening); } else if (!strcmp(buffer, "q")) { break; } } main_param_finalize(¶m); return 0; }
それでは実際に対局してみましょう。
コンピュータが序盤で打つ手が一通りでなくなっています。
"open_trans.txt"に棋譜を追加すれば、もっと多くの定石を打つことができるようになります。