Tablebase suggestion

Discussion of anything and everything relating to chess playing software and machines.

Moderator: Ras

User avatar
Ajedrecista
Posts: 2242
Joined: Wed Jul 13, 2011 9:04 pm
Location: Madrid, Spain.

Re: Tablebase suggestion.

Post by Ajedrecista »

Hello:
syzygy wrote: Sun Jan 18, 2026 2:29 am[...]

The 8-men positions with 1 opposing pair have been computed by Marc Bourzutschky (DTC metric) and are accessible here:
https://op1-tables.info/

8-men with 1 opposing pair only needs 82.5 GB of RAM with 1 byte per position if you split it up in pawn slices with the pair fixed, so it does not need a big machine.
9-men with 2 opposing pairs should only need 1.2 GB of RAM with 1 byte per position.
Why not go further and do 10-men with 3 opposing pairs, 11-men with 4 opposing pairs and 12-men with 5 opposing pairs (and 2 kings).

[...]
I am very pleased to say again that Op1 8-man EGTB work like a charm. I composed an entertaining, challenging position to play on your own in either side, though generally without only moves to find. Engines find it easy, but is full of swindles, stalemate threats, queening threats, pins and even a sort of desperado queen (instead of the more usual desperado rook):

[d]8/3Q4/8/8/8/5p2/2pn1P1p/3k3K w - - 0 1

Only 1.- Qa4! draws, pinning the passed pawn. Then, there are only three moves that draw: 1.- ..., Nb1, 1.- ..., Ne4 and 1.- ..., Nf1 (this one with stalemate threats). I post some sidelines in three sepatated PGN under the same PGN tag, for saving space in the post. I annotate with exclamation marks (!) in only moves and question marks (?) in the moves that lose points in result (win to draw or lose; or draw to lose):

[pgn][Event ""]
[Site ""]
[Date "2026.05.09"]
[SetUp "1"]
[FEN "8/3Q4/8/8/8/5p2/2pn1P1p/3k3K w - - 0 1"]
[Result "1/2-1/2"]

1.Qa4! Nb1 2.Qd4+ Nd2! (2...Kc1? 3.Kxh2! Nd2 4.Kg3 Nb3 (4...Kd1 5.Qa4! Kc1 6.Qa1+ Nb1 7.Kxf3 1-0) 5.Qc3! Nd2 6.Qa1+ Nb1 7.Kxf3 1-0) 3.Qa4! 1/2-1/2

[Event ""]
[Site ""]
[Date "2026.05.09"]
[SetUp "1"]
[FEN "8/3Q4/8/8/8/5p2/2pn1P1p/3k3K w - - 0 1"]
[Result "1/2-1/2"]

1.Qa4! Ne4 2.Qxe4 (2.Qd4+ (2.Kxh2 Nxf2! 3.Qxc2+ Kxc2! 4.Kg3! 1/2-1/2) Nd2! 3.Qa4! 1/2-1/2) c1=Q! 3.Kxh2 (3.Qd3+ Ke1 (3...Qd2 4.Qxf3+ (4.Qxd2+? Kxd2! 5.Kxh2 Ke1 0-1) Ke1! (4.Kc2? Qf5+ ({(DTM 80 moves)}) 1-0) 1/2-1/2) 4.Qxf3 Kd2+ 5.Kxh2! Qc7+! 6.Qg3 Qh7+! 1/2-1/2) Qc7+ (3...Qc3 4.Qb1+ Ke2 5.Qe4+ Kxf2 6.Qh4! Kf1 7.Qh3+ Kf2 8.Qg2+ fxg2 1/2-1/2) 4.Kh1 (4.Kg1? Qg7+! 0-1) 1/2-1/2

[Event ""]
[Site ""]
[Date "2026.05.09"]
[SetUp "1"]
[FEN "8/3Q4/8/8/8/5p2/2pn1P1p/3k3K w - - 0 1"]
[Result "1/2-1/2"]

1.Qa4! Nf1 2.Qxc2+ (2.Qa1+ (2.Qd4+ Nd2 (2...Kc1 (2...Ke1? 3.Qc3 Kd1 4.Qxf3+! 1-0) 3.Qa1+ Kd2 4.Qe1+ Kxe1! (4...Kd3? 5.Qxf1! Kd2 6.Kxh2! c1=Q 7.Qxc1+! Kxc1 8.Kg3 1-0) 1/2-1/2) 3.Qa4! 1/2-1/2) c1=Q 3.Qxc1+ Kxc1! (3...Ke2? 4.Qa1 Kxf2 (4...Nd2 5.Kxh2 Kxf2 6.Qe5 1-0) 5.Qd1 1-0) 1/2-1/2) Kxc2! (2...Ke1? 3.Qd3 (3.Qe4+ Kxf2 4.Qd3 1-0) Nd2 4.Kxh2 1-0) 1/2-1/2[/pgn]

Regards from Spain.

Ajedrecista.
Rein Halbersma
Posts: 775
Joined: Tue May 22, 2007 11:13 am

Re: Tablebase suggestion

Post by Rein Halbersma »

Rein Halbersma wrote: Tue Mar 03, 2026 11:42 pm
syzygy wrote: Tue Mar 03, 2026 11:01 pm There should be a better solution....
I’ll have time to further analyze this later in the week. The approach I’m looking at now is to first partition pieces over orbits, then enumerate residual symmetry groups per orbit, and (bottleneck so far) across-orbit residual symmetry interaction, then assign squares within orbits. I’m hoping to fully decompose 4 kings indexing on a checkerboard into a sum of binomial products in a way that matches the Polya-counted number. The goal is to be able to this for general piece counts. For checkers/draughts this is a lot easier than chess, because the Hasse diagram (that encodes the relations between the various subgroups) for D2 is much simpler than for D4.
I think I have managed to coax GPT-5.5 Thinking into solving this, for basically arbitrary coloring ("piece placement") over arbitrary geometrical objects ("boards") with arbitrary finite symmetry groups.

The first idea is to decompose the board into orbits under the board's full symmetry group (for draughts: 10 orbits of size 4, 5 of size 2; for chess: 6 orbits or size 8, 4 orbits of size 4). For kings-only draughts endgames, each orbit has 81 (3^4) or 9 (3^2) different "colorings" of empty, white king, black king. These few orbital configurations can be tabulated, including their symmetry transformations on each orbit and their stabilizer subgroups. This decomposition helps transforming a full position and also determining the stabilizer of a full position (basically a bitwise-and of the stabilizer bitmasks, where each stabilizer has a 1 for all itself and its own subgroups). Indexing is then done for each target_stabilizer separately, with an offset relative to the previous stabilizer.

The second idea is to use dynamic programming to have a small lookup table structered as: table(block_nr, remW, remB, current_stabilizer, status). The 5-tuple (block_nr, remW, remB, current_stabilizer, status) is called a "state" for the dynamic programming. It turns out to be useful to also define a transition(state, orbit_i) function that fill the next orbit_i (and subtracts the placed white and black kings, updates the stabilizer etc.) and returns the new "state"

To initialize this table, for a given target_stabilizer, loop over all 81 or 9 configurations of the current block_nr, count the number of white and black kings and subtract those from the remaining white/black kings. Then lookup the stabilizer of that block configuration and bitwise-and it with the mask of the current_stabilizer, and then check whether the target_stabilizer is still viable.

The third idea is to also have a secondary lookup table to efficiently rank/unrank. The secondary table basically caches summations over various entries of the primary table.

Ranking/unranking are then very similar to how combinadic ranking works for the full unconstrained ranking/unranking using scans over binomial lookup tables. Except here it is not done square by square, but orbit by orbit, and the orbit squares are simply extracted from each orbit after canonicalization.

In any case, for the case of draughts, in both Python and C++, preliminary tests indicate that the overhead of symmetry-reduced ranking/unranking is less than 5x. So essentially doing symmetry-reduced indexing saves 4x of space and costs 5x in runtime per position, so an almost equal space-time tradeoff. The auxiliary tables are of the order of 40Mb for 37 billion positions (4.5Gb bitmap without symmetry reductions, about 1.1Gb bitmap with symmetry reductions).

I think I might write a small paper about this approach, since it's over 20 years that I first started thinking about this. Total code size in C++ is about 600 lines, so I think I might also release that if anyone is interested.
syzygy
Posts: 5991
Joined: Tue Feb 28, 2012 11:56 pm

Re: Tablebase suggestion

Post by syzygy »

Rein Halbersma wrote: Sun May 17, 2026 8:55 pm
Rein Halbersma wrote: Tue Mar 03, 2026 11:42 pm
syzygy wrote: Tue Mar 03, 2026 11:01 pm There should be a better solution....
I’ll have time to further analyze this later in the week. The approach I’m looking at now is to first partition pieces over orbits, then enumerate residual symmetry groups per orbit, and (bottleneck so far) across-orbit residual symmetry interaction, then assign squares within orbits. I’m hoping to fully decompose 4 kings indexing on a checkerboard into a sum of binomial products in a way that matches the Polya-counted number. The goal is to be able to this for general piece counts. For checkers/draughts this is a lot easier than chess, because the Hasse diagram (that encodes the relations between the various subgroups) for D2 is much simpler than for D4.
I think I have managed to coax GPT-5.5 Thinking into solving this, for basically arbitrary coloring ("piece placement") over arbitrary geometrical objects ("boards") with arbitrary finite symmetry groups.

The first idea is to decompose the board into orbits under the board's full symmetry group (for draughts: 10 orbits of size 4, 5 of size 2; for chess: 6 orbits or size 8, 4 orbits of size 4). For kings-only draughts endgames, each orbit has 81 (3^4) or 9 (3^2) different "colorings" of empty, white king, black king. These few orbital configurations can be tabulated, including their symmetry transformations on each orbit and their stabilizer subgroups. This decomposition helps transforming a full position and also determining the stabilizer of a full position (basically a bitwise-and of the stabilizer bitmasks, where each stabilizer has a 1 for all itself and its own subgroups). Indexing is then done for each target_stabilizer separately, with an offset relative to the previous stabilizer.

The second idea is to use dynamic programming to have a small lookup table structered as: table(block_nr, remW, remB, current_stabilizer, status). The 5-tuple (block_nr, remW, remB, current_stabilizer, status) is called a "state" for the dynamic programming. It turns out to be useful to also define a transition(state, orbit_i) function that fill the next orbit_i (and subtracts the placed white and black kings, updates the stabilizer etc.) and returns the new "state"

To initialize this table, for a given target_stabilizer, loop over all 81 or 9 configurations of the current block_nr, count the number of white and black kings and subtract those from the remaining white/black kings. Then lookup the stabilizer of that block configuration and bitwise-and it with the mask of the current_stabilizer, and then check whether the target_stabilizer is still viable.

The third idea is to also have a secondary lookup table to efficiently rank/unrank. The secondary table basically caches summations over various entries of the primary table.

Ranking/unranking are then very similar to how combinadic ranking works for the full unconstrained ranking/unranking using scans over binomial lookup tables. Except here it is not done square by square, but orbit by orbit, and the orbit squares are simply extracted from each orbit after canonicalization.

In any case, for the case of draughts, in both Python and C++, preliminary tests indicate that the overhead of symmetry-reduced ranking/unranking is less than 5x. So essentially doing symmetry-reduced indexing saves 4x of space and costs 5x in runtime per position, so an almost equal space-time tradeoff. The auxiliary tables are of the order of 40Mb for 37 billion positions (4.5Gb bitmap without symmetry reductions, about 1.1Gb bitmap with symmetry reductions).

I think I might write a small paper about this approach, since it's over 20 years that I first started thinking about this. Total code size in C++ is about 600 lines, so I think I might also release that if anyone is interested.
Would you be doing this on all 10 (5+5) kings at the same time? Or just on 5 white kings, then place the 5 black kings on the remaining squares as normal in Bin(45,5) ways? (I think earlier I suggested to place them in Bin(50,5) ways during generation in order to be able to tile the table and loop through it stripe by stripe, and then after generation switch to a Bin(45,5) for storage.)
Rein Halbersma
Posts: 775
Joined: Tue May 22, 2007 11:13 am

Re: Tablebase suggestion

Post by Rein Halbersma »

syzygy wrote: Sun May 17, 2026 9:48 pm Would you be doing this on all 10 (5+5) kings at the same time? Or just on 5 white kings, then place the 5 black kings on the remaining squares as normal in Bin(45,5) ways? (I think earlier I suggested to place them in Bin(50,5) ways during generation in order to be able to tile the table and loop through it stripe by stripe, and then after generation switch to a Bin(45,5) for storage.)
The approach works for arbitrary board colorings, so in principle it works for the full 5v5 kings configuration, or only for a single color, as you wish.
syzygy
Posts: 5991
Joined: Tue Feb 28, 2012 11:56 pm

Re: Tablebase suggestion

Post by syzygy »

Rein Halbersma wrote: Sun May 17, 2026 8:55 pmIn any case, for the case of draughts, in both Python and C++, preliminary tests indicate that the overhead of symmetry-reduced ranking/unranking is less than 5x. So essentially doing symmetry-reduced indexing saves 4x of space and costs 5x in runtime per position, so an almost equal space-time tradeoff. The auxiliary tables are of the order of 40Mb for 37 billion positions (4.5Gb bitmap without symmetry reductions, about 1.1Gb bitmap with symmetry reductions).
What are these auxiliary tables? The tables you need for the index calculation? (Or subtables reached by captures during generation?)
I think I might write a small paper about this approach, since it's over 20 years that I first started thinking about this. Total code size in C++ is about 600 lines, so I think I might also release that if anyone is interested.
So given a material combination (and a board with symmetry group), you can automatically construct perfect indexing and de-indexing functions? That is very nice.
Rein Halbersma
Posts: 775
Joined: Tue May 22, 2007 11:13 am

Re: Tablebase suggestion

Post by Rein Halbersma »

syzygy wrote: Sun May 17, 2026 10:51 pm
Rein Halbersma wrote: Sun May 17, 2026 8:55 pmIn any case, for the case of draughts, in both Python and C++, preliminary tests indicate that the overhead of symmetry-reduced ranking/unranking is less than 5x. So essentially doing symmetry-reduced indexing saves 4x of space and costs 5x in runtime per position, so an almost equal space-time tradeoff. The auxiliary tables are of the order of 40Mb for 37 billion positions (4.5Gb bitmap without symmetry reductions, about 1.1Gb bitmap with symmetry reductions).
What are these auxiliary tables? The tables you need for the index calculation? (Or subtables reached by captures during generation?)
These tables count the number of canonical positions that are still possible after you have already put down some of the pieces in some of the orbits. Essentially, instead of ordering squares colexicographically and doing the combinadic ranking/unranking using binomial coefficients, here we compute a canonical version of a position by lexicographical ordering each orbit in increasing orbit number (15 for draughts, 10 for chess). Then the tables count how many canonical positions precede or follow the current position (even when halfway through the placement). What is astonishing that the tables are very small (and they initialize in milliseconds).
I think I might write a small paper about this approach, since it's over 20 years that I first started thinking about this. Total code size in C++ is about 600 lines, so I think I might also release that if anyone is interested.
So given a material combination (and a board with symmetry group), you can automatically construct perfect indexing and de-indexing functions? That is very nice.
Yes, I can. Yes, that is very nice :-) I tested it for 5v5 all-kings endgames and the slowdown is 5.5x now for a 4x space improvement.
syzygy
Posts: 5991
Joined: Tue Feb 28, 2012 11:56 pm

Re: Tablebase suggestion

Post by syzygy »

I gave your explanation to ChatGPT and it implemented a symmetry indexer and an example for 8x8 draughts in Python.

Code: Select all

$ python3 example_draughts.py 
orbits: [[0, 3, 28, 31], [1, 2, 29, 30], [4, 7, 24, 27], [5, 6, 25, 26], [8, 11, 20, 23], [9, 10, 21, 22], [12, 15, 16, 19], [13, 14, 17, 18]]
count for 2 white kings + 2 black kings: 54120
rank: 32629
unranked: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
roundtrip ok: True
I don't know if it makes sense, but it looks impressive :)
syzygy
Posts: 5991
Joined: Tue Feb 28, 2012 11:56 pm

Re: Tablebase suggestion

Post by syzygy »

syzygy wrote: Tue May 19, 2026 2:07 am I gave your explanation to ChatGPT and it implemented a symmetry indexer and an example for 8x8 draughts in Python.

Code: Select all

$ python3 example_draughts.py 
orbits: [[0, 3, 28, 31], [1, 2, 29, 30], [4, 7, 24, 27], [5, 6, 25, 26], [8, 11, 20, 23], [9, 10, 21, 22], [12, 15, 16, 19], [13, 14, 17, 18]]
count for 2 white kings + 2 black kings: 54120
rank: 32629
unranked: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
roundtrip ok: True
I don't know if it makes sense, but it looks impressive :)
Ok, so it does make sense. In the example code the white kings are placed on square 0,5 and the black kings on 10,20. It computes a rank, and the unrank operation returns the board position (squares enumerated from 31 to 0, apparently).

However, the orbit [0, 3, 28, 31] seems wrong.
syzygy
Posts: 5991
Joined: Tue Feb 28, 2012 11:56 pm

Re: Tablebase suggestion

Post by syzygy »

syzygy wrote: Tue May 19, 2026 2:27 am
syzygy wrote: Tue May 19, 2026 2:07 am I gave your explanation to ChatGPT and it implemented a symmetry indexer and an example for 8x8 draughts in Python.

Code: Select all

$ python3 example_draughts.py 
orbits: [[0, 3, 28, 31], [1, 2, 29, 30], [4, 7, 24, 27], [5, 6, 25, 26], [8, 11, 20, 23], [9, 10, 21, 22], [12, 15, 16, 19], [13, 14, 17, 18]]
count for 2 white kings + 2 black kings: 54120
rank: 32629
unranked: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
roundtrip ok: True
I don't know if it makes sense, but it looks impressive :)
Ok, so it does make sense. In the example code the white kings are placed on square 0,5 and the black kings on 10,20. It computes a rank, and the unrank operation returns the board position (squares enumerated from 31 to 0, apparently).

However, the orbit [0, 3, 28, 31] seems wrong.
After pointing this out, ChatGPT realised its mistake.
The original definition of the board symmetries:

Code: Select all

    transforms = [
        lambda r, c: (r, c),          # identity
        lambda r, c: (7 - r, 7 - c),  # 180 rotation
        lambda r, c: (r, 7 - c),      # vertical mirror
        lambda r, c: (7 - r, c),      # horizontal mirror
    ]
After correction:

Code: Select all

    transforms = [
        lambda r, c: (r, c),              # identity
        lambda r, c: (7 - r, 7 - c),      # 180 rotation
        lambda r, c: (c, r),              # main diagonal mirror
        lambda r, c: (7 - c, 7 - r),      # anti-diagonal mirror
    ]
Now we get:

Code: Select all

$ python3 example_draughts.py 
orbits: [[3, 28], [7, 24], [10, 21], [14, 17], [0, 4, 27, 31], [1, 12, 19, 30], [2, 11, 20, 29], [5, 8, 23, 26], [6, 15, 16, 25], [9, 13, 18, 22]]
count for 2 white kings + 2 black kings: 54366
rank: 28368
unranked: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
roundtrip ok: True
syzygy
Posts: 5991
Joined: Tue Feb 28, 2012 11:56 pm

Re: Tablebase suggestion

Post by syzygy »

Implementation of the symmetry indexer (symmetry_indexer.py):

Code: Select all

from functools import cache
from itertools import product

class SymmetryIndexer:
    def __init__(self, n_squares, perms, n_colors):
        self.n = n_squares
        self.perms = perms              # perms[g][sq] = transformed square
        self.G = len(perms)
        self.C = n_colors
        self.full_mask = (1 << self.G) - 1

        self.inv_perms = []
        for p in perms:
            inv = [0] * self.n
            for i, j in enumerate(p):
                inv[j] = i
            self.inv_perms.append(inv)

        self.orbits = self._compute_orbits()
        self.blocks = [self._make_block(orb) for orb in self.orbits]

    def _compute_orbits(self):
        seen = [False] * self.n
        orbits = []

        for s in range(self.n):
            if seen[s]:
                continue

            orb = sorted({p[s] for p in self.perms})
            for t in orb:
                seen[t] = True
            orbits.append(orb)

        # deterministic order; you may want a better order for speed
        return sorted(orbits, key=lambda o: (len(o), o[0]))

    def _make_block(self, orbit):
        local = {sq: i for i, sq in enumerate(orbit)}
        m = len(orbit)

        transform_maps = []
        for inv in self.inv_perms:
            mp = []
            for sq in orbit:
                src = inv[sq]
                mp.append(local[src])
            transform_maps.append(mp)

        configs = []

        for cfg in product(range(self.C), repeat=m):
            counts = tuple(cfg.count(c) for c in range(1, self.C))

            eq_mask = 0
            lt_mask = 0
            gt_mask = 0

            for g, mp in enumerate(transform_maps):
                transformed = tuple(cfg[mp[i]] for i in range(m))

                if cfg == transformed:
                    eq_mask |= 1 << g
                elif cfg < transformed:
                    lt_mask |= 1 << g
                else:
                    gt_mask |= 1 << g

            configs.append({
                "cfg": cfg,
                "counts": counts,
                "eq": eq_mask,
                "lt": lt_mask,
                "gt": gt_mask,
            })

        configs.sort(key=lambda x: x["cfg"])
        return {"orbit": orbit, "configs": configs}

    def canonicalize(self, pos):
        best = tuple(pos)

        for p in self.perms:
            q = [0] * self.n
            for s in range(self.n):
                q[p[s]] = pos[s]
            q = tuple(q)
            if q < best:
                best = q

        return best

    def _transition(self, eq_mask, config):
        active_gt = eq_mask & config["gt"]
        if active_gt:
            return None

        # If previously equal under g, it remains equal only if this block is equal.
        # If previously already smaller, it stays smaller and is no longer in eq_mask.
        return eq_mask & config["eq"]

    def make_counter(self, target_counts, target_stabilizer=None):
        target_counts = tuple(target_counts)

        @cache
        def dp(block_nr, rem, eq_mask):
            if block_nr == len(self.blocks):
                if any(rem):
                    return 0
                if target_stabilizer is not None and eq_mask != target_stabilizer:
                    return 0
                return 1

            total = 0
            block = self.blocks[block_nr]

            for c in block["configs"]:
                if any(c["counts"][i] > rem[i] for i in range(len(rem))):
                    continue

                new_eq = self._transition(eq_mask, c)
                if new_eq is None:
                    continue

                new_rem = tuple(rem[i] - c["counts"][i] for i in range(len(rem)))
                total += dp(block_nr + 1, new_rem, new_eq)

            return total

        return dp

    def count(self, target_counts, target_stabilizer=None):
        dp = self.make_counter(target_counts, target_stabilizer)
        return dp(0, tuple(target_counts), self.full_mask)

    def rank(self, pos, target_counts, target_stabilizer=None):
        pos = self.canonicalize(pos)
        dp = self.make_counter(target_counts, target_stabilizer)

        rem = tuple(target_counts)
        eq_mask = self.full_mask
        rank = 0

        for block_nr, block in enumerate(self.blocks):
            orbit = block["orbit"]
            actual = tuple(pos[sq] for sq in orbit)

            for c in block["configs"]:
                if c["cfg"] >= actual:
                    break

                if any(c["counts"][i] > rem[i] for i in range(len(rem))):
                    continue

                new_eq = self._transition(eq_mask, c)
                if new_eq is None:
                    continue

                new_rem = tuple(rem[i] - c["counts"][i] for i in range(len(rem)))
                rank += dp(block_nr + 1, new_rem, new_eq)

            chosen = next(c for c in block["configs"] if c["cfg"] == actual)

            if any(chosen["counts"][i] > rem[i] for i in range(len(rem))):
                raise ValueError("position has wrong material count")

            new_eq = self._transition(eq_mask, chosen)
            if new_eq is None:
                raise ValueError("position is not canonical")

            rem = tuple(rem[i] - chosen["counts"][i] for i in range(len(rem)))
            eq_mask = new_eq

        if any(rem):
            raise ValueError("position has wrong material count")

        if target_stabilizer is not None and eq_mask != target_stabilizer:
            raise ValueError("position has wrong stabilizer")

        return rank

    def unrank(self, rank, target_counts, target_stabilizer=None):
        dp = self.make_counter(target_counts, target_stabilizer)

        rem = tuple(target_counts)
        eq_mask = self.full_mask
        pos = [0] * self.n

        for block_nr, block in enumerate(self.blocks):
            orbit = block["orbit"]

            for c in block["configs"]:
                if any(c["counts"][i] > rem[i] for i in range(len(rem))):
                    continue

                new_eq = self._transition(eq_mask, c)
                if new_eq is None:
                    continue

                new_rem = tuple(rem[i] - c["counts"][i] for i in range(len(rem)))
                cnt = dp(block_nr + 1, new_rem, new_eq)

                if rank >= cnt:
                    rank -= cnt
                    continue

                for sq, color in zip(orbit, c["cfg"]):
                    pos[sq] = color

                rem = new_rem
                eq_mask = new_eq
                break
            else:
                raise ValueError("rank out of range")

        if rank != 0:
            raise ValueError("rank out of range")

        return tuple(pos)
Corrected implementation of 8x8 draughts board (example_draughts.py):

Code: Select all

def dark_index(r, c):
    # Only dark squares, where (r + c) % 2 == 1.
    return r * 4 + (c // 2)

def dark_square(i):
    r = i // 4
    k = i % 4
    c = 2 * k + ((r + 1) & 1)
    return r, c

def make_draughts_D2():
    perms = []

    transforms = [
        lambda r, c: (r, c),              # identity
        lambda r, c: (7 - r, 7 - c),      # 180 rotation
        lambda r, c: (c, r),              # main diagonal mirror
        lambda r, c: (7 - c, 7 - r),      # anti-diagonal mirror
    ]

    for f in transforms:
        p = [0] * 32
        for i in range(32):
            r, c = dark_square(i)
            rr, cc = f(r, c)
            p[i] = dark_index(rr, cc)
        perms.append(p)

    return perms
When I complained it didn't do anything, ChatGPT told me to add this to example_draughts.py:

Code: Select all

from symmetry_indexer import SymmetryIndexer

# ... dark_index, dark_square, make_draughts_D2 definitions ...

if __name__ == "__main__":
    idx = SymmetryIndexer(
        n_squares=32,
        perms=make_draughts_D2(),
        n_colors=3,
    )

    print("orbits:", [b["orbit"] for b in idx.blocks])

    total = idx.count(target_counts=(2, 2))
    print("count for 2 white kings + 2 black kings:", total)

    pos = [0] * 32
    pos[0] = 1
    pos[5] = 1
    pos[10] = 2
    pos[20] = 2

    r = idx.rank(pos, target_counts=(2, 2))
    print("rank:", r)

    pos2 = idx.unrank(r, target_counts=(2, 2))
    print("unranked:", pos2)

    print("roundtrip ok:", tuple(idx.canonicalize(pos)) == pos2)
All I did was "Please explain this forum post:" with your post pasted. This resulted in a long explanation including reasons why this was "actually clever". Then I asked "can you create code for this?"