#!/usr/bin/env python3 # -*- coding: utf-8 -*- import random from qtpy import QtCore, QtGui from . import consts from .consts import L, R, U, D, CLOCKWISE, COUNTERCLOCKWISE from .point import Point class Block: """ Mino or block Mino : A single square-shaped building block of a shape called a Tetrimino. Four Minos arranged into any of their various connected patterns is known as a Tetrimino Block : A single block locked in a cell in the Grid """ # Colors BORDER_COLOR = consts.BLOCK_BORDER_COLOR FILL_COLOR = consts.BLOCK_FILL_COLOR GLOWING_BORDER_COLOR = consts.BLOCK_GLOWING_BORDER_COLOR GLOWING_FILL_COLOR = consts.BLOCK_GLOWING_FILL_COLOR LIGHT_COLOR = consts.BLOCK_LIGHT_COLOR TRANSPARENT = consts.BLOCK_TRANSPARENT GLOWING = consts.BLOCK_GLOWING side = consts.BLOCK_INITIAL_SIDE def __init__(self, coord, trail=0): self.coord = coord self.trail = trail self.border_color = self.BORDER_COLOR self.fill_color = self.FILL_COLOR self.glowing = self.GLOWING def paint(self, painter, top_left_corner, spotlight): p = top_left_corner + self.coord * Block.side block_center = Point(Block.side / 2, Block.side / 2) self.center = p + block_center spotlight = top_left_corner + Block.side * spotlight + block_center self.glint = 0.15 * spotlight + 0.85 * self.center if self.trail: start = ( top_left_corner + (self.coord + Point(0, self.trail * U)) * Block.side ) stop = top_left_corner + (self.coord + Point(0, 2 * D)) * Block.side fill = QtGui.QLinearGradient(start, stop) fill.setColorAt(0, self.LIGHT_COLOR) fill.setColorAt(1, self.GLOWING_FILL_COLOR) painter.setBrush(fill) painter.setPen(QtCore.Qt.NoPen) painter.drawRoundedRect( start.x, start.y, Block.side, Block.side * (1 + self.trail), 20, 20, QtCore.Qt.RelativeSize, ) if self.glowing: fill = QtGui.QRadialGradient(self.center, self.glowing * Block.side) fill.setColorAt(0, self.TRANSPARENT) fill.setColorAt(0.5 / self.glowing, self.LIGHT_COLOR) fill.setColorAt(1, self.TRANSPARENT) painter.setBrush(QtGui.QBrush(fill)) painter.setPen(QtCore.Qt.NoPen) painter.drawEllipse( self.center.x - self.glowing * Block.side, self.center.y - self.glowing * Block.side, 2 * self.glowing * Block.side, 2 * self.glowing * Block.side, ) painter.setBrush(self.brush()) painter.setPen(self.pen()) painter.drawRoundedRect( p.x + 1, p.y + 1, Block.side - 2, Block.side - 2, 20, 20, QtCore.Qt.RelativeSize, ) def brush(self): if self.fill_color is None: return QtCore.Qt.NoBrush fill = QtGui.QRadialGradient(self.glint, 1.5 * Block.side) fill.setColorAt(0, self.fill_color.lighter()) fill.setColorAt(1, self.fill_color) return QtGui.QBrush(fill) def pen(self): if self.border_color is None: return QtCore.Qt.NoPen border = QtGui.QRadialGradient(self.glint, Block.side) border.setColorAt(0, self.border_color.lighter()) border.setColorAt(1, self.border_color.darker()) return QtGui.QPen(QtGui.QBrush(border), 1, join=QtCore.Qt.RoundJoin) def shine(self, glowing=2, delay=None): self.border_color = Block.GLOWING_BORDER_COLOR self.fill_color = Block.GLOWING_FILL_COLOR self.glowing = glowing if delay: QtCore.QTimer.singleShot(delay, self.fade) def fade(self): self.border_color = Block.BORDER_COLOR self.fill_color = Block.FILL_COLOR self.glowing = 0 self.trail = 0 class GhostBlock(Block): """ Mino of the ghost piece """ BORDER_COLOR = consts.GHOST_BLOCK_BORDER_COLOR FILL_COLOR = consts.GHOST_BLOCK_FILL_COLOR GLOWING_FILL_COLOR = consts.GHOST_BLOCK_GLOWING_FILL_COLOR GLOWING = consts.GHOST_BLOCK_GLOWING class MetaTetro(type): """ Save the different shapes of Tetrominoes """ def __init__(cls, name, bases, dico): type.__init__(cls, name, bases, dico) Tetromino.classes.append(cls) Tetromino.nb_classes += 1 class Tetromino: """ Geometric Tetris® shape formed by four Minos connected along their sides. A total of seven possible Tetriminos can be made using four Minos. """ COORDS = NotImplemented SUPER_ROTATION_SYSTEM = ( { COUNTERCLOCKWISE: ((0, 0), (R, 0), (R, U), (0, 2 * D), (R, 2 * D)), CLOCKWISE: ((0, 0), (L, 0), (L, U), (0, 2 * D), (L, 2 * D)), }, { COUNTERCLOCKWISE: ((0, 0), (R, 0), (R, D), (0, 2 * U), (R, 2 * U)), CLOCKWISE: ((0, 0), (R, 0), (R, D), (0, 2 * U), (R, 2 * U)), }, { COUNTERCLOCKWISE: ((0, 0), (L, 0), (L, U), (0, 2 * D), (L, 2 * D)), CLOCKWISE: ((0, 0), (R, 0), (R, U), (0, 2 * D), (R, 2 * D)), }, { COUNTERCLOCKWISE: ((0, 0), (L, 0), (L, D), (0, 2 * U), (L, 2 * U)), CLOCKWISE: ((0, 0), (L, 0), (L, D), (0, 2 * D), (L, 2 * U)), }, ) classes = [] nb_classes = 0 random_bag = [] def __new__(cls): """ Return a Tetromino using the 7-bag Random Generator Tetris uses a “bag” system to determine the sequence of Tetriminos that appear during game play. This system allows for equal distribution among the seven Tetriminos. The seven different Tetriminos are placed into a virtual bag, then shuffled into a random order. This order is the sequence that the bag “feeds” the Next Queue. Every time a new Tetrimino is generated and starts its fall within the Matrix, the Tetrimino at the front of the line in the bag is placed at the end of the Next Queue, pushing all Tetriminos in the Next Queue forward by one. The bag is refilled and reshuffled once it is empty. """ if not cls.random_bag: cls.random_bag = random.sample(cls.classes, cls.nb_classes) return super().__new__(cls.random_bag.pop()) def __init__(self): self.orientation = 0 self.t_spin = "" def insert_into(self, matrix, position): self.matrix = matrix self.minoes = tuple(Block(Point(*coord) + position) for coord in self.COORDS) def _try_movement(self, next_coords_generator, trail=0, update=True): """ Test if self can fit in the Grid with new coordinates, i.e. all cells are empty. If it can, change self's coordinates and return True. Else, make no changes and return False Update the Grid if there is no drop trail """ futures_coords = [] for p in next_coords_generator: if not self.matrix.is_empty_cell(p): return False futures_coords.append(p) for block, future_coord in zip(self.minoes, futures_coords): block.coord = future_coord block.trail = trail if update: self.matrix.update() return True def move(self, horizontally, vertically, trail=0, update=True): """ Try to translate self horizontally or vertically The Tetrimino in play falls from just above the Skyline one cell at a time, and moves left and right one cell at a time. Each Mino of a Tetrimino “snaps” to the appropriate cell position at the completion of a move, although intermediate Tetrimino movement appears smooth. Only right, left, and downward movement are allowed. Movement into occupied cells and Matrix walls and floors is not allowed Update the Grid if there is no drop trail """ return self._try_movement( (block.coord + Point(horizontally, vertically) for block in self.minoes), trail, update ) def rotate(self, direction=CLOCKWISE): """ Try to rotate self through 90° CLOCKWISE or COUNTERCLOCKWISE around its center Tetriminos can rotate clockwise and counterclockwise using the Super Rotation System. This system allows Tetrimino rotation in situations that the original Classic Rotation System did not allow, such as rotating against walls. Each time a rotation button is pressed, the Tetrimino in play rotates 90 degrees in the clockwise or counterclockwise direction. Rotation can be performed while the Tetrimino is Auto-Repeating left or right. There is no Auto-Repeat for rotation itself. """ rotated_coords = tuple( mino.coord.rotate(self.minoes[0].coord, direction) for mino in self.minoes ) for movement in self.SUPER_ROTATION_SYSTEM[self.orientation][direction]: if self._try_movement(coord + Point(*movement) for coord in rotated_coords): self.orientation = (self.orientation + direction) % 4 return True return False def soft_drop(self): """ Causes the Tetrimino to drop at an accelerated rate (s.AUTO_REPEAT_RATE) from its current location """ return self.move(0, D, trail=1) def hard_drop(self, show_trail=True, update=True): """ Causes the Tetrimino in play to drop straight down instantly from its current location and Lock Down on the first Surface it lands on. It does not allow for further player manipulation of the Tetrimino in play. """ trail = 0 while self.move(0, D, trail=trail, update=update): if show_trail: trail += 1 return trail class TetroI(Tetromino, metaclass=MetaTetro): """ Tetromino shaped like a capital I four minoes in a straight line """ COORDS = (L, 0), (2 * L, 0), (0, 0), (R, 0) SUPER_ROTATION_SYSTEM = ( { COUNTERCLOCKWISE: ((0, D), (L, D), (2 * R, D), (L, U), (2 * R, 2 * D)), CLOCKWISE: ((R, 0), (L, 0), (2 * R, 0), (L, D), (2 * R, 2 * U)), }, { COUNTERCLOCKWISE: ((L, 0), (R, 0), (2 * L, 0), (R, U), (2 * L, 2 * D)), CLOCKWISE: ((0, D), (L, D), (2 * R, D), (L, U), (2 * R, 2 * D)), }, { COUNTERCLOCKWISE: ((0, U), (R, U), (2 * L, U), (R, D), (2 * L, 2 * U)), CLOCKWISE: ((L, 0), (R, 0), (2 * L, 0), (R, U), (2 * L, 2 * D)), }, { COUNTERCLOCKWISE: ((R, 0), (L, 0), (2 * R, 0), (L, D), (2 * R, 2 * U)), CLOCKWISE: ((0, U), (R, U), (2 * L, U), (R, D), (2 * L, 2 * U)), }, ) class TetroT(Tetromino, metaclass=MetaTetro): """ Tetromino shaped like a capital T a row of three minoes with one added above the center Can perform a T-Spin """ COORDS = (0, 0), (L, 0), (0, U), (R, 0) T_SLOT_A = ((L, U), (R, U), (R, D), (L, D)) T_SLOT_B = ((R, U), (R, D), (L, D), (L, U)) T_SLOT_C = ((L, D), (L, U), (R, U), (R, D)) T_SLOT_D = ((R, D), (L, D), (L, U), (R, U)) def __init__(self): super().__init__() def rotate(self, direction=CLOCKWISE): """ Detects T-Spins: this action can be achieved by first landing a T-Tetrimino, and before it Locks Down, rotating it in a T-Slot (any Block formation such that when the T-Tetrimino is spun into it, any three of the four cells diagonally adjacent to the center of self are occupied by existing Blocks.) """ rotated = super().rotate(direction) if rotated: center = self.minoes[0].coord pa = center + Point(*self.T_SLOT_A[self.orientation]) pb = center + Point(*self.T_SLOT_B[self.orientation]) pc = center + Point(*self.T_SLOT_C[self.orientation]) pd = center + Point(*self.T_SLOT_D[self.orientation]) a = not self.matrix.is_empty_cell(pa) b = not self.matrix.is_empty_cell(pb) c = not self.matrix.is_empty_cell(pc) d = not self.matrix.is_empty_cell(pd) if a and b and (c or d): self.t_spin = "T-Spin" elif c and d and (a or b): self.t_spin = "Mini T-Spin" return rotated class TetroZ(Tetromino, metaclass=MetaTetro): """ Tetromino shaped like a capital Z two stacked horizontal dominoes with the top one offset to the left """ COORDS = (0, 0), (L, U), (0, U), (R, 0) class TetroS(Tetromino, metaclass=MetaTetro): """ Tetromino shaped like a capital S two stacked horizontal dominoes with the top one offset to the right """ COORDS = (0, 0), (0, U), (L, 0), (R, U) class TetroL(Tetromino, metaclass=MetaTetro): """ Tetromino shaped like a capital L a row of three minoes with one added above the right side """ COORDS = (0, 0), (L, 0), (R, 0), (R, U) class TetroJ(Tetromino, metaclass=MetaTetro): """ Tetromino shaped like a capital J a row of three minoes with one added above the left side """ COORDS = (0, 0), (L, U), (L, 0), (R, 0) class TetroO(Tetromino, metaclass=MetaTetro): """ Square shape four minoes in a 2×2 square. """ COORDS = (0, 0), (L, 0), (0, U), (L, U) def rotate(self, direction=1): """ irrelevant """ return False class GhostPiece(Tetromino): """ A graphical representation of where the Tetrimino in play will come to rest if it is dropped from its current position. """ def __new__(cls, piece): return object.__new__(cls) def __init__(self, piece): self.matrix = piece.matrix self.minoes = tuple( GhostBlock(Point(mino.coord.x, mino.coord.y)) for mino in piece.minoes ) self.hard_drop(show_trail=False, update=False)