File size: 2,187 Bytes
4be6a52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Normalized tetromino rotations and the versioned deterministic seven-bag."""

from functools import lru_cache
from random import Random

from stackcraft.schema import PIECES, RULES_VERSION, Cells

_SHAPES: dict[str, Cells] = {
    "I": ((0, 0), (1, 0), (2, 0), (3, 0)),
    "O": ((0, 0), (1, 0), (0, 1), (1, 1)),
    "T": ((1, 0), (0, 1), (1, 1), (2, 1)),
    "S": ((1, 0), (2, 0), (0, 1), (1, 1)),
    "Z": ((0, 0), (1, 0), (1, 1), (2, 1)),
    "J": ((0, 0), (0, 1), (1, 1), (2, 1)),
    "L": ((2, 0), (0, 1), (1, 1), (2, 1)),
}


def _normalize(cells: Cells) -> Cells:
    left = min(x for x, _ in cells)
    top = min(y for _, y in cells)
    return tuple(sorted((x - left, y - top) for x, y in cells))


@lru_cache(maxsize=7)
def rotations(piece: str) -> tuple[Cells, ...]:
    """Return distinct clockwise rotations, normalized to top-left (0, 0)."""
    if piece not in _SHAPES:
        raise ValueError(f"unknown piece: {piece!r}")
    current = _normalize(_SHAPES[piece])
    result: list[Cells] = []
    for _ in range(4):
        if current not in result:
            result.append(current)
        current = _normalize(tuple((-y, x) for x, y in current))
    return tuple(result)


@lru_cache(maxsize=4096)
def _bag(seed: int, index: int) -> tuple[str, ...]:
    # Each bag has its own local PRNG: random access does not depend on call order.
    # Freeze the shuffle algorithm as Fisher-Yates using Random.random(), whose
    # compatible-seeder sequence Python guarantees, rather than randrange().
    rng = Random(f"{RULES_VERSION}:{seed}:{index}")
    bag = list(PIECES)
    for position in range(len(bag) - 1, 0, -1):
        other = int(rng.random() * (position + 1))
        bag[position], bag[other] = bag[other], bag[position]
    return tuple(bag)


def piece_at(seed: int, index: int) -> str:
    """Read an indexed piece without revealing or advancing global RNG state."""
    if type(seed) is not int:
        raise ValueError("seed must be an integer")
    if type(index) is not int or index < 0:
        raise ValueError("piece index must be a nonnegative integer")
    bag_index, offset = divmod(index, len(PIECES))
    return _bag(seed, bag_index)[offset]