Created
May 26, 2026 00:43
-
-
Save tlhakhan/84f0905b43fe598b9fa789b383b9bc52 to your computer and use it in GitHub Desktop.
Implementation of Multi Color Knights by Claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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") |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Source video: https://www.youtube.com/watch?v=VgmDuBCayPw