1924 lines
66 KiB
Python
1924 lines
66 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Another TETRIS® clone
|
||
Tetris Game Design by Alexey Pajitnov.
|
||
Parts of comments issued from 2009 Tetris Design Guideline
|
||
"""
|
||
|
||
__author__ = "Adrien Malingrey"
|
||
__title__ = "Tetris 2000"
|
||
__version__ = "0.2"
|
||
|
||
|
||
import sys
|
||
import os
|
||
import random
|
||
import locale
|
||
import time
|
||
import itertools
|
||
import ctypes
|
||
import collections
|
||
|
||
try: # PyQt5
|
||
from PyQt5 import QtWidgets, QtCore, QtGui, QtMultimedia
|
||
except ImportError:
|
||
try: # PySide2
|
||
from PySide2 import QtWidgets, QtCore, QtGui, QtMultimedia
|
||
except ImportError:
|
||
sys.exit(
|
||
"This program require a Qt library.\n"
|
||
"You can install PyQt5 package (recommended) :\n"
|
||
" pip install PyQt5\n"
|
||
"or PySide2 package :\n"
|
||
" pip install PySide2\n"
|
||
"NB : On Windows, PySide2 may require to install\n"
|
||
"Visual C++ Redistributable Packages separately."
|
||
)
|
||
else:
|
||
os.environ["QT_API"] = "pyside2"
|
||
else:
|
||
os.environ["QT_API"] = "pyqt5"
|
||
QtCore.Signal = QtCore.pyqtSignal
|
||
|
||
|
||
# Consts
|
||
# Paths
|
||
ICON_PATH = "data/icons/icon.ico"
|
||
bg_IMAGE_DIR = "data/backgrounds/"
|
||
MUSIC_PATH = "data/sounds/Tetris - Song A.mp3"
|
||
SOUNDS_DIR = "data/sounds/"
|
||
LOCALE_PATH = "data/locale/"
|
||
FONTS_DIR = "data/fonts/"
|
||
# Coordinates and direction
|
||
L, R, U, D = -1, 1, -1, 1 # Left, Right, Up, Down
|
||
CLOCKWISE, COUNTERCLOCKWISE = 1, -1
|
||
# Delay
|
||
ANIMATION_DELAY = 100 # milliseconds
|
||
|
||
|
||
class Point(QtCore.QPoint):
|
||
"""
|
||
Point of coordinates (x, y)
|
||
"""
|
||
|
||
def __init__(self, x, y):
|
||
super().__init__(x, y)
|
||
|
||
def __add__(self, o):
|
||
return Point(self.x() + o.x(), self.y() + o.y())
|
||
|
||
def __sub__(self, o):
|
||
return Point(self.x() - o.x(), self.y() - o.y())
|
||
|
||
def __mul__(self, k):
|
||
return Point(k * self.x(), k * self.y())
|
||
|
||
def __truediv__(self, k):
|
||
return Point(self.x() / k, k * self.y() / k)
|
||
|
||
__radd__ = __add__
|
||
__rsub__ = __sub__
|
||
__rmul__ = __mul__
|
||
__rtruediv__ = __truediv__
|
||
|
||
def rotate(self, center, direction=CLOCKWISE):
|
||
""" Returns the Point image of the rotation of self
|
||
through 90° CLOKWISE or COUNTERCLOCKWISE around center"""
|
||
if self == center:
|
||
return self
|
||
|
||
p = self - center
|
||
p = Point(-direction * p.y(), direction * p.x())
|
||
p += center
|
||
return p
|
||
|
||
|
||
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 = QtGui.QColor(0, 159, 218, 255)
|
||
FILL_COLOR = QtGui.QColor(0, 159, 218, 25)
|
||
GLOWING_BORDER_COLOR = None
|
||
GLOWING_FILL_COLOR = QtGui.QColor(186, 211, 255, 70)
|
||
LIGHT_COLOR = QtGui.QColor(204, 255, 255, 40)
|
||
TRANSPARENT = QtGui.QColor(255, 255, 255, 0)
|
||
GLOWING = 0
|
||
|
||
side = 0
|
||
|
||
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
|
||
self.center = p + QtCore.QPoint(Block.side / 2, Block.side / 2)
|
||
self.glint = 0.15 * Block.side * 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 = QtGui.QColor(135, 213, 255, 255)
|
||
FILL_COLOR = None
|
||
GLOWING_FILL_COLOR = QtGui.QColor(201, 149, 205, 255)
|
||
GLOWING = 1
|
||
|
||
|
||
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=False):
|
||
"""
|
||
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
|
||
if not trail:
|
||
self.matrix.update()
|
||
return True
|
||
|
||
def move(self, horizontally, vertically, trail=False):
|
||
"""
|
||
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,
|
||
)
|
||
|
||
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(
|
||
block.coord.rotate(self.minoes[0].coord, direction) for block 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
|
||
"""
|
||
dropped = self.move(0, D, trail=True)
|
||
if dropped:
|
||
for block in self.minoes:
|
||
block.trail = 1
|
||
self.matrix.update()
|
||
return dropped
|
||
|
||
def hard_drop(self):
|
||
"""
|
||
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=True):
|
||
trail += 1
|
||
for block in self.minoes:
|
||
block.trail = trail
|
||
self.matrix.update()
|
||
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):
|
||
if c:
|
||
pe = (pa + pc) / 2
|
||
elif d:
|
||
pe = (pb + pd) / 2
|
||
if not self.matrix.is_empty_cell(pe):
|
||
self.t_spin = "T-Spin"
|
||
elif (a or b) and (c and d):
|
||
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 """
|
||
pass
|
||
|
||
|
||
class Ghost(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()
|
||
|
||
def hard_drop(self):
|
||
while self.move(0, D):
|
||
pass
|
||
|
||
|
||
class Grid(QtWidgets.QWidget):
|
||
"""
|
||
Mother class of Hold queue, Next piece Queue, Matrix, and Next queue
|
||
"""
|
||
|
||
ROWS = 6
|
||
COLUMNS = 6
|
||
STARTING_POSITION = Point(3, 4)
|
||
GRIDLINE_COLOR = QtGui.QColor(255, 255, 255, 60)
|
||
HARD_DROP_MOVEMENT = 0.2
|
||
SPOTLIGHT = Point(0, 0)
|
||
|
||
def __init__(self, frames):
|
||
super().__init__(frames)
|
||
self.setStyleSheet("background-color: transparent")
|
||
self.frames = frames
|
||
self.spotlight = self.SPOTLIGHT
|
||
self.piece = None
|
||
|
||
def insert(self, piece, position=None):
|
||
"""
|
||
Add a Tetromino to self
|
||
Update its coordinates
|
||
"""
|
||
piece.insert_into(self, position or self.STARTING_POSITION)
|
||
self.piece = piece
|
||
self.update()
|
||
|
||
def resizeEvent(self, event):
|
||
self.bottom = Block.side * self.ROWS
|
||
self.grid_top = 2 * Block.side
|
||
width = Block.side * self.COLUMNS
|
||
self.left = (self.width() - width) // 2
|
||
self.right = width + self.left
|
||
self.top_left_corner = Point(self.left, 0)
|
||
|
||
def paintEvent(self, event=None):
|
||
painter = QtGui.QPainter(self)
|
||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||
|
||
self.paint_grid(painter)
|
||
|
||
if (not self.frames.paused or not self.frames.playing) and self.piece:
|
||
self.paint_piece(painter, self.piece)
|
||
|
||
def paint_grid(self, painter):
|
||
painter.setPen(self.GRIDLINE_COLOR)
|
||
for x in (self.left + i * Block.side for i in range(self.COLUMNS + 1)):
|
||
painter.drawLine(x, self.grid_top, x, self.bottom)
|
||
for y in (j * Block.side for j in range(2, self.ROWS + 1)):
|
||
painter.drawLine(self.left, y, self.right, y)
|
||
|
||
def paint_piece(self, painter, piece):
|
||
for mino in piece.minoes:
|
||
mino.paint(painter, self.top_left_corner, self.spotlight)
|
||
|
||
|
||
class Matrix(Grid):
|
||
"""
|
||
The rectangular arrangement of cells creating the active game area.
|
||
Tetriminos fall from the top-middle just above the Skyline (off-screen) to the bottom.
|
||
"""
|
||
|
||
ROWS = 22
|
||
COLUMNS = 10
|
||
STARTING_POSITION = Point(COLUMNS // 2, 1)
|
||
TEXT_COLOR = QtGui.QColor(204, 255, 255, 128)
|
||
TEMPORARY_TEXT_DURATION = 1000 # milliseconds
|
||
|
||
# Delays
|
||
INITIAL_SPEED = 1000 # row per milliseconds
|
||
ENTRY_DELAY = 80 # millisecondes
|
||
LINE_CLEAR_DELAY = 80 # millisecondes
|
||
LOCK_DELAY = 500 # millisecondes
|
||
|
||
drop_signal = QtCore.Signal(int)
|
||
lock_signal = QtCore.Signal(int, str)
|
||
|
||
def __init__(self, frames):
|
||
super().__init__(frames)
|
||
|
||
self.game_over = False
|
||
self.text = ""
|
||
self.temporary_texts = []
|
||
|
||
self.load_sounds()
|
||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||
|
||
self.auto_repeat_delay = 0
|
||
self.auto_repeat_timer = QtCore.QTimer()
|
||
self.auto_repeat_timer.timeout.connect(self.auto_repeat)
|
||
self.fall_timer = QtCore.QTimer()
|
||
self.fall_timer.timeout.connect(self.fall)
|
||
|
||
self.cells = []
|
||
self.apply_settings()
|
||
|
||
def load_sounds(self):
|
||
self.hard_drop_sound = QtMultimedia.QSoundEffect(self)
|
||
self.hard_drop_sound.setSource(
|
||
QtCore.QUrl.fromLocalFile(SOUNDS_DIR + "hard_drop.wav")
|
||
)
|
||
self.rotate_sound = QtMultimedia.QSoundEffect(self)
|
||
self.rotate_sound.setSource(
|
||
QtCore.QUrl.fromLocalFile(SOUNDS_DIR + "rotate.wav")
|
||
)
|
||
|
||
def apply_settings(self):
|
||
self.keys = {
|
||
getattr(QtCore.Qt, "Key_" + name): action
|
||
for action, name in settings[s.KEYBOARD].items()
|
||
}
|
||
self.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
|
||
self.spotlight = self.SPOTLIGHT
|
||
for sound in self.hard_drop_sound, self.rotate_sound:
|
||
sound.setVolume(settings[s.SOUND][s.EFFECTS_VOLUME])
|
||
|
||
def new_game(self):
|
||
self.game_over = False
|
||
self.lock_delay = self.LOCK_DELAY
|
||
self.cells = [self.empty_row() for y in range(self.ROWS)]
|
||
self.setFocus()
|
||
self.actions_to_repeat = []
|
||
|
||
def new_level(self, level):
|
||
self.show_temporary_text(self.tr("Level\n") + str(level))
|
||
self.speed = self.INITIAL_SPEED * (0.8 - ((level - 1) * 0.007)) ** (level - 1)
|
||
self.fall_timer.start(self.speed)
|
||
if level > 15:
|
||
self.lock_delay *= 0.9
|
||
|
||
def empty_row(self):
|
||
return [None for x in range(self.COLUMNS)]
|
||
|
||
def is_empty_cell(self, coord):
|
||
x, y = coord.x(), coord.y()
|
||
return 0 <= x < self.COLUMNS and y < self.ROWS and not self.cells[y][x]
|
||
|
||
def keyPressEvent(self, event):
|
||
if event.isAutoRepeat():
|
||
return
|
||
|
||
if not self.frames.playing:
|
||
return
|
||
|
||
try:
|
||
action = self.keys[event.key()]
|
||
except KeyError:
|
||
return
|
||
|
||
self.do(action)
|
||
if action in (s.MOVE_LEFT, s.MOVE_RIGHT, s.SOFT_DROP):
|
||
if action not in self.actions_to_repeat:
|
||
self.actions_to_repeat.append(action)
|
||
self.auto_repeat_wait()
|
||
|
||
def keyReleaseEvent(self, event):
|
||
if event.isAutoRepeat():
|
||
return
|
||
|
||
if not self.frames.playing:
|
||
return
|
||
|
||
try:
|
||
self.actions_to_repeat.remove(self.keys[event.key()])
|
||
except (KeyError, ValueError):
|
||
pass
|
||
else:
|
||
self.auto_repeat_wait()
|
||
|
||
if not self.actions_to_repeat:
|
||
for mino in self.piece.minoes:
|
||
mino.fade()
|
||
self.update()
|
||
|
||
def auto_repeat_wait(self):
|
||
self.auto_repeat_delay = (
|
||
time.time() + settings[s.DELAYS][s.AUTO_SHIFT_DELAY] / 1000
|
||
)
|
||
|
||
def auto_repeat(self):
|
||
"""
|
||
Tapping the move button allows a single cell movement of the Tetrimino
|
||
in the direction pressed.
|
||
Holding down the move button triggers an Auto-Repeat movement
|
||
that allows the player to move a Tetrimino from one side of the Matrix
|
||
to the other in about 0.5 seconds.
|
||
This is essential on higher levels when the Fall Speed of a Tetrimino is very fast.
|
||
There is a slight delay between the time the move button is pressed
|
||
and the time when Auto-Repeat kicks in : s.AUTO_SHIFT_DELAY.
|
||
This delay prevents unwanted extra movement of a Tetrimino.
|
||
Auto-Repeat only affects Left/Right movement.
|
||
Auto-Repeat continues to the Next Tetrimino (after Lock Down)
|
||
as long as the move button remains pressed.
|
||
In addition, when Auto-Repeat begins,
|
||
and the player then holds the opposite direction button,
|
||
the Tetrimino then begins moving the opposite direction with the initial delay.
|
||
When any single button is then released,
|
||
the Tetrimino should again move in the direction still held,
|
||
with the Auto-Shift delay applied once more.
|
||
"""
|
||
if (
|
||
not self.frames.playing
|
||
or self.frames.paused
|
||
or time.time() < self.auto_repeat_delay
|
||
):
|
||
return
|
||
|
||
if self.actions_to_repeat:
|
||
self.do(self.actions_to_repeat[-1])
|
||
|
||
def do(self, action):
|
||
"""The player can move, rotate, Soft Drop, Hard Drop,
|
||
and Hold the falling Tetrimino (i.e., the Tetrimino in play).
|
||
"""
|
||
if action == s.PAUSE:
|
||
self.frames.pause(not self.frames.paused)
|
||
|
||
if not self.frames.playing or self.frames.paused or not self.piece:
|
||
return
|
||
|
||
for mino in self.piece.minoes:
|
||
mino.shine(0)
|
||
|
||
if action == s.MOVE_LEFT:
|
||
if self.piece.move(L, 0):
|
||
self.lock_wait()
|
||
|
||
elif action == s.MOVE_RIGHT:
|
||
if self.piece.move(R, 0):
|
||
self.lock_wait()
|
||
|
||
elif action == s.ROTATE_CLOCKWISE:
|
||
if self.piece.rotate(direction=CLOCKWISE):
|
||
self.rotate_sound.play()
|
||
self.lock_wait()
|
||
|
||
elif action == s.ROTATE_COUNTERCLOCKWISE:
|
||
if self.piece.rotate(direction=COUNTERCLOCKWISE):
|
||
self.rotate_sound.play()
|
||
self.lock_wait()
|
||
|
||
elif action == s.SOFT_DROP:
|
||
if self.piece.soft_drop():
|
||
self.drop_signal.emit(1)
|
||
|
||
elif action == s.HARD_DROP:
|
||
trail = self.piece.hard_drop()
|
||
self.hard_drop_sound.play()
|
||
self.top_left_corner += Point(0, self.HARD_DROP_MOVEMENT * Block.side)
|
||
self.drop_signal.emit(2 * trail)
|
||
QtCore.QTimer.singleShot(ANIMATION_DELAY, self.after_hard_drop)
|
||
self.lock_phase()
|
||
|
||
elif action == s.HOLD:
|
||
self.frames.hold()
|
||
|
||
def after_hard_drop(self):
|
||
""" Reset the animation movement of the Matrix on a hard drop """
|
||
self.top_left_corner -= Point(0, self.HARD_DROP_MOVEMENT * Block.side)
|
||
|
||
def lock_wait(self):
|
||
self.fall_delay = time.time() + (self.speed + self.lock_delay) / 1000
|
||
|
||
def fall(self):
|
||
"""
|
||
Once a Tetrimino is generated,
|
||
it immediately drops one row (if no existing Block is in its path).
|
||
From here, it begins its descent to the bottom of the Matrix.
|
||
The Tetrimino will fall at its normal Fall Speed
|
||
whether or not it is being manipulated by the player.
|
||
"""
|
||
if self.piece:
|
||
if self.piece.move(0, 1):
|
||
self.lock_wait()
|
||
else:
|
||
if time.time() >= self.fall_delay:
|
||
self.lock_phase()
|
||
|
||
def lock_phase(self):
|
||
"""
|
||
The player can perform the same actions on a Tetrimino in this phase
|
||
as he/she can in the Falling Phase,
|
||
as long as the Tetrimino is not yet Locked Down.
|
||
A Tetrimino that is Hard Dropped Locks Down immediately.
|
||
However, if a Tetrimino naturally falls or Soft Drops onto a landing Surface,
|
||
it is given a delay (self.fall_delay) on a Lock Down Timer
|
||
before it actually Locks Down.
|
||
"""
|
||
# Enter minoes into the matrix
|
||
for mino in self.piece.minoes:
|
||
if mino.coord.y() >= 0:
|
||
self.cells[mino.coord.y()][mino.coord.x()] = mino
|
||
mino.shine(glowing=2, delay=ANIMATION_DELAY)
|
||
self.update()
|
||
|
||
if all(mino.coord.y() <= 1 for mino in self.piece.minoes):
|
||
self.frames.game_over()
|
||
return
|
||
|
||
"""
|
||
In this phase,
|
||
the engine looks for patterns made from Locked Down Blocks in the Matrix.
|
||
Once a pattern has been matched,
|
||
it can trigger any number of Tetris variant-related effects.
|
||
The classic pattern is the Line Clear pattern.
|
||
This pattern is matched when one or more rows of 10 horizontally aligned
|
||
Matrix cells are occupied by Blocks.
|
||
The matching Blocks are then marked for removal on a hit list.
|
||
Blocks on the hit list are cleared from the Matrix at a later time
|
||
in the Eliminate Phase.
|
||
"""
|
||
# Dectect complete lines
|
||
self.complete_lines = []
|
||
for y, row in enumerate(self.cells):
|
||
if all(cell for cell in row):
|
||
self.complete_lines.append(y)
|
||
for block in row:
|
||
block.shine()
|
||
self.spotlight = row[self.COLUMNS // 2].coord
|
||
self.auto_repeat_timer.stop()
|
||
self.lock_signal.emit(len(self.complete_lines), self.piece.t_spin)
|
||
|
||
if not self.complete_lines:
|
||
self.frames.new_piece()
|
||
return
|
||
|
||
self.fall_timer.stop()
|
||
QtCore.QTimer.singleShot(self.LINE_CLEAR_DELAY, self.eliminate_phase)
|
||
|
||
def eliminate_phase(self):
|
||
"""
|
||
Any Minos marked for removal, i.e., on the hit list,
|
||
are cleared from the Matrix in this phase.
|
||
If this results in one or more complete 10-cell rows in the Matrix
|
||
becoming unoccupied by Minos,
|
||
then all Minos above that row(s) collapse,
|
||
or fall by the number of complete rows cleared from the Matrix.
|
||
"""
|
||
for y in self.complete_lines:
|
||
del self.cells[y]
|
||
self.cells.insert(0, self.empty_row())
|
||
|
||
for y, row in enumerate(self.cells):
|
||
for x, block in enumerate(row):
|
||
if block:
|
||
block.coord.setX(x)
|
||
block.coord.setY(y)
|
||
|
||
self.update()
|
||
self.auto_repeat_wait()
|
||
self.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
|
||
|
||
self.frames.new_piece()
|
||
|
||
def paintEvent(self, event):
|
||
"""
|
||
Draws grid, actual piece, blocks in the Matrix and show texts
|
||
"""
|
||
painter = QtGui.QPainter(self)
|
||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||
|
||
self.paint_grid(painter)
|
||
|
||
if not self.frames.paused or self.game_over:
|
||
if self.piece:
|
||
if settings[s.OTHER][s.GHOST]:
|
||
self.ghost = Ghost(self.piece)
|
||
self.spotlight = self.ghost.minoes[0].coord
|
||
self.paint_piece(painter, self.ghost)
|
||
self.paint_piece(painter, self.piece)
|
||
|
||
# Blocks in matrix
|
||
for row in self.cells:
|
||
for block in row:
|
||
if block:
|
||
block.paint(painter, self.top_left_corner, self.spotlight)
|
||
|
||
if self.frames.playing and self.frames.paused:
|
||
painter.setFont(QtGui.QFont("Maassslicer", 0.75 * Block.side))
|
||
painter.setPen(self.TEXT_COLOR)
|
||
painter.drawText(
|
||
self.rect(),
|
||
QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
|
||
self.tr("PAUSE\n\nPress %s\nto resume") % settings[s.KEYBOARD][s.PAUSE],
|
||
)
|
||
if self.game_over:
|
||
painter.setFont(QtGui.QFont("Maassslicer", Block.side))
|
||
painter.setPen(self.TEXT_COLOR)
|
||
painter.drawText(
|
||
self.rect(),
|
||
QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
|
||
self.tr("GAME\nOVER"),
|
||
)
|
||
if self.temporary_texts:
|
||
painter.setFont(self.temporary_text_font)
|
||
painter.setPen(self.TEXT_COLOR)
|
||
painter.drawText(
|
||
self.rect(),
|
||
QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap,
|
||
"\n\n".join(self.temporary_texts),
|
||
)
|
||
|
||
def resizeEvent(self, event):
|
||
super().resizeEvent(event)
|
||
self.temporary_text_font = QtGui.QFont("Maassslicer", Block.side)
|
||
|
||
def show_temporary_text(self, text):
|
||
self.temporary_texts.append(text.upper())
|
||
self.font = self.temporary_text_font
|
||
self.update()
|
||
QtCore.QTimer.singleShot(self.TEMPORARY_TEXT_DURATION, self.delete_text)
|
||
|
||
def delete_text(self):
|
||
del self.temporary_texts[0]
|
||
self.update()
|
||
|
||
def focusOutEvent(self, event):
|
||
if self.frames.playing:
|
||
self.frames.pause(True)
|
||
|
||
|
||
class HoldQueue(Grid):
|
||
"""
|
||
The Hold Queue allows the player to “hold” a falling Tetrimino for as long as they wish.
|
||
Holding a Tetrimino releases the Tetrimino already in the Hold Queue (if one exists).
|
||
"""
|
||
|
||
def paintEvent(self, event):
|
||
if not settings[s.OTHER][s.HOLD_ENABLED]:
|
||
return
|
||
|
||
super().paintEvent(event)
|
||
|
||
|
||
class NextQueue(Grid):
|
||
"""
|
||
The Next Queue allows the player to see the Next Tetrimino that will be generated
|
||
and put into play.
|
||
"""
|
||
|
||
ROWS = 16
|
||
COLUMNS = 6
|
||
|
||
def __init__(self, parent):
|
||
super().__init__(parent)
|
||
self.pieces = []
|
||
|
||
def new_piece(self):
|
||
self.pieces = self.pieces[1:] + [Tetromino()]
|
||
self.insert_pieces()
|
||
|
||
def insert_pieces(self):
|
||
for y, piece in enumerate(self.pieces):
|
||
piece.insert_into(self, Point(3, 3 * y + 1))
|
||
|
||
def paintEvent(self, event=None):
|
||
if not settings[s.OTHER][s.SHOW_NEXT_QUEUE]:
|
||
return
|
||
|
||
painter = QtGui.QPainter(self)
|
||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||
|
||
if not self.frames.paused:
|
||
for piece in self.pieces:
|
||
self.paint_piece(painter, piece)
|
||
|
||
|
||
class Stats(QtWidgets.QWidget):
|
||
"""
|
||
Show informations relevant to the game being played is displayed on-screen.
|
||
Looks for patterns made from Locked Down Blocks in the Matrix and calculate score.
|
||
"""
|
||
|
||
ROWS = 15
|
||
COLUMNS = 6
|
||
TEXT_COLOR = QtGui.QColor(0, 159, 218, 128)
|
||
|
||
temporary_text = QtCore.Signal(str)
|
||
|
||
def __init__(self, frames):
|
||
super().__init__(frames)
|
||
self.frames = frames
|
||
self.setStyleSheet("background-color: transparent")
|
||
|
||
self.SCORES = (
|
||
{"name": "", "": 0, "Mini T-Spin": 1, "T-Spin": 4},
|
||
{"name": self.tr("Single"), "": 1, "Mini T-Spin": 2, "T-Spin": 8},
|
||
{"name": self.tr("Double"), "": 3, "T-Spin": 12},
|
||
{"name": self.tr("Triple"), "": 5, "T-Spin": 16},
|
||
{"name": self.tr("Tetris"), "": 8},
|
||
)
|
||
|
||
self.load_sounds()
|
||
|
||
self.setSizePolicy(
|
||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
|
||
)
|
||
self.text_options = QtGui.QTextOption(QtCore.Qt.AlignRight)
|
||
self.text_options.setWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
|
||
|
||
self.clock = QtCore.QTimer()
|
||
self.clock.timeout.connect(self.tick)
|
||
|
||
self.high_score = int(qsettings.value(self.tr("High score"), 0))
|
||
|
||
def load_sounds(self):
|
||
self.line_clear_sound = QtMultimedia.QSoundEffect(self)
|
||
self.line_clear_sound.setSource(
|
||
QtCore.QUrl.fromLocalFile(SOUNDS_DIR + "line_clear.wav")
|
||
)
|
||
self.tetris_sound = QtMultimedia.QSoundEffect(self)
|
||
self.tetris_sound.setSource(
|
||
QtCore.QUrl.fromLocalFile(SOUNDS_DIR + "tetris.wav")
|
||
)
|
||
for sound in self.line_clear_sound, self.tetris_sound:
|
||
sound.setVolume(settings[s.SOUND][s.EFFECTS_VOLUME])
|
||
|
||
def new_game(self):
|
||
self.level -= 1
|
||
self.goal = 0
|
||
self.complete_lines_total = 0
|
||
self.score_total = 1
|
||
self.t_spin_total = 0
|
||
self.mini_t_spin_total = 0
|
||
self.nb_back_to_back = 0
|
||
self.back_to_back_scores = None
|
||
self.combo = -1
|
||
self.combos_total = 0
|
||
self.max_combo = 0
|
||
self.chronometer = 0
|
||
self.nb_tetro = 0
|
||
self.clock.start(1000)
|
||
self.lines_stats = [0, 0, 0, 0, 0]
|
||
|
||
def new_level(self):
|
||
self.level += 1
|
||
self.goal += 5 * self.level
|
||
return self.level
|
||
|
||
def update_score(self, nb_complete_lines, t_spin):
|
||
"""
|
||
The player scores points by performing Single, Double, Triple,
|
||
and Tetris Line Clears, as well as T-Spins and Mini T-Spins.
|
||
Soft and Hard Drops also award points.
|
||
There is a special bonus for Back-to-Backs,
|
||
which is when two actions such as a Tetris and T-Spin Double take place
|
||
without a Single, Double, or Triple Line Clear occurring between them.
|
||
Scoring for Line Clears, T-Spins, and Mini T-Spins are level dependent,
|
||
while Hard and Soft Drop point values remain constant.
|
||
"""
|
||
self.nb_tetro += 1
|
||
if nb_complete_lines:
|
||
self.complete_lines_total += nb_complete_lines
|
||
self.lines_stats[nb_complete_lines] += 1
|
||
if t_spin == "T-Spin":
|
||
self.t_spin_total += 1
|
||
elif t_spin == "Mini T-Spin":
|
||
self.mini_t_spin_total += 1
|
||
|
||
score = self.SCORES[nb_complete_lines][t_spin]
|
||
|
||
if score:
|
||
text = " ".join((t_spin, self.SCORES[nb_complete_lines]["name"]))
|
||
if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
|
||
self.tetris_sound.play()
|
||
if 1 <= nb_complete_lines <= 3:
|
||
self.line_clear_sound.play()
|
||
|
||
self.goal -= score
|
||
score = 100 * self.level * score
|
||
self.score_total += score
|
||
|
||
self.temporary_text.emit(text + "\n{:n}".format(score))
|
||
|
||
# ==============================================================================
|
||
# Combo
|
||
# Bonus for complete lines on each consecutive lock downs
|
||
# if nb_complete_lines:
|
||
# ==============================================================================
|
||
if nb_complete_lines:
|
||
self.combo += 1
|
||
if self.combo > 0:
|
||
if nb_complete_lines == 1:
|
||
combo_score = 20 * self.combo * self.level
|
||
else:
|
||
combo_score = 50 * self.combo * self.level
|
||
self.score_total += combo_score
|
||
self.max_combo = max(self.max_combo, self.combo)
|
||
self.combos_total += 1
|
||
self.temporary_text.emit(
|
||
self.tr("COMBO x{:n}\n{:n}").format(self.combo, combo_score)
|
||
)
|
||
else:
|
||
self.combo = -1
|
||
|
||
# ==============================================================================
|
||
# Back-to_back sequence
|
||
# Two major bonus actions, such as two Tetrises, performed without
|
||
# a Single, Double, or Triple Line Clear occurring between them.
|
||
# Bonus for Tetrises, T-Spin Line Clears, and Mini T-Spin Line Clears
|
||
# performed consecutively in a B2B sequence.
|
||
# ==============================================================================
|
||
if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
|
||
if self.back_to_back_scores is not None:
|
||
self.back_to_back_scores.append(score // 2)
|
||
else:
|
||
# The first Line Clear in the Back-to-Back sequence
|
||
# does not receive the Back-to-Back Bonus.
|
||
self.back_to_back_scores = []
|
||
elif nb_complete_lines and not t_spin:
|
||
# A Back-to-Back sequence is only broken by a Single, Double, or Triple Line Clear.
|
||
# Locking down a Tetrimino without clearing a line
|
||
# or holding a Tetrimino does not break the Back-to-Back sequence.
|
||
# T-Spins and Mini T-Spins that do not clear any lines
|
||
# do not receive the Back-to-Back Bonus; instead they are scored as normal.
|
||
# They also cannot start a Back-to-Back sequence, however,
|
||
# they do not break an existing Back-to-Back sequence.
|
||
if self.back_to_back_scores:
|
||
b2b_score = sum(self.back_to_back_scores)
|
||
self.score_total += b2b_score
|
||
self.nb_back_to_back += 1
|
||
self.temporary_text.emit(
|
||
self.tr("BACK TO BACK\n{:n}").format(b2b_score)
|
||
)
|
||
self.back_to_back_scores = None
|
||
|
||
self.high_score = max(self.score_total, self.high_score)
|
||
self.update()
|
||
|
||
def update_drop_score(self, n):
|
||
""" Tetrimino is Soft Dropped for n lines or Hard Dropped for (n/2) lines"""
|
||
self.score_total += n
|
||
self.high_score = max(self.score_total, self.high_score)
|
||
self.update()
|
||
|
||
def tick(self):
|
||
self.chronometer += 1
|
||
self.update()
|
||
|
||
def paintEvent(self, event):
|
||
if not self.frames.playing and not self.frames.board.game_over:
|
||
return
|
||
|
||
painter = QtGui.QPainter(self)
|
||
painter.setFont(self.font)
|
||
painter.setPen(self.TEXT_COLOR)
|
||
|
||
painter.drawText(
|
||
QtCore.QRectF(self.rect()), self.text(sep="\n\n"), self.text_options
|
||
)
|
||
|
||
def text(self, full_stats=False, sep="\n"):
|
||
text = (
|
||
self.tr("Score: ")
|
||
+ locale.format("%i", self.score_total, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("High score: ")
|
||
+ locale.format("%i", self.high_score, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Time: {}\n").format(
|
||
time.strftime("%H:%M:%S", time.gmtime(self.chronometer))
|
||
)
|
||
+ sep
|
||
+ self.tr("Level: ")
|
||
+ locale.format("%i", self.level, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Goal: ")
|
||
+ locale.format("%i", self.goal, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Lines: ")
|
||
+ locale.format(
|
||
"%i", self.complete_lines_total, grouping=True, monetary=True
|
||
)
|
||
+ sep
|
||
+ self.tr("Mini T-Spins: ")
|
||
+ locale.format("%i", self.mini_t_spin_total, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("T-Spins: ")
|
||
+ locale.format("%i", self.t_spin_total, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Back-to-back: ")
|
||
+ locale.format("%i", self.nb_back_to_back, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Max combo: ")
|
||
+ locale.format("%i", self.max_combo, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Combos: ")
|
||
+ locale.format("%i", self.combos_total, grouping=True, monetary=True)
|
||
)
|
||
if full_stats:
|
||
minutes = self.chronometer / 60
|
||
text += (
|
||
"\n"
|
||
+ sep
|
||
+ self.tr("Lines per minute: {:.1f}").format(
|
||
self.complete_lines_total / minutes
|
||
)
|
||
+ sep
|
||
+ self.tr("Tetrominos locked down: ")
|
||
+ locale.format("%i", self.nb_tetro, grouping=True, monetary=True)
|
||
+ sep
|
||
+ self.tr("Tetrominos per minute: {:.1f}").format(
|
||
self.nb_tetro / minutes
|
||
)
|
||
+ sep
|
||
)
|
||
text += sep.join(
|
||
score_type["name"]
|
||
+ ": "
|
||
+ locale.format("%i", nb, grouping=True, monetary=True)
|
||
for score_type, nb in tuple(zip(self.SCORES, self.lines_stats))[1:]
|
||
)
|
||
return text
|
||
|
||
def resizeEvent(self, event):
|
||
self.font = QtGui.QFont("PixelCaps!", Block.side / 3.5)
|
||
|
||
|
||
class AspectRatioWidget(QtWidgets.QWidget):
|
||
"""
|
||
Keeps aspect ratio of child widget on resize
|
||
https://stackoverflow.com/questions/48043469/how-to-lock-aspect-ratio-while-resizing-the-window
|
||
"""
|
||
|
||
def __init__(self, widget, parent):
|
||
super().__init__(parent)
|
||
self.aspect_ratio = widget.size().width() / widget.size().height()
|
||
self.setLayout(QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight, self))
|
||
# add spacer, then widget, then spacer
|
||
self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
|
||
self.layout().addWidget(widget)
|
||
self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
|
||
|
||
def resizeEvent(self, e):
|
||
w = e.size().width()
|
||
h = e.size().height()
|
||
|
||
if w / h > self.aspect_ratio: # too wide
|
||
self.layout().setDirection(QtWidgets.QBoxLayout.LeftToRight)
|
||
widget_stretch = h * self.aspect_ratio
|
||
outer_stretch = (w - widget_stretch) / 2 + 0.5
|
||
else: # too tall
|
||
self.layout().setDirection(QtWidgets.QBoxLayout.TopToBottom)
|
||
widget_stretch = w / self.aspect_ratio
|
||
outer_stretch = (h - widget_stretch) / 2 + 0.5
|
||
|
||
self.layout().setStretch(0, outer_stretch)
|
||
self.layout().setStretch(1, widget_stretch)
|
||
self.layout().setStretch(2, outer_stretch)
|
||
|
||
|
||
class Frames(QtWidgets.QWidget):
|
||
"""
|
||
Display Hold queue, Matrix, Next piece, Next queue and Stats.
|
||
Manage interactions between them.
|
||
"""
|
||
|
||
def __init__(self, parent):
|
||
super().__init__(parent)
|
||
self.setSizePolicy(
|
||
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
|
||
)
|
||
|
||
self.playing = False
|
||
self.paused = False
|
||
|
||
self.backgrounds = tuple(
|
||
QtGui.QImage(bg_IMAGE_DIR + entry.name)
|
||
for entry in os.scandir(bg_IMAGE_DIR)
|
||
if entry.is_file() and ".jpg" in entry.name
|
||
)
|
||
self.reset_backgrounds()
|
||
|
||
self.load_music()
|
||
|
||
self.hold_queue = HoldQueue(self)
|
||
self.board = Matrix(self)
|
||
self.next_piece = Grid(self)
|
||
self.stats = Stats(self)
|
||
self.next_queue = NextQueue(self)
|
||
|
||
self.matrices = (self.hold_queue, self.board, self.next_piece)
|
||
self.columns = sum(matrix.COLUMNS + 1 for matrix in self.matrices) + 1
|
||
self.rows = self.board.ROWS + 3
|
||
|
||
w = QtWidgets.QWidget(self)
|
||
w.setStyleSheet("background-color: transparent")
|
||
grid = QtWidgets.QGridLayout()
|
||
for x in range(self.rows):
|
||
grid.setRowStretch(x, 1)
|
||
for y in range(self.columns):
|
||
grid.setColumnStretch(y, 1)
|
||
grid.setSpacing(0)
|
||
x, y = 1, 1
|
||
grid.addWidget(
|
||
self.hold_queue, y, x, self.hold_queue.ROWS, self.hold_queue.COLUMNS + 1
|
||
)
|
||
x += self.hold_queue.COLUMNS + 1
|
||
grid.addWidget(self.board, y, x, self.board.ROWS + 2, self.board.COLUMNS + 1)
|
||
x += self.board.COLUMNS + 1
|
||
grid.addWidget(
|
||
self.next_piece, y, x, self.next_piece.ROWS, self.next_piece.COLUMNS + 1
|
||
)
|
||
x, y = 0, self.hold_queue.ROWS + 2
|
||
grid.addWidget(self.stats, y, x, self.stats.ROWS, self.stats.COLUMNS + 1)
|
||
x += self.stats.COLUMNS + self.board.COLUMNS + 3
|
||
grid.addWidget(
|
||
self.next_queue, y, x, self.next_queue.ROWS, self.next_queue.COLUMNS + 1
|
||
)
|
||
w.setLayout(grid)
|
||
w.resize(self.columns, self.rows)
|
||
asw = AspectRatioWidget(w, self)
|
||
layout = QtWidgets.QGridLayout()
|
||
layout.addWidget(asw)
|
||
self.setLayout(layout)
|
||
|
||
self.stats.temporary_text.connect(self.board.show_temporary_text)
|
||
self.board.drop_signal.connect(self.stats.update_drop_score)
|
||
self.board.lock_signal.connect(self.stats.update_score)
|
||
|
||
def load_music(self):
|
||
playlist = QtMultimedia.QMediaPlaylist(self)
|
||
music_path = QtMultimedia.QMediaContent(
|
||
QtCore.QUrl.fromLocalFile(
|
||
os.path.dirname(os.path.abspath(__file__)) + "/" + MUSIC_PATH
|
||
)
|
||
)
|
||
playlist.addMedia(music_path)
|
||
playlist.setPlaybackMode(QtMultimedia.QMediaPlaylist.Loop)
|
||
self.music = QtMultimedia.QMediaPlayer(self)
|
||
self.music.setAudioRole(QtMultimedia.QAudio.GameRole)
|
||
self.music.setPlaylist(playlist)
|
||
self.music.setVolume(settings[s.SOUND][s.MUSIC_VOLUME])
|
||
|
||
def resizeEvent(self, event):
|
||
Block.side = 0.9 * min(self.width() // self.columns, self.height() // self.rows)
|
||
self.resize_bg_image()
|
||
|
||
def resize_bg_image(self):
|
||
self.resized_bg_image = QtGui.QPixmap.fromImage(self.bg_image)
|
||
self.resized_bg_image = self.resized_bg_image.scaled(
|
||
self.size(),
|
||
QtCore.Qt.KeepAspectRatioByExpanding,
|
||
QtCore.Qt.SmoothTransformation,
|
||
)
|
||
self.resized_bg_image = self.resized_bg_image.copy(
|
||
(self.resized_bg_image.width() - self.width()) // 2,
|
||
(self.resized_bg_image.height() - self.height()) // 2,
|
||
self.width(),
|
||
self.height(),
|
||
)
|
||
|
||
def reset_backgrounds(self):
|
||
self.backgrounds_cycle = itertools.cycle(self.backgrounds)
|
||
self.set_new_background()
|
||
|
||
def set_new_background(self):
|
||
self.bg_image = QtGui.QImage(next(self.backgrounds_cycle))
|
||
self.resize_bg_image()
|
||
|
||
def new_game(self):
|
||
if self.playing:
|
||
answer = QtWidgets.QMessageBox.question(
|
||
self,
|
||
self.tr("New game"),
|
||
self.tr("A game is in progress.\n" "Do you want to abord it?"),
|
||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||
QtWidgets.QMessageBox.Cancel,
|
||
)
|
||
if answer == QtWidgets.QMessageBox.Cancel:
|
||
self.pause(False)
|
||
return
|
||
self.music.stop()
|
||
|
||
self.reset_backgrounds()
|
||
self.stats.level, ok = QtWidgets.QInputDialog.getInt(
|
||
self,
|
||
self.tr("New game"),
|
||
self.tr("Start level:"),
|
||
1,
|
||
1,
|
||
15,
|
||
flags=QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint,
|
||
)
|
||
if not ok:
|
||
return
|
||
|
||
self.playing = True
|
||
self.music.play()
|
||
self.hold_queue.piece = None
|
||
self.stats.new_game()
|
||
self.board.new_game()
|
||
self.next_queue.pieces = [Tetromino() for _ in range(5)]
|
||
self.next_queue.insert_pieces()
|
||
self.next_piece.insert(Tetromino())
|
||
self.pause(False)
|
||
self.new_level()
|
||
|
||
def new_level(self):
|
||
self.set_new_background()
|
||
level = self.stats.new_level()
|
||
self.board.new_level(level)
|
||
self.new_piece()
|
||
|
||
def new_piece(self):
|
||
if self.stats.goal <= 0:
|
||
self.new_level()
|
||
self.board.insert(self.next_piece.piece)
|
||
self.board.lock_wait()
|
||
self.next_piece.insert(self.next_queue.pieces[0])
|
||
self.next_queue.new_piece()
|
||
self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
|
||
self.update()
|
||
|
||
if not self.board.piece.move(0, 0):
|
||
self.game_over()
|
||
return
|
||
|
||
self.board.fall_timer.start(self.board.speed)
|
||
|
||
def pause(self, paused):
|
||
if not self.playing:
|
||
return
|
||
|
||
if paused:
|
||
self.paused = True
|
||
self.update()
|
||
self.board.fall_timer.stop()
|
||
self.stats.clock.stop()
|
||
self.board.auto_repeat_timer.stop()
|
||
self.music.pause()
|
||
else:
|
||
self.board.text = ""
|
||
self.update()
|
||
QtCore.QTimer.singleShot(1000, self.resume)
|
||
|
||
def resume(self):
|
||
self.paused = False
|
||
self.update()
|
||
self.board.fall_timer.start(self.board.speed)
|
||
self.stats.clock.start(1000)
|
||
self.board.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
|
||
self.music.play()
|
||
|
||
def hold(self):
|
||
"""
|
||
Using the Hold command places the Tetrimino in play into the Hold Queue.
|
||
The previously held Tetrimino (if one exists) will then start falling
|
||
from the top of the Matrix,
|
||
beginning from its generation position and North Facing orientation.
|
||
Only one Tetrimino may be held at a time.
|
||
A Lock Down must take place between Holds.
|
||
For example, at the beginning, the first Tetrimino is generated and begins to fall.
|
||
The player decides to hold this Tetrimino.
|
||
Immediately the Next Tetrimino is generated from the Next Queue and begins to fall.
|
||
The player must first Lock Down this Tetrimino before holding another Tetrimino.
|
||
In other words, you may not Hold the same Tetrimino more than once.
|
||
"""
|
||
|
||
if not self.hold_enabled:
|
||
return
|
||
|
||
piece = self.hold_queue.piece
|
||
if piece:
|
||
self.hold_queue.insert(self.board.piece)
|
||
self.board.insert(piece)
|
||
self.update()
|
||
else:
|
||
self.hold_queue.insert(self.board.piece)
|
||
self.new_piece()
|
||
self.hold_enabled = False
|
||
|
||
def game_over(self):
|
||
self.board.fall_timer.stop()
|
||
self.stats.clock.stop()
|
||
self.board.auto_repeat_timer.stop()
|
||
self.music.stop()
|
||
self.playing = False
|
||
self.board.game_over = True
|
||
if self.stats.score_total == self.stats.high_score:
|
||
text = self.tr("Congratulations!\nYou have the high score!") + "\n\n"
|
||
else:
|
||
text = ""
|
||
text += self.stats.text(full_stats=True)
|
||
qsettings.setValue(self.tr("High score"), self.stats.high_score)
|
||
QtWidgets.QMessageBox.information(self, self.tr("Game over"), text)
|
||
|
||
def paintEvent(self, event):
|
||
painter = QtGui.QPainter(self)
|
||
painter.drawPixmap(self.rect(), self.resized_bg_image)
|
||
|
||
|
||
class KeyButton(QtWidgets.QPushButton):
|
||
""" Button widget capturing key name on focus """
|
||
|
||
names = {
|
||
value: name.replace("Key_", "")
|
||
for name, value in QtCore.Qt.__dict__.items()
|
||
if "Key_" in name
|
||
}
|
||
|
||
def __init__(self, *args):
|
||
super().__init__(*args)
|
||
|
||
def keyPressEvent(self, event):
|
||
key = event.key()
|
||
self.setText(self.names[key])
|
||
|
||
|
||
class SettingsGroup(QtWidgets.QGroupBox):
|
||
""" Group box of a type of settings """
|
||
|
||
def __init__(self, group, parent, cls):
|
||
super().__init__(group, parent)
|
||
layout = QtWidgets.QFormLayout(self)
|
||
self.widgets = {}
|
||
for setting, value in settings[group].items():
|
||
if cls == KeyButton:
|
||
widget = KeyButton(value)
|
||
elif cls == QtWidgets.QCheckBox:
|
||
widget = QtWidgets.QCheckBox(setting)
|
||
widget.setChecked(value)
|
||
elif cls == QtWidgets.QSpinBox:
|
||
widget = QtWidgets.QSpinBox()
|
||
widget.setRange(0, 1000)
|
||
widget.setValue(value)
|
||
widget.setSuffix(" ms")
|
||
elif cls == QtWidgets.QSlider:
|
||
widget = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
||
widget.setValue(value)
|
||
if cls == QtWidgets.QCheckBox:
|
||
layout.addRow(widget)
|
||
else:
|
||
layout.addRow(setting, widget)
|
||
self.widgets[setting] = widget
|
||
self.setLayout(layout)
|
||
|
||
|
||
class SettingsDialog(QtWidgets.QDialog):
|
||
""" Show settings dialog """
|
||
|
||
def __init__(self, parent):
|
||
super().__init__(parent)
|
||
self.setWindowTitle(self.tr("Settings"))
|
||
self.setModal(True)
|
||
|
||
vlayout = QtWidgets.QVBoxLayout()
|
||
|
||
self.groups = {}
|
||
self.groups[s.KEYBOARD] = SettingsGroup(s.KEYBOARD, self, KeyButton)
|
||
self.groups[s.DELAYS] = SettingsGroup(s.DELAYS, self, QtWidgets.QSpinBox)
|
||
self.groups[s.SOUND] = SettingsGroup(s.SOUND, self, QtWidgets.QSlider)
|
||
self.groups[s.OTHER] = SettingsGroup(s.OTHER, self, QtWidgets.QCheckBox)
|
||
|
||
for group in self.groups.values():
|
||
vlayout.addWidget(group)
|
||
|
||
buttons = QtWidgets.QDialogButtonBox(
|
||
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
|
||
)
|
||
buttons.accepted.connect(self.ok)
|
||
buttons.rejected.connect(self.close)
|
||
vlayout.addWidget(buttons)
|
||
|
||
self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].valueChanged.connect(
|
||
parent.frames.music.setVolume
|
||
)
|
||
|
||
self.setLayout(vlayout)
|
||
self.show()
|
||
|
||
def ok(self):
|
||
""" Save settings """
|
||
|
||
for group, elements in self.groups.items():
|
||
for setting, widget in elements.widgets.items():
|
||
if isinstance(widget, KeyButton):
|
||
value = widget.text()
|
||
elif isinstance(widget, QtWidgets.QCheckBox):
|
||
value = widget.isChecked()
|
||
elif isinstance(widget, QtWidgets.QSpinBox):
|
||
value = widget.value()
|
||
elif isinstance(widget, QtWidgets.QSlider):
|
||
value = widget.value()
|
||
settings[group][setting] = value
|
||
qsettings.setValue(group + "/" + setting, value)
|
||
self.close()
|
||
|
||
|
||
class SettingStrings(QtCore.QObject):
|
||
"""
|
||
Setting string for translation
|
||
"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
|
||
self.KEYBOARD = self.tr("Keyboard settings")
|
||
self.MOVE_LEFT = self.tr("Move left")
|
||
self.MOVE_RIGHT = self.tr("Move right")
|
||
self.ROTATE_CLOCKWISE = self.tr("Rotate clockwise")
|
||
self.ROTATE_COUNTERCLOCKWISE = self.tr("Rotate counterclockwise")
|
||
self.SOFT_DROP = self.tr("Soft drop")
|
||
self.HARD_DROP = self.tr("Hard drop")
|
||
self.HOLD = self.tr("Hold")
|
||
self.PAUSE = self.tr("Pause")
|
||
self.OTHER = self.tr("Other settings")
|
||
|
||
self.DELAYS = self.tr("Delays")
|
||
self.AUTO_SHIFT_DELAY = self.tr("Auto-shift delay")
|
||
self.AUTO_REPEAT_RATE = self.tr("Auto-repeat rate")
|
||
|
||
self.SOUND = self.tr("Sound")
|
||
self.MUSIC_VOLUME = self.tr("Music volume")
|
||
self.EFFECTS_VOLUME = self.tr("Effects volume")
|
||
|
||
self.GHOST = self.tr("Show ghost piece")
|
||
self.SHOW_NEXT_QUEUE = self.tr("Show next queue")
|
||
self.HOLD_ENABLED = self.tr("Hold enabled")
|
||
|
||
|
||
class Window(QtWidgets.QMainWindow):
|
||
""" Main window """
|
||
|
||
def __init__(self):
|
||
self.set_locale()
|
||
|
||
super().__init__()
|
||
self.setWindowTitle(__title__.upper())
|
||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||
|
||
self.setWindowIcon(QtGui.QIcon(ICON_PATH))
|
||
# Windows' taskbar icon
|
||
try:
|
||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
||
".".join((__author__, __title__, __version__))
|
||
)
|
||
except AttributeError:
|
||
pass
|
||
|
||
self.load_settings()
|
||
|
||
# Stylesheet
|
||
try:
|
||
import qdarkstyle
|
||
except ImportError:
|
||
pass
|
||
else:
|
||
self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment())
|
||
|
||
for font_name in "maass slicer Italic.ttf", "PixelCaps!.otf":
|
||
QtGui.QFontDatabase.addApplicationFont(FONTS_DIR + font_name)
|
||
|
||
self.frames = Frames(self)
|
||
self.setCentralWidget(self.frames)
|
||
self.hold_queue = self.frames.hold_queue
|
||
self.board = self.frames.board
|
||
self.stats = self.frames.stats
|
||
|
||
self.menu = self.menuBar()
|
||
|
||
geometry = qsettings.value("WindowGeometry")
|
||
if geometry:
|
||
self.restoreGeometry(geometry)
|
||
else:
|
||
self.resize(839, 807)
|
||
self.setWindowState(
|
||
QtCore.Qt.WindowStates(
|
||
int(qsettings.value("WindowState", QtCore.Qt.WindowActive))
|
||
)
|
||
)
|
||
|
||
self.show()
|
||
self.frames.new_game()
|
||
|
||
def set_locale(self):
|
||
app = QtWidgets.QApplication.instance()
|
||
|
||
# Set appropriate thounsand separator characters
|
||
locale.setlocale(locale.LC_ALL, "")
|
||
# Qt
|
||
language = QtCore.QLocale.system().name()[:2]
|
||
|
||
qt_translator = QtCore.QTranslator(app)
|
||
qt_translation_path = QtCore.QLibraryInfo.location(
|
||
QtCore.QLibraryInfo.TranslationsPath
|
||
)
|
||
if qt_translator.load("qt_" + language, qt_translation_path):
|
||
app.installTranslator(qt_translator)
|
||
|
||
tetris2000_translator = QtCore.QTranslator(app)
|
||
if tetris2000_translator.load(language, LOCALE_PATH):
|
||
app.installTranslator(tetris2000_translator)
|
||
|
||
def load_settings(self):
|
||
global qsettings
|
||
qsettings = QtCore.QSettings(__author__, __title__)
|
||
global s
|
||
s = SettingStrings()
|
||
global settings
|
||
settings = collections.OrderedDict(
|
||
[
|
||
(
|
||
s.KEYBOARD,
|
||
collections.OrderedDict(
|
||
[
|
||
(
|
||
s.MOVE_LEFT,
|
||
qsettings.value(s.KEYBOARD + "/" + s.MOVE_LEFT, "Left"),
|
||
),
|
||
(
|
||
s.MOVE_RIGHT,
|
||
qsettings.value(
|
||
s.KEYBOARD + "/" + s.MOVE_RIGHT, "Right"
|
||
),
|
||
),
|
||
(
|
||
s.ROTATE_CLOCKWISE,
|
||
qsettings.value(
|
||
s.KEYBOARD + "/" + s.ROTATE_CLOCKWISE, "Up"
|
||
),
|
||
),
|
||
(
|
||
s.ROTATE_COUNTERCLOCKWISE,
|
||
qsettings.value(
|
||
s.KEYBOARD + "/" + s.ROTATE_COUNTERCLOCKWISE,
|
||
"Control",
|
||
),
|
||
),
|
||
(
|
||
s.SOFT_DROP,
|
||
qsettings.value(s.KEYBOARD + "/" + s.SOFT_DROP, "Down"),
|
||
),
|
||
(
|
||
s.HARD_DROP,
|
||
qsettings.value(
|
||
s.KEYBOARD + "/" + s.HARD_DROP, "Space"
|
||
),
|
||
),
|
||
(
|
||
s.HOLD,
|
||
qsettings.value(s.KEYBOARD + "/" + s.HOLD, "Shift"),
|
||
),
|
||
(
|
||
s.PAUSE,
|
||
qsettings.value(s.KEYBOARD + "/" + s.PAUSE, "Escape"),
|
||
),
|
||
]
|
||
),
|
||
),
|
||
(
|
||
s.DELAYS,
|
||
collections.OrderedDict(
|
||
[
|
||
(
|
||
s.AUTO_SHIFT_DELAY,
|
||
int(
|
||
qsettings.value(
|
||
s.DELAYS + "/" + s.AUTO_SHIFT_DELAY, 170
|
||
)
|
||
),
|
||
),
|
||
(
|
||
s.AUTO_REPEAT_RATE,
|
||
int(
|
||
qsettings.value(
|
||
s.DELAYS + "/" + s.AUTO_REPEAT_RATE, 20
|
||
)
|
||
),
|
||
),
|
||
]
|
||
),
|
||
),
|
||
(
|
||
s.SOUND,
|
||
collections.OrderedDict(
|
||
[
|
||
(
|
||
s.MUSIC_VOLUME,
|
||
int(
|
||
qsettings.value(s.SOUND + "/" + s.MUSIC_VOLUME, 25)
|
||
),
|
||
),
|
||
(
|
||
s.EFFECTS_VOLUME,
|
||
int(
|
||
qsettings.value(
|
||
s.SOUND + "/" + s.EFFECTS_VOLUME, 50
|
||
)
|
||
),
|
||
),
|
||
]
|
||
),
|
||
),
|
||
(
|
||
s.OTHER,
|
||
collections.OrderedDict(
|
||
[
|
||
(
|
||
s.GHOST,
|
||
bool(qsettings.value(s.OTHER + "/" + s.GHOST, True)),
|
||
),
|
||
(
|
||
s.SHOW_NEXT_QUEUE,
|
||
bool(
|
||
qsettings.value(
|
||
s.OTHER + "/" + s.SHOW_NEXT_QUEUE, True
|
||
)
|
||
),
|
||
),
|
||
(
|
||
s.HOLD_ENABLED,
|
||
bool(
|
||
qsettings.value(
|
||
s.OTHER + "/" + s.HOLD_ENABLED, True
|
||
)
|
||
),
|
||
),
|
||
]
|
||
),
|
||
),
|
||
]
|
||
)
|
||
|
||
def menuBar(self):
|
||
menu = super().menuBar()
|
||
|
||
new_game_action = QtWidgets.QAction(self.tr("&New game"), self)
|
||
new_game_action.triggered.connect(self.frames.new_game)
|
||
menu.addAction(new_game_action)
|
||
|
||
settings_action = QtWidgets.QAction(self.tr("&Settings"), self)
|
||
settings_action.triggered.connect(self.show_settings_dialog)
|
||
menu.addAction(settings_action)
|
||
|
||
about_action = QtWidgets.QAction(self.tr("&About"), self)
|
||
about_action.triggered.connect(self.about)
|
||
menu.addAction(about_action)
|
||
return menu
|
||
|
||
def show_settings_dialog(self):
|
||
SettingsDialog(self).exec_()
|
||
if self.frames.music.volume() and self.frames.playing:
|
||
self.frames.music.play()
|
||
else:
|
||
self.frames.music.pause()
|
||
self.frames.board.apply_settings()
|
||
self.frames.stats.line_clear_sound.setVolume(
|
||
settings[s.SOUND][s.EFFECTS_VOLUME]
|
||
)
|
||
self.frames.stats.tetris_sound.setVolume(settings[s.SOUND][s.EFFECTS_VOLUME])
|
||
if self.frames.playing:
|
||
self.frames.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
|
||
self.frames.pause(False)
|
||
|
||
def about(self):
|
||
QtWidgets.QMessageBox.about(
|
||
self,
|
||
__title__,
|
||
self.tr(
|
||
"Tetris® clone\n"
|
||
"by Adrien Malingrey\n\n"
|
||
"Tetris Game Design by Alekseï Pajitnov\n"
|
||
"Graphism inspired by Tetris Effect\n"
|
||
"Window style sheet by Colin Duquesnoy\n"
|
||
"PixelCaps! font by Markus Koellmann\n"
|
||
"Maass slicer font by Peter Wiegel\n"
|
||
"Traditional song Korobeiniki arranged by Kobashik\n"
|
||
"Sound effects made with voc-one by Simple-Media\n"
|
||
"Background images found on xshyfc.com"
|
||
),
|
||
)
|
||
if self.playing:
|
||
self.pause(False)
|
||
|
||
def closeEvent(self, event):
|
||
if self.frames.playing:
|
||
answer = QtWidgets.QMessageBox.question(
|
||
self,
|
||
self.tr("Quit game?"),
|
||
self.tr("A game is in progress.\nDo you want to abord it?"),
|
||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||
QtWidgets.QMessageBox.Cancel,
|
||
)
|
||
if answer == QtWidgets.QMessageBox.Cancel:
|
||
event.ignore()
|
||
self.frames.pause(False)
|
||
return
|
||
|
||
self.frames.music.stop()
|
||
|
||
# Save settings
|
||
qsettings.setValue(self.tr("High score"), self.stats.high_score)
|
||
qsettings.setValue("WindowGeometry", self.saveGeometry())
|
||
qsettings.setValue("WindowState", int(self.windowState()))
|
||
|
||
|
||
def main(args={}):
|
||
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(args)
|
||
win = Window()
|
||
return app.exec_()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
return_code = main(sys.argv)
|
||
sys.exit(return_code)
|