Загрузка данных
import random
import numpy as np
WIDTH = 10
HEIGHT = 24
CELL = 1.0
# 7 фигур Tricky Towers / стандартных тетрамино.
SHAPES = {
"S": [
[(1, 0), (2, 0), (0, 1), (1, 1)]
],
"Z": [
[(0, 0), (1, 0), (1, 1), (2, 1)]
],
"O": [
[(0, 0), (1, 0), (0, 1), (1, 1)]
],
"I": [
[(0, 0), (1, 0), (2, 0), (3, 0)]
],
"J": [
[(0, 0), (0, 1), (1, 1), (2, 1)]
],
"L": [
[(2, 0), (0, 1), (1, 1), (2, 1)]
],
"T": [
[(0, 0), (1, 0), (2, 0), (1, 1)]
],
}
class TrickySimulator:
def __init__(self, seed=None):
self.rng = random.Random(seed)
self.reset()
def reset(self):
self.grid = np.zeros((HEIGHT, WIDTH), dtype=np.int8)
self.score = 0
self.pieces = 0
self.game_over = False
self.current_name = None
self.current_shape = None
self.x = 0
self.y = 0
self.rotation = 0
self.previous_height = 0
self.max_height = 0
self.spawn_piece()
return self.get_state()
# ---------------------------------------------------------
# PIECES
# ---------------------------------------------------------
def spawn_piece(self):
self.current_name = self.rng.choice(list(SHAPES.keys()))
self.current_shape = [
list(p) for p in SHAPES[self.current_name][0]
]
self.rotation = 0
min_x = min(x for x, y in self.current_shape)
max_x = max(x for x, y in self.current_shape)
piece_width = max_x - min_x + 1
self.x = (WIDTH - piece_width) // 2 - min_x
self.y = 0
if self.collision(self.current_shape, self.x, self.y):
self.game_over = True
def rotated_shape(self):
# Поворот вокруг условного центра.
points = self.current_shape
xs = [p[0] for p in points]
ys = [p[1] for p in points]
cx = (min(xs) + max(xs)) / 2
cy = (min(ys) + max(ys)) / 2
result = []
for x, y in points:
dx = x - cx
dy = y - cy
nx = round(cx - dy)
ny = round(cy + dx)
result.append((nx, ny))
min_x = min(x for x, y in result)
min_y = min(y for x, y in result)
result = [
(x - min_x, y - min_y)
for x, y in result
]
return result
# ---------------------------------------------------------
# COLLISION
# ---------------------------------------------------------
def collision(self, shape, px, py):
for x, y in shape:
gx = px + x
gy = py + y
if gx < 0 or gx >= WIDTH:
return True
if gy >= HEIGHT:
return True
if gy >= 0 and self.grid[gy, gx]:
return True
return False
# ---------------------------------------------------------
# MOVEMENT
# ---------------------------------------------------------
def move_left(self):
if not self.collision(
self.current_shape,
self.x - 1,
self.y
):
self.x -= 1
return True
return False
def move_right(self):
if not self.collision(
self.current_shape,
self.x + 1,
self.y
):
self.x += 1
return True
return False
def rotate(self):
new_shape = self.rotated_shape()
if not self.collision(
new_shape,
self.x,
self.y
):
self.current_shape = new_shape
self.rotation = (self.rotation + 1) % 4
return True
# Простое wall-kick поведение
for dx in [-1, 1, -2, 2]:
if not self.collision(
new_shape,
self.x + dx,
self.y
):
self.x += dx
self.current_shape = new_shape
self.rotation = (self.rotation + 1) % 4
return True
return False
def move_down(self):
if not self.collision(
self.current_shape,
self.x,
self.y + 1
):
self.y += 1
return True
self.lock_piece()
return False
# ---------------------------------------------------------
# LOCK
# ---------------------------------------------------------
def lock_piece(self):
for x, y in self.current_shape:
gx = self.x + x
gy = self.y + y
if 0 <= gx < WIDTH and 0 <= gy < HEIGHT:
self.grid[gy, gx] = 1
self.pieces += 1
self.clear_lines()
self.spawn_piece()
# ---------------------------------------------------------
# LINES
# ---------------------------------------------------------
def clear_lines(self):
full = np.all(self.grid == 1, axis=1)
count = int(np.sum(full))
if count > 0:
self.score += count * 100
remaining = self.grid[~full]
new_grid = np.zeros_like(self.grid)
new_grid[-len(remaining):] = remaining
self.grid = new_grid
# ---------------------------------------------------------
# TOWER ANALYSIS
# ---------------------------------------------------------
def column_heights(self):
heights = []
for x in range(WIDTH):
ys = np.where(self.grid[:, x] > 0)[0]
if len(ys) == 0:
heights.append(0)
else:
heights.append(HEIGHT - ys[0])
return heights
def tower_height(self):
heights = self.column_heights()
return max(heights)
def holes(self):
result = 0
for x in range(WIDTH):
ys = np.where(self.grid[:, x] > 0)[0]
if len(ys) == 0:
continue
top = ys[0]
result += int(
np.sum(self.grid[top:, x] == 0)
)
return result
def bumpiness(self):
h = self.column_heights()
return sum(
abs(h[i] - h[i + 1])
for i in range(WIDTH - 1)
)
def center_of_mass(self):
ys, xs = np.where(self.grid > 0)
if len(xs) == 0:
return WIDTH / 2
return float(np.mean(xs))
def stability(self):
h = self.column_heights()
if max(h) == 0:
return 1.0
# Чем сильнее разница между соседними колонками,
# тем менее устойчива башня.
b = self.bumpiness()
value = 1.0 - b / (WIDTH * HEIGHT)
return max(0.0, min(1.0, value))
# ---------------------------------------------------------
# STATE
# ---------------------------------------------------------
def get_state(self):
heights = self.column_heights()
normalized_heights = [
h / HEIGHT for h in heights
]
shape_mask = np.zeros(16, dtype=np.float32)
for x, y in self.current_shape:
ix = y * 4 + x
if 0 <= ix < 16:
shape_mask[ix] = 1.0
state = np.array(
normalized_heights
+ list(shape_mask)
+ [
self.x / WIDTH,
self.y / HEIGHT,
self.tower_height() / HEIGHT,
self.holes() / 100.0,
self.bumpiness() / 50.0,
self.stability(),
self.rotation / 4.0,
],
dtype=np.float32
)
return state
# ---------------------------------------------------------
# REWARD
# ---------------------------------------------------------
def calculate_reward(
self,
old_height,
old_holes,
old_bumpiness,
old_stability
):
height = self.tower_height()
holes = self.holes()
bump = self.bumpiness()
stability = self.stability()
reward = 0.0
# Выживание
reward += 0.02
# Чем меньше дыр
reward += (old_holes - holes) * 0.8
# Стараемся не создавать резких ступеней
reward += (old_bumpiness - bump) * 0.08
# Устойчивость
reward += (stability - old_stability) * 2.0
# Слишком быстро растущая башня опасна
if height > old_height:
reward -= (height - old_height) * 0.04
# Очистка линии
if self.score > 0:
reward += 0.1
# Проигрыш
if self.game_over:
reward -= 15.0
return reward
# ---------------------------------------------------------
# STEP
# ---------------------------------------------------------
def step(self, action):
if self.game_over:
return self.get_state(), -15.0, True
old_height = self.tower_height()
old_holes = self.holes()
old_bumpiness = self.bumpiness()
old_stability = self.stability()
# 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:
# ↓ ускоряет падение
for _ in range(2):
if not self.move_down():
break
else:
# Естественная гравитация
self.move_down()
reward = self.calculate_reward(
old_height,
old_holes,
old_bumpiness,
old_stability
)
return self.get_state(), reward, self.game_over
# ---------------------------------------------------------
# DEBUG
# ---------------------------------------------------------
def render(self):
board = self.grid.copy()
if not self.game_over:
for x, y in self.current_shape:
gx = self.x + x
gy = self.y + y
if (
0 <= gx < WIDTH
and 0 <= gy < HEIGHT
):
board[gy, gx] = 2
print("\n" + "=" * (WIDTH + 2))
for y in range(HEIGHT):
line = ""
for x in range(WIDTH):
if board[y, x] == 2:
line += "@"
elif board[y, x] == 1:
line += "#"
else:
line += "."
print("|" + line + "|")
print("=" * (WIDTH + 2))
print(
"piece:",
self.current_name,
"x:",
self.x,
"y:",
self.y,
"rotation:",
self.rotation
)
print(
"score:",
self.score,
"pieces:",
self.pieces,
"height:",
self.tower_height(),
"holes:",
self.holes(),
"stability:",
round(self.stability(), 3)
)
if __name__ == "__main__":
env = TrickySimulator()
print("Simulator started.")
for i in range(30):
action = random.randint(0, 4)
state, reward, done = env.step(action)
if i % 5 == 0:
env.render()
if done:
print("GAME OVER")
break