게시판장기로 이해하는 규칙 엔진과 평가 함수 탐색예제 소스

예제 소스

장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개

Game.cpp

#include "Game.h"

#include <algorithm>
#include <cmath>

namespace {

constexpr int indexOf(Pos p) {
    return p.rank * 9 + p.file;
}

const std::array<Pos, 3> kHanDiagA{{{3, 0}, {4, 1}, {5, 2}}};
const std::array<Pos, 3> kHanDiagB{{{5, 0}, {4, 1}, {3, 2}}};
const std::array<Pos, 3> kChoDiagA{{{3, 7}, {4, 8}, {5, 9}}};
const std::array<Pos, 3> kChoDiagB{{{5, 7}, {4, 8}, {3, 9}}};

bool onLine(Pos p, const std::array<Pos, 3>& line) {
    return p == line[0] || p == line[1] || p == line[2];
}

}  // namespace

Game::Game() {
    newGame(Formation::InnerElephant, Formation::InnerElephant);
}

Piece Game::at(Pos p) const {
    if (!inBoard(p)) {
        return {};
    }
    return squares_[static_cast<size_t>(indexOf(p))];
}

Piece& Game::ref(Pos p) {
    return squares_[static_cast<size_t>(indexOf(p))];
}

const Piece& Game::ref(Pos p) const {
    return squares_[static_cast<size_t>(indexOf(p))];
}

void Game::clear() {
    squares_.fill({});
    history_.clear();
    capturedCho_.clear();
    capturedHan_.clear();
    sideToMove_ = Side::Cho;
    result_ = GameResult::Playing;
    consecutivePasses_ = 0;
    positionKeys_.clear();
}

void Game::placeFormation(Side side, Formation formation) {
    const int back = (side == Side::Han) ? 0 : 9;
    const int kingRank = (side == Side::Han) ? 1 : 8;
    const int cannonRank = (side == Side::Han) ? 2 : 7;
    const int pawnRank = (side == Side::Han) ? 3 : 6;

    auto place = [&](int file, int rank, PieceType type) {
        ref({file, rank}) = Piece{type, side};
    };

    place(0, back, PieceType::Chariot);
    place(8, back, PieceType::Chariot);
    place(3, back, PieceType::Advisor);
    place(5, back, PieceType::Advisor);
    place(4, kingRank, PieceType::King);
    place(1, cannonRank, PieceType::Cannon);
    place(7, cannonRank, PieceType::Cannon);
    for (int file = 0; file < 9; file += 2) {
        place(file, pawnRank, PieceType::Pawn);
    }

    auto setWing = [&](int leftFile, bool elephant) {
        place(leftFile, back, elephant ? PieceType::Elephant : PieceType::Horse);
    };

    switch (formation) {
    case Formation::InnerElephant:  // 마 상 상 마
        setWing(1, false);
        setWing(2, true);
        setWing(6, true);
        setWing(7, false);
        break;
    case Formation::OuterElephant:  // 상 마 마 상
        setWing(1, true);
        setWing(2, false);
        setWing(6, false);
        setWing(7, true);
        break;
    case Formation::LeftElephant:  // 상 마 상 마
        setWing(1, true);
        setWing(2, false);
        setWing(6, true);
        setWing(7, false);
        break;
    case Formation::RightElephant:  // 마 상 마 상
        setWing(1, false);
        setWing(2, true);
        setWing(6, false);
        setWing(7, true);
        break;
    }
}

void Game::newGame(Formation cho, Formation han) {
    clear();
    placeFormation(Side::Han, han);
    placeFormation(Side::Cho, cho);
    recomputeKings();
    positionKeys_.clear();
    positionKeys_.push_back(positionKey());
}

void Game::setPosition(const std::array<Piece, 90>& squares, Side toMove) {
    clear();
    squares_ = squares;
    sideToMove_ = toMove;
    recomputeKings();
    positionKeys_.push_back(positionKey());
}

void Game::recomputeKings() {
    choKing_ = {-1, -1};
    hanKing_ = {-1, -1};
    for (int rank = 0; rank < 10; ++rank) {
        for (int file = 0; file < 9; ++file) {
            const Piece p = at({file, rank});
            if (p.type == PieceType::King) {
                if (p.side == Side::Cho) {
                    choKing_ = {file, rank};
                } else {
                    hanKing_ = {file, rank};
                }
            }
        }
    }
}

Pos Game::kingPos(Side side) const {
    return side == Side::Cho ? choKing_ : hanKing_;
}

std::optional<Move> Game::lastMove() const {
    if (history_.empty()) {
        return std::nullopt;
    }
    return history_.back();
}

int Game::pieceScore(Side side) const {
    int score = 0;
    for (const Piece& p : squares_) {
        if (!p.empty() && p.side == side && p.type != PieceType::King) {
            score += materialValue(p.type);
        }
    }
    if (side == Side::Han) {
        score += 15;  // 덤 1.5점
    }
    return score;
}

std::vector<Piece> Game::capturedBy(Side side) const {
    return side == Side::Cho ? capturedCho_ : capturedHan_;
}

bool Game::onPalaceDiagonalStep(Pos a, Pos b) const {
    const int df = std::abs(a.file - b.file);
    const int dr = std::abs(a.rank - b.rank);
    if (df != 1 || dr != 1) {
        return false;
    }
    return (onLine(a, kHanDiagA) && onLine(b, kHanDiagA))
        || (onLine(a, kHanDiagB) && onLine(b, kHanDiagB))
        || (onLine(a, kChoDiagA) && onLine(b, kChoDiagA))
        || (onLine(a, kChoDiagB) && onLine(b, kChoDiagB));
}

void Game::pushMove(Pos from, Pos to, Piece piece, std::vector<Move>& out) const {
    if (!inBoard(to)) {
        return;
    }
    const Piece target = at(to);
    if (target && target.side == piece.side) {
        return;
    }
    Move m;
    m.from = from;
    m.to = to;
    m.moving = piece;
    m.captured = target;
    out.push_back(m);
}

void Game::addKingAdvisorMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    static const int kDf[] = {-1, -1, -1, 0, 0, 1, 1, 1};
    static const int kDr[] = {-1, 0, 1, -1, 1, -1, 0, 1};
    for (int i = 0; i < 8; ++i) {
        const Pos to{from.file + kDf[i], from.rank + kDr[i]};
        if (!inPalace(to, piece.side)) {
            continue;
        }
        const bool ortho = (kDf[i] == 0) != (kDr[i] == 0);
        if (!ortho && !onPalaceDiagonalStep(from, to)) {
            continue;
        }
        pushMove(from, to, piece, out);
    }
}

void Game::addSliderOnLine(Pos from, Piece piece, const std::array<Pos, 3>& line, bool cannon,
                           std::vector<Move>& out) const {
    if (!onLine(from, line)) {
        return;
    }
    int idx = 0;
    for (int i = 0; i < 3; ++i) {
        if (line[static_cast<size_t>(i)] == from) {
            idx = i;
        }
    }

    auto scan = [&](int step) {
        if (cannon) {
            bool jumped = false;
            Piece screen{};
            for (int i = idx + step; i >= 0 && i < 3; i += step) {
                const Pos to = line[static_cast<size_t>(i)];
                const Piece target = at(to);
                if (!jumped) {
                    if (target) {
                        if (target.type == PieceType::Cannon) {
                            return;
                        }
                        jumped = true;
                        screen = target;
                    }
                    continue;
                }
                if (!target) {
                    pushMove(from, to, piece, out);
                    continue;
                }
                if (target.type != PieceType::Cannon) {
                    pushMove(from, to, piece, out);
                }
                return;
            }
            (void)screen;
        } else {
            for (int i = idx + step; i >= 0 && i < 3; i += step) {
                const Pos to = line[static_cast<size_t>(i)];
                const Piece target = at(to);
                if (!target) {
                    pushMove(from, to, piece, out);
                    continue;
                }
                pushMove(from, to, piece, out);
                return;
            }
        }
    };

    scan(-1);
    scan(1);
}

void Game::addChariotMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    static const int kDf[] = {1, -1, 0, 0};
    static const int kDr[] = {0, 0, 1, -1};
    for (int d = 0; d < 4; ++d) {
        for (int step = 1; step < 10; ++step) {
            const Pos to{from.file + kDf[d] * step, from.rank + kDr[d] * step};
            if (!inBoard(to)) {
                break;
            }
            const Piece target = at(to);
            if (!target) {
                pushMove(from, to, piece, out);
                continue;
            }
            pushMove(from, to, piece, out);
            break;
        }
    }
    addSliderOnLine(from, piece, kHanDiagA, false, out);
    addSliderOnLine(from, piece, kHanDiagB, false, out);
    addSliderOnLine(from, piece, kChoDiagA, false, out);
    addSliderOnLine(from, piece, kChoDiagB, false, out);
}

void Game::addCannonMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    static const int kDf[] = {1, -1, 0, 0};
    static const int kDr[] = {0, 0, 1, -1};
    for (int d = 0; d < 4; ++d) {
        bool jumped = false;
        for (int step = 1; step < 10; ++step) {
            const Pos to{from.file + kDf[d] * step, from.rank + kDr[d] * step};
            if (!inBoard(to)) {
                break;
            }
            const Piece target = at(to);
            if (!jumped) {
                if (target) {
                    if (target.type == PieceType::Cannon) {
                        break;
                    }
                    jumped = true;
                }
                continue;
            }
            if (!target) {
                pushMove(from, to, piece, out);
                continue;
            }
            if (target.type != PieceType::Cannon) {
                pushMove(from, to, piece, out);
            }
            break;
        }
    }
    addSliderOnLine(from, piece, kHanDiagA, true, out);
    addSliderOnLine(from, piece, kHanDiagB, true, out);
    addSliderOnLine(from, piece, kChoDiagA, true, out);
    addSliderOnLine(from, piece, kChoDiagB, true, out);
}

void Game::addHorseMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    struct Step {
        int df, dr, bf, br;
    };
    static const Step kSteps[] = {
        {1, 2, 0, 1}, {1, -2, 0, -1}, {-1, 2, 0, 1}, {-1, -2, 0, -1},
        {2, 1, 1, 0}, {2, -1, 1, 0}, {-2, 1, -1, 0}, {-2, -1, -1, 0},
    };
    for (const Step& s : kSteps) {
        const Pos block{from.file + s.bf, from.rank + s.br};
        if (!inBoard(block) || at(block)) {
            continue;
        }
        pushMove(from, {from.file + s.df, from.rank + s.dr}, piece, out);
    }
}

void Game::addElephantMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    struct Step {
        int df, dr, b1f, b1r, b2f, b2r;
    };
    static const Step kSteps[] = {
        {2, 3, 0, 1, 1, 2},   {2, -3, 0, -1, 1, -2},  {-2, 3, 0, 1, -1, 2},
        {-2, -3, 0, -1, -1, -2}, {3, 2, 1, 0, 2, 1},   {3, -2, 1, 0, 2, -1},
        {-3, 2, -1, 0, -2, 1}, {-3, -2, -1, 0, -2, -1},
    };
    for (const Step& s : kSteps) {
        const Pos b1{from.file + s.b1f, from.rank + s.b1r};
        const Pos b2{from.file + s.b2f, from.rank + s.b2r};
        if (!inBoard(b1) || at(b1) || !inBoard(b2) || at(b2)) {
            continue;
        }
        pushMove(from, {from.file + s.df, from.rank + s.dr}, piece, out);
    }
}

void Game::addPawnMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    const int fwd = pawnForward(piece.side);
    pushMove(from, {from.file, from.rank + fwd}, piece, out);
    pushMove(from, {from.file - 1, from.rank}, piece, out);
    pushMove(from, {from.file + 1, from.rank}, piece, out);

    const Side enemy = opposite(piece.side);
    if (inPalace(from, enemy) || inPalace({from.file - 1, from.rank + fwd}, enemy)
        || inPalace({from.file + 1, from.rank + fwd}, enemy)) {
        const Pos left{from.file - 1, from.rank + fwd};
        const Pos right{from.file + 1, from.rank + fwd};
        if (onPalaceDiagonalStep(from, left)) {
            pushMove(from, left, piece, out);
        }
        if (onPalaceDiagonalStep(from, right)) {
            pushMove(from, right, piece, out);
        }
    }
}

void Game::addPieceMoves(Pos from, Piece piece, std::vector<Move>& out) const {
    switch (piece.type) {
    case PieceType::King:
    case PieceType::Advisor:
        addKingAdvisorMoves(from, piece, out);
        break;
    case PieceType::Chariot:
        addChariotMoves(from, piece, out);
        break;
    case PieceType::Cannon:
        addCannonMoves(from, piece, out);
        break;
    case PieceType::Horse:
        addHorseMoves(from, piece, out);
        break;
    case PieceType::Elephant:
        addElephantMoves(from, piece, out);
        break;
    case PieceType::Pawn:
        addPawnMoves(from, piece, out);
        break;
    default:
        break;
    }
}

std::vector<Move> Game::pseudoMoves(Side side) const {
    std::vector<Move> out;
    out.reserve(64);
    for (int rank = 0; rank < 10; ++rank) {
        for (int file = 0; file < 9; ++file) {
            const Pos from{file, rank};
            const Piece piece = at(from);
            if (piece && piece.side == side) {
                addPieceMoves(from, piece, out);
            }
        }
    }
    return out;
}

bool Game::attacks(Pos target, Side attacker) const {
    for (const Move& m : pseudoMoves(attacker)) {
        if (m.to == target) {
            return true;
        }
    }
    return false;
}

bool Game::isCheck(Side side) const {
    const Pos king = kingPos(side);
    if (!inBoard(king)) {
        return true;
    }
    return attacks(king, opposite(side));
}

bool Game::isBikjang() const {
    if (!inBoard(choKing_) || !inBoard(hanKing_)) {
        return false;
    }
    if (choKing_.file != hanKing_.file) {
        return false;
    }
    const int lo = std::min(choKing_.rank, hanKing_.rank);
    const int hi = std::max(choKing_.rank, hanKing_.rank);
    for (int rank = lo + 1; rank < hi; ++rank) {
        if (at({choKing_.file, rank})) {
            return false;
        }
    }
    return true;
}

namespace {
char pieceKeyChar(PieceType type) {
    switch (type) {
    case PieceType::King: return 'K';
    case PieceType::Advisor: return 'A';
    case PieceType::Chariot: return 'R';
    case PieceType::Cannon: return 'C';
    case PieceType::Horse: return 'H';
    case PieceType::Elephant: return 'E';
    case PieceType::Pawn: return 'P';
    default: return '.';
    }
}
}  // namespace

std::string Game::positionKey() const {
    std::string key;
    key.reserve(1 + 90 * 2);
    key.push_back(sideToMove_ == Side::Cho ? 'c' : 'h');
    key.push_back('|');
    for (const Piece& p : squares_) {
        if (!p) {
            key.push_back('.');
            continue;
        }
        key.push_back(p.side == Side::Cho ? 'c' : 'h');
        key.push_back(pieceKeyChar(p.type));
    }
    return key;
}

int Game::repetitionCount(const std::string& key) const {
    int n = 0;
    for (const std::string& k : positionKeys_) {
        if (k == key) {
            ++n;
        }
    }
    return n;
}

bool Game::isForbiddenRepetition() const {
    return repetitionLimit_ > 0 && repetitionCount(positionKey()) >= repetitionLimit_;
}

std::vector<Move> Game::legalMoves() const {
    std::vector<Move> legal;
    if (result_ != GameResult::Playing) {
        return legal;
    }
    Game probe = *this;
    for (const Move& m : pseudoMoves(sideToMove_)) {
        probe.applyTrusted(m);
        const bool safe = !probe.isCheck(sideToMove_) && !probe.isForbiddenRepetition();
        probe.undo();
        if (safe) {
            legal.push_back(m);
        }
    }
    return legal;
}

std::vector<Pos> Game::legalTargets(Pos from) const {
    std::vector<Pos> targets;
    const Piece piece = at(from);
    if (!piece || piece.side != sideToMove_) {
        return targets;
    }
    for (const Move& m : legalMoves()) {
        if (m.from == from) {
            targets.push_back(m.to);
        }
    }
    return targets;
}

void Game::applyTrusted(const Move& move) {
    if (move.pass) {
        consecutivePasses_ += 1;
        history_.push_back(move);
        sideToMove_ = opposite(sideToMove_);
        positionKeys_.push_back(positionKey());
        return;
    }
    consecutivePasses_ = 0;
    ref(move.from) = {};
    ref(move.to) = move.moving;
    if (move.captured) {
        if (move.moving.side == Side::Cho) {
            capturedCho_.push_back(move.captured);
        } else {
            capturedHan_.push_back(move.captured);
        }
    }
    if (move.moving.type == PieceType::King) {
        if (move.moving.side == Side::Cho) {
            choKing_ = move.to;
        } else {
            hanKing_ = move.to;
        }
    }
    if (move.captured.type == PieceType::King) {
        if (move.captured.side == Side::Cho) {
            choKing_ = {-1, -1};
        } else {
            hanKing_ = {-1, -1};
        }
    }
    history_.push_back(move);
    sideToMove_ = opposite(sideToMove_);
    positionKeys_.push_back(positionKey());
}

void Game::finishTurn() {
    if (!inBoard(choKing_)) {
        result_ = GameResult::HanWin;
        return;
    }
    if (!inBoard(hanKing_)) {
        result_ = GameResult::ChoWin;
        return;
    }
    if (consecutivePasses_ >= 2) {
        result_ = GameResult::Draw;
        return;
    }
    if (isCheck(sideToMove_) && legalMoves().empty()) {
        result_ = (sideToMove_ == Side::Cho) ? GameResult::HanWin : GameResult::ChoWin;
    }
}

bool Game::tryMove(Pos from, Pos to) {
    if (result_ != GameResult::Playing) {
        return false;
    }
    for (const Move& m : legalMoves()) {
        if (m.from == from && m.to == to) {
            applyTrusted(m);
            finishTurn();
            return true;
        }
    }
    return false;
}

bool Game::passTurn() {
    if (result_ != GameResult::Playing) {
        return false;
    }
    if (isCheck(sideToMove_)) {
        return false;
    }
    Game probe = *this;
    probe.applyTrusted(Move::makePass());
    if (probe.isForbiddenRepetition()) {
        return false;
    }
    applyTrusted(Move::makePass());
    finishTurn();
    return true;
}

bool Game::resign(Side side) {
    if (result_ != GameResult::Playing) {
        return false;
    }
    result_ = (side == Side::Cho) ? GameResult::HanWin : GameResult::ChoWin;
    return true;
}

bool Game::claimBikjang() {
    if (result_ != GameResult::Playing || !bikjangEnabled_) {
        return false;
    }
    if (!isBikjang() || isCheck(sideToMove_)) {
        return false;
    }
    result_ = GameResult::Draw;
    return true;
}

bool Game::undo() {
    if (history_.empty() || result_ != GameResult::Playing) {
        if (history_.empty()) {
            return false;
        }
        // 대국 종료 후에도 한 수는 되돌릴 수 있게 함
        result_ = GameResult::Playing;
    }
    const Move move = history_.back();
    history_.pop_back();
    if (positionKeys_.size() > 1) {
        positionKeys_.pop_back();
    }
    sideToMove_ = opposite(sideToMove_);

    if (move.pass) {
        consecutivePasses_ = 0;
        if (!history_.empty() && history_.back().pass) {
            consecutivePasses_ = 1;
        }
        result_ = GameResult::Playing;
        return true;
    }

    ref(move.to) = move.captured;
    ref(move.from) = move.moving;
    if (move.captured) {
        auto& bag = (move.moving.side == Side::Cho) ? capturedCho_ : capturedHan_;
        if (!bag.empty()) {
            bag.pop_back();
        }
    }
    recomputeKings();
    consecutivePasses_ = 0;
    if (!history_.empty() && history_.back().pass) {
        consecutivePasses_ = 1;
    }
    result_ = GameResult::Playing;
    return true;
}

std::string Game::moveText(const Move& move) const {
    if (move.pass) {
        return "한수쉼";
    }
    std::string text;
    text += (move.moving.side == Side::Cho) ? "초 " : "한 ";
    text += pieceNameKo(move.moving.type);
    text += " ";
    text += posLabel(move.from);
    text += " → ";
    text += posLabel(move.to);
    if (move.captured) {
        text += " (";
        text += pieceNameKo(move.captured.type);
        text += ")";
    }
    return text;
}