From a264f0cae0b326ac348a3ed43ecb38a4afd81a2e Mon Sep 17 00:00:00 2001 From: adrienmalin <41926238+adrienmalin@users.noreply.github.com> Date: Mon, 6 Aug 2018 23:00:06 +0200 Subject: [PATCH] Revert back merge error 4h ago I nearly lost my mind --- LICENSE | 42 +- Tetris2000.py | 10 +- block.py | 130 ++++++ frames.py | 333 +++++++++++++++ grids.py | 230 +++++------ locale/Tetris2000.ts | 646 ++++++++++++++--------------- locale/fr.ts | 704 ++++++++++++++++---------------- locale/update_ts.bat | 3 +- locale/update_ts_noobsolete.bat | 3 +- matrix.py | 393 ++++++++++++++++++ point.py | 86 ++-- settings.py | 277 +++++++++++++ stats.py | 252 ++++++++++++ tetromino.py | 169 +------- window.py | 156 +++++++ 15 files changed, 2411 insertions(+), 1023 deletions(-) create mode 100644 block.py create mode 100644 frames.py create mode 100644 matrix.py create mode 100644 settings.py create mode 100644 stats.py create mode 100644 window.py diff --git a/LICENSE b/LICENSE index 8c624ec..6b5f64a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 adrienmalin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2018 adrienmalin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Tetris2000.py b/Tetris2000.py index 0dd4144..4a4ef58 100644 --- a/Tetris2000.py +++ b/Tetris2000.py @@ -2,9 +2,17 @@ # -*- 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 game_gui import Window +from window import Window diff --git a/block.py b/block.py new file mode 100644 index 0000000..87c0907 --- /dev/null +++ b/block.py @@ -0,0 +1,130 @@ +#!/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 new file mode 100644 index 0000000..5307876 --- /dev/null +++ b/frames.py @@ -0,0 +1,333 @@ +#!/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 index 9105575..1f785a3 100644 --- a/grids.py +++ b/grids.py @@ -1,116 +1,116 @@ -#!/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 + 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 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: +#!/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 bb9b98f..8f89ae9 100644 --- a/locale/Tetris2000.ts +++ b/locale/Tetris2000.ts @@ -1,323 +1,323 @@ - - - - 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: {} - - - - - Matrix - - - Level - - - - - - PAUSE - -Press %s -to resume - - - - - GAME -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 - - - - - SettingsDialog - - - Settings - - - - - 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} - - - - - : - - - - - 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 -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 - - - - + + + + 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: {} + + + + + Matrix + + + Level + + + + + + PAUSE + +Press %s +to resume + + + + + GAME +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 + + + + + SettingsDialog + + + Settings + + + + + 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} + + + + + : + + + + + 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 +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 + + + + diff --git a/locale/fr.ts b/locale/fr.ts index 9326a05..1110bfa 100644 --- a/locale/fr.ts +++ b/locale/fr.ts @@ -1,352 +1,352 @@ - - - - - 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 : {} -Meilleur score : {} - - - - Matrix - - - Level - - Niveau - - - - - PAUSE - -Press %s -to resume - PAUSE - -Appuyez sur -%s -pour reprendre - - - - GAME -OVER - PARTIE -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 - - - - SettingsDialog - - - Settings - Préférences - - - - 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 : - - - - : - : - - - - 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 -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 - Clone de Tetris® par Adrien Malingrey - -Conception du jeu : Alekseï Pajitnov -Graphismes inspirés de Tetris Effect -Style de fenêtre : qdarkstyle par Colin Duquesnoy -Polices par Markus Koellmann, Peter Wiegel -Images issues de : -OpenGameArt.org par beren77, Duion -Pexels.com par Min An, Jaymantri, Felix Mittermeier -Pixabay.com par LoganArt -Pixnio.com par Adrian Pelletier -Unsplash.com par Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja -StockSnap.io par Nathan Anderson, José Ignacio Pompé -Musiques issues de ocremix.org par : -CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS -Effets sonores réalisés avec voc-one de Simple-Media - - - + + + + + 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 : {} +Meilleur score : {} + + + + Matrix + + + Level + + Niveau + + + + + PAUSE + +Press %s +to resume + PAUSE + +Appuyez sur +%s +pour reprendre + + + + GAME +OVER + PARTIE +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 + + + + SettingsDialog + + + Settings + Préférences + + + + 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 : + + + + : + : + + + + 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 +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 + Clone de Tetris® par Adrien Malingrey + +Conception du jeu : Alekseï Pajitnov +Graphismes inspirés de Tetris Effect +Style de fenêtre : qdarkstyle par Colin Duquesnoy +Polices par Markus Koellmann, Peter Wiegel +Images issues de : +OpenGameArt.org par beren77, Duion +Pexels.com par Min An, Jaymantri, Felix Mittermeier +Pixabay.com par LoganArt +Pixnio.com par Adrian Pelletier +Unsplash.com par Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja +StockSnap.io par Nathan Anderson, José Ignacio Pompé +Musiques issues de ocremix.org par : +CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS +Effets sonores réalisés avec voc-one de Simple-Media + + + diff --git a/locale/update_ts.bat b/locale/update_ts.bat index 6079dec..f8c1e86 100644 --- a/locale/update_ts.bat +++ b/locale/update_ts.bat @@ -1,2 +1,3 @@ -for /F %%n in ('dir /B *.ts') do pylupdate5 ..\game_gui.py -ts %%n +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 pause diff --git a/locale/update_ts_noobsolete.bat b/locale/update_ts_noobsolete.bat index d6262e5..a9c7836 100644 --- a/locale/update_ts_noobsolete.bat +++ b/locale/update_ts_noobsolete.bat @@ -1,2 +1,3 @@ -for /F %%n in ('dir /B *.ts') do pylupdate5 ..\game_gui.py -ts -noobsolete %%n +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 pause diff --git a/matrix.py b/matrix.py new file mode 100644 index 0000000..53e29d7 --- /dev/null +++ b/matrix.py @@ -0,0 +1,393 @@ +#!/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 index 9e23b56..99babfa 100644 --- a/point.py +++ b/point.py @@ -1,44 +1,44 @@ -#!/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 __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()) - +#!/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 new file mode 100644 index 0000000..4c55179 --- /dev/null +++ b/settings.py @@ -0,0 +1,277 @@ +#!/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 new file mode 100644 index 0000000..46c073a --- /dev/null +++ b/stats.py @@ -0,0 +1,252 @@ +#!/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 c57edb6..85d624f 100644 --- a/tetromino.py +++ b/tetromino.py @@ -4,172 +4,9 @@ import random -import consts from consts import L, R, U, D, CLOCKWISE, COUNTERCLOCKWISE -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 +from point import Point +from block import Block, GhostBlock class MetaTetro(type): @@ -443,7 +280,7 @@ class TetroO(Tetromino, metaclass=MetaTetro): return False -class Ghost(Tetromino): +class GhostPiece(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 new file mode 100644 index 0000000..337c984 --- /dev/null +++ b/window.py @@ -0,0 +1,156 @@ +#!/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