第I部 基礎とデータ構造

第2章 型とデータ表現

将棋エンジンの設計は、盤面・駒・指し手をどのようなデータ構造で表現するかから始まります。 本章では types.h(304行)に定義されたHayanagiの基本型を詳しく見ていきます。 ここで定義される型と関数は、エンジン全体のあらゆる場所で使われる土台です。

2.1 定数定義

まず、ファイル冒頭で定義されるグローバル定数を確認します。

src/types.h(抜粋)

constexpr int kBoardSize = 9;
constexpr int kSquareCount = kBoardSize * kBoardSize;  // = 81
constexpr int kHandPieceKinds = 8;
constexpr int kMaxDepth = 64;
constexpr int kMateScore = 30000;
constexpr int kInfinity = 32000;
constexpr const char* kStartposSfen =
    "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1";
定数値用途
kBoardSize9盤面の辺の長さ
kSquareCount81盤面のマス数(9x9)
kHandPieceKinds8持ち駒の種類数(歩〜飛 + 空)
kMaxDepth64探索の最大深さ
kMateScore30000詰みを表す評価値
kInfinity32000alpha-beta窓の初期値
kStartposSfen-平手初期局面のSFEN文字列

2.2 盤面の座標系

Hayanagiでは、81マスの盤面を1次元配列 int board[81]で表現します。 配列のインデックスはrow-major順で、以下の式で計算されます。

square = row * 9 + col

ここで row は段(0〜8)、col は内部的な列(0〜8)です。 row = 0 が一段目(先手から見て最も奥、先手の成り駒ゾーン)に対応します。

src/types.h(抜粋)

inline int square_row(int square) {
    return square / kBoardSize;
}

inline int square_col(int square) {
    return square % kBoardSize;
}

inline int make_square(int row, int col) {
    return row * kBoardSize + col;
}

inline bool is_on_board(int row, int col) {
    return row >= 0 && row < kBoardSize && col >= 0 && col < kBoardSize;
}

2.2.1 座標の視覚的対応

将棋の盤面は通常、筋(右から1〜9)と段(上からa〜i、またはUSI記法では a〜i)で表されます。 内部インデックスとの対応を図で示します。

     col: 0    1    2    3    4    5    6    7    8
          9筋  8筋  7筋  6筋  5筋  4筋  3筋  2筋  1筋
row 0 (a段): [ 0]  [ 1]  [ 2]  [ 3]  [ 4]  [ 5]  [ 6]  [ 7]  [ 8]
row 1 (b段): [ 9]  [10]  [11]  [12]  [13]  [14]  [15]  [16]  [17]
row 2 (c段): [18]  [19]  [20]  [21]  [22]  [23]  [24]  [25]  [26]
row 3 (d段): [27]  [28]  [29]  [30]  [31]  [32]  [33]  [34]  [35]
row 4 (e段): [36]  [37]  [38]  [39]  [40]  [41]  [42]  [43]  [44]
row 5 (f段): [45]  [46]  [47]  [48]  [49]  [50]  [51]  [52]  [53]
row 6 (g段): [54]  [55]  [56]  [57]  [58]  [59]  [60]  [61]  [62]
row 7 (h段): [63]  [64]  [65]  [66]  [67]  [68]  [69]  [70]  [71]
row 8 (i段): [72]  [73]  [74]  [75]  [76]  [77]  [78]  [79]  [80]
筋(file)とcol の関係
USI記法の筋番号(1〜9)と内部のcol(0〜8)は逆順です。 file = 9 - col という関係になります。 例えば、7七(7筋g段)は col = 2, row = 6 なので square = 6 * 9 + 2 = 56 です。

2.2.2 USI記法との変換

USIプロトコルでは、マスを 7g(7筋g段)のように表記します。 以下の関数で内部インデックスとUSI記法を相互変換します。

src/types.h(抜粋)

inline std::string square_to_usi(int square) {
    const int file = 9 - square_col(square);
    const char rank = static_cast<char>('a' + square_row(square));
    return std::string{static_cast<char>('0' + file), rank};
}

inline std::optional<int> square_from_usi(const std::string& text) {
    if (text.size() != 2) {
        return std::nullopt;
    }
    const char file = text[0];
    const char rank = text[1];
    if (file < '1' || file > '9' || rank < 'a' || rank > 'i') {
        return std::nullopt;
    }
    const int col = '9' - file;
    const int row = rank - 'a';
    return make_square(row, col);
}

以下に代表的なマスの対応表を示します。

USI記法筋段rowcolsquare
5a5筋a段(一段)044
1a1筋a段088
7g7筋g段(七段)6256
5e5筋e段(五段)4440
9i9筋i段(九段)8072

2.3 Color(手番)

手番は Color 列挙型で表現します。先手が Black = 0、後手が White = 1 です。

src/types.h(抜粋)

enum class Color : int {
    Black = 0,
    White = 1,
};

inline Color opposite(Color color) {
    return color == Color::Black ? Color::White : Color::Black;
}

opposite() 関数で相手の手番を取得できます。 Color を配列のインデックスとして使う場面も多く(例えば hand[static_cast<int>(color)])、 0と1の値が便利に活用されます。

2.4 PieceType(駒種)

駒の種類は PieceType 列挙型で定義されています。 空マス(Empty)を含めて15種類です。

src/types.h(抜粋)

enum class PieceType : int {
    Empty    = 0,
    Pawn     = 1,   // 歩
    Lance    = 2,   // 香
    Knight   = 3,   // 桂
    Silver   = 4,   // 銀
    Gold     = 5,   // 金
    Bishop   = 6,   // 角
    Rook     = 7,   // 飛
    King     = 8,   // 玉
    ProPawn  = 9,   // と金
    ProLance = 10,  // 成香
    ProKnight= 11,  // 成桂
    ProSilver= 12,  // 成銀
    Horse    = 13,  // 馬
    Dragon   = 14,  // 龍
};
値定数名日本語名成駒元
0Empty空-
1Pawn歩-
2Lance香-
3Knight桂-
4Silver銀-
5Gold金-
6Bishop角-
7Rook飛-
8King玉-
9ProPawnと金歩 (1)
10ProLance成香香 (2)
11ProKnight成桂桂 (3)
12ProSilver成銀銀 (4)
13Horse馬角 (6)
14Dragon龍飛 (7)
成駒の番号付け
成駒は元駒の値に8を加えた値になっています(歩=1 → と金=9、香=2 → 成香=10 ...)。 ただし、角(6)→馬(13)と飛(7)→龍(14)は +7 です。金(5)と玉(8)は成れないため、 対応する9〜12の番号は歩〜銀の成駒に使われています。

2.5 駒の符号エンコーディング

Hayanagiでは、盤面上の駒を符号付き整数で表現します。 正の値が先手(Black)の駒、負の値が後手(White)の駒、0が空マスです。

src/types.h(抜粋)

inline int encode_piece(Color color, PieceType type) {
    if (type == PieceType::Empty) {
        return 0;
    }
    return color == Color::Black ? static_cast<int>(type)
                                 : -static_cast<int>(type);
}

inline bool is_empty(int piece) {
    return piece == 0;
}

inline Color piece_color(int piece) {
    return piece > 0 ? Color::Black : Color::White;
}

inline PieceType piece_type(int piece) {
    return static_cast<PieceType>(std::abs(piece));
}

このエンコーディングの利点は、1つの int 値だけで「駒種」と「所有者」の両方を表現できることです。 駒種を知りたければ abs() を、所有者を知りたければ符号を調べるだけです。

具体例を以下に示します。

値意味piece_color()piece_type()
+1先手の歩BlackPawn
-1後手の歩WhitePawn
+7先手の飛車BlackRook
-13後手の馬WhiteHorse
0空マス-Empty

2.6 Move構造体

指し手は Move 構造体で表現されます。 盤上の駒を動かす手と、持ち駒を打つ手の両方を同じ構造体で扱います。

src/types.h(抜粋)

struct Move {
    int from = -1;
    int to = -1;
    PieceType piece = PieceType::Empty;
    bool promote = false;
    bool drop = false;

    bool is_valid() const {
        return to >= 0 && to < kSquareCount && piece != PieceType::Empty;
    }
};
フィールド型説明
fromint移動元のマス番号(打ち駒の場合は -1)
toint移動先のマス番号
piecePieceType動かす(打つ)駒の種類
promotebool成る手なら true
dropbool駒を打つ手なら true

例えば「7七の歩を7六へ移動する手」は以下のように表されます。

Move m;
m.from = 56;                 // 7七 = row 6, col 2
m.to   = 47;                 // 7六 = row 5, col 2
m.piece = PieceType::Pawn;
m.promote = false;
m.drop = false;

「持ち駒の歩を3四に打つ手」は以下のようになります。

Move m;
m.from = -1;                 // 打ち駒なのでfromは無い
m.to   = 33;                 // 3四 = row 3, col 6
m.piece = PieceType::Pawn;
m.promote = false;
m.drop = true;
is_valid() の意味
is_valid() は、Moveが有効な手であることの最低限の検証です。 to が盤面内(0〜80)で、かつ piece が空でなければ有効とみなします。 合法手であるかどうかの完全な検証ではなく、 初期化されていないMoveオブジェクトを検出するための簡易チェックです。

2.7 成り判定と成り処理

駒が成れるかどうか、成った結果の駒種、不成に戻した駒種を扱う関数群です。

2.7.1 成り可否の判定

src/types.h(抜粋)

inline bool can_promote(PieceType type) {
    switch (type) {
        case PieceType::Pawn:
        case PieceType::Lance:
        case PieceType::Knight:
        case PieceType::Silver:
        case PieceType::Bishop:
        case PieceType::Rook:
            return true;
        default:
            return false;
    }
}

金と玉は成れません。すでに成駒(と金、成香など)の場合も false を返します。

2.7.2 成り・不成の変換

src/types.h(抜粋)

inline PieceType promote(PieceType type) {
    switch (type) {
        case PieceType::Pawn:   return PieceType::ProPawn;
        case PieceType::Lance:  return PieceType::ProLance;
        case PieceType::Knight: return PieceType::ProKnight;
        case PieceType::Silver: return PieceType::ProSilver;
        case PieceType::Bishop: return PieceType::Horse;
        case PieceType::Rook:   return PieceType::Dragon;
        default:                return type;
    }
}

inline PieceType unpromote(PieceType type) {
    switch (type) {
        case PieceType::ProPawn:   return PieceType::Pawn;
        case PieceType::ProLance:  return PieceType::Lance;
        case PieceType::ProKnight: return PieceType::Knight;
        case PieceType::ProSilver: return PieceType::Silver;
        case PieceType::Horse:     return PieceType::Bishop;
        case PieceType::Dragon:    return PieceType::Rook;
        default:                   return type;
    }
}

promote() と unpromote() は互いに逆関数の関係です。 成れない駒種を渡した場合はそのまま返します。 unpromote() は、持ち駒として管理する際に成駒を元の駒種に戻すためにも使われます。

2.7.3 成り強制の判定

歩・香が最奥段に入った場合や、桂が奥から2段以内に入った場合は成りが強制されます。

src/types.h(抜粋)

inline bool must_promote(PieceType type, Color color, int to_row) {
    switch (type) {
        case PieceType::Pawn:
        case PieceType::Lance:
            return is_last_rank(color, to_row);
        case PieceType::Knight:
            return is_last_two_ranks(color, to_row);
        default:
            return false;
    }
}

ここで使われる補助関数は以下の通りです。

inline bool is_in_promotion_zone(Color color, int row) {
    return color == Color::Black ? row <= 2 : row >= 6;
}

inline bool is_last_rank(Color color, int row) {
    return color == Color::Black ? row == 0 : row == 8;
}

inline bool is_last_two_ranks(Color color, int row) {
    return color == Color::Black ? row <= 1 : row >= 7;
}

先手の成りゾーンは row <= 2(a段〜c段)、後手は row >= 6(g段〜i段)です。

2.8 駒の価値テーブル

評価関数や手の順序付けで使われる、駒の価値を返す関数です。

src/types.h(抜粋)

inline int piece_value(PieceType type) {
    static constexpr std::array<int, 15> kValues = {
        0, 100, 300, 320, 450, 550, 800, 1000, 20000,
        550, 550, 550, 550, 950, 1150,
    };
    return kValues[static_cast<int>(type)];
}
駒種値備考
Empty (0)0空マス
Pawn (1)100歩 — 基準値
Lance (2)300香
Knight (3)320桂
Silver (4)450銀
Gold (5)550金
Bishop (6)800角
Rook (7)1000飛
King (8)20000玉(事実上の無限大)
ProPawn (9)550と金 — 金と同じ価値
ProLance (10)550成香
ProKnight (11)550成桂
ProSilver (12)550成銀
Horse (13)950馬(角+150)
Dragon (14)1150龍(飛+150)
駒の価値について
歩を100として他の駒をその倍数で表すのは、将棋エンジンの伝統的な手法です。 と金・成香・成桂・成銀はいずれも金と同じ動きなので同じ550が割り当てられています。 馬(角+斜め全方向に加え上下左右1マス)と龍(飛+斜め1マス)は 元駒より150ポイント高い値になっています。 玉の値20000は「玉を取られたら負け」を表す事実上の無限大です。

2.9 入玉ポイント

将棋には「入玉宣言勝ち」というルールがあり、互いの玉が相手陣に入った場合、 盤上・持ち駒の点数に基づいて勝敗を判定します。 この点数計算に使われるのが impasse_point_value() です。

src/types.h(抜粋)

inline int impasse_point_value(PieceType type) {
    switch (unpromote(type)) {
        case PieceType::Bishop:
        case PieceType::Rook:
            return 5;
        case PieceType::Pawn:
        case PieceType::Lance:
        case PieceType::Knight:
        case PieceType::Silver:
        case PieceType::Gold:
            return 1;
        default:
            return 0;
    }
}

大駒(角・飛、およびその成駒である馬・龍)は5点、 小駒(歩・香・桂・銀・金、およびそれらの成駒)は1点、 玉は0点です。unpromote(type) を使うことで、成駒も元駒と同じ点数になります。

駒分類駒種点数
大駒角・飛・馬・龍5点
小駒歩・香・桂・銀・金・と金・成香・成桂・成銀1点
玉玉0点

2.10 駒の文字表現

SFEN文字列の読み書きや、USI形式での指し手の表示に使われる 駒種と文字の変換関数です。

src/types.h(抜粋)

inline char piece_letter(PieceType type) {
    switch (unpromote(type)) {
        case PieceType::Pawn:   return 'P';
        case PieceType::Lance:  return 'L';
        case PieceType::Knight: return 'N';
        case PieceType::Silver: return 'S';
        case PieceType::Gold:   return 'G';
        case PieceType::Bishop: return 'B';
        case PieceType::Rook:   return 'R';
        case PieceType::King:   return 'K';
        default:                return '?';
    }
}

inline std::optional<PieceType> piece_from_letter(char letter) {
    switch (std::toupper(static_cast<unsigned char>(letter))) {
        case 'P': return PieceType::Pawn;
        case 'L': return PieceType::Lance;
        case 'N': return PieceType::Knight;
        case 'S': return PieceType::Silver;
        case 'G': return PieceType::Gold;
        case 'B': return PieceType::Bishop;
        case 'R': return PieceType::Rook;
        case 'K': return PieceType::King;
        default:  return std::nullopt;
    }
}

piece_letter() は unpromote() を使うため、 成駒を渡しても元駒の文字を返します(例: Horse → 'B')。 piece_from_letter() は std::optional を返し、 不正な文字の場合は std::nullopt を返します。

持ち駒の管理にも unpromote() が活用されます。 駒を取ったとき、成駒は不成に戻して持ち駒にするためです。

inline int hand_index(PieceType type) {
    return static_cast<int>(unpromote(type));
}

2.11 終局状態の表現

将棋には詰み以外にもさまざまな終局条件があります。 Hayanagiではこれらを TerminalOutcome、TerminalReason、 TerminalStatus の3つの型で表現します。

src/types.h(抜粋)

enum class TerminalOutcome : int {
    None = 0,    // 終局していない
    Win,         // 手番側の勝ち
    Draw,        // 引き分け
    Loss,        // 手番側の負け
};

enum class TerminalReason : int {
    None = 0,          // 終局していない
    DeclarationWin,    // 入玉宣言勝ち
    Repetition,        // 千日手(引き分け)
    PerpetualCheck,    // 連続王手の千日手(王手側の負け)
    Impasse,           // 持将棋
    MoveLimit,         // 手数上限
    TryRule,           // トライルール
};

struct TerminalStatus {
    TerminalOutcome outcome = TerminalOutcome::None;
    TerminalReason reason = TerminalReason::None;

    bool is_terminal() const {
        return outcome != TerminalOutcome::None;
    }
};

TerminalOutcome は「結果」(勝ち・負け・引き分け・未終局)を、 TerminalReason は「理由」(千日手・入玉宣言など)を表します。 TerminalStatus はこの2つを組み合わせた構造体で、 is_terminal() で終局しているかどうかを判定できます。

理由結果説明
DeclarationWinWin入玉宣言勝ち — 条件を満たした側の勝ち
RepetitionDraw同一局面が4回出現(千日手)
PerpetualCheckLoss連続王手の千日手 — 王手をかけ続けた側の負け
ImpasseDraw/Win持将棋 — 点数に基づく判定
MoveLimitDraw手数上限に到達
TryRuleWinトライルール — 玉が相手の玉の初期位置に到達

2.12 まとめ

本章では types.h に定義されたHayanagiの基本型を学びました。

次章では、盤面上の駒の集合を効率的に表現・操作するための「ビットボード」について学びます。