From fd39b59111c76c1bd4451877baaee4a3e99c97da Mon Sep 17 00:00:00 2001 From: adrienmalin <41926238+adrienmalin@users.noreply.github.com> Date: Mon, 6 Aug 2018 20:49:07 +0200 Subject: [PATCH] Revert "Merge branch 'master' of https://github.com/adrienmalin/Tetris2000" This reverts commit 99d6da5990bed75097b70926f7622d852a38080a, reversing changes made to c4914eedd844b97661796b3b2c8abfa78353dfc5. --- Tetris2000.py | 10 +- block.py | 130 ----------- frames.py | 333 --------------------------- grids.py | 116 ---------- locale/Tetris2000.ts | 110 ++++----- locale/fr.ts | 110 ++++----- locale/update_ts.bat | 3 +- locale/update_ts_noobsolete.bat | 3 +- matrix.py | 393 -------------------------------- point.py | 44 ---- settings.py | 277 ---------------------- stats.py | 252 -------------------- tetromino.py | 169 +++++++++++++- window.py | 156 ------------- 14 files changed, 279 insertions(+), 1827 deletions(-) delete mode 100644 block.py delete mode 100644 frames.py delete mode 100644 grids.py delete mode 100644 matrix.py delete mode 100644 point.py delete mode 100644 settings.py delete mode 100644 stats.py delete mode 100644 window.py diff --git a/Tetris2000.py b/Tetris2000.py index 4a4ef58..0dd4144 100644 --- a/Tetris2000.py +++ b/Tetris2000.py @@ -2,17 +2,9 @@ # -*- coding: utf-8 -*- -""" -Another TETRIS® clone -Tetris Game Design by Alexey Pajitnov. -Parts of comments issued from 2009 Tetris Design Guideline -""" - - import sys - from qt5 import QtWidgets -from window import Window +from game_gui import Window diff --git a/block.py b/block.py deleted file mode 100644 index efb8237..0000000 --- a/block.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import consts -from consts import U, D -from point import Point -from qt5 import QtCore, QtGui - - -class Block: - """ - Mino or block - Mino : A single square-shaped building block of a shape called a Tetrimino. - Four Minos arranged into any of their various connected patterns is known as a Tetrimino - Block : A single block locked in a cell in the Grid - """ - - # Colors - BORDER_COLOR = consts.BLOCK_BORDER_COLOR - FILL_COLOR = consts.BLOCK_FILL_COLOR - GLOWING_BORDER_COLOR = consts.BLOCK_GLOWING_BORDER_COLOR - GLOWING_FILL_COLOR = consts.BLOCK_GLOWING_FILL_COLOR - LIGHT_COLOR = consts.BLOCK_LIGHT_COLOR - TRANSPARENT = consts.BLOCK_TRANSPARENT - GLOWING = consts.BLOCK_GLOWING - - side = consts.BLOCK_INITIAL_SIDE - - def __init__(self, coord, trail=0): - self.coord = coord - self.trail = trail - self.border_color = self.BORDER_COLOR - self.fill_color = self.FILL_COLOR - self.glowing = self.GLOWING - - def paint(self, painter, top_left_corner, spotlight): - p = top_left_corner + self.coord * Block.side - block_center = Point(Block.side/2, Block.side/2) - self.center = p + block_center - spotlight = top_left_corner + Block.side * spotlight + block_center - self.glint = 0.15 * spotlight + 0.85 * self.center - - if self.trail: - start = ( - top_left_corner + (self.coord + Point(0, self.trail * U)) * Block.side - ) - stop = top_left_corner + (self.coord + Point(0, 2 * D)) * Block.side - fill = QtGui.QLinearGradient(start, stop) - fill.setColorAt(0, self.LIGHT_COLOR) - fill.setColorAt(1, self.GLOWING_FILL_COLOR) - painter.setBrush(fill) - painter.setPen(QtCore.Qt.NoPen) - painter.drawRoundedRect( - start.x(), - start.y(), - Block.side, - Block.side * (1 + self.trail), - 20, - 20, - QtCore.Qt.RelativeSize, - ) - - if self.glowing: - fill = QtGui.QRadialGradient(self.center, self.glowing * Block.side) - fill.setColorAt(0, self.TRANSPARENT) - fill.setColorAt(0.5 / self.glowing, self.LIGHT_COLOR) - fill.setColorAt(1, self.TRANSPARENT) - painter.setBrush(QtGui.QBrush(fill)) - painter.setPen(QtCore.Qt.NoPen) - painter.drawEllipse( - self.center.x() - self.glowing * Block.side, - self.center.y() - self.glowing * Block.side, - 2 * self.glowing * Block.side, - 2 * self.glowing * Block.side, - ) - - painter.setBrush(self.brush()) - painter.setPen(self.pen()) - painter.drawRoundedRect( - p.x() + 1, - p.y() + 1, - Block.side - 2, - Block.side - 2, - 20, - 20, - QtCore.Qt.RelativeSize, - ) - - def brush(self): - if self.fill_color is None: - return QtCore.Qt.NoBrush - - fill = QtGui.QRadialGradient(self.glint, 1.5 * Block.side) - fill.setColorAt(0, self.fill_color.lighter()) - fill.setColorAt(1, self.fill_color) - return QtGui.QBrush(fill) - - def pen(self): - if self.border_color is None: - return QtCore.Qt.NoPen - - border = QtGui.QRadialGradient(self.glint, Block.side) - border.setColorAt(0, self.border_color.lighter()) - border.setColorAt(1, self.border_color.darker()) - return QtGui.QPen(QtGui.QBrush(border), 1, join=QtCore.Qt.RoundJoin) - - def shine(self, glowing=2, delay=None): - self.border_color = Block.GLOWING_BORDER_COLOR - self.fill_color = Block.GLOWING_FILL_COLOR - self.glowing = glowing - if delay: - QtCore.QTimer.singleShot(delay, self.fade) - - def fade(self): - self.border_color = Block.BORDER_COLOR - self.fill_color = Block.FILL_COLOR - self.glowing = 0 - self.trail = 0 - - -class GhostBlock(Block): - """ - Mino of the ghost piece - """ - - BORDER_COLOR = consts.GHOST_BLOCK_BORDER_COLOR - FILL_COLOR = consts.GHOST_BLOCK_FILL_COLOR - GLOWING_FILL_COLOR = consts.GHOST_BLOCK_GLOWING_FILL_COLOR - GLOWING = consts.GHOST_BLOCK_GLOWING \ No newline at end of file diff --git a/frames.py b/frames.py deleted file mode 100644 index d0ed2cd..0000000 --- a/frames.py +++ /dev/null @@ -1,333 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import os -import itertools - -import consts -from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia -from settings import s, qsettings, settings -from block import Block -from tetromino import Tetromino -from grids import Grid, HoldQueue, NextQueue -from matrix import Matrix -from stats import Stats - - - - -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_() \ No newline at end of file diff --git a/grids.py b/grids.py deleted file mode 100644 index cee194f..0000000 --- a/grids.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import consts -from qt5 import QtWidgets, QtGui -from block import Block -from point import Point -from settings import s, settings -from tetromino import Tetromino - - -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 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) \ No newline at end of file diff --git a/locale/Tetris2000.ts b/locale/Tetris2000.ts index 8d1486a..bb9b98f 100644 --- a/locale/Tetris2000.ts +++ b/locale/Tetris2000.ts @@ -3,39 +3,39 @@ Frames - + New game - + A game is in progress. Do you want to abord it? - + Start level: - + High score - + Game over - + Congratulations! You have the high score: {} - + Score: {} High score: {} @@ -44,13 +44,13 @@ High score: {} Matrix - + Level - + PAUSE Press %s @@ -58,7 +58,7 @@ to resume - + GAME OVER @@ -67,97 +67,97 @@ OVER SettingStrings - + Keyboard settings - + Move left - + Move right - + Rotate clockwise - + Rotate counterclockwise - + Soft drop - + Hard drop - + Hold - + Pause - + Other settings - + Delays - + Auto-shift delay - + Auto-repeat rate - + Sound - + Music volume - + Effects volume - + Show ghost piece - + Show next queue - + Hold enabled @@ -165,7 +165,7 @@ OVER SettingsDialog - + Settings @@ -173,95 +173,95 @@ OVER Stats - + High score - + COMBO x{:n} {:n} - + BACK TO BACK {:n} - + Score: - + High score: - + Time: {} - + Level: - + Goal: - + Lines: - + Mini T-Spins: - + T-Spins: - + Back-to-back: - + Max combo: - + Combos: - + Lines per minute: {:.1f} - + Tetrominos locked down: - + Tetrominos per minute: {:.1f} - + : @@ -269,38 +269,38 @@ OVER Window - + &New game - + &Settings - + &About - + Quit game? - + A game is in progress. Do you want to abord it? - + High score - + Tetris® clone by Adrien Malingrey Tetris Game Design by Alekseï Pajitnov diff --git a/locale/fr.ts b/locale/fr.ts index fff7efc..9326a05 100644 --- a/locale/fr.ts +++ b/locale/fr.ts @@ -4,41 +4,41 @@ Frames - + New game Nouvelle partie - + A game is in progress. Do you want to abord it? Une partie est en cours. Voulez-vous l'abandonner ? - + Start level: Commencer au niveau : - + High score Meilleur score - + Game over Partie terminée - + Congratulations! You have the high score: {} Bravo ! Vous avez atteint le meilleur score : {} - + Score: {} High score: {} Score : {} @@ -48,14 +48,14 @@ Meilleur score : {} Matrix - + Level Niveau - + PAUSE Press %s @@ -67,7 +67,7 @@ Appuyez sur pour reprendre - + GAME OVER PARTIE @@ -77,97 +77,97 @@ TERMINÉE SettingStrings - + Keyboard settings Configuration du clavier - + Move left Déplacer à gauche - + Move right Déplacer à droite - + Rotate clockwise Tourner dans le sens horaire - + Rotate counterclockwise Tourner dans le sens anti-horaire - + Soft drop Chute lente - + Hard drop Chute rapide - + Hold Réserve - + Pause Pause - + Other settings Autres paramètres - + Delays Temporisation - + Auto-shift delay Délai avant répétition - + Auto-repeat rate Vitesse de répétition - + Sound Son - + Music volume Volume de la musique - + Effects volume Volume des effets sonores - + Show ghost piece Afficher la pièce fantôme - + Show next queue Afficher les 6 prochaines pièces - + Hold enabled Activer la réserve @@ -175,7 +175,7 @@ TERMINÉE SettingsDialog - + Settings Préférences @@ -183,98 +183,98 @@ TERMINÉE Stats - + High score Meilleur score - + COMBO x{:n} {:n} COMBO x{:n} {:n} - + BACK TO BACK {:n} BACK TO BACK {:n} - + Time: {} Temps : {} - + Lines per minute: {:.1f} Lignes par minute : {:.1f} - + Tetrominos per minute: {:.1f} Tétrominos par minute : {:.1f} - + Score: Score : - + High score: Meilleur score : - + Level: Niveau : - + Goal: Objectif : - + Lines: Lignes : - + Mini T-Spins: Mini T-Spins : - + T-Spins: T-Spins : - + Back-to-back: Back-to-back : - + Max combo: Combo max : - + Combos: Combos : - + Tetrominos locked down: Tétrominos bloqués : - + : : @@ -282,39 +282,39 @@ TERMINÉE Window - + High score Meilleur score - + &New game &Nouvelle partie - + &Settings &Préférences - + &About &À propos - + A game is in progress. Do you want to abord it? Une partie est en cours. Voulez-vous l'abandonner ? - + Quit game? Quitter la partie ? - + Tetris® clone by Adrien Malingrey Tetris Game Design by Alekseï Pajitnov diff --git a/locale/update_ts.bat b/locale/update_ts.bat index f8c1e86..6079dec 100644 --- a/locale/update_ts.bat +++ b/locale/update_ts.bat @@ -1,3 +1,2 @@ -for /F %%n in ('dir /B *.ts') do pylupdate5 -verbose ..\window.py ..\settings.py ..\stats.py ..\matrix.py ..\frames.py -ts %%n -echo You may need to edit *.ts files with a text editor to correct special characters +for /F %%n in ('dir /B *.ts') do pylupdate5 ..\game_gui.py -ts %%n pause diff --git a/locale/update_ts_noobsolete.bat b/locale/update_ts_noobsolete.bat index a9c7836..d6262e5 100644 --- a/locale/update_ts_noobsolete.bat +++ b/locale/update_ts_noobsolete.bat @@ -1,3 +1,2 @@ -for /F %%n in ('dir /B *.ts') do pylupdate5 -verbose ..\window.py ..\settings.py ..\stats.py ..\matrix.py ..\frames.py -ts -noobsolete %%n -echo You may need to edit *.ts files with a text editor to correct special characters +for /F %%n in ('dir /B *.ts') do pylupdate5 ..\game_gui.py -ts -noobsolete %%n pause diff --git a/matrix.py b/matrix.py deleted file mode 100644 index efcbe8d..0000000 --- a/matrix.py +++ /dev/null @@ -1,393 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import time - -import consts -from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE -from qt5 import QtGui, QtCore, QtMultimedia -from grids import Grid -from point import Point -from block import Block -from tetromino import GhostPiece -from settings import s, settings - - -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) \ No newline at end of file diff --git a/point.py b/point.py deleted file mode 100644 index 011178a..0000000 --- a/point.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -from consts import CLOCKWISE -from qt5 import QtCore - -class Point(QtCore.QPoint): - """ - Point of coordinates (x, y) - """ - - def rotate(self, center, direction=CLOCKWISE): - """ Returns the Point image of the rotation of self - through 90° CLOKWISE or COUNTERCLOCKWISE around center""" - if self == center: - return self - - p = self - center - p = Point(-direction * p.y(), direction * p.x()) - p += center - return p - - def __add__(self, o): - return Point(self.x() + o.x(), self.y() + o.y()) - - def __sub__(self, o): - return Point(self.x() - o.x(), self.y() - o.y()) - - def __mul__(self, k): - return Point(k * self.x(), k * self.y()) - - def __truediv__(self, k): - return Point(self.x() / k, self.y() / k) - - __radd__ = __add__ - __rsub__ = __sub__ - __rmul__ = __mul__ - __rtruediv__ = __truediv__ - - def __repr__(self): - return "Point({}, {})".format(self.x(), self.y()) - - __str__ = __repr__ \ No newline at end of file diff --git a/settings.py b/settings.py deleted file mode 100644 index ba88142..0000000 --- a/settings.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import collections - -import consts -from __version__ import __author__, __title__ -from qt5 import QtWidgets, QtCore - - -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() - - -s = SettingStrings() - -qsettings = QtCore.QSettings(__author__, __title__) - -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 - ) - ), - ), - ] - ), - ), - ] -) \ No newline at end of file diff --git a/stats.py b/stats.py deleted file mode 100644 index 20b422a..0000000 --- a/stats.py +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import locale -import time - -import consts -from qt5 import QtWidgets, QtGui, QtCore, QtMultimedia -from settings import qsettings -from block import Block - - - -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) \ No newline at end of file diff --git a/tetromino.py b/tetromino.py index 85d624f..c57edb6 100644 --- a/tetromino.py +++ b/tetromino.py @@ -4,9 +4,172 @@ import random +import consts from consts import L, R, U, D, CLOCKWISE, COUNTERCLOCKWISE -from point import Point -from block import Block, GhostBlock +from qt5 import QtCore, QtGui + +class Point(QtCore.QPoint): + """ + Point of coordinates (x, y) + """ + + def __init__(self, x, y): + super().__init__(x, y) + + def __add__(self, o): + return Point(self.x() + o.x(), self.y() + o.y()) + + def __sub__(self, o): + return Point(self.x() - o.x(), self.y() - o.y()) + + def __mul__(self, k): + return Point(k * self.x(), k * self.y()) + + def __truediv__(self, k): + return Point(self.x() / k, self.y() / k) + + __radd__ = __add__ + __rsub__ = __sub__ + __rmul__ = __mul__ + __rtruediv__ = __truediv__ + + def rotate(self, center, direction=CLOCKWISE): + """ Returns the Point image of the rotation of self + through 90° CLOKWISE or COUNTERCLOCKWISE around center""" + if self == center: + return self + + p = self - center + p = Point(-direction * p.y(), direction * p.x()) + p += center + return p + + def __repr__(self): + return "Point({}, {})".format(self.x(), self.y()) + + __str__ = __repr__ + + +class Block: + """ + Mino or block + Mino : A single square-shaped building block of a shape called a Tetrimino. + Four Minos arranged into any of their various connected patterns is known as a Tetrimino + Block : A single block locked in a cell in the Grid + """ + + # Colors + BORDER_COLOR = consts.BLOCK_BORDER_COLOR + FILL_COLOR = consts.BLOCK_FILL_COLOR + GLOWING_BORDER_COLOR = consts.BLOCK_GLOWING_BORDER_COLOR + GLOWING_FILL_COLOR = consts.BLOCK_GLOWING_FILL_COLOR + LIGHT_COLOR = consts.BLOCK_LIGHT_COLOR + TRANSPARENT = consts.BLOCK_TRANSPARENT + GLOWING = consts.BLOCK_GLOWING + + side = consts.BLOCK_INITIAL_SIDE + + def __init__(self, coord, trail=0): + self.coord = coord + self.trail = trail + self.border_color = self.BORDER_COLOR + self.fill_color = self.FILL_COLOR + self.glowing = self.GLOWING + + def paint(self, painter, top_left_corner, spotlight): + p = top_left_corner + self.coord * Block.side + block_center = Point(Block.side/2, Block.side/2) + self.center = p + block_center + spotlight = top_left_corner + Block.side * spotlight + block_center + self.glint = 0.15 * spotlight + 0.85 * self.center + + if self.trail: + start = ( + top_left_corner + (self.coord + Point(0, self.trail * U)) * Block.side + ) + stop = top_left_corner + (self.coord + Point(0, 2 * D)) * Block.side + fill = QtGui.QLinearGradient(start, stop) + fill.setColorAt(0, self.LIGHT_COLOR) + fill.setColorAt(1, self.GLOWING_FILL_COLOR) + painter.setBrush(fill) + painter.setPen(QtCore.Qt.NoPen) + painter.drawRoundedRect( + start.x(), + start.y(), + Block.side, + Block.side * (1 + self.trail), + 20, + 20, + QtCore.Qt.RelativeSize, + ) + + if self.glowing: + fill = QtGui.QRadialGradient(self.center, self.glowing * Block.side) + fill.setColorAt(0, self.TRANSPARENT) + fill.setColorAt(0.5 / self.glowing, self.LIGHT_COLOR) + fill.setColorAt(1, self.TRANSPARENT) + painter.setBrush(QtGui.QBrush(fill)) + painter.setPen(QtCore.Qt.NoPen) + painter.drawEllipse( + self.center.x() - self.glowing * Block.side, + self.center.y() - self.glowing * Block.side, + 2 * self.glowing * Block.side, + 2 * self.glowing * Block.side, + ) + + painter.setBrush(self.brush()) + painter.setPen(self.pen()) + painter.drawRoundedRect( + p.x() + 1, + p.y() + 1, + Block.side - 2, + Block.side - 2, + 20, + 20, + QtCore.Qt.RelativeSize, + ) + + def brush(self): + if self.fill_color is None: + return QtCore.Qt.NoBrush + + fill = QtGui.QRadialGradient(self.glint, 1.5 * Block.side) + fill.setColorAt(0, self.fill_color.lighter()) + fill.setColorAt(1, self.fill_color) + return QtGui.QBrush(fill) + + def pen(self): + if self.border_color is None: + return QtCore.Qt.NoPen + + border = QtGui.QRadialGradient(self.glint, Block.side) + border.setColorAt(0, self.border_color.lighter()) + border.setColorAt(1, self.border_color.darker()) + return QtGui.QPen(QtGui.QBrush(border), 1, join=QtCore.Qt.RoundJoin) + + def shine(self, glowing=2, delay=None): + self.border_color = Block.GLOWING_BORDER_COLOR + self.fill_color = Block.GLOWING_FILL_COLOR + self.glowing = glowing + if delay: + QtCore.QTimer.singleShot(delay, self.fade) + + def fade(self): + self.border_color = Block.BORDER_COLOR + self.fill_color = Block.FILL_COLOR + self.glowing = 0 + self.trail = 0 + + +class GhostBlock(Block): + """ + Mino of the ghost piece + """ + + BORDER_COLOR = consts.GHOST_BLOCK_BORDER_COLOR + FILL_COLOR = consts.GHOST_BLOCK_FILL_COLOR + GLOWING_FILL_COLOR = consts.GHOST_BLOCK_GLOWING_FILL_COLOR + GLOWING = consts.GHOST_BLOCK_GLOWING class MetaTetro(type): @@ -280,7 +443,7 @@ class TetroO(Tetromino, metaclass=MetaTetro): return False -class GhostPiece(Tetromino): +class Ghost(Tetromino): """ A graphical representation of where the Tetrimino in play will come to rest if it is dropped from its current position. diff --git a/window.py b/window.py deleted file mode 100644 index 513abd8..0000000 --- a/window.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import locale -import ctypes - -import consts -from qt5 import QtWidgets, QtCore, QtGui -from __version__ import __title__, __author__, __version__ -from settings import SettingsDialog, qsettings -from frames import Frames - - -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 - - # 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 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