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.

Left: the opening. Right: red's a1 chariot and b3 cannon have traded places. The old key cannot tell them apart.

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:

AfterOld countReal count
the start1opening: 1
the swap, 5 moves a side2swapped: 1
a horse out and back3, drawswapped: 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.