260 lines
8.6 KiB
Python
260 lines
8.6 KiB
Python
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()
|