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