第III部 探索と評価

第11章 評価関数

探索アルゴリズムがどれほど優れていても、局面の良し悪しを正確に判断できなければ強い将棋エンジンにはなりません。 評価関数は探索の末端ノードで呼び出され、現在の局面が手番側にとってどれだけ有利かを数値(centipawn単位)で返します。 本章では、Hayanagiの手作り評価関数 evaluate() の全体構造と各構成要素を詳しく解説します。

11.1 evaluate() 関数の全体構造

evaluate() は先手(Black)と後手(White)のスコアを個別に計算し、最終的にその差を手番側の視点で返します。

int Search::evaluate(const Position& position) const {
    int black_score = 0;
    int white_score = 0;
    int phase = 0;

    for (int square = 0; square < kSquareCount; ++square) {
        const int piece = position.piece_at(square);
        if (piece == 0) {
            continue;
        }

        const Color color = piece_color(piece);
        const PieceType type = piece_type(piece);
        const int row = square_row(square);
        const int col = square_col(square);

        int contribution = piece_value(type);
        if (type != PieceType::King) {
            contribution += piece_square_bonus(type, color, row, col);
            contribution += mobility_bonus(position, square, type, color);
            if (is_in_promotion_zone(color, row)) {
                contribution += camp_bonus(type);
            }
            phase += piece_value(unpromote(type)) / 100;
        }

        if (color == Color::Black) {
            black_score += contribution;
        } else {
            white_score += contribution;
        }
    }

    for (int index = 1; index <= static_cast<int>(PieceType::Rook); ++index) {
        const PieceType piece = static_cast<PieceType>(index);
        const int material = piece_value(piece);
        const int bonus = hand_bonus(piece);
        const int black_count = position.hand_count(Color::Black, piece);
        const int white_count = position.hand_count(Color::White, piece);

        black_score += black_count * (material + bonus);
        white_score += white_count * (material + bonus);
        phase += (black_count + white_count) * (material / 100);
    }

    black_score += king_safety_score(position, Color::Black, phase);
    white_score += king_safety_score(position, Color::White, phase);

    if (position.is_in_check(Color::Black)) {
        black_score -= 40;
    }
    if (position.is_in_check(Color::White)) {
        white_score -= 40;
    }

    int score = black_score - white_score;
    score += position.side_to_move() == Color::Black ? kTempoBonus : -kTempoBonus;
    return position.side_to_move() == Color::Black ? score : -score;
}

全体の流れは以下の通りです。

  1. 盤上の全駒について、駒得・位置評価・利き評価・敵陣ボーナスを加算
  2. 持ち駒について、駒得と持ち駒ボーナスを加算
  3. 両玉の安全度を加算
  4. 王手がかかっている側にペナルティを課す
  5. 先手スコアと後手スコアの差にテンポボーナスを加え、手番側の視点に変換して返す

11.2 駒得(Material)

評価の基盤となるのは駒の素点(piece_value(type))です。 各駒種に対して定義された静的な値で、例えば歩=100、銀=500、飛車=1000といった相対的な価値を持ちます。 盤上の駒も持ち駒も、まずこの素点が加算されます。

持ち駒にはさらに hand_bonus() が上乗せされます。 持ち駒は盤上の駒よりも柔軟に使える(好きな場所に打てる)ため、同じ駒種でも追加の価値があります。

int hand_bonus(PieceType type) {
    switch (type) {
        case PieceType::Pawn:   return 18;
        case PieceType::Lance:
        case PieceType::Knight: return 24;
        case PieceType::Silver: return 28;
        case PieceType::Gold:   return 24;
        case PieceType::Bishop: return 36;
        case PieceType::Rook:   return 42;
        default:                return 0;
    }
}

11.3 テンポボーナス

Hayanagiでは手番を持つこと自体に kTempoBonus = 12 の価値を与えています。

score += position.side_to_move() == Color::Black ? kTempoBonus : -kTempoBonus;

将棋では手番側が先に指せるため、わずかながら有利です。 テンポボーナスはこの「先手の利」を評価値に反映するもので、 ヌルムーブ枝刈りの判定にも間接的に影響します。 12cpという値は控えめな設定で、評価を大きく歪めることなく手番の優位性を表現しています。

11.4 駒の位置評価 piece_square_bonus()

同じ駒でも、盤上のどこにいるかによって価値が異なります。 piece_square_bonus() は駒種・手番・位置から追加の評価値を計算します。

int piece_square_bonus(PieceType type, Color color, int row, int col) {
    const int progress = forward_progress(color, row);
    const int center = center_bonus(row, col);

    switch (type) {
        case PieceType::Pawn:
            return progress * 10 - std::abs(col - 4) * 2;
        case PieceType::Lance:
            return progress * 6 - std::abs(col - 4) * 2;
        case PieceType::Knight:
            return progress * 7 + center * 4;
        case PieceType::Silver:
            return progress * 5 + center * 5;
        case PieceType::Gold:
            return progress * 4 + center * 3;
        case PieceType::Bishop:
            return center * 8 + progress * 2;
        case PieceType::Rook:
            return center * 5 + progress * 2;
        case PieceType::ProPawn:
        case PieceType::ProLance:
        case PieceType::ProKnight:
        case PieceType::ProSilver:
            return progress * 4 + center * 5;
        case PieceType::Horse:
            return center * 12 + progress * 3;
        case PieceType::Dragon:
            return center * 10 + progress * 3;
        case PieceType::King:
        case PieceType::Empty:
        default:
            return 0;
    }
}

ここで使われる2つの基本量を説明します。

前進度(forward_progress)

int forward_progress(Color color, int row) {
    return color == Color::Black ? 8 - row : row;
}

先手は盤面上部(row=0)に向かって進むため、8 - row が前進度になります。 後手はその逆です。前進度が大きいほど敵陣に近く、歩や香、桂などの駒にとって有利な位置です。

中央ボーナス(center_bonus)

int center_distance(int row, int col) {
    return std::abs(row - 4) + std::abs(col - 4);
}
int center_bonus(int row, int col) {
    return 8 - center_distance(row, col);
}

盤面中央(5五の位置、row=4, col=4)からのマンハッタン距離を基に計算されます。 中央に近いほどボーナスが大きくなります。角や馬など、利きが中央で最大化される駒に重要です。

各駒種の位置評価の考え方

さらに、敵陣(成れるゾーン)にいる駒には camp_bonus() が追加されます。 成駒や大駒が敵陣にいることの戦略的価値を反映しています。

int camp_bonus(PieceType type) {
    switch (type) {
        case PieceType::Pawn:             return 10;
        case PieceType::Lance:
        case PieceType::Knight:           return 8;
        case PieceType::Silver:
        case PieceType::Gold:             return 10;
        case PieceType::Bishop:
        case PieceType::Rook:             return 12;
        case PieceType::ProPawn:
        case PieceType::ProLance:
        case PieceType::ProKnight:
        case PieceType::ProSilver:        return 14;
        case PieceType::Horse:
        case PieceType::Dragon:           return 18;
        default:                          return 0;
    }
}

11.5 利きの広さ mobility_bonus()

駒の利き(移動可能なマスの数)は、その駒の活動度を表す重要な指標です。 mobility_bonus() は各駒の利き数を計算し、駒種ごとの重みを乗じて返します。

int mobility_bonus(const Position& position, int square, PieceType type, Color color) {
    switch (type) {
        case PieceType::Pawn:
            return count_step_mobility(position, square, kPawnDirections, color) * 2;
        case PieceType::Lance:
            return count_slider_mobility(position, square, kLanceDirections, color) * 2;
        case PieceType::Knight:
            return count_step_mobility(position, square, kKnightDirections, color) * 3;
        case PieceType::Silver:
            return count_step_mobility(position, square, kSilverDirections, color) * 3;
        case PieceType::Gold:
            return count_step_mobility(position, square, kGoldDirections, color) * 2;
        case PieceType::Bishop:
            return count_slider_mobility(position, square, kBishopDirections, color) * 5;
        case PieceType::Rook:
            return count_slider_mobility(position, square, kRookDirections, color) * 4;
        case PieceType::King:
            return 0;
        case PieceType::ProPawn:
        case PieceType::ProLance:
        case PieceType::ProKnight:
        case PieceType::ProSilver:
            return count_step_mobility(position, square, kGoldDirections, color) * 3;
        case PieceType::Horse:
            return count_slider_mobility(position, square, kBishopDirections, color) * 5 +
                   count_step_mobility(position, square, kRookDirections, color) * 2;
        case PieceType::Dragon:
            return count_slider_mobility(position, square, kRookDirections, color) * 4 +
                   count_step_mobility(position, square, kBishopDirections, color) * 3;
        case PieceType::Empty:
        default:
            return 0;
    }
}

ステップ型駒の利き数計算

歩・桂・銀・金・成駒など、1マスずつ移動する駒の利き数は count_step_mobility() で計算します。

template <std::size_t N>
int count_step_mobility(const Position& position,
                        int square,
                        const int (&directions)[N][2],
                        Color color) {
    const int sign = orientation(color);
    const int row = square_row(square);
    const int col = square_col(square);
    int mobility = 0;

    for (const auto& direction : directions) {
        const int to_row = row + direction[0] * sign;
        const int to_col = col + direction[1] * sign;
        if (!is_on_board(to_row, to_col)) {
            continue;
        }
        const int target = position.piece_at(make_square(to_row, to_col));
        if (target == 0 || piece_color(target) != color) {
            ++mobility;
        }
    }
    return mobility;
}

各方向について、盤内かつ味方駒で塞がれていなければ利きとしてカウントします。 orientation(color) により先手・後手で方向が反転されます。

スライダー型駒の利き数計算

飛車・角・香など、直線上を複数マス移動できる駒は count_slider_mobility() で計算します。

template <std::size_t N>
int count_slider_mobility(const Position& position,
                          int square,
                          const int (&directions)[N][2],
                          Color color) {
    const int sign = orientation(color);
    const int row = square_row(square);
    const int col = square_col(square);
    int mobility = 0;

    for (const auto& direction : directions) {
        int to_row = row + direction[0] * sign;
        int to_col = col + direction[1] * sign;
        while (is_on_board(to_row, to_col)) {
            const int target = position.piece_at(make_square(to_row, to_col));
            if (target != 0 && piece_color(target) == color) {
                break;
            }
            ++mobility;
            if (target != 0) {
                break;
            }
            to_row += direction[0] * sign;
            to_col += direction[1] * sign;
        }
    }

    return mobility;
}

各方向に対して、味方駒に当たるまで(または敵駒を取れるマスまで)利きを延ばしてカウントします。

駒種ごとの重み

利き数に乗じる重みは駒種によって異なります。

11.6 玉の安全度 king_safety_score()

玉の安全度は将棋の評価において極めて重要な要素です。 king_safety_score() は局面のフェーズ(序盤〜終盤)に応じて玉の評価を動的に変化させます。

int king_safety_score(const Position& position, Color color, int phase) {
    const int king_square = position.find_king(color);
    if (king_square == -1) {
        return 0;
    }

    const int row = square_row(king_square);
    const int col = square_col(king_square);
    const int opening_weight = std::min(phase, 96);
    const int endgame_weight = 96 - opening_weight;
    int defenders = 0;
    int enemy_attacks = 0;
    int safe_squares = 0;

    for (const auto& direction : kKingDirections) {
        const int to_row = row + direction[0];
        const int to_col = col + direction[1];
        if (!is_on_board(to_row, to_col)) {
            continue;
        }
        const int square = make_square(to_row, to_col);
        const int piece = position.piece_at(square);
        if (piece != 0 && piece_color(piece) == color) {
            ++defenders;
        }
        if (position.is_square_attacked(square, opposite(color))) {
            ++enemy_attacks;
        } else {
            ++safe_squares;
        }
    }

    int score = defenders * (6 + opening_weight / 12);
    score -= enemy_attacks * (10 + opening_weight / 10);
    score += safe_squares * 2;

    const int distance = center_distance(row, col);
    score += distance * (opening_weight / 6);
    score += center_bonus(row, col) * (endgame_weight / 6);

    const int front_row_delta = -orientation(color);
    for (int step = 1; step <= 2; ++step) {
        const int front_row = row + front_row_delta * step;
        if (!is_on_board(front_row, col)) {
            continue;
        }
        const int square = make_square(front_row, col);
        const int piece = position.piece_at(square);
        if (piece == 0) {
            score -= 4 + opening_weight / 16;
        } else if (piece_color(piece) != color) {
            score -= 8 + opening_weight / 14;
        }
        if (position.is_square_attacked(square, opposite(color))) {
            score -= 8;
        }
    }

    return score;
}

フェーズ計算

phase は盤上と持ち駒の駒価値合計を100で割った値の累積で、局面の「重さ」を表します。 駒が多いほどphaseは大きくなり序盤的、駒交換が進んでphaseが小さくなると終盤的になります。 opening_weight は最大96にクランプされ、endgame_weight = 96 - opening_weight と合わせて 序盤要素と終盤要素のブレンド比率を決定します。

序盤: 中央からの距離

score += distance * (opening_weight / 6);

序盤では玉が盤面中央から離れていることが安全とされます。 将棋の序盤では玉を隅に囲うのが基本戦略であり、中央に玉がいると攻撃を受けやすくなります。 opening_weight が大きい(駒が多い)ほどこのボーナスの影響が大きくなります。

終盤: 中央への接近

score += center_bonus(row, col) * (endgame_weight / 6);

終盤では逆に玉が中央に近い方が有利になります。 駒が少なくなると囲いの意味が薄れ、玉の機動力(逃げ道の多さ)が重要になるためです。

守り駒・敵の攻撃・安全マス

int score = defenders * (6 + opening_weight / 12);
score -= enemy_attacks * (10 + opening_weight / 10);
score += safe_squares * 2;

玉の周囲8マスについて3つの指標を計算します。

前方の歩の構造

const int front_row_delta = -orientation(color);
for (int step = 1; step <= 2; ++step) {
    const int front_row = row + front_row_delta * step;
    // ...
    if (piece == 0) {
        score -= 4 + opening_weight / 16;
    } else if (piece_color(piece) != color) {
        score -= 8 + opening_weight / 14;
    }
    if (position.is_square_attacked(square, opposite(color))) {
        score -= 8;
    }
}

玉の前方1〜2マスの状態を調べます。 前方に味方の駒(特に歩)がないと玉頭が弱くなり、ペナルティが課されます。 敵駒がいる場合はさらに大きなペナルティとなり、そのマスが敵に利かれていれば追加の減点があります。

11.7 全体のスコア計算

最終的なスコアは以下の手順で合算されます。

  1. 盤上の各駒について: 駒得 + 位置評価 + 利き評価 + 敵陣ボーナス
  2. 持ち駒について: 駒得 + 持ち駒ボーナス
  3. 各玉の安全度スコア
  4. 王手ペナルティ(王手がかかっている側に -40)
  5. score = black_score - white_score + tempo_bonus
  6. 手番側の視点に変換(後手番なら符号を反転)

この設計は Negamax フレームワークと自然に整合します。 evaluate() は常に手番側にとってのスコアを返すため、 探索関数は単純に -negamax(...) と符号を反転するだけで相手の評価を得られます。

11.8 まとめ

Hayanagiの評価関数は、ニューラルネットワークを使わない古典的な手作り評価関数です。 駒得・位置評価・利き・玉の安全度という4つの要素を組み合わせ、 フェーズに応じて重みを動的に変化させることで、序盤から終盤まで一貫した評価を実現しています。