"""Attack candidates across several unrelated areas.

Each block is a different kind of question. The point is to find which ones show
a pattern strong enough to be worth asking about and which collapse.
"""
from math import isqrt, gcd
from itertools import combinations

OUT = []


def say(s=""):
    print(s)
    OUT.append(str(s))


# ================= 1. Subtraction games with infinite move sets =================
# Grundy value g(n); P-position iff g(n)==0. For a FINITE move set the Grundy
# sequence is always eventually periodic. For an infinite move set it need not
# be, and periodicity is the natural question.
def grundy(move_set_upto, N):
    g = [0] * (N + 1)
    for n in range(1, N + 1):
        seen = set()
        for m in move_set_upto:
            if m > n:
                break
            seen.add(g[n - m])
        v = 0
        while v in seen:
            v += 1
        g[n] = v
    return g


def sorted_set(pred, N):
    return [m for m in range(1, N + 1) if pred(m)]


def is_prime(m):
    if m < 2:
        return False
    if m % 2 == 0:
        return m == 2
    for p in range(3, isqrt(m) + 1, 2):
        if m % p == 0:
            return False
    return True


def is_pal(m):
    s = str(m)
    return s == s[::-1]


N = 60000
GAMES = {
    "squares": sorted_set(lambda m: isqrt(m) ** 2 == m, N),
    "triangular": sorted_set(lambda m: isqrt(8 * m + 1) ** 2 == 8 * m + 1, N),
    "primes": sorted_set(is_prime, N),
    "palindromes": sorted_set(is_pal, N),
    "powers_of_2": sorted_set(lambda m: m & (m - 1) == 0, N),
    "cubes": sorted_set(lambda m: round(m ** (1 / 3)) ** 3 == m, N),
    "squarefree": sorted_set(
        lambda m: all(m % (p * p) for p in range(2, isqrt(m) + 1)), N),
}

say("=== 1. Subtraction games: Grundy behaviour for infinite move sets ===")
say(f"{'move set':14} {'max grundy':>11} {'P-density':>11}  {'periodic tail?':>15}")
say("-" * 60)
gr = {}
for name, S in GAMES.items():
    g = grundy(S, N)
    gr[name] = g
    P = [n for n in range(1, N + 1) if g[n] == 0]
    dens = len(P) / N
    # crude eventual-periodicity probe on the last half
    tail = g[N // 2:]
    per = None
    for p in range(1, 4001):
        if all(tail[k] == tail[k + p] for k in range(0, len(tail) - p, max(1, (len(tail) - p) // 400))):
            per = p
            break
    say(f"{name:14} {max(g):>11} {dens:>11.5f}  {str(per):>15}")

say()
say("P-position counts (how sparse are the losing positions):")
for name in GAMES:
    P = [n for n in range(1, 20001) if gr[name][n] == 0]
    say(f"  {name:14} first 12: {P[:12]}   count<=20000: {len(P)}")

# ================= 2. Distance graphs: chromatic number =================
# chi(Z, D): colour the integers so that any two at a distance in D differ.
say()
say("=== 2. Chromatic number of distance graphs on Z ===")


def chi_window(D, width, kmax=8):
    """Exact chromatic number of the induced graph on {0..width-1}: a lower
    bound for chi(Z,D), and for periodic D usually tight quickly."""
    adj = [set() for _ in range(width)]
    for i in range(width):
        for d in D:
            if i + d < width:
                adj[i].add(i + d)
                adj[i + d].add(i)
    for k in range(1, kmax + 1):
        colour = [-1] * width
        def bt(v):
            if v == width:
                return True
            for c in range(k):
                if all(colour[u] != c for u in adj[v] if u < v):
                    colour[v] = c
                    if bt(v + 1):
                        return True
                    colour[v] = -1
            return False
        if bt(0):
            return k
    return None


DSETS = {
    "squares": [m for m in range(1, 200) if isqrt(m) ** 2 == m],
    "triangular": [m for m in range(1, 200) if isqrt(8 * m + 1) ** 2 == 8 * m + 1],
    "primes": [m for m in range(1, 200) if is_prime(m)],
    "cubes": [m for m in range(1, 200) if round(m ** (1 / 3)) ** 3 == m],
    "palindromes": [m for m in range(1, 200) if is_pal(m)],
    "powers_of_2": [m for m in range(1, 200) if m & (m - 1) == 0],
}
say(f"{'D':14}  chi on windows of width 20/30/40/60")
for name, D in DSETS.items():
    vals = [chi_window(D, w) for w in (20, 30, 40, 60)]
    say(f"  {name:14} {vals}")

# ================= 3. A digit-driven iteration =================
say()
say("=== 3. Iteration n -> n + (product of nonzero decimal digits) ===")


def prod_nonzero(n):
    p = 1
    for ch in str(n):
        if ch != "0":
            p *= int(ch)
    return p


def orbit_hits(start, steps=4000):
    n, seen = start, set()
    for _ in range(steps):
        if n in seen:
            return "cycle"
        seen.add(n)
        n = n + prod_nonzero(n)
    return n


tails = {}
for s in range(1, 200):
    n = s
    for _ in range(60):
        n = n + prod_nonzero(n)
    tails[s] = n
say(f"  distinct values after 60 steps from starts 1..199: {len(set(tails.values()))}")
say(f"  sample trajectory from 1: "
    f"{[ (lambda x: x)(v) for v in [1] ]}")
n = 1
traj = [1]
for _ in range(14):
    n = n + prod_nonzero(n)
    traj.append(n)
say(f"    {traj}")

# ================= 4. No-four-concyclic points in a grid =================
say()
say("=== 4. Max points in an n x n grid with no four concyclic (and no 3 collinear) ===")


def concyclic(p, q, r, s):
    def det4(pts):
        M = [[x * x + y * y, x, y, 1] for (x, y) in pts]
        # 4x4 determinant
        def d3(a):
            return (a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
                    - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
                    + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]))
        tot = 0
        for c in range(4):
            minor = [[M[r2][c2] for c2 in range(4) if c2 != c] for r2 in range(1, 4)]
            tot += ((-1) ** c) * M[0][c] * d3(minor)
        return tot
    return det4([p, q, r, s]) == 0


def collinear(p, q, r):
    return (q[0] - p[0]) * (r[1] - p[1]) == (q[1] - p[1]) * (r[0] - p[0])


def max_grid(n, cap=7):
    pts = [(x, y) for x in range(n) for y in range(n)]
    best = [0]
    chosen = []
    def ok(c):
        for a, b in combinations(chosen, 2):
            if collinear(a, b, c):
                return False
        for a, b, d in combinations(chosen, 3):
            if concyclic(a, b, d, c):
                return False
        return True
    def bt(idx):
        if len(chosen) > best[0]:
            best[0] = len(chosen)
        if best[0] >= cap or idx == len(pts):
            return
        if len(chosen) + (len(pts) - idx) <= best[0]:
            return
        c = pts[idx]
        if ok(c):
            chosen.append(c)
            bt(idx + 1)
            chosen.pop()
        bt(idx + 1)
    bt(0)
    return best[0]


for n in range(2, 8):
    say(f"  n={n}: {max_grid(n)}")

open("/private/tmp/claude-501/-Users-philipweiss-novel/df47b768-6632-45b7-87c6-030f4d5fa425/scratchpad/multi_out.txt", "w").write("\n".join(OUT))
