fluepdot-scripts/tetris/tetris.py

261 lines
8.6 KiB
Python
Raw Normal View History

import random
import time
from fluepdot import Fluepdot, Mode
from dotenv import dotenv_values
from logging import getLogger, basicConfig
#from keys import KeyPoller
# replacement for own keypoller using pynput
from pynput import keyboard
def get_on_press(tetris):
def on_press(key):
if key == keyboard.Key.esc:
tetris.running = False
return False # stop listener
try:
k = key.char
except:
k = key.name
match k:
case "q":
tetris.rotate(True)
case "e":
tetris.rotate()
case "a":
tetris.shift(-1)
case "d":
tetris.shift(+1)
case "space":
tetris.drop()
case _:
print(f'Key pressed: {k}')
return on_press
config = {
"LOGLEVEL": 'INFO',
"TERMINAL_OUT": "False",
"DISPLAY": "True",
**dotenv_values()
}
tout = config["TERMINAL_OUT"].lower() not in ['false', '0']
display_out = config["DISPLAY"].lower() not in ['false', '0']
log = getLogger("Fluedot-Tetris")
basicConfig(level=config["LOGLEVEL"].upper())
log.info(f'Fluepdot-Tetris loading with log level {config["LOGLEVEL"]}')
log.info(f'Terminal output is {"enabled" if tout else "disabled"}')
NUMS = [[[1, 1, 1], [1], [1, 1, 1], [1, 1, 1], [1, 0, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]],
[[1, 0, 1], [1], [0, 0, 1], [0, 0, 1], [1, 0, 1], [1, 0, 0], [1, 0, 0], [0, 0, 1], [1, 0, 1], [1, 0, 1]],
[[1, 0, 1], [1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 1, 1]],
[[1, 0, 1], [1], [1, 0, 0], [0, 0, 1], [0, 0, 1], [0, 0, 1], [1, 0, 1], [0, 0, 1], [1, 0, 1], [0, 0, 1]],
[[1, 1, 1], [1], [1, 1, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 1, 1], [0, 0, 1], [1, 1, 1], [1, 1, 1]]]
# Game settings
WIDTH = 16
HEIGHT = 45
# Define Tetris shapes
# TODO: Implement rotation like TETЯIS
SHAPES = [
[[1, 1, 1, 1]], # I Shape
[[1, 1], [1, 1]], # O Shape
[[0, 1, 0], [1, 1, 1]], # T Shape
[[1, 1, 0], [0, 1, 1]], # S Shape
[[0, 1, 1], [1, 1, 0]], # Z Shape
[[1, 0, 0], [1, 1, 1]], # L Shape
[[0, 0, 1], [1, 1, 1]] # J Shape
]
def _or(a, b):
return a | b
def _and(a, b):
return a & b
def _2d_boards_apply(board1, board2, func=_or):
rows1, cols1 = len(board1), len(board1[0]) if board1 else 0
rows2, cols2 = len(board2), len(board2[0]) if board2 else 0
rows = max(rows1, rows2)
cols = max(cols1, cols2)
result = [[0] * cols for _ in range(rows)]
for i in range(rows):
for j in range(cols):
value1 = board1[i][j] if i < rows1 and j < cols1 else 0
value2 = board2[i][j] if i < rows2 and j < cols2 else 0
result[i][j] = func(value1, value2)
return result
class Tetris:
def __init__(self, address):
self.running = False
self.fd = Fluepdot(address)
if display_out:
self.fd_size = self.fd.get_size()
else:
self.fd_size = [100, 16]
log.debug(f"fluepdot size is {self.fd_size}")
if display_out:
self.fd.set_mode(Mode.DIFFERENTIAL)
self.level = 0
self.board = [[0] * WIDTH for _ in range(HEIGHT)]
self.current_piece = None
self.current_position = (0, 0)
self.next_piece = random.choice(SHAPES)
self.padding = None
self.score = 0
def get_score_board(self):
sb = [[]] * 5
num = self.score
while num > 0:
d = num % 10
num //= 10
for i in range(len(NUMS)):
sb[i] = NUMS[i][d] + [0] + sb[i]
return sb
def get_padding(self):
if HEIGHT < self.fd_size[0]:
pad = self.fd_size[0] - HEIGHT - 1
padding = [[0] * WIDTH] * pad + [[1] * WIDTH]
if pad > 4:
padding = _2d_boards_apply(padding, self.offset_piece(1, 1, self.next_piece))
if pad > 10:
sb = self.get_score_board()
if len(sb[0]) <= 16:
padding = _2d_boards_apply(padding, self.offset_piece(WIDTH - len(sb[0]) - 1, 4, sb))
self.padding = list(reversed(list(map(list, zip(*padding)))))
else:
self.padding = [[0] * WIDTH]
def new_piece(self):
self.current_piece = self.next_piece
# NES Tetris randomizer
# (8 options, if equal to prev or reroll -> 7 options, final)
self.next_piece = random.choice(SHAPES + [[]])
if not self.next_piece or self.next_piece == self.current_piece:
self.next_piece = random.choice(SHAPES)
self.get_padding()
self.current_position = (WIDTH // 2 - len(self.current_piece[0]) // 2, 0)
def offset_piece(self, x, y, piece=None):
if not piece:
piece = self.current_piece
piece = [[0] * x + l for l in piece]
return [[0] * len(piece[0])] * y + piece
def draw_board(self):
piece = self.offset_piece(*self.current_position)
board = _2d_boards_apply(piece, self.board)
board = list(reversed(list(map(list, zip(*board)))))
board = [p + f for p, f in zip(self.padding, board)]
if display_out:
self.fd.post_frame([[v == 1 for v in l] for l in board])
if tout:
for l in board:
print("." + "".join(['X' if x else ' ' for x in l]) + ".")
print('')
def rotate(self, reversed_=False):
for l in self.current_piece:
log.debug(l)
if reversed_:
direction = "counter clockwise"
self.current_piece = list(reversed(list(map(list, zip(*self.current_piece)))))
else:
direction = "clockwise"
self.current_piece = list(map(list, map(reversed, list(map(list, zip(*self.current_piece))))))
log.debug(f"Rotate piece {direction}.")
def shift(self, direction: int = 0) -> bool:
x, y = self.current_position
max_x = WIDTH - len(self.current_piece[0])
x = max(0, min(max_x, x + direction))
log.debug(f"Shifting the piece {'left' if direction == -1 else 'right'}. {direction}")
if not self.collide((x, y)):
self.current_position = (x, y)
return True
return False
def drop(self):
log.debug(f"Dropping the piece.")
x, y = self.current_position
oy = y
y_offset = HEIGHT - y + 1
while not (self.collide(pos=(x, y + y_offset)) and not self.collide(pos=(x, y + y_offset - 1))):
log.debug(f"{y_offset=}, {y + y_offset}")
if self.collide(pos=(x, y + y_offset)):
y_offset //= 2
else:
y += y_offset // 2
log.debug(f"Found the drop offset.")
self.current_position = (x, y + y_offset)
self.score += y + y_offset - oy # TODO: softdrop 1 point for each cell, harddrop 2
self.step()
def step(self):
log.debug(f"Runing a step.")
if self.collide():
if self.current_position[1] == 0:
return False
self.board = _2d_boards_apply(self.board, self.offset_piece(*self.current_position))
self.clears()
self.new_piece()
else:
self.current_position = (self.current_position[0], self.current_position[1] + 1)
return True
def collide(self, pos=None):
if not pos:
x, y = self.current_position
else:
x, y = pos
y += 1
piece = self.offset_piece(x, y)
if y + len(self.current_piece) > HEIGHT:
return True
board = _2d_boards_apply(piece, self.board, _and)
return any(map(any, board))
def clears(self):
rows = sum(all(l) for l in self.board)
if rows > 0:
self.score += [40, 100, 300, 1200][rows - 1] * (self.level + 1)
self.board = [[0] * WIDTH] * rows + [l for l in self.board if not all(l)]
def end_game(self):
log.info(f"Game ended. Score: {self.score}")
if display_out:
self.fd.set_mode(Mode.FULL)
self.fd.post_text(f"Score: {self.score}")
def run(self):
self.new_piece()
#log.debug(self.current_piece)
self.running = True
listener = keyboard.Listener(on_press=get_on_press(self))
listener.start()
while self.running:
self.running = self.step()
self.draw_board()
time.sleep(.5) # Game loop delay
self.end_game()
if __name__ == "__main__":
log.info(f'Connecting to fluepdot at {config["DOTS_HOST"]}')
game = Tetris(config["DOTS_HOST"])
game.run()