The key used the first letter of each role
Threefold repetition needs a way to say "this is the same position." Mistboard turns each position into a string and counts how often each one comes up.
function repetitionKey(position):
parts = []
for sq, p in position.pieces (sorted by sq):
# red chariot on a1: "a1" + "r" + "c"
parts.append(sq + p.color[0] + p.role[0])
return position.turn + "|" + join(parts, ",")
after every move:
seen[repetitionKey(position)] += 1
if it reaches 3: draw (or a perpetual-check loss)role[0] is the first letter of the role, and two of xiangqi's seven roles start with c. A red chariot on a1 and cannon on b3 write a1rc and b3rc. Swap them and they still write a1rc and b3rc.
Fourteen legal plies drew a game
Red makes the swap in five moves while Black walks a chariot out and home, then each side moves a horse out and back:
| After | Old count | Real count |
|---|---|---|
| the start | 1 | opening: 1 |
| the swap, 5 moves a side | 2 | swapped: 1 |
| a horse out and back | 3, draw | swapped: 2 |
Fourteen legal plies, and the site scored a draw. No player reported it; it turned up in a read of this code while jieqi was getting a repetition rule.
The fix writes the role out in full
# red chariot on a1: "a1:r.chariot"
parts.append(sq + ":" + p.color[0] + "." + p.role)A test plays those fourteen plies and checks the game goes on. The same shortcut was in Fog Chess (king and knight both k) and Duck Xiangqi; both are fixed.
What it means on the site
Finished games: none change. Replayed under the corrected key, all 29 xiangqi repetition draws, 2 perpetual-check losses, 2 Duck Xiangqi draws and 19 Fog Chess draws end the same way.
New games: xiangqi, Fog Xiangqi, Fog Chess and Duck Xiangqi end on repetition only when a position has really occurred three times.
Puzzles: the miner's duplicate check reads both spellings, so no published puzzle comes back as new.
A key built from abbreviations needs one check that no single abbreviation shows: that no two names shorten to the same thing. A letter table written by hand makes that check when someone picks r for the chariot. role[0] never did.