Source
import itertools
import random
from typing import Iterable, Sequence
# Type to represent the layout of tiles
State = list[int]
# Map the values of a number when flipped
FLIP_MAP: dict[int, int] = {
1: 1,
6: 9,
8: 8,
9: 6,
}
# All the tiles represented
ALL_TILES: State = [
66,
66,
19,
19,
86,
86,
89,
89,
16,
16,
18,
18,
88,
96,
69,
11,
]
# The goal that each row, column, and diagonal should sum up to
GOAL = 264
def flip(tile: int, flip_map: dict[int, int] = FLIP_MAP) -> int:
"""
Take a tile and flip it. So, 89 will become 68.
Calling it twice should return the original tile. 89->68->89
"""
assert tile > 9 and tile < 100
ones = tile % 10
tens = tile // 10
return flip_map[ones] * 10 + flip_map[tens]
def add_rows(tiles: State) -> list[int]:
"""
Return the sum of all rows
"""
return [sum(tiles[0:4]), sum(tiles[4:8]), sum(tiles[8:12]), sum(tiles[12:16])]
def add_columns(tiles: State) -> list[int]:
"""
Return the sum of all columns
"""
return [
sum(each for i, each in enumerate(tiles) if i % 4 == 0),
sum(each for i, each in enumerate(tiles) if i % 4 == 1),
sum(each for i, each in enumerate(tiles) if i % 4 == 2),
sum(each for i, each in enumerate(tiles) if i % 4 == 3),
]
def add_diagonals(tiles: State) -> list[int]:
"""
Return the sum of the two diagonals
"""
return [
tiles[0] + tiles[5] + tiles[10] + tiles[15],
tiles[12] + tiles[9] + tiles[6] + tiles[3],
]
def score_iter(tiles: State) -> Iterable[int]:
"""
Return a score for each row, column, and diagonal.
The score is the distance away from the goal.
"""
return (
abs(each - GOAL)
for each in itertools.chain(
add_rows(tiles), add_columns(tiles), add_diagonals(tiles)
)
)
def score(tiles: State) -> float:
"Return a sum of how far away from goal for each row, column, and diagonal. This score is divided by the goal."
return sum(each / float(GOAL) for each in score_iter(tiles))
def score_breakdown(tiles: State) -> list[float]:
"Return a list of all the scores"
return list(score_iter(tiles))
def chance(probability: float) -> bool:
"Return True with probability between 1 and 0"
return random.random() < probability
def exchange(
tiles: State,
probability_to_flip: float = 0.5,
probability_to_exchange: float = 0.95,
) -> State:
"Return a new state of tiles after exchanging two and possibly flipping either"
choices = random.sample(range(len(tiles)), k=2)
new_tiles = tiles.copy()
for index in choices:
if chance(probability_to_flip):
new_tiles[index] = flip(new_tiles[index])
if chance(probability_to_exchange):
new_tiles[choices[0]], new_tiles[choices[1]] = (
new_tiles[choices[1]],
new_tiles[choices[0]],
)
return new_tiles
def scramble(tiles: State, probability_to_flip: float = 0.5) -> State:
"Scramble all of the tiles"
result = random.sample(tiles, k=len(tiles))
return [flip(each) if chance(probability_to_flip) else each for each in result]