Загрузка данных
import random
import numpy as np
WIDTH = 10
HEIGHT = 20
# Фигуры
SHAPES = [
# O
np.array([
[1, 1],
[1, 1]
], dtype=np.uint8),
# I
np.array([
[1, 1, 1, 1]
], dtype=np.uint8),
# T
np.array([
[0, 1, 0],
[1, 1, 1]
], dtype=np.uint8),
# L
np.array([
[1, 0],
[1, 0],
[1, 1]
], dtype=np.uint8),
# J
np.array([
[0, 1],
[0, 1],
[1, 1]
], dtype=np.uint8),
# S
np.array([
[0, 1, 1],
[1, 1, 0]
], dtype=np.uint8),
# Z
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()
# ==========================================
# СОЗДАНИЕ НОВОЙ ФИГУРЫ
# ==========================================
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
# ==========================================
# ПРОВЕРКА СТОЛКНОВЕНИЯ
# ==========================================
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
# Область поля под фигурой
area = self.board[
y:y + h,
x:x + w
]
return np.any(
(area == 1) &
(piece == 1)
)
# ==========================================
# ДВИЖЕНИЕ ВЛЕВО
# ==========================================
def move_left(self):
new_x = self.piece_x - 1
if not self.collision(
self.piece,
new_x,
self.piece_y
):
self.piece_x = new_x
return True
return False
# ==========================================
# ДВИЖЕНИЕ ВПРАВО
# ==========================================
def move_right(self):
new_x = self.piece_x + 1
if not self.collision(
self.piece,
new_x,
self.piece_y
):
self.piece_x = new_x
return True
return False
# ==========================================
# ПОВОРОТ
# ==========================================
def rotate(self):
rotated = np.rot90(
self.piece,
-1
)
# Пробуем немного сдвинуть фигуру,
# если возле стены она не помещается
for dx in [0, -1, 1, -2, 2]:
new_x = self.piece_x + dx
if not self.collision(
rotated,
new_x,
self.piece_y
):
self.piece = rotated
self.piece_x = new_x
return True
return False
# ==========================================
# МОЖЕТ ЛИ ФИГУРА ПАДАТЬ
# ==========================================
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
# ==========================================
# ЗАФИКСИРОВАТЬ ФИГУРУ
# ==========================================
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()
# ==========================================
# УДАЛЕНИЕ ЗАПОЛНЕННЫХ ЛИНИЙ
# ==========================================
def clear_lines(self):
full_rows = np.all(
self.board == 1,
axis=1
)
count = int(
np.sum(full_rows)
)
if count == 0:
return
self.board = self.board[
~full_rows
]
empty_rows = np.zeros(
(count, WIDTH),
dtype=np.uint8
)
self.board = np.vstack([
empty_rows,
self.board
])
self.lines += count
self.score += count * 100
# ==========================================
# ВЫСОТА КАЖДОГО СТОЛБЦА
# ==========================================
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,
dtype=np.float32
)
# ==========================================
# ВЫСОТА БАШНИ
# ==========================================
def get_tower_height(self):
heights = self.get_column_heights()
return int(
np.max(heights)
)
# ==========================================
# КОЛИЧЕСТВО ДЫР
# ==========================================
def count_holes(self):
holes = 0
for x in range(WIDTH):
block_found = False
for y in range(HEIGHT):
if self.board[y, x]:
block_found = True
elif block_found:
holes += 1
return holes
# ==========================================
# СТАБИЛЬНОСТЬ БАШНИ
# ==========================================
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
)
return float(
1.0 / (1.0 + roughness)
)
# ==========================================
# СОСТОЯНИЕ ДЛЯ AI
# ==========================================
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
# ==========================================
# ОДИН ШАГ ИГРЫ
# ==========================================
def step(self, action):
if self.game_over:
return (
self.get_state(),
-100.0,
True
)
old_height = self.get_tower_height()
old_holes = self.count_holes()
old_pieces = self.pieces_placed
old_lines = self.lines
# 0 = ничего
# 1 = влево
# 2 = вправо
# 3 = поворот
# 4 = вниз
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.0
# Рост башни
if new_height > old_height:
reward += (
new_height - old_height
) * 1.0
# Появились новые дыры
if new_holes > old_holes:
reward -= (
new_holes - old_holes
) * 2.0
# Очистили линии
if self.lines > old_lines:
reward += (
self.lines - old_lines
) * 100.0
# Проигрыш
if self.game_over:
reward -= 100.0
return (
self.get_state(),
reward,
self.game_over
)
# ==========================================
# ПЕЧАТЬ ПОЛЯ
# ==========================================
def print_board(sim):
board = sim.get_state()
print()
print(
"+" + "-" * WIDTH + "+"
)
for row in board:
print(
"|" +
"".join(
"#" if cell else " "
for cell 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("================================")
print(" TRICKY TOWERS SIM")
print("================================")
print()
print("a = ←")
print("d = →")
print("w = ↑ поворот")
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
)
_, reward, done = sim.step(
action
)
print(
f"reward = {reward:.2f}"
)
print_board(sim)
if done:
print()
print("==============================")
print(" GAME OVER")
print("==============================")
print(
"score:",
sim.score
)
print(
"pieces:",
sim.pieces_placed
)
print(
"lines:",
sim.lines
)
break