Skip to content

Commit bf57e18

Browse files
committed
Spell-chess: persist potion state in FEN
1 parent 14ae1c1 commit bf57e18

File tree

2 files changed

+171
-3
lines changed

2 files changed

+171
-3
lines changed

src/position.cpp

Lines changed: 162 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,61 @@ namespace {
4040

4141
inline Variant::PotionType potion_type_from_piece(const Variant* var, PieceType pt) {
4242
if (!var || !var->potions)
43-
return static_cast<Variant::PotionType>(Variant::POTION_TYPE_NB);
43+
return static_cast<Variant::PotionType>(Variant::POTION_TYPE_NB);
4444
if (pt == var->potionPiece[Variant::POTION_FREEZE])
4545
return Variant::POTION_FREEZE;
4646
if (pt == var->potionPiece[Variant::POTION_JUMP])
4747
return Variant::POTION_JUMP;
4848
return static_cast<Variant::PotionType>(Variant::POTION_TYPE_NB);
4949
}
5050

51+
inline Square potion_zone_center(const Position& pos, Variant::PotionType potion,
52+
Bitboard zone) {
53+
if (zone == Bitboard(0))
54+
return SQ_NONE;
55+
if (potion == Variant::POTION_JUMP)
56+
return lsb(zone);
57+
Bitboard candidates = zone;
58+
while (candidates)
59+
{
60+
Square s = pop_lsb(candidates);
61+
if (pos.freeze_zone_from_square(s) == zone)
62+
return s;
63+
}
64+
return lsb(zone);
65+
}
66+
67+
inline Bitboard potion_zone_from_center(const Position& pos, Variant::PotionType potion,
68+
Square s) {
69+
if (s == SQ_NONE)
70+
return Bitboard(0);
71+
if (potion == Variant::POTION_FREEZE)
72+
return pos.freeze_zone_from_square(s);
73+
return square_bb(s);
74+
}
75+
76+
inline Square parse_fen_square(const Position& pos, const std::string& token) {
77+
if (token.size() < 2)
78+
return SQ_NONE;
79+
char fileChar = char(tolower(token[0]));
80+
if (fileChar < 'a' || fileChar > char('a' + pos.max_file()))
81+
return SQ_NONE;
82+
int file = fileChar - 'a';
83+
int rank = 0;
84+
for (size_t i = 1; i < token.size(); ++i)
85+
{
86+
if (!isdigit(token[i]))
87+
return SQ_NONE;
88+
rank = rank * 10 + (token[i] - '0');
89+
}
90+
if (rank <= 0)
91+
return SQ_NONE;
92+
int rankIndex = rank - 1;
93+
if (rankIndex > pos.max_rank())
94+
return SQ_NONE;
95+
return make_square(File(file), Rank(rankIndex));
96+
}
97+
5198
struct SpellContextScope {
5299
Position& pos;
53100
bool active;
@@ -402,7 +449,87 @@ Position& Position::set(const Variant* v, const string& fenStr, bool isChess960,
402449
add_to_hand(Piece(idx));
403450
}
404451

452+
if (!sfen && potions_enabled())
453+
{
454+
std::string potionState;
455+
ss >> std::ws;
456+
if (ss.peek() == '{')
457+
{
458+
ss >> token;
459+
while (ss >> token)
460+
{
461+
if (token == '}')
462+
break;
463+
if (!isspace(token))
464+
potionState.push_back(char(token));
465+
}
466+
}
467+
468+
auto parse_potion_state = [&](const std::string& state) {
469+
size_t start = 0;
470+
const int maxCooldown = (1u << POTION_COOLDOWN_BITS) - 1;
471+
while (start < state.size())
472+
{
473+
size_t end = state.find(',', start);
474+
std::string entry = state.substr(start, end == std::string::npos ? end : end - start);
475+
if (entry.size() >= 4)
476+
{
477+
char pieceChar = entry[0];
478+
Color c = islower(pieceChar) ? BLACK : WHITE;
479+
Variant::PotionType potion = static_cast<Variant::PotionType>(Variant::POTION_TYPE_NB);
480+
for (int pt = 0; pt < Variant::POTION_TYPE_NB; ++pt)
481+
{
482+
Variant::PotionType ptEnum = static_cast<Variant::PotionType>(pt);
483+
PieceType potionPiece = potion_piece(ptEnum);
484+
if (potionPiece == NO_PIECE_TYPE)
485+
continue;
486+
char expected = piece_to_char()[make_piece(WHITE, potionPiece)];
487+
if (tolower(expected) == tolower(pieceChar))
488+
{
489+
potion = ptEnum;
490+
break;
491+
}
492+
}
493+
494+
size_t at = entry.find('@', 1);
495+
size_t colon = entry.find(':', at == std::string::npos ? 0 : at + 1);
496+
if (potion != Variant::POTION_TYPE_NB && at != std::string::npos && colon != std::string::npos)
497+
{
498+
std::string squareToken = entry.substr(at + 1, colon - at - 1);
499+
std::string cooldownToken = entry.substr(colon + 1);
500+
Square center = SQ_NONE;
501+
if (!squareToken.empty() && squareToken != "-")
502+
center = parse_fen_square(*this, squareToken);
503+
504+
int cooldown = 0;
505+
for (char ch : cooldownToken)
506+
{
507+
if (!isdigit(ch))
508+
{
509+
cooldown = 0;
510+
break;
511+
}
512+
cooldown = cooldown * 10 + (ch - '0');
513+
}
514+
if (cooldown > maxCooldown)
515+
cooldown = maxCooldown;
516+
517+
st->potionCooldown[c][potion] = cooldown;
518+
st->potionZones[c][potion] = potion_zone_from_center(*this, potion, center);
519+
}
520+
}
521+
if (end == std::string::npos)
522+
break;
523+
start = end + 1;
524+
}
525+
};
526+
527+
if (!potionState.empty())
528+
parse_potion_state(potionState);
529+
}
530+
405531
// 2. Active color
532+
ss >> std::ws;
406533
ss >> token;
407534
sideToMove = (token != (sfen ? 'w' : 'b') ? WHITE : BLACK); // Invert colors for SFEN
408535
ss >> token;
@@ -836,6 +963,38 @@ string Position::fen(bool sfen, bool showPromoted, int countStarted, std::string
836963
ss << ']';
837964
}
838965

966+
if (potions_enabled())
967+
{
968+
std::string potionState;
969+
const int maxCooldown = (1u << POTION_COOLDOWN_BITS) - 1;
970+
for (Color c : {WHITE, BLACK})
971+
for (int pt = 0; pt < Variant::POTION_TYPE_NB; ++pt)
972+
{
973+
Variant::PotionType potion = static_cast<Variant::PotionType>(pt);
974+
PieceType potionPiece = potion_piece(potion);
975+
if (potionPiece == NO_PIECE_TYPE)
976+
continue;
977+
if (!potionState.empty())
978+
potionState += ",";
979+
potionState += piece_to_char()[make_piece(c, potionPiece)];
980+
potionState += "@";
981+
Bitboard zone = potion_zone(c, potion);
982+
if (zone)
983+
potionState += UCI::square(*this, potion_zone_center(*this, potion, zone));
984+
else
985+
potionState += "-";
986+
potionState += ":";
987+
int cooldown = potion_cooldown(c, potion);
988+
if (cooldown < 0)
989+
cooldown = 0;
990+
if (cooldown > maxCooldown)
991+
cooldown = maxCooldown;
992+
potionState += std::to_string(cooldown);
993+
}
994+
if (!potionState.empty())
995+
ss << " {" << potionState << "}";
996+
}
997+
839998
ss << (sideToMove == WHITE ? " w " : " b ");
840999

8411000
// Disambiguation for chess960 "king" square
@@ -1352,8 +1511,8 @@ bool Position::legal(Move m) const {
13521511
if (freeze_squares() & to_sq(m))
13531512
return false;
13541513

1355-
// Non-royal pieces can not be impeded from castling
1356-
if (type_of(piece_on(from)) != KING)
1514+
// Only the castling king piece is subject to attack checks
1515+
if (type_of(piece_on(from)) != castling_king_piece(us))
13571516
return true;
13581517

13591518
for (Square s = to; s != from; s += step)

tests/perft.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ if [[ $1 == "all" || $1 == "variant" ]]; then
157157
expect perft.exp extinction "fen rnbqb1kr/pppppppp/8/8/8/8/PPPPPPPP/RNBQB1KR w AHah - 0 1" 4 195286 true > /dev/null
158158
expect perft.exp seirawan "fen qbbrnkrn/pppppppp/8/8/8/8/PPPPPPPP/QBBRNKRN[HEhe] w ABCDEFGHabcdefgh - 0 1" 3 21170 true > /dev/null
159159
expect perft.exp spell-chess "fen 4k3/p7/8/8/8/8/8/4K2R[f] b K - 0 1 moves f@h1,a7a6" 1 5 > /dev/null
160+
castling_output=$(printf "setoption name UCI_Variant value spell-chess\nposition fen rnb1k2r/pp1pnppp/2p1p3/q1b1P3/3P4/5N2/PPP1BPPP/RNBQK2R[JJFFFFjjffff] w KQkq - 1 6\ngo perft 1\nquit\n" | ./stockfish)
161+
if echo "$castling_output" | grep -q "e1g1:"; then
162+
echo "spell-chess castling-in-check test failed (e1g1)"
163+
exit 1
164+
fi
165+
if echo "$castling_output" | grep -q "e1c1:"; then
166+
echo "spell-chess castling-in-check test failed (e1c1)"
167+
exit 1
168+
fi
160169
fi
161170

162171
# large-board variants

0 commit comments

Comments
 (0)