예제 소스
장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 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;
}