From d1c2be7d1fed5b375463baf79933bfc1e165f56c Mon Sep 17 00:00:00 2001 From: adrienmalin <41926238+adrienmalin@users.noreply.github.com> Date: Tue, 7 Aug 2018 01:07:44 +0200 Subject: [PATCH] More file merging --- Tetris2000.py | 1481 ++++++++++++++++++++++++++++++++++++++++++++++++- game_gui.py | 1473 ------------------------------------------------ tetromino.py | 1 - 3 files changed, 1472 insertions(+), 1483 deletions(-) delete mode 100644 game_gui.py diff --git a/Tetris2000.py b/Tetris2000.py index af3acba..673ec59 100644 --- a/Tetris2000.py +++ b/Tetris2000.py @@ -9,18 +9,1481 @@ Parts of comments issued from 2009 Tetris Design Guideline """ +import ctypes +import collections +import itertools +import locale +import os +import time import sys -from qt5 import QtWidgets -from game_gui import Window - - -def play(): +import consts +from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE +from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia +from __version__ import __title__, __author__, __version__ +from block import Block +from point import Point +from tetromino import Tetromino, GhostPiece + + +class Grid(QtWidgets.QWidget): + """ + Mother class of Hold queue, Matrix, Next piece, and Next queue + """ + + ROWS = consts.GRID_DEFAULT_ROWS + consts.GRID_INVISIBLE_ROWS + COLUMNS = consts.GRID_DEFAULT_COLUMNS + STARTING_POSITION = Point( + consts.GRID_DEFAULT_COLUMNS // 2, + consts.GRID_DEFAULT_ROWS // 2 + consts.GRID_INVISIBLE_ROWS + ) + GRIDLINE_COLOR = consts.GRID_GRIDLINE_COLOR + HARD_DROP_MOVEMENT = consts.GRID_HARD_DROP_MOVEMENT + SPOTLIGHT = Point(*consts.GRID_SPOTLIGHT ) + + 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 = consts.GRID_INVISIBLE_ROWS * 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(consts.GRID_INVISIBLE_ROWS, 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 = consts.MATRIX_ROWS + consts.GRID_INVISIBLE_ROWS + COLUMNS = consts.MATRIX_COLUMNS + STARTING_POSITION = Point(COLUMNS // 2, consts.GRID_INVISIBLE_ROWS - 1) + TEXT_COLOR = consts.MATRIX_TEXT_COLOR + + drop_signal = QtCore.Signal(int) + lock_signal = QtCore.Signal(int, str) + + def __init__(self, frames): + super().__init__(frames) + + self.load_sfx() + + self.game_over = False + self.text = "" + self.temporary_texts = [] + + 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 = [] + + def load_sfx(self): + self.wall_sfx = QtMultimedia.QSoundEffect(self) + url = QtCore.QUrl.fromLocalFile(consts.WALL_SFX_PATH) + self.wall_sfx.setSource(url) + + self.rotate_sfx = QtMultimedia.QSoundEffect(self) + url = QtCore.QUrl.fromLocalFile(consts.ROTATE_SFX_PATH) + self.rotate_sfx.setSource(url) + + self.hard_drop_sfx = QtMultimedia.QSoundEffect(self) + url = QtCore.QUrl.fromLocalFile(consts.HARD_DROP_SFX_PATH) + self.hard_drop_sfx.setSource(url) + + def new_game(self): + self.game_over = False + self.lock_delay = consts.LOCK_DELAY + self.cells = [self.empty_row() for y in range(self.ROWS)] + self.setFocus() + self.actions_to_repeat = [] + self.wall_hit = False + + def new_level(self, level): + self.show_temporary_text(self.tr("Level\n") + str(level)) + self.speed = consts.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 (0 <= y and 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() + self.wall_hit = False + elif not self.wall_hit: + self.wall_hit = True + self.wall_sfx.play() + + elif action == s.MOVE_RIGHT: + if self.piece.move(R, 0): + self.rotate_sfx.play() + self.lock_wait() + self.wall_hit = False + elif not self.wall_hit: + self.wall_hit = True + self.wall_sfx.play() + + elif action == s.ROTATE_CLOCKWISE: + if self.piece.rotate(direction=CLOCKWISE): + self.rotate_sfx.play() + self.lock_wait() + self.wall_hit = False + elif not self.wall_hit: + self.wall_hit = True + self.wall_sfx.play() + + elif action == s.ROTATE_COUNTERCLOCKWISE: + if self.piece.rotate(direction=COUNTERCLOCKWISE): + self.lock_wait() + self.wall_hit = False + elif not self.wall_hit: + self.wall_hit = True + self.wall_sfx.play() + + elif action == s.SOFT_DROP: + if self.piece.soft_drop(): + self.drop_signal.emit(1) + self.wall_hit = False + elif not self.wall_hit: + self.wall_hit = True + self.wall_sfx.play() + + elif action == s.HARD_DROP: + trail = self.piece.hard_drop() + self.top_left_corner += Point(0, self.HARD_DROP_MOVEMENT * Block.side) + self.drop_signal.emit(2 * trail) + QtCore.QTimer.singleShot(consts.ANIMATION_DELAY, self.after_hard_drop) + self.hard_drop_sfx.play() + 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. + """ + + self.wall_sfx.play() + + # 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=consts.ANIMATION_DELAY) + self.update() + + if all(mino.coord.y() < consts.GRID_INVISIBLE_ROWS 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 self.complete_lines: + self.fall_timer.stop() + QtCore.QTimer.singleShot(consts.LINE_CLEAR_DELAY, self.eliminate_phase) + else: + self.frames.new_piece() + + + 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 = GhostPiece(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\n" + "\n\n".join(self.temporary_texts), + ) + + def resizeEvent(self, event): + super().resizeEvent(event) + self.temporary_text_font = QtGui.QFont(consts.MATRIX_FONT_NAME, 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(consts.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 = consts.NEXT_QUEUE_ROWS + COLUMNS = consts.NEXT_QUEUE_COLUMNS + + 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 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 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 = consts.STATS_ROWS + COLUMNS = consts.STATS_COLUMNS + TEXT_COLOR = consts.STATS_TEXT_COLOR + + temporary_text = QtCore.Signal(str) + + def __init__(self, frames): + super().__init__(frames) + self.frames = frames + self.setStyleSheet("background-color: transparent") + + self.load_sfx() + + 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_sfx(self): + self.line_clear_sfx = QtMultimedia.QSoundEffect(self) + url = QtCore.QUrl.fromLocalFile(consts.LINE_CLEAR_SFX_PATH) + self.line_clear_sfx.setSource(url) + + self.tetris_sfx = QtMultimedia.QSoundEffect(self) + url = QtCore.QUrl.fromLocalFile(consts.TETRIS_SFX_PATH) + self.tetris_sfx.setSource(url) + + 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 = consts.SCORES[nb_complete_lines][t_spin] + + if score: + text = " ".join((t_spin, consts.SCORES[nb_complete_lines]["name"])) + if (t_spin and nb_complete_lines) or nb_complete_lines == 4: + self.tetris_sfx.play() + elif t_spin or nb_complete_lines: + self.line_clear_sfx.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.matrix.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"] + + self.tr(": ") + + locale.format("%i", nb, grouping=True, monetary=True) + for score_type, nb in tuple(zip(consts.SCORES, self.lines_stats))[1:] + ) + return text + + def resizeEvent(self, event): + self.font = QtGui.QFont(consts.STATS_FONT_NAME, Block.side / 3.5) + + +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.load_music() + + self.hold_queue = HoldQueue(self) + self.matrix = Matrix(self) + self.next_piece = Grid(self) + self.stats = Stats(self) + self.next_queue = NextQueue(self) + + self.matrices = (self.hold_queue, self.matrix, self.next_piece) + self.columns = sum(matrix.COLUMNS + 2 for matrix in self.matrices) + self.rows = self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS + + 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 = 0, 0 + grid.addWidget( + self.hold_queue, y, x, self.hold_queue.ROWS + 1, self.hold_queue.COLUMNS + 2 + ) + x += self.hold_queue.COLUMNS + 2 + grid.addWidget(self.matrix, y, x, self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS, self.matrix.COLUMNS + 2) + x += self.matrix.COLUMNS + 3 + grid.addWidget( + self.next_piece, y, x, self.next_piece.ROWS + 1, self.next_piece.COLUMNS + 2 + ) + 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.matrix.COLUMNS + 5 + grid.addWidget( + self.next_queue, y, x, self.next_queue.ROWS, self.next_queue.COLUMNS + 2 + ) + 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.matrix.show_temporary_text) + self.matrix.drop_signal.connect(self.stats.update_drop_score) + self.matrix.lock_signal.connect(self.stats.update_score) + + self.set_background(os.path.join(consts.BG_IMAGE_DIR, consts.START_BG_IMAGE_NAME)) + + self.apply_settings() + + def load_music(self): + playlist = QtMultimedia.QMediaPlaylist(self) + for entry in os.scandir(consts.MUSICS_DIR): + path = os.path.join(consts.MUSICS_DIR, entry.name) + url = QtCore.QUrl.fromLocalFile(path) + music = QtMultimedia.QMediaContent(url) + playlist.addMedia(music) + 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 apply_settings(self): + if self.music.volume() > 5 and self.playing: + self.music.play() + else: + self.music.pause() + + if self.playing: + self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED] + self.pause(False) + + self.matrix.keys = { + getattr(QtCore.Qt, "Key_" + name): action + for action, name in settings[s.KEYBOARD].items() + } + self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE]) + self.matrix.spotlight = Matrix.SPOTLIGHT + + for sfx in ( + self.matrix.rotate_sfx, self.matrix.wall_sfx, + self.stats.line_clear_sfx, self.stats.tetris_sfx + ): + sfx.setVolume(settings[s.SOUND][s.SFX_VOLUME]) + + def resizeEvent(self, event): + Block.side = 0.9 * min(self.width() // self.columns, self.height() // self.rows) + self.resize_bg_image() + + def reset_backgrounds(self): + backgrounds_paths = ( + os.path.join(consts.BG_IMAGE_DIR, entry.name) + for entry in os.scandir(consts.BG_IMAGE_DIR) + ) + self.backgrounds_cycle = itertools.cycle(backgrounds_paths) + + def set_background(self, path): + self.bg_image = QtGui.QImage(path) + 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() + ) + self.update() + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.drawPixmap( + self.rect(), + self.resized_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.load_music() + self.music.play() + self.hold_queue.piece = None + self.stats.new_game() + self.matrix.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() + self.new_piece() + + def new_level(self): + self.set_background(next(self.backgrounds_cycle)) + level = self.stats.new_level() + self.matrix.new_level(level) + + def new_piece(self): + if self.stats.goal <= 0: + self.new_level() + self.matrix.insert(self.next_piece.piece) + self.matrix.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.matrix.piece.move(0, 0): + self.game_over() + return + + self.matrix.fall_timer.start(self.matrix.speed) + + def pause(self, paused): + if not self.playing: + return + + if paused: + self.paused = True + self.update() + self.matrix.fall_timer.stop() + self.stats.clock.stop() + self.matrix.auto_repeat_timer.stop() + self.music.pause() + else: + self.matrix.text = "" + self.update() + QtCore.QTimer.singleShot(1000, self.resume) + + def resume(self): + self.paused = False + self.update() + self.matrix.fall_timer.start(self.matrix.speed) + self.stats.clock.start(1000) + self.matrix.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.matrix.piece) + self.matrix.insert(piece) + self.update() + else: + self.hold_queue.insert(self.matrix.piece) + self.new_piece() + self.hold_enabled = False + + def game_over(self): + self.matrix.fall_timer.stop() + self.stats.clock.stop() + self.matrix.auto_repeat_timer.stop() + self.music.stop() + self.playing = False + self.matrix.game_over = True + msgbox = QtWidgets.QMessageBox(self) + msgbox.setWindowTitle(self.tr("Game over")) + msgbox.setIcon(QtWidgets.QMessageBox.Information) + if self.stats.score_total == self.stats.high_score: + msgbox.setText( + self.tr( + "Congratulations!\nYou have the high score: {}" + ).format( + + locale.format("%i", self.stats.high_score, grouping=True, monetary=True) + ) + ) + qsettings.setValue(self.tr("High score"), self.stats.high_score) + else: + msgbox.setText( + self.tr( + "Score: {}\nHigh score: {}" + ).format( + locale.format("%i", self.stats.score_total, grouping=True, monetary=True), + locale.format("%i", self.stats.high_score, grouping=True, monetary=True) + ) + ) + msgbox.setDetailedText(self.stats.text(full_stats=True)) + msgbox.exec_() + + +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.SFX_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 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) + + layout = QtWidgets.QGridLayout() + + 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) + + layout.addWidget(self.groups[s.KEYBOARD], 0, 0, 3, 1) + layout.addWidget(self.groups[s.DELAYS], 0, 1) + layout.addWidget(self.groups[s.SOUND], 1, 1) + layout.addWidget(self.groups[s.OTHER], 2, 1) + + buttons = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel + ) + buttons.accepted.connect(self.ok) + buttons.rejected.connect(self.close) + layout.addWidget(buttons, 3, 0, 1, 2) + + self.setLayout(layout) + + self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].valueChanged.connect( + parent.frames.music.setVolume + ) + self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderPressed.connect( + parent.frames.music.play) + self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderReleased.connect( + parent.frames.music.pause) + + self.groups[s.SOUND].widgets[s.SFX_VOLUME].sliderReleased.connect( + parent.frames.stats.line_clear_sfx.play) + + 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 Window(QtWidgets.QMainWindow): + """ Main window """ + + def __init__(self): + splash_screen = QtWidgets.QSplashScreen( + QtGui.QPixmap(consts.SPLASH_SCREEN_PATH) + ) + splash_screen.show() + + self.set_locale() + + self.load_settings() + + super().__init__() + self.setWindowTitle(__title__.upper()) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + self.setWindowIcon(QtGui.QIcon(consts.ICON_PATH)) + # Windows' taskbar icon + try: + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + ".".join((__author__, __title__, __version__)) + ) + except AttributeError: + pass + + # Stylesheet + try: + import qdarkstyle + except ImportError: + pass + else: + self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment()) + + for font_path in consts.STATS_FONT_PATH, consts.MATRIX_FONT_PATH: + QtGui.QFontDatabase.addApplicationFont(font_path) + + self.frames = Frames(self) + self.setCentralWidget(self.frames) + self.hold_queue = self.frames.hold_queue + self.matrix = self.frames.matrix + self.stats = self.frames.stats + + self.menu = self.menuBar() + + geometry = qsettings.value("WindowGeometry") + if geometry: + self.restoreGeometry(geometry) + else: + self.resize(*consts.DEFAULT_WINDOW_SIZE) + self.setWindowState( + QtCore.Qt.WindowStates( + int(qsettings.value("WindowState", QtCore.Qt.WindowActive)) + ) + ) + + splash_screen.finish(self); + + 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, consts.LOCALE_PATH): + app.installTranslator(tetris2000_translator) + + def load_settings(self): + global s + s = SettingStrings() + global qsettings + qsettings = QtCore.QSettings(__author__, __title__) + global settings + settings = collections.OrderedDict( + [ + ( + s.KEYBOARD, + collections.OrderedDict( + [ + ( + s.MOVE_LEFT, + qsettings.value(s.KEYBOARD + "/" + s.MOVE_LEFT, consts.DEFAULT_MOVE_LEFT_KEY), + ), + ( + s.MOVE_RIGHT, + qsettings.value( + s.KEYBOARD + "/" + s.MOVE_RIGHT, consts.DEFAULT_MOVE_RIGHT_KEY + ), + ), + ( + s.ROTATE_CLOCKWISE, + qsettings.value( + s.KEYBOARD + "/" + s.ROTATE_CLOCKWISE, consts.DEFAULT_ROTATE_CLOCKWISE_KEY + ), + ), + ( + s.ROTATE_COUNTERCLOCKWISE, + qsettings.value( + s.KEYBOARD + "/" + s.ROTATE_COUNTERCLOCKWISE, + consts.DEFAULT_ROTATE_COUNTERCLOCKWISE_KEY, + ), + ), + ( + s.SOFT_DROP, + qsettings.value(s.KEYBOARD + "/" + s.SOFT_DROP, consts.DEFAULT_SOFT_DROP_KEY), + ), + ( + s.HARD_DROP, + qsettings.value( + s.KEYBOARD + "/" + s.HARD_DROP, consts.DEFAULT_HARD_DROP_KEY + ), + ), + ( + s.HOLD, + qsettings.value(s.KEYBOARD + "/" + s.HOLD, consts.DEFAULT_HOLD_KEY), + ), + ( + s.PAUSE, + qsettings.value(s.KEYBOARD + "/" + s.PAUSE, consts.DEFAULT_PAUSE_KEY), + ), + ] + ), + ), + ( + s.DELAYS, + collections.OrderedDict( + [ + ( + s.AUTO_SHIFT_DELAY, + int( + qsettings.value( + s.DELAYS + "/" + s.AUTO_SHIFT_DELAY, consts.DEFAULT_AUTO_SHIFT_DELAY + ) + ), + ), + ( + s.AUTO_REPEAT_RATE, + int( + qsettings.value( + s.DELAYS + "/" + s.AUTO_REPEAT_RATE, consts.DEFAULT_AUTO_REPEAT_RATE + ) + ), + ), + ] + ), + ), + ( + s.SOUND, + collections.OrderedDict( + [ + ( + s.MUSIC_VOLUME, + int( + qsettings.value(s.SOUND + "/" + s.MUSIC_VOLUME, consts.DEFAUT_MUSIC_VOLUME) + ), + ), + ( + s.SFX_VOLUME, + int( + qsettings.value( + s.SOUND + "/" + s.SFX_VOLUME, consts.DEFAULT_SFX_VOLUME + ) + ), + ), + ] + ), + ), + ( + s.OTHER, + collections.OrderedDict( + [ + ( + s.GHOST, + bool(qsettings.value(s.OTHER + "/" + s.GHOST, consts.DEFAULT_SHOW_GHOST)), + ), + ( + s.SHOW_NEXT_QUEUE, + bool( + qsettings.value( + s.OTHER + "/" + s.SHOW_NEXT_QUEUE, consts.DEFAULT_SHOW_NEXT_QUEUE + ) + ), + ), + ( + s.HOLD_ENABLED, + bool( + qsettings.value( + s.OTHER + "/" + s.HOLD_ENABLED, consts.DEFAULT_HOLD_ENABLED + ) + ), + ), + ] + ), + ), + ] + ) + + 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_() + + self.frames.apply_settings() + + def about(self): + QtWidgets.QMessageBox.about( + self, + __title__, + self.tr( +"""Tetris® clone by Adrien Malingrey + +Tetris Game Design by Alekseï Pajitnov +Graphism inspired by Tetris Effect +Window style sheet: qdarkstyle by Colin Duquesnoy +Fonts by Markus Koellmann, Peter Wiegel +Images from: +OpenGameArt.org by beren77, Duion +Pexels.com by Min An, Jaymantri, Felix Mittermeier +Pixabay.com by LoganArt +Pixnio.com by Adrian Pelletier +Unsplash.com by Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja +StockSnap.io by Nathan Anderson, José Ignacio Pompé +Musics from ocremix.org by: +CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS +Sound effects made with voc-one by Simple-Media""" + ), + ) + if self.frames.playing: + self.frames.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())) + + +if __name__ == "__main__": app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv) win = Window() win.show() win.frames.new_game() - sys.exit(app.exec_()) - -if __name__ == "__main__": - play() \ No newline at end of file + sys.exit(app.exec_()) \ No newline at end of file diff --git a/game_gui.py b/game_gui.py deleted file mode 100644 index c0b42ab..0000000 --- a/game_gui.py +++ /dev/null @@ -1,1473 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import ctypes -import collections -import itertools -import locale -import os -import time - -import consts -from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE -from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia -from __version__ import __title__, __author__, __version__ -from block import Block -from point import Point -from tetromino import Tetromino, GhostPiece - - -class Grid(QtWidgets.QWidget): - """ - Mother class of Hold queue, Matrix, Next piece, and Next queue - """ - - ROWS = consts.GRID_DEFAULT_ROWS + consts.GRID_INVISIBLE_ROWS - COLUMNS = consts.GRID_DEFAULT_COLUMNS - STARTING_POSITION = Point( - consts.GRID_DEFAULT_COLUMNS // 2, - consts.GRID_DEFAULT_ROWS // 2 + consts.GRID_INVISIBLE_ROWS - ) - GRIDLINE_COLOR = consts.GRID_GRIDLINE_COLOR - HARD_DROP_MOVEMENT = consts.GRID_HARD_DROP_MOVEMENT - SPOTLIGHT = Point(*consts.GRID_SPOTLIGHT ) - - 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 = consts.GRID_INVISIBLE_ROWS * 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(consts.GRID_INVISIBLE_ROWS, 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 = consts.MATRIX_ROWS + consts.GRID_INVISIBLE_ROWS - COLUMNS = consts.MATRIX_COLUMNS - STARTING_POSITION = Point(COLUMNS // 2, consts.GRID_INVISIBLE_ROWS - 1) - TEXT_COLOR = consts.MATRIX_TEXT_COLOR - - drop_signal = QtCore.Signal(int) - lock_signal = QtCore.Signal(int, str) - - def __init__(self, frames): - super().__init__(frames) - - self.load_sfx() - - self.game_over = False - self.text = "" - self.temporary_texts = [] - - 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 = [] - - def load_sfx(self): - self.wall_sfx = QtMultimedia.QSoundEffect(self) - url = QtCore.QUrl.fromLocalFile(consts.WALL_SFX_PATH) - self.wall_sfx.setSource(url) - - self.rotate_sfx = QtMultimedia.QSoundEffect(self) - url = QtCore.QUrl.fromLocalFile(consts.ROTATE_SFX_PATH) - self.rotate_sfx.setSource(url) - - self.hard_drop_sfx = QtMultimedia.QSoundEffect(self) - url = QtCore.QUrl.fromLocalFile(consts.HARD_DROP_SFX_PATH) - self.hard_drop_sfx.setSource(url) - - def new_game(self): - self.game_over = False - self.lock_delay = consts.LOCK_DELAY - self.cells = [self.empty_row() for y in range(self.ROWS)] - self.setFocus() - self.actions_to_repeat = [] - self.wall_hit = False - - def new_level(self, level): - self.show_temporary_text(self.tr("Level\n") + str(level)) - self.speed = consts.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 (0 <= y and 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() - self.wall_hit = False - elif not self.wall_hit: - self.wall_hit = True - self.wall_sfx.play() - - elif action == s.MOVE_RIGHT: - if self.piece.move(R, 0): - self.rotate_sfx.play() - self.lock_wait() - self.wall_hit = False - elif not self.wall_hit: - self.wall_hit = True - self.wall_sfx.play() - - elif action == s.ROTATE_CLOCKWISE: - if self.piece.rotate(direction=CLOCKWISE): - self.rotate_sfx.play() - self.lock_wait() - self.wall_hit = False - elif not self.wall_hit: - self.wall_hit = True - self.wall_sfx.play() - - elif action == s.ROTATE_COUNTERCLOCKWISE: - if self.piece.rotate(direction=COUNTERCLOCKWISE): - self.lock_wait() - self.wall_hit = False - elif not self.wall_hit: - self.wall_hit = True - self.wall_sfx.play() - - elif action == s.SOFT_DROP: - if self.piece.soft_drop(): - self.drop_signal.emit(1) - self.wall_hit = False - elif not self.wall_hit: - self.wall_hit = True - self.wall_sfx.play() - - elif action == s.HARD_DROP: - trail = self.piece.hard_drop() - self.top_left_corner += Point(0, self.HARD_DROP_MOVEMENT * Block.side) - self.drop_signal.emit(2 * trail) - QtCore.QTimer.singleShot(consts.ANIMATION_DELAY, self.after_hard_drop) - self.hard_drop_sfx.play() - 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. - """ - - self.wall_sfx.play() - - # 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=consts.ANIMATION_DELAY) - self.update() - - if all(mino.coord.y() < consts.GRID_INVISIBLE_ROWS 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 self.complete_lines: - self.fall_timer.stop() - QtCore.QTimer.singleShot(consts.LINE_CLEAR_DELAY, self.eliminate_phase) - else: - self.frames.new_piece() - - - 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 = GhostPiece(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\n" + "\n\n".join(self.temporary_texts), - ) - - def resizeEvent(self, event): - super().resizeEvent(event) - self.temporary_text_font = QtGui.QFont(consts.MATRIX_FONT_NAME, 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(consts.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 = consts.NEXT_QUEUE_ROWS - COLUMNS = consts.NEXT_QUEUE_COLUMNS - - 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 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 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 = consts.STATS_ROWS - COLUMNS = consts.STATS_COLUMNS - TEXT_COLOR = consts.STATS_TEXT_COLOR - - temporary_text = QtCore.Signal(str) - - def __init__(self, frames): - super().__init__(frames) - self.frames = frames - self.setStyleSheet("background-color: transparent") - - self.load_sfx() - - 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_sfx(self): - self.line_clear_sfx = QtMultimedia.QSoundEffect(self) - url = QtCore.QUrl.fromLocalFile(consts.LINE_CLEAR_SFX_PATH) - self.line_clear_sfx.setSource(url) - - self.tetris_sfx = QtMultimedia.QSoundEffect(self) - url = QtCore.QUrl.fromLocalFile(consts.TETRIS_SFX_PATH) - self.tetris_sfx.setSource(url) - - 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 = consts.SCORES[nb_complete_lines][t_spin] - - if score: - text = " ".join((t_spin, consts.SCORES[nb_complete_lines]["name"])) - if (t_spin and nb_complete_lines) or nb_complete_lines == 4: - self.tetris_sfx.play() - elif t_spin or nb_complete_lines: - self.line_clear_sfx.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.matrix.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"] - + self.tr(": ") - + locale.format("%i", nb, grouping=True, monetary=True) - for score_type, nb in tuple(zip(consts.SCORES, self.lines_stats))[1:] - ) - return text - - def resizeEvent(self, event): - self.font = QtGui.QFont(consts.STATS_FONT_NAME, Block.side / 3.5) - - -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.load_music() - - self.hold_queue = HoldQueue(self) - self.matrix = Matrix(self) - self.next_piece = Grid(self) - self.stats = Stats(self) - self.next_queue = NextQueue(self) - - self.matrices = (self.hold_queue, self.matrix, self.next_piece) - self.columns = sum(matrix.COLUMNS + 2 for matrix in self.matrices) - self.rows = self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS - - 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 = 0, 0 - grid.addWidget( - self.hold_queue, y, x, self.hold_queue.ROWS + 1, self.hold_queue.COLUMNS + 2 - ) - x += self.hold_queue.COLUMNS + 2 - grid.addWidget(self.matrix, y, x, self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS, self.matrix.COLUMNS + 2) - x += self.matrix.COLUMNS + 3 - grid.addWidget( - self.next_piece, y, x, self.next_piece.ROWS + 1, self.next_piece.COLUMNS + 2 - ) - 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.matrix.COLUMNS + 5 - grid.addWidget( - self.next_queue, y, x, self.next_queue.ROWS, self.next_queue.COLUMNS + 2 - ) - 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.matrix.show_temporary_text) - self.matrix.drop_signal.connect(self.stats.update_drop_score) - self.matrix.lock_signal.connect(self.stats.update_score) - - self.set_background(os.path.join(consts.BG_IMAGE_DIR, consts.START_BG_IMAGE_NAME)) - - self.apply_settings() - - def load_music(self): - playlist = QtMultimedia.QMediaPlaylist(self) - for entry in os.scandir(consts.MUSICS_DIR): - path = os.path.join(consts.MUSICS_DIR, entry.name) - url = QtCore.QUrl.fromLocalFile(path) - music = QtMultimedia.QMediaContent(url) - playlist.addMedia(music) - 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 apply_settings(self): - if self.music.volume() > 5 and self.playing: - self.music.play() - else: - self.music.pause() - - if self.playing: - self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED] - self.pause(False) - - self.matrix.keys = { - getattr(QtCore.Qt, "Key_" + name): action - for action, name in settings[s.KEYBOARD].items() - } - self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE]) - self.matrix.spotlight = Matrix.SPOTLIGHT - - for sfx in ( - self.matrix.rotate_sfx, self.matrix.wall_sfx, - self.stats.line_clear_sfx, self.stats.tetris_sfx - ): - sfx.setVolume(settings[s.SOUND][s.SFX_VOLUME]) - - def resizeEvent(self, event): - Block.side = 0.9 * min(self.width() // self.columns, self.height() // self.rows) - self.resize_bg_image() - - def reset_backgrounds(self): - backgrounds_paths = ( - os.path.join(consts.BG_IMAGE_DIR, entry.name) - for entry in os.scandir(consts.BG_IMAGE_DIR) - ) - self.backgrounds_cycle = itertools.cycle(backgrounds_paths) - - def set_background(self, path): - self.bg_image = QtGui.QImage(path) - 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() - ) - self.update() - - def paintEvent(self, event): - painter = QtGui.QPainter(self) - painter.drawPixmap( - self.rect(), - self.resized_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.load_music() - self.music.play() - self.hold_queue.piece = None - self.stats.new_game() - self.matrix.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() - self.new_piece() - - def new_level(self): - self.set_background(next(self.backgrounds_cycle)) - level = self.stats.new_level() - self.matrix.new_level(level) - - def new_piece(self): - if self.stats.goal <= 0: - self.new_level() - self.matrix.insert(self.next_piece.piece) - self.matrix.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.matrix.piece.move(0, 0): - self.game_over() - return - - self.matrix.fall_timer.start(self.matrix.speed) - - def pause(self, paused): - if not self.playing: - return - - if paused: - self.paused = True - self.update() - self.matrix.fall_timer.stop() - self.stats.clock.stop() - self.matrix.auto_repeat_timer.stop() - self.music.pause() - else: - self.matrix.text = "" - self.update() - QtCore.QTimer.singleShot(1000, self.resume) - - def resume(self): - self.paused = False - self.update() - self.matrix.fall_timer.start(self.matrix.speed) - self.stats.clock.start(1000) - self.matrix.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.matrix.piece) - self.matrix.insert(piece) - self.update() - else: - self.hold_queue.insert(self.matrix.piece) - self.new_piece() - self.hold_enabled = False - - def game_over(self): - self.matrix.fall_timer.stop() - self.stats.clock.stop() - self.matrix.auto_repeat_timer.stop() - self.music.stop() - self.playing = False - self.matrix.game_over = True - msgbox = QtWidgets.QMessageBox(self) - msgbox.setWindowTitle(self.tr("Game over")) - msgbox.setIcon(QtWidgets.QMessageBox.Information) - if self.stats.score_total == self.stats.high_score: - msgbox.setText( - self.tr( - "Congratulations!\nYou have the high score: {}" - ).format( - - locale.format("%i", self.stats.high_score, grouping=True, monetary=True) - ) - ) - qsettings.setValue(self.tr("High score"), self.stats.high_score) - else: - msgbox.setText( - self.tr( - "Score: {}\nHigh score: {}" - ).format( - locale.format("%i", self.stats.score_total, grouping=True, monetary=True), - locale.format("%i", self.stats.high_score, grouping=True, monetary=True) - ) - ) - msgbox.setDetailedText(self.stats.text(full_stats=True)) - msgbox.exec_() - - -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.SFX_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 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) - - layout = QtWidgets.QGridLayout() - - 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) - - layout.addWidget(self.groups[s.KEYBOARD], 0, 0, 3, 1) - layout.addWidget(self.groups[s.DELAYS], 0, 1) - layout.addWidget(self.groups[s.SOUND], 1, 1) - layout.addWidget(self.groups[s.OTHER], 2, 1) - - buttons = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel - ) - buttons.accepted.connect(self.ok) - buttons.rejected.connect(self.close) - layout.addWidget(buttons, 3, 0, 1, 2) - - self.setLayout(layout) - - self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].valueChanged.connect( - parent.frames.music.setVolume - ) - self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderPressed.connect( - parent.frames.music.play) - self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderReleased.connect( - parent.frames.music.pause) - - self.groups[s.SOUND].widgets[s.SFX_VOLUME].sliderReleased.connect( - parent.frames.stats.line_clear_sfx.play) - - 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 Window(QtWidgets.QMainWindow): - """ Main window """ - - def __init__(self): - splash_screen = QtWidgets.QSplashScreen( - QtGui.QPixmap(consts.SPLASH_SCREEN_PATH) - ) - splash_screen.show() - - self.set_locale() - - self.load_settings() - - super().__init__() - self.setWindowTitle(__title__.upper()) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - - self.setWindowIcon(QtGui.QIcon(consts.ICON_PATH)) - # Windows' taskbar icon - try: - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( - ".".join((__author__, __title__, __version__)) - ) - except AttributeError: - pass - - # Stylesheet - try: - import qdarkstyle - except ImportError: - pass - else: - self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment()) - - for font_path in consts.STATS_FONT_PATH, consts.MATRIX_FONT_PATH: - QtGui.QFontDatabase.addApplicationFont(font_path) - - self.frames = Frames(self) - self.setCentralWidget(self.frames) - self.hold_queue = self.frames.hold_queue - self.matrix = self.frames.matrix - self.stats = self.frames.stats - - self.menu = self.menuBar() - - geometry = qsettings.value("WindowGeometry") - if geometry: - self.restoreGeometry(geometry) - else: - self.resize(*consts.DEFAULT_WINDOW_SIZE) - self.setWindowState( - QtCore.Qt.WindowStates( - int(qsettings.value("WindowState", QtCore.Qt.WindowActive)) - ) - ) - - splash_screen.finish(self); - - 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, consts.LOCALE_PATH): - app.installTranslator(tetris2000_translator) - - def load_settings(self): - global s - s = SettingStrings() - global qsettings - qsettings = QtCore.QSettings(__author__, __title__) - global settings - settings = collections.OrderedDict( - [ - ( - s.KEYBOARD, - collections.OrderedDict( - [ - ( - s.MOVE_LEFT, - qsettings.value(s.KEYBOARD + "/" + s.MOVE_LEFT, consts.DEFAULT_MOVE_LEFT_KEY), - ), - ( - s.MOVE_RIGHT, - qsettings.value( - s.KEYBOARD + "/" + s.MOVE_RIGHT, consts.DEFAULT_MOVE_RIGHT_KEY - ), - ), - ( - s.ROTATE_CLOCKWISE, - qsettings.value( - s.KEYBOARD + "/" + s.ROTATE_CLOCKWISE, consts.DEFAULT_ROTATE_CLOCKWISE_KEY - ), - ), - ( - s.ROTATE_COUNTERCLOCKWISE, - qsettings.value( - s.KEYBOARD + "/" + s.ROTATE_COUNTERCLOCKWISE, - consts.DEFAULT_ROTATE_COUNTERCLOCKWISE_KEY, - ), - ), - ( - s.SOFT_DROP, - qsettings.value(s.KEYBOARD + "/" + s.SOFT_DROP, consts.DEFAULT_SOFT_DROP_KEY), - ), - ( - s.HARD_DROP, - qsettings.value( - s.KEYBOARD + "/" + s.HARD_DROP, consts.DEFAULT_HARD_DROP_KEY - ), - ), - ( - s.HOLD, - qsettings.value(s.KEYBOARD + "/" + s.HOLD, consts.DEFAULT_HOLD_KEY), - ), - ( - s.PAUSE, - qsettings.value(s.KEYBOARD + "/" + s.PAUSE, consts.DEFAULT_PAUSE_KEY), - ), - ] - ), - ), - ( - s.DELAYS, - collections.OrderedDict( - [ - ( - s.AUTO_SHIFT_DELAY, - int( - qsettings.value( - s.DELAYS + "/" + s.AUTO_SHIFT_DELAY, consts.DEFAULT_AUTO_SHIFT_DELAY - ) - ), - ), - ( - s.AUTO_REPEAT_RATE, - int( - qsettings.value( - s.DELAYS + "/" + s.AUTO_REPEAT_RATE, consts.DEFAULT_AUTO_REPEAT_RATE - ) - ), - ), - ] - ), - ), - ( - s.SOUND, - collections.OrderedDict( - [ - ( - s.MUSIC_VOLUME, - int( - qsettings.value(s.SOUND + "/" + s.MUSIC_VOLUME, consts.DEFAUT_MUSIC_VOLUME) - ), - ), - ( - s.SFX_VOLUME, - int( - qsettings.value( - s.SOUND + "/" + s.SFX_VOLUME, consts.DEFAULT_SFX_VOLUME - ) - ), - ), - ] - ), - ), - ( - s.OTHER, - collections.OrderedDict( - [ - ( - s.GHOST, - bool(qsettings.value(s.OTHER + "/" + s.GHOST, consts.DEFAULT_SHOW_GHOST)), - ), - ( - s.SHOW_NEXT_QUEUE, - bool( - qsettings.value( - s.OTHER + "/" + s.SHOW_NEXT_QUEUE, consts.DEFAULT_SHOW_NEXT_QUEUE - ) - ), - ), - ( - s.HOLD_ENABLED, - bool( - qsettings.value( - s.OTHER + "/" + s.HOLD_ENABLED, consts.DEFAULT_HOLD_ENABLED - ) - ), - ), - ] - ), - ), - ] - ) - - 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_() - - self.frames.apply_settings() - - def about(self): - QtWidgets.QMessageBox.about( - self, - __title__, - self.tr( -"""Tetris® clone by Adrien Malingrey - -Tetris Game Design by Alekseï Pajitnov -Graphism inspired by Tetris Effect -Window style sheet: qdarkstyle by Colin Duquesnoy -Fonts by Markus Koellmann, Peter Wiegel -Images from: -OpenGameArt.org by beren77, Duion -Pexels.com by Min An, Jaymantri, Felix Mittermeier -Pixabay.com by LoganArt -Pixnio.com by Adrian Pelletier -Unsplash.com by Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja -StockSnap.io by Nathan Anderson, José Ignacio Pompé -Musics from ocremix.org by: -CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS -Sound effects made with voc-one by Simple-Media""" - ), - ) - if self.frames.playing: - self.frames.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())) \ No newline at end of file diff --git a/tetromino.py b/tetromino.py index 85d624f..c5b354b 100644 --- a/tetromino.py +++ b/tetromino.py @@ -299,4 +299,3 @@ class GhostPiece(Tetromino): def hard_drop(self): while self.move(0, D): pass -