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()