python/3b1b cessboard encoding puzzle.py
DomNomNomVR 028fb0bc3a dump
2025-04-14 15:58:38 +12:00

181 lines
6.4 KiB
Python

# https://youtu.be/wTJI_WuZSwE
from functools import reduce, cache
from typing import List, Tuple, Callable, Iterable
from math import sqrt, log2
from random import Random
def get_flip_pos_2x2(board: List[int], key: int) -> int:
assert all(x in [0,1] for x in board)
assert len(board) == 4
s = sum(board)
if s==0:
return key
if s==1:
pos = board.index(1)
if key == 0:
return pos # empty
if key == 1:
return [1,0,3,2][pos] # horizontal
if key == 2:
return [2,3,0,1][pos] # vertical
if key == 3:
return [3,2,1,0][pos] # diagonal
assert False
if s==2:
# ensure ``sum(board) in [1,3]``, pointing to key
if board[key] == 0: # sum(flipped board) == 3
possibilities = [i for i in range(len(board)) if i != key and board[i]==0]
else: # sum(flipped board) == 3
possibilities = [i for i in range(len(board)) if i != key and board[i]==1]
assert len(possibilities) == 1
return possibilities[0]
if s==3:
return get_flip_pos_2x2(all_flipped(board), key)
if s==4:
return get_flip_pos_2x2(all_flipped(board), key)
assert False
def all_flipped(board: List[int]) -> List[int]:
return [1-x for x in board]
def guess_key_2x2(board: List[int]) -> int:
s = sum(board)
if s==0:
return 0
if s==1:
return board.index(1)
if s==2:
if board==[1,1,0,0] or board==[0,0,1,1]:
return 1 # horizontal
if board==[1,0,1,0] or board==[0,1,0,1]:
return 2 # vertical
if board==[1,0,0,1] or board==[0,1,1,0]:
return 3 # diagonal
if s==3:
return guess_key_2x2(all_flipped(board))
if s==4:
return guess_key_2x2(all_flipped(board))
assert False
def flip_mutate(board: List[int], flip_pos: int):
board[flip_pos] = 1-board[flip_pos]
def flip_copy(board: List[int], flip_pos: int) -> List[int]:
board = board[:]
flip_mutate(board, flip_pos)
return board
def print_board(board: List[int]):
sidelength = sqrt(len(board))
assert sidelength == int(sidelength)
sidelength = int(sidelength)
for row_i in range(sidelength):
print(''.join(str(x) for x in board[row_i*sidelength:(row_i+1)*sidelength]))
def generate_boards(board_squre_count: int, sample_count: int) -> Iterable[int]:
if sample_count >= 2**board_squre_count:
print('doing exhaustive verification.')
yield from range(2**board_squre_count)
print(f'not doing exhaustive. need sample_count>={2**board_squre_count}, got sample_count={sample_count}')
rng = Random(4)
for i in range(sample_count):
yield rng.getrandbits(board_squre_count)
def validate(board_squre_count, get_flip_pos: Callable, guess_key: Callable, sample_count=2**16) -> bool:
success_boards = set()
for sample_i, board_int in enumerate(generate_boards(board_squre_count, sample_count)):
if board_int in success_boards:
continue
if sample_i %100 == 0:
print(sample_i/sample_count)
for key in range(board_squre_count):
# potential optimization: could re-use board and unflip guess
board = [(board_int>>i)&1 for i in reversed(range(board_squre_count))]
flip_pos = get_flip_pos(board, key)
flip_mutate(board, flip_pos)
guess = guess_key(board)
if guess != key:
print(f'Found counter example. Got guess {guess}, want {key}')
flip_mutate(board, flip_pos)
print('before flip:')
print_board(board)
flip_mutate(board, flip_pos)
print('after flip:')
print_board(board)
return False
success_boards.add(board_int)
if len(success_boards) == 2**board_squre_count:
# All boards have been validated
print('exhaustive, yay')
return True
print(f'non exhaustive finish. covered {len(success_boards)} cases ({len(success_boards) / 2**board_squre_count:%} of cases) successfully')
return True
# validate(4, get_flip_pos_2x2, guess_key_2x2)
# Scaler possibilities:
# Total key index % 4 if we split into 4-squares
# is each reg
# XOR each 4x4 section into a bit to determine which quadrant
# layer each 4x4 section with XOR, to determine which quadrant in the above 4x4
# layer each 2x2 section with XOR, to determine which quadrant in 2x2
def guess_key_pow2board(board: List[int]) -> int:
if len(board) == 4:
return guess_key_2x2(board)
sidelength = sqrt(len(board))
assert sidelength == int(sidelength)
sidelength = int(sidelength)
assert log2(sidelength) == int(log2(sidelength))
quarter = len(board) / 4
assert quarter == int(quarter)
quarter = int(quarter)
# Not actually quadrants but good enough
quadrants = [ board[i*quarter:(i+1)*quarter] for i in range(0,4) ]
quarter_i = guess_key_2x2([ reduce(lambda a,b: a^b, q) for q in quadrants ])
sub_i = guess_key_pow2board([a^b^c^d for a,c,b,d in zip(*quadrants)])
return quarter * quarter_i + sub_i
def make_get_flip_pos(guess_key: Callable) -> Callable:
# Flip the thing that works
def get_flip_pos(board, key) -> int:
possibilities = [
flip_pos for flip_pos in range(len(board))
if guess_key(flip_copy(board, flip_pos)) == key
]
assert len(possibilities) == 1, f"possibilities={possibilities} want length 1"
return possibilities[0]
return get_flip_pos
def get_flip_pos_pow2board(board: List[int], key: int) -> int:
if len(board) == 4:
return get_flip_pos_2x2(board, key)
quarter = len(board) / 4
assert quarter == int(quarter)
quarter = int(quarter)
# Not actually quadrants but good enough
quadrants = [ board[i*quarter:(i+1)*quarter] for i in range(0,4) ]
quarter_i = get_flip_pos_2x2([ reduce(lambda a,b: a^b, q) for q in quadrants ], key//4)
sub_i = get_flip_pos_pow2board([a^b^c^d for a,c,b,d in zip(*quadrants)], key%4)
return quarter * quarter_i + sub_i
# validate(4, make_get_flip_pos(guess_key_2x2), guess_key_2x2)
# validate(16, make_get_flip_pos(guess_key_pow2board), guess_key_pow2board)
# validate(16, get_flip_pos_pow2board, guess_key_pow2board)
validate(64, get_flip_pos_pow2board, guess_key_pow2board)
# validate(64, make_get_flip_pos(guess_key_pow2board), guess_key_pow2board)