File size: 3,316 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""Pure, deterministic vertical-placement falling-block game rules."""

from dataclasses import replace

from stackcraft.pieces import piece_at, rotations
from stackcraft.schema import COLORS, HEIGHT, WIDTH, Board, Cells, GameState, Placement, Transition

_SCORES = (0, 100, 300, 500, 800)


def new_game(seed: int) -> GameState:
    return GameState(
        board=tuple((0,) * WIDTH for _ in range(HEIGHT)),
        seed=seed,
        piece_index=0,
        current=piece_at(seed, 0),
        next_piece=piece_at(seed, 1),
    )


def _fits(state: GameState, cells: Cells, x: int, y: int) -> bool:
    return all(
        0 <= x + dx < WIDTH and 0 <= y + dy < HEIGHT and state.board[y + dy][x + dx] == 0
        for dx, dy in cells
    )


def legal_actions(state: GameState) -> tuple[Placement, ...]:
    """Enumerate rotation then column; each move starts fully inside row zero."""
    if state.terminal:
        return ()
    result = []
    for rotation, cells in enumerate(rotations(state.current)):
        width = max(x for x, _ in cells) + 1
        for x in range(WIDTH - width + 1):
            if not _fits(state, cells, x, 0):
                continue
            y = 0
            while _fits(state, cells, x, y + 1):
                y += 1
            absolute = tuple((x + dx, y + dy) for dx, dy in cells)
            result.append(Placement(f"r{rotation}x{x}", rotation, x, y, absolute))
    return tuple(result)


def place(board: Board, piece: str, action: Placement) -> tuple[Board, int]:
    """Place a previously validated action without reading any future pieces.

    Callers must supply an action from legal_actions for this board and piece.
    This low-level helper checks occupied/boundary cells but not hard-drop paths;
    user-controlled IDs must go through step instead.
    """
    if piece not in COLORS:
        raise ValueError(f"unknown piece: {piece!r}")
    if len(set(action.cells)) != 4 or any(
        not (0 <= x < WIDTH and 0 <= y < HEIGHT) or board[y][x] for x, y in action.cells
    ):
        raise ValueError("placement cells must be four distinct empty in-bounds cells")
    rows = [list(row) for row in board]
    for x, y in action.cells:
        rows[y][x] = COLORS[piece]
    remaining = [tuple(row) for row in rows if not all(row)]
    cleared = HEIGHT - len(remaining)
    if cleared > 4:
        raise ValueError("invalid starting board: more than four completed rows")
    return tuple([(0,) * WIDTH] * cleared + remaining), cleared


def step(state: GameState, action_id: str) -> Transition:
    """Validate, place, clear simultaneously, advance, and detect next top-out."""
    action = next((move for move in legal_actions(state) if move.id == action_id), None)
    if action is None:
        raise ValueError(f"illegal action {action_id!r}")
    board, cleared = place(state.board, state.current, action)
    next_state = GameState(
        board=board,
        seed=state.seed,
        piece_index=state.piece_index + 1,
        current=state.next_piece,
        next_piece=piece_at(state.seed, state.piece_index + 2),
        score=state.score + _SCORES[cleared],
        lines=state.lines + cleared,
    )
    if not legal_actions(next_state):
        next_state = replace(next_state, terminal=True)
    return Transition(next_state, action, cleared)