Загрузка данных


import random
import numpy as np

WIDTH = 10
HEIGHT = 20

SHAPES = [
    np.array([[1, 1],
              [1, 1]], dtype=np.uint8),

    np.array([[1, 1, 1, 1]], dtype=np.uint8),

    np.array([[0, 1, 0],
              [1, 1, 1]], dtype=np.uint8),

    np.array([[1, 0],
              [1, 0],
              [1, 1]], dtype=np.uint8),

    np.array([[0, 1],
              [0, 1],
              [1, 1]], dtype=np.uint8),

    np.array([[0, 1, 1],
              [1, 1, 0]], dtype=np.uint8),

    np.array([[1, 1, 0],
              [0, 1, 1]], dtype=np.uint8),
]


class TrickySimulator:

    def __init__(self):
        self.reset()

    def reset(self):

        self.board = np.zeros(
            (HEIGHT, WIDTH),
            dtype=np.uint8
        )

        self.score = 0
        self.pieces_placed = 0
        self.lines = 0
        self.game_over = False

        self.spawn_piece()

        return self.get_state()

    # -------------------------
    # FIGURE
    # -------------------------

    def spawn_piece(self):

        self.piece = random.choice(SHAPES).copy()

        self.piece_x = (
            WIDTH // 2
            - self.piece.shape[1] // 2
        )

        self.piece_y = 0

        if self.collision(
            self.piece,
            self.piece_x,
            self.piece_y
        ):
            self.game_over = True

    # -------------------------
    # COLLISION
    # -------------------------

    def collision(self, piece, x, y):

        h, w = piece.shape

        if x < 0:
            return True

        if x + w > WIDTH:
            return True

        if y < 0:
            return True

        if y + h > HEIGHT:
            return True

        region = self.board[
            y:y+h,
            x:x+w
        ]

        return np.any(
            (region & piece) != 0
        )

    # -------------------------
    # MOVEMENT
    # -------------------------

    def move_left(self):

        if not self.collision(
            self.piece,
            self.piece_x - 1,
            self.piece_y
        ):

            self.piece_x -= 1
            return True

        return False

    def move_right(self):

        if not self.collision(
            self.piece,
            self.piece_x + 1,
            self.piece_y
        ):

            self.piece_x += 1
            return True

        return False

    def rotate(self):

        rotated = np.rot90(
            self.piece,
            -1
        )

        # Небольшой wall kick
        for dx in [0, -1, 1, -2, 2]:

            if not self.collision(
                rotated,
                self.piece_x + dx,
                self.piece_y
            ):

                self.piece = rotated
                self.piece_x += dx

                return True

        return False

    # -------------------------
    # FALL
    # -------------------------

    def can_move_down(self):

        return not self.collision(
            self.piece,
            self.piece_x,
            self.piece_y + 1
        )

    def move_down(self):

        if self.can_move_down():

            self.piece_y += 1
            return True

        return False

    # -------------------------
    # PLACE
    # -------------------------

    def lock_piece(self):

        h, w = self.piece.shape

        self.board[
            self.piece_y:self.piece_y+h,
            self.piece_x:self.piece_x+w
        ] |= self.piece

        self.pieces_placed += 1

        # Главная награда:
        # фигура успешно поставлена
        self.score += 10

        self.clear_lines()

        self.spawn_piece()

    # -------------------------
    # LINES
    # -------------------------

    def clear_lines(self):

        full = np.all(
            self.board == 1,
            axis=1
        )

        count = int(np.sum(full))

        if count == 0:
            return

        self.board = self.board[~full]

        empty = np.zeros(
            (count, WIDTH),
            dtype=np.uint8
        )

        self.board = np.vstack([
            empty,
            self.board
        ])

        self.lines += count

        self.score += count * 100

    # -------------------------
    # HEIGHT
    # -------------------------

    def get_column_heights(self):

        heights = []

        for x in range(WIDTH):

            column = self.board[:, x]

            occupied = np.where(
                column == 1
            )[0]

            if len(occupied) == 0:
                heights.append(0)
            else:
                heights.append(
                    HEIGHT - occupied[0]
                )

        return np.array(heights)

    def get_tower_height(self):

        heights = self.get_column_heights()

        return int(np.max(heights))

    # -------------------------
    # HOLES
    # -------------------------

    def count_holes(self):

        holes = 0

        for x in range(WIDTH):

            found = False

            for y in range(HEIGHT):

                if self.board[y, x]:

                    found = True

                elif found:

                    holes += 1

        return holes

    # -------------------------
    # STABILITY
    # -------------------------

    def get_stability(self):

        heights = self.get_column_heights()

        if np.max(heights) == 0:
            return 1.0

        differences = np.abs(
            np.diff(heights)
        )

        roughness = np.sum(
            differences
        )

        stability = 1.0 / (
            1.0 + roughness
        )

        return float(stability)

    # -------------------------
    # STATE
    # -------------------------

    def get_state(self):

        state = self.board.copy()

        if self.game_over:
            return state

        h, w = self.piece.shape

        y1 = self.piece_y
        y2 = min(
            self.piece_y + h,
            HEIGHT
        )

        x1 = self.piece_x
        x2 = min(
            self.piece_x + w,
            WIDTH
        )

        if (
            y1 >= 0
            and x1 >= 0
            and y1 < HEIGHT
            and x1 < WIDTH
        ):

            state[
                y1:y2,
                x1:x2
            ] |= self.piece[
                :y2-y1,
                :x2-x1
            ]

        return state

    # -------------------------
    # STEP
    # -------------------------

    def step(self, action):

        if self.game_over:

            return (
                self.get_state(),
                -100,
                True
            )

        old_height = self.get_tower_height()
        old_holes = self.count_holes()
        old_pieces = self.pieces_placed

        # 0 = ничего
        # 1 = left
        # 2 = right
        # 3 = rotate
        # 4 = down

        if action == 1:
            self.move_left()

        elif action == 2:
            self.move_right()

        elif action == 3:
            self.rotate()

        elif action == 4:
            self.move_down()

        # Гравитация
        if self.can_move_down():

            self.piece_y += 1

        else:

            self.lock_piece()

        new_height = self.get_tower_height()
        new_holes = self.count_holes()

        reward = 0.0

        # Поставили фигуру
        if self.pieces_placed > old_pieces:
            reward += 10

        # Рост башни
        if new_height > old_height:
            reward += (
                new_height - old_height
            ) * 1.0

        # Новые дырки — плохо
        if new_holes > old_holes:
            reward -= (
                new_holes - old_holes
            ) * 2.0

        # Убрали линии
        # clear_lines уже добавил score,
        # здесь добавляем отдельную награду
        if self.lines > 0:
            reward += self.lines * 0.05

        if self.game_over:
            reward -= 100

        return (
            self.get_state(),
            reward,
            self.game_over
        )


# ==================================
# ПЕЧАТЬ ПОЛЯ
# ==================================

def print_board(sim):

    board = sim.get_state()

    print("\n")

    print(
        "+" + "-" * WIDTH + "+"
    )

    for row in board:

        print(
            "|" +
            "".join(
                "#" if x else " "
                for x in row
            ) +
            "|"
        )

    print(
        "+" + "-" * WIDTH + "+"
    )

    print(
        f"score={sim.score} | "
        f"pieces={sim.pieces_placed} | "
        f"lines={sim.lines} | "
        f"height={sim.get_tower_height()} | "
        f"holes={sim.count_holes()} | "
        f"stability={sim.get_stability():.3f}"
    )


# ==================================
# ТЕСТ
# ==================================

if __name__ == "__main__":

    sim = TrickySimulator()

    print("TRICKY TOWERS SIMULATOR")
    print()
    print("a = ←")
    print("d = →")
    print("w = ↑ rotate")
    print("s = ↓")
    print("Enter = ничего")
    print("q = выход")

    print_board(sim)

    while not sim.game_over:

        command = input("> ").lower().strip()

        if command == "q":
            break

        actions = {
            "": 0,
            "a": 1,
            "d": 2,
            "w": 3,
            "s": 4
        }

        action = actions.get(command, 0)

        state, reward, done = sim.step(action)

        print(
            f"reward = {reward:.2f}"
        )

        print_board(sim)

        if done:

            print()
            print("========== GAME OVER ==========")
            print(
                "Итоговый score:",
                sim.score
            )
            print(
                "Поставлено фигур:",
                sim.pieces_placed
            )
            print(
                "Линий:",
                sim.lines
            )
            break