第I部 基礎とデータ構造

第1章 はじめに

本章では、将棋エンジンとは何か、エンジンとGUIがどのように分離されているのかを解説し、 その通信プロトコルであるUSI(Universal Shogi Interface)の概要を説明します。 続いて、本書の題材であるHayanagiエンジンの全体アーキテクチャを俯瞰し、 実際にビルドして動かすところまでを体験します。

1.1 将棋エンジンとは何か

将棋エンジンとは、将棋の局面を受け取り、最善と考えられる手を返すプログラムです。 チェスの世界では「チェスエンジン」と呼ばれ、Stockfishなどが有名ですが、 将棋でも同様のアーキテクチャが広く採用されています。

重要な設計原則として、将棋エンジンはGUI(グラフィカルユーザーインターフェース)とは完全に分離されています。 エンジンは盤面の描画や駒のドラッグ操作といった表示機能を一切持たず、 テキストベースのコマンドを標準入出力でやり取りするだけのプログラムです。

エンジンとGUIの分離
この分離により、任意のGUIソフト(将棋所、ShogiGUIなど)と任意のエンジンを自由に組み合わせることができます。 エンジン開発者はGUIを作る必要がなく、思考アルゴリズムの開発に集中できます。
  ┌──────────────┐          stdin / stdout          ┌──────────────┐
  │              │  ─── USIコマンド (テキスト) ───>  │              │
  │   GUI        │                                   │   エンジン   │
  │  (将棋所等)  │  <── 応答 (bestmove等) ─────────  │  (Hayanagi)  │
  │              │                                   │              │
  └──────────────┘                                   └──────────────┘

GUIはユーザーの操作を受けてエンジンに局面を送り、エンジンが返した最善手を盤面に反映します。 この通信に使われるのがUSIプロトコルです。

1.2 USIプロトコルの概要

USI(Universal Shogi Interface)は、将棋エンジンとGUIの間の通信規約です。 チェスにおけるUCI(Universal Chess Interface)を将棋向けに拡張したもので、 テキスト行ベースの簡潔なプロトコルです。

1.2.1 主要コマンド一覧

以下の表に、USIプロトコルの主要コマンドをまとめます。

方向 コマンド 説明
GUI→エンジンusiUSIモードの開始を要求
エンジン→GUIid name ...エンジン名の応答
エンジン→GUIid author ...作者名の応答
エンジン→GUIoption name ... type ...設定可能なオプションの通知
エンジン→GUIusiokUSI初期化完了の応答
GUI→エンジンsetoption name ... value ...オプションの設定
GUI→エンジンisready準備完了の確認
エンジン→GUIreadyok準備完了の応答
GUI→エンジンusinewgame新しい対局の開始
GUI→エンジンposition ...局面の設定(SFEN + 指し手列)
GUI→エンジンgo ...思考開始(深さ・時間等を指定)
エンジン→GUIinfo ...探索途中の情報(評価値・読み筋等)
エンジン→GUIbestmove ...最善手の応答
GUI→エンジンstop思考の中断要求
GUI→エンジンponderhit先読みが的中した通知
GUI→エンジンquitエンジンの終了
GUI→エンジンgameover ...対局結果の通知

1.2.2 典型的な通信フロー

エンジンが起動されてから1手指すまでの典型的な通信フローは以下の通りです。

GUI:    usi
Engine: id name Hayanagi
Engine: id author OpenAI
Engine: option name USI_Ponder type check default false
Engine: option name MultiPV type spin default 1 min 1 max 500
Engine: option name Threads type spin default 1 min 1 max 256
Engine: option name Hash type spin default 256 min 1 max 65536
Engine: usiok

GUI:    isready
Engine: readyok

GUI:    usinewgame
GUI:    position startpos
GUI:    go btime 60000 wtime 60000 byoyomi 10000

Engine: info depth 1 score cp 50 nodes 20 time 1 pv 7g7f
Engine: info depth 2 score cp 30 nodes 150 time 3 pv 7g7f 3c3d
  ...
Engine: bestmove 7g7f ponder 3c3d

まず usi コマンドでプロトコルを確立し、 isready / readyok で初期化を確認した後、 position で局面を設定し、go で思考を開始します。 エンジンは info で途中経過を報告し、最終的に bestmove で最善手を返します。

SFEN記法
USIでは局面をSFEN(Shogi Forsyth-Edwards Notation)という文字列で表現します。 初期局面のSFENは以下の通りです:
lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1
大文字が先手(Black)、小文字が後手(White)の駒を表します。

1.3 Hayanagiの全体アーキテクチャ

Hayanagiは約4,900行のC++17コードで構成される、コンパクトな将棋エンジンです。 ソースコードは src/ ディレクトリにあり、以下のコンパイル単位で構成されています。

                        ┌──────────────────┐
                        │    main.cpp      │  エントリポイント(12行)
                        │  UsiEngineを起動 │
                        └────────┬─────────┘
                                 │ 依存
                                 v
                        ┌──────────────────┐
                        │  usi_engine.h/cpp │  USIプロトコル処理
                        │  コマンド解析     │  探索スレッド管理
                        │  オプション管理   │  bench / perft
                        └───┬──────────┬───┘
                            │          │ 依存
                    ┌───────┘          └───────┐
                    v                          v
          ┌──────────────────┐       ┌──────────────────┐
          │   search.h/cpp   │       │    book.h/cpp    │
          │  反復深化探索     │       │  定跡データベース │
          │  alpha-beta      │       │  読み込み・検索   │
          │  評価関数        │       └──────────────────┘
          │  置換表          │
          └────────┬─────────┘
                   │ 依存
                   v
          ┌──────────────────┐
          │  position.h/cpp  │  局面管理
          │  合法手生成       │  SFEN解析
          │  王手検出        │  千日手判定
          │  SEE             │  入玉判定
          └────────┬─────────┘
                   │ 依存
          ┌────────┴─────────┐
          v                  v
  ┌──────────────┐   ┌──────────────┐
  │   types.h    │   │  bitboard.h  │
  │  駒・座標    │   │  81マス      │
  │  Move構造体  │   │  ビットボード │
  │  定数定義    │   │  集合演算    │
  └──────────────┘   └──────────────┘

各コンパイル単位の概要を以下の表にまとめます。

ファイル行数役割
types.h304Color, PieceType, Move などの基本型、定数、駒操作ユーティリティ
bitboard.h11481マスビットボード(2つの uint64_t で実装)
position.h/cpp1,848盤面管理、合法手生成、王手検出、ピン判定、千日手、入玉判定
search.h/cpp1,608反復深化、alpha-beta、PVS、静止探索、置換表、評価関数、詰み探索
usi_engine.h/cpp924USIプロトコル処理、コマンドディスパッチ、オプション管理
book.h/cpp87定跡ファイル(やねうら王DB2016形式)の読み込みと検索
main.cpp12エントリポイント

1.4 ビルド方法

HayanagiはCMakeを使ってビルドします。C++17対応のコンパイラ(GCC 7以降、Clang 5以降、MSVC 2017以降)が必要です。

1.4.1 ビルド手順

# ビルドディレクトリを作成してCMakeを実行
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

# ビルド実行
cmake --build build

ビルドが成功すると、build/hayanagi という実行ファイルが生成されます。

コンパイラ警告
Hayanagiでは -Wall -Wextra -Wpedantic(GCC/Clang) または /W4(MSVC)の警告フラグが有効になっています。 コードの品質を保つため、警告が出ない状態を維持しています。

1.4.2 動作確認:benchコマンド

bench コマンドは、組み込みのテスト局面群に対して探索を行い、 ノード数や速度を計測するベンチマーク機能です。 エンジンが正しくビルドできたことの確認に使えます。

$ echo "bench nodes 10000" | ./build/hayanagi

これにより、固定ノード数での探索が実行され、合計ノード数、経過時間、NPS(ノード/秒)が表示されます。

1.4.3 動作確認:perftコマンド

perft は、指定した深さまでの合法手の総数を数えるテストです。 合法手生成が正しいことを検証するために使います。

$ echo -e "position startpos\nperft depth 3 divide" | ./build/hayanagi

divide オプションを付けると、初手ごとの合法手数内訳が表示され、 バグの特定に役立ちます。

1.4.4 動作確認:goコマンド

USIの go コマンドで実際に探索させることもできます。

$ echo -e "position startpos\ngo depth 5" | ./build/hayanagi

go depth N は深さN手まで、go nodes N はN局面まで探索します。 探索の途中経過が info 行で表示され、最後に bestmove が出力されます。

1.5 main.cppのコード解説

Hayanagiのエントリポイントである main.cpp は、わずか12行です。 将棋エンジンとしての複雑なロジックはすべて UsiEngine クラスに委譲されており、 main 関数は驚くほどシンプルです。

src/main.cpp

#include <iostream>

#include "usi_engine.h"

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    shogi::UsiEngine engine;
    engine.loop();
    return 0;
}

各行を見ていきましょう。

1.5.1 入出力の高速化

std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

sync_with_stdio(false) は、C++の標準入出力ストリーム(cin/cout)と Cの標準入出力(stdin/stdout)の同期を解除します。 これにより、入出力のバッファリングが最適化され、速度が向上します。

cin.tie(nullptr) は、cin と cout の紐付けを解除します。 通常は cin からの入力前に cout のバッファが自動フラッシュされますが、 USIプロトコルでは std::endl で明示的にフラッシュするため、 この自動フラッシュは不要です。

1.5.2 エンジンのメインループ

shogi::UsiEngine engine;
engine.loop();

UsiEngine クラスのインスタンスを生成し、loop() を呼び出すだけです。 loop() メソッドは標準入力から1行ずつ読み取り、 各行をUSIコマンドとして解釈・処理します。

実際の loop() の実装は以下のようになっています。

src/usi_engine.cpp(抜粋)

void UsiEngine::loop() {
    std::string line;
    while (std::getline(std::cin, line)) {
        handle_line(line);
        if (line == "quit") {
            break;
        }
    }
}

標準入力が閉じられるか、quit コマンドが送られるまで、 無限ループでコマンドを処理し続けます。 handle_line() がコマンドのディスパッチを行い、 usi、isready、position、go などの 各コマンドに応じた処理を呼び出します。

12行の意味
main.cpp がこれほど短いのは、すべての責務が適切にクラスに分離されているためです。 エントリポイントの役割は「入出力の初期化」と「メインループの起動」だけであり、 これは将棋エンジンに限らず、よく設計されたC++プログラムに共通する特徴です。

1.6 まとめ

本章では以下の内容を学びました。

次章では、Hayanagiの最も基本的な型定義を見ていきます。 盤面の座標系、駒の表現、指し手の表現など、エンジン全体の土台となるデータ構造を理解しましょう。