Skip to content

Instantly share code, notes, and snippets.

@tlhakhan
Created May 26, 2026 00:43
Show Gist options
  • Select an option

  • Save tlhakhan/84f0905b43fe598b9fa789b383b9bc52 to your computer and use it in GitHub Desktop.

Select an option

Save tlhakhan/84f0905b43fe598b9fa789b383b9bc52 to your computer and use it in GitHub Desktop.
Implementation of Multi Color Knights by Claude
"""
Multi-player generalization of the Red & Black Knights construction.
Same square spiral, same "smallest unoccupied cell not threatened by a
hostile piece" rule, but the number of players, the move set of each
player's piece, and the directed threat relation between players are all
configurable. The two-knight case (OEIS A392177) is recovered with two
knight players that mutually threaten each other.
See https://jonka364.github.io/stendhal/stendhal.html for Karlsson's gallery
of variants, including the three-knight case implemented here.
"""
import math
from dataclasses import dataclass, field
from collections import namedtuple
from red_black_knights import cell_to_xy, KNIGHT_OFFSETS
# Other piece types from Karlsson's gallery, in case you want to experiment.
# Each piece's "offsets" are the squares it can attack from its current cell.
PIECES = {
"knight": [( 1, 2), ( 1, -2), (-1, 2), (-1, -2),
( 2, 1), ( 2, -1), (-2, 1), (-2, -1)],
"fers": [( 1, 1), ( 1, -1), (-1, 1), (-1, -1)],
"vazir": [( 1, 0), (-1, 0), ( 0, 1), ( 0, -1)],
"camel": [( 1, 3), ( 1, -3), (-1, 3), (-1, -3),
( 3, 1), ( 3, -1), (-3, 1), (-3, -1)],
"zebra": [( 2, 3), ( 2, -3), (-2, 3), (-2, -3),
( 3, 2), ( 3, -2), (-3, 2), (-3, -2)],
"antelope": [( 3, 4), ( 3, -4), (-3, 4), (-3, -4),
( 4, 3), ( 4, -3), (-4, 3), (-4, -3)],
"satrap": [( 2, 0), (-2, 0), ( 0, 2), ( 0, -2)],
"aspbad": [( 2, 2), ( 2, -2), (-2, 2), (-2, -2)],
"spehbed": [( 3, 0), (-3, 0), ( 0, 3), ( 0, -3)],
}
@dataclass
class Player:
name: str
color_rgb: tuple # (r, g, b) for visualization
offsets: list # attack vectors of this player's piece
SimResult = namedtuple("SimResult", ["players", "owner_of_cell", "xy_of_cell",
"cells_by_player"])
def simulate(players, threatens, num_moves):
"""Run the multi-player construction for `num_moves` total turns.
`threatens[i][j]` should be truthy iff a piece of player i threatens a
piece of player j (and therefore player j must avoid squares attacked by
player i's existing pieces). The diagonal is conventionally False
(a piece doesn't threaten itself).
Players move in round-robin order: turn k is taken by player k % len(players).
"""
n = len(players)
assert len(threatens) == n and all(len(row) == n for row in threatens)
owner_of_cell = {} # cell index -> player index
xy_of_cell = {} # cell index -> (x, y)
cells_by_player = [[] for _ in range(n)]
# For each player j, the set of (x, y) squares that *some hostile piece*
# currently threatens. (Hostile to j, that is.)
threats_against = [set() for _ in range(n)]
# Per-player pointer: smallest cell index this player might legally take.
pointer = [0] * n
for turn in range(num_moves):
pi = turn % n
p = players[pi]
my_threats = threats_against[pi]
c = pointer[pi]
while True:
if c not in owner_of_cell:
xy = cell_to_xy(c)
if xy not in my_threats:
break
c += 1
owner_of_cell[c] = pi
xy_of_cell[c] = xy
cells_by_player[pi].append(c)
# This new piece extends the threat sets of every player it threatens.
for j in range(n):
if threatens[pi][j]:
tj = threats_against[j]
x0, y0 = xy
for dx, dy in p.offsets:
tj.add((x0 + dx, y0 + dy))
pointer[pi] = c + 1
return SimResult(players, owner_of_cell, xy_of_cell, cells_by_player)
# --------------------------------------------------------------------------
# Self-check: the two-knight case should reproduce A392177.
# --------------------------------------------------------------------------
def verify_two_knight_matches_oeis():
from red_black_knights import A392177_HEAD
black = Player("Black", (17, 17, 17), PIECES["knight"])
red = Player("Red", (200, 30, 30), PIECES["knight"])
threatens = [[False, True], [True, False]]
sim = simulate([black, red], threatens, num_moves=2 * 250)
blacks = sorted(sim.cells_by_player[0])[:len(A392177_HEAD)]
assert blacks == A392177_HEAD, "two-knight regression failed"
print("two-knight self-check passed (matches A392177).")
# --------------------------------------------------------------------------
# Visualization
# --------------------------------------------------------------------------
def plot(sim, out_path, title=None, figsize=10, dpi=200):
import numpy as np
import matplotlib.pyplot as plt
radius = max(max(abs(x), abs(y)) for x, y in sim.xy_of_cell.values())
side = 2 * radius + 1
img = np.full((side, side, 3), 255, dtype=np.uint8)
colors = np.array([p.color_rgb for p in sim.players], dtype=np.uint8)
for c, (x, y) in sim.xy_of_cell.items():
row = radius - y
col = radius + x
img[row, col] = colors[sim.owner_of_cell[c]]
fig, ax = plt.subplots(figsize=(figsize, figsize), facecolor="white")
ax.imshow(img, interpolation="nearest")
ax.axhline(radius, color="#888", linewidth=0.4)
ax.axvline(radius, color="#888", linewidth=0.4)
ax.set_xticks([]); ax.set_yticks([])
for spine in ax.spines.values():
spine.set_visible(False)
if title:
ax.set_title(title, fontsize=11)
fig.tight_layout()
fig.savefig(out_path, dpi=dpi, bbox_inches="tight")
plt.close(fig)
counts = " ".join(f"{p.name} {len(sim.cells_by_player[i]):,}"
for i, p in enumerate(sim.players))
print(f"wrote {out_path} (radius {radius}, {counts})")
# --------------------------------------------------------------------------
# Pre-built scenarios
# --------------------------------------------------------------------------
def three_knights_full_enmity():
"""Three knight-players, every pair mutually hostile.
Turn order: Black, Red, Cyan, Black, Red, Cyan, ...
"""
players = [
Player("Black", ( 17, 17, 17), PIECES["knight"]),
Player("Red", (200, 30, 30), PIECES["knight"]),
Player("Cyan", ( 30, 170, 200), PIECES["knight"]),
]
# Full enmity: everyone threatens everyone else; nobody threatens themselves.
threatens = [[i != j for j in range(3)] for i in range(3)]
return players, threatens
def three_knights_cyclic_enmity():
"""Asymmetric variant: Black threatens Red, Red threatens Cyan,
Cyan threatens Black. Each player only worries about ONE opponent.
"""
players = [
Player("Black", ( 17, 17, 17), PIECES["knight"]),
Player("Red", (200, 30, 30), PIECES["knight"]),
Player("Cyan", ( 30, 170, 200), PIECES["knight"]),
]
threatens = [
[False, True, False], # Black -> Red
[False, False, True ], # Red -> Cyan
[True, False, False], # Cyan -> Black
]
return players, threatens
if __name__ == "__main__":
verify_two_knight_matches_oeis()
for N in (60_000, 600_000):
players, threatens = three_knights_full_enmity()
sim = simulate(players, threatens, num_moves=N)
plot(sim, f"/home/claude/three_knights_full_{N}.png",
title=f"Three knights, full enmity — {N:,} moves")
for N in (60_000, 600_000):
players, threatens = three_knights_cyclic_enmity()
sim = simulate(players, threatens, num_moves=N)
plot(sim, f"/home/claude/three_knights_cyclic_{N}.png",
title=f"Three knights, cyclic enmity (B→R→C→B) — {N:,} moves")
@tlhakhan

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment