Files
project_game/cursor/tetris.py
Chris Proctor c8d1ddd58f Started retro tetris game
Started developing a retro version of Tetris, following
the planning on the board of the classroom
(see planning.jpg).

Moved cursor work into cursor.
2025-12-18 17:45:39 -05:00

123 lines
3.8 KiB
Python

import random
class Tetris:
W = 10
H = 20
PIECES = {
'I': [[(0,1),(1,1),(2,1),(3,1)], [(2,0),(2,1),(2,2),(2,3)]],
'O': [[(1,0),(2,0),(1,1),(2,1)]],
'T': [[(1,0),(0,1),(1,1),(2,1)], [(1,0),(1,1),(2,1),(1,2)], [(0,1),(1,1),(2,1),(1,2)], [(1,0),(0,1),(1,1),(1,2)]],
'L': [[(2,0),(0,1),(1,1),(2,1)], [(1,0),(1,1),(1,2),(2,2)], [(0,1),(1,1),(2,1),(0,2)], [(0,0),(1,0),(1,1),(1,2)]],
'J': [[(0,0),(0,1),(1,1),(2,1)], [(1,0),(2,0),(1,1),(1,2)], [(0,1),(1,1),(2,1),(2,2)], [(1,0),(1,1),(1,2),(0,2)]],
'S': [[(1,0),(2,0),(0,1),(1,1)], [(1,0),(1,1),(2,1),(2,2)]],
'Z': [[(0,0),(1,0),(1,1),(2,1)], [(2,0),(1,1),(2,1),(1,2)]],
}
def __init__(self, agents=None, state=None):
self.score = 0
self.over = False
self.grid = [[0] * self.W for _ in range(self.H)]
self.rng = random.Random(0)
self.next_piece = self._rand_piece()
self._spawn()
self.last_render = ''
def _rand_piece(self):
return self.rng.choice(list(self.PIECES.keys()))
def _spawn(self):
self.piece = self.next_piece
self.next_piece = self._rand_piece()
self.rot = 0
self.px = (self.W // 2) - 2
self.py = 0
if not self._fits(self.px, self.py, self.rot):
self.over = True
def _cells(self, px, py, rot):
rstates = self.PIECES[self.piece]
r = rot % len(rstates)
return [(px + x, py + y) for (x, y) in rstates[r]]
def _fits(self, px, py, rot):
for x, y in self._cells(px, py, rot):
if x < 0 or x >= self.W or y < 0 or y >= self.H:
return False
if self.grid[y][x]:
return False
return True
def _lock(self):
for x, y in self._cells(self.px, self.py, self.rot):
if 0 <= y < self.H and 0 <= x < self.W:
self.grid[y][x] = 1
# clear lines
newg = [row for row in self.grid if not all(row)]
cleared = self.H - len(newg)
if cleared:
for _ in range(cleared):
newg.insert(0, [0] * self.W)
self.grid = newg
self.score += 100 * cleared
self._spawn()
# API methods used by tests / agent
def step(self, action=0):
# actions: 0 noop, 1 left, 2 right, 3 rotate, 4 soft drop
if self.over:
return
if action == 1:
if self._fits(self.px - 1, self.py, self.rot):
self.px -= 1
elif action == 2:
if self._fits(self.px + 1, self.py, self.rot):
self.px += 1
elif action == 3:
if self._fits(self.px, self.py, self.rot + 1):
self.rot += 1
elif action == 4:
if self._fits(self.px, self.py + 1, self.rot):
self.py += 1
else:
self._lock()
return
# gravity
if self._fits(self.px, self.py + 1, self.rot):
self.py += 1
else:
self._lock()
def is_over(self):
return bool(self.over)
def get_score(self):
return int(self.score)
# rendering stubs (retro compatibility)
def clear(self):
pass
def draw_board(self):
lines = []
occupied = set(self._cells(self.px, self.py, self.rot)) if not self.over else set()
for y in range(self.H):
row = ''
for x in range(self.W):
if (x, y) in occupied:
row += '[]'
else:
row += '##' if self.grid[y][x] else '..'
lines.append(row)
self.last_render = '\n'.join(lines)
return self.last_render
def draw_piece(self):
return
def refresh(self):
return
def reset(self):
self.__init__()