diff --git a/game_gui.py b/game_gui.py deleted file mode 100644 index f025fd4..0000000 --- a/game_gui.py +++ /dev/null @@ -1,1473 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import os -import locale -import time -import itertools -import ctypes -import collections - -import consts -from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE -from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia -from __version__ import __title__, __author__, __version__ -from tetromino import Point, Tetromino, Ghost, Block - - -class Grid(QtWidgets.QWidget): - """ - Mother class of Hold queue, Next piece Queue, Matrix, 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 + 2 - ) - 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 = 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\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 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 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.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 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 = { - s.KEYBOARD: SettingsGroup(s.KEYBOARD, self, KeyButton), - s.DELAYS: SettingsGroup(s.DELAYS, self, QtWidgets.QSpinBox), - s.SOUND: SettingsGroup(s.SOUND, self, QtWidgets.QSlider), - 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 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 Window(QtWidgets.QMainWindow): - """ Main window """ - - def __init__(self): - splash_screen = QtWidgets.QSplashScreen( - QtGui.QPixmap(consts.SPLASH_SCREEN_PATH) - ) - splash_screen.show() - - self.set_locale() - - 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 - - self.load_settings() - - # 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 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, 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