diff --git a/Tetris2000.py b/Tetris2000.py
index 4a4ef58..af3acba 100644
--- a/Tetris2000.py
+++ b/Tetris2000.py
@@ -12,8 +12,7 @@ Parts of comments issued from 2009 Tetris Design Guideline
import sys
from qt5 import QtWidgets
-from window import Window
-
+from game_gui import Window
def play():
diff --git a/dist/Tetris2000.exe b/dist/Tetris2000.exe
index 4449ca5..d77160b 100644
Binary files a/dist/Tetris2000.exe and b/dist/Tetris2000.exe differ
diff --git a/frames.py b/frames.py
deleted file mode 100644
index 5307876..0000000
--- a/frames.py
+++ /dev/null
@@ -1,333 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-import os
-import itertools
-
-import consts
-from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia
-from settings import s, qsettings, settings
-from block import Block
-from tetromino import Tetromino
-from grids import Grid, HoldQueue, NextQueue
-from matrix import Matrix
-from stats import Stats
-
-
-
-
-class AspectRatioWidget(QtWidgets.QWidget):
- """
- Keeps aspect ratio of child widget on resize
- https://stackoverflow.com/questions/48043469/how-to-lock-aspect-ratio-while-resizing-the-window
- """
-
- def __init__(self, widget, parent):
- super().__init__(parent)
- self.aspect_ratio = widget.size().width() / widget.size().height()
- self.setLayout(QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight, self))
- # add spacer, then widget, then spacer
- self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
- self.layout().addWidget(widget)
- self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
-
- def resizeEvent(self, e):
- w = e.size().width()
- h = e.size().height()
-
- if w / h > self.aspect_ratio: # too wide
- self.layout().setDirection(QtWidgets.QBoxLayout.LeftToRight)
- widget_stretch = h * self.aspect_ratio
- outer_stretch = (w - widget_stretch) / 2 + 0.5
- else: # too tall
- self.layout().setDirection(QtWidgets.QBoxLayout.TopToBottom)
- widget_stretch = w / self.aspect_ratio
- outer_stretch = (h - widget_stretch) / 2 + 0.5
-
- self.layout().setStretch(0, outer_stretch)
- self.layout().setStretch(1, widget_stretch)
- self.layout().setStretch(2, outer_stretch)
-
-
-class Frames(QtWidgets.QWidget):
- """
- Display Hold queue, Matrix, Next piece, Next queue and Stats.
- Manage interactions between them.
- """
-
- def __init__(self, parent):
- super().__init__(parent)
- self.setSizePolicy(
- QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
- )
-
- self.playing = False
- self.paused = False
- self.load_music()
-
- self.hold_queue = HoldQueue(self)
- self.matrix = Matrix(self)
- self.next_piece = Grid(self)
- self.stats = Stats(self)
- self.next_queue = NextQueue(self)
-
- self.matrices = (self.hold_queue, self.matrix, self.next_piece)
- self.columns = sum(matrix.COLUMNS + 2 for matrix in self.matrices)
- self.rows = self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS
-
- w = QtWidgets.QWidget(self)
- w.setStyleSheet("background-color: transparent")
- grid = QtWidgets.QGridLayout()
- for x in range(self.rows):
- grid.setRowStretch(x, 1)
- for y in range(self.columns):
- grid.setColumnStretch(y, 1)
- grid.setSpacing(0)
- x, y = 0, 0
- grid.addWidget(
- self.hold_queue, y, x, self.hold_queue.ROWS + 1, self.hold_queue.COLUMNS + 2
- )
- x += self.hold_queue.COLUMNS + 2
- grid.addWidget(self.matrix, y, x, self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS, self.matrix.COLUMNS + 2)
- x += self.matrix.COLUMNS + 3
- grid.addWidget(
- self.next_piece, y, x, self.next_piece.ROWS + 1, self.next_piece.COLUMNS + 2
- )
- x, y = 0, self.hold_queue.ROWS + 2
- grid.addWidget(self.stats, y, x, self.stats.ROWS, self.stats.COLUMNS + 1)
- x += self.stats.COLUMNS + self.matrix.COLUMNS + 5
- grid.addWidget(
- self.next_queue, y, x, self.next_queue.ROWS, self.next_queue.COLUMNS + 2
- )
- w.setLayout(grid)
- w.resize(self.columns, self.rows)
- asw = AspectRatioWidget(w, self)
- layout = QtWidgets.QGridLayout()
- layout.addWidget(asw)
- self.setLayout(layout)
-
- self.stats.temporary_text.connect(self.matrix.show_temporary_text)
- self.matrix.drop_signal.connect(self.stats.update_drop_score)
- self.matrix.lock_signal.connect(self.stats.update_score)
-
- self.set_background(os.path.join(consts.BG_IMAGE_DIR, consts.START_BG_IMAGE_NAME))
-
- self.apply_settings()
-
- def load_music(self):
- playlist = QtMultimedia.QMediaPlaylist(self)
- for entry in os.scandir(consts.MUSICS_DIR):
- path = os.path.join(consts.MUSICS_DIR, entry.name)
- url = QtCore.QUrl.fromLocalFile(path)
- music = QtMultimedia.QMediaContent(url)
- playlist.addMedia(music)
- playlist.setPlaybackMode(QtMultimedia.QMediaPlaylist.Loop)
- self.music = QtMultimedia.QMediaPlayer(self)
- self.music.setAudioRole(QtMultimedia.QAudio.GameRole)
- self.music.setPlaylist(playlist)
- self.music.setVolume(settings[s.SOUND][s.MUSIC_VOLUME])
-
- def apply_settings(self):
- if self.music.volume() > 5 and self.playing:
- self.music.play()
- else:
- self.music.pause()
-
- if self.playing:
- self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
- self.pause(False)
-
- self.matrix.keys = {
- getattr(QtCore.Qt, "Key_" + name): action
- for action, name in settings[s.KEYBOARD].items()
- }
- self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
- self.matrix.spotlight = Matrix.SPOTLIGHT
-
- for sfx in (
- self.matrix.rotate_sfx, self.matrix.wall_sfx,
- self.stats.line_clear_sfx, self.stats.tetris_sfx
- ):
- sfx.setVolume(settings[s.SOUND][s.SFX_VOLUME])
-
- def resizeEvent(self, event):
- Block.side = 0.9 * min(self.width() // self.columns, self.height() // self.rows)
- self.resize_bg_image()
-
- def reset_backgrounds(self):
- backgrounds_paths = (
- os.path.join(consts.BG_IMAGE_DIR, entry.name)
- for entry in os.scandir(consts.BG_IMAGE_DIR)
- )
- self.backgrounds_cycle = itertools.cycle(backgrounds_paths)
-
- def set_background(self, path):
- self.bg_image = QtGui.QImage(path)
- self.resize_bg_image()
-
- def resize_bg_image(self):
- self.resized_bg_image = QtGui.QPixmap.fromImage(self.bg_image)
- self.resized_bg_image = self.resized_bg_image.scaled(
- self.size(),
- QtCore.Qt.KeepAspectRatioByExpanding,
- QtCore.Qt.SmoothTransformation
- )
- self.resized_bg_image = self.resized_bg_image.copy(
- (self.resized_bg_image.width() - self.width()) // 2,
- (self.resized_bg_image.height() - self.height()) // 2,
- self.width(),
- self.height()
- )
- self.update()
-
- def paintEvent(self, event):
- painter = QtGui.QPainter(self)
- painter.drawPixmap(
- self.rect(),
- self.resized_bg_image)
-
- def new_game(self):
- if self.playing:
- answer = QtWidgets.QMessageBox.question(
- self,
- self.tr("New game"),
- self.tr("A game is in progress.\n" "Do you want to abord it?"),
- QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
- QtWidgets.QMessageBox.Cancel,
- )
- if answer == QtWidgets.QMessageBox.Cancel:
- self.pause(False)
- return
- self.music.stop()
-
- self.reset_backgrounds()
- self.stats.level, ok = QtWidgets.QInputDialog.getInt(
- self,
- self.tr("New game"),
- self.tr("Start level:"),
- 1,
- 1,
- 15,
- flags=QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint,
- )
- if not ok:
- return
-
- self.playing = True
- self.load_music()
- self.music.play()
- self.hold_queue.piece = None
- self.stats.new_game()
- self.matrix.new_game()
- self.next_queue.pieces = [Tetromino() for _ in range(5)]
- self.next_queue.insert_pieces()
- self.next_piece.insert(Tetromino())
- self.pause(False)
- self.new_level()
- self.new_piece()
-
- def new_level(self):
- self.set_background(next(self.backgrounds_cycle))
- level = self.stats.new_level()
- self.matrix.new_level(level)
-
- def new_piece(self):
- if self.stats.goal <= 0:
- self.new_level()
- self.matrix.insert(self.next_piece.piece)
- self.matrix.lock_wait()
- self.next_piece.insert(self.next_queue.pieces[0])
- self.next_queue.new_piece()
- self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
- self.update()
-
- if not self.matrix.piece.move(0, 0):
- self.game_over()
- return
-
- self.matrix.fall_timer.start(self.matrix.speed)
-
- def pause(self, paused):
- if not self.playing:
- return
-
- if paused:
- self.paused = True
- self.update()
- self.matrix.fall_timer.stop()
- self.stats.clock.stop()
- self.matrix.auto_repeat_timer.stop()
- self.music.pause()
- else:
- self.matrix.text = ""
- self.update()
- QtCore.QTimer.singleShot(1000, self.resume)
-
- def resume(self):
- self.paused = False
- self.update()
- self.matrix.fall_timer.start(self.matrix.speed)
- self.stats.clock.start(1000)
- self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
- self.music.play()
-
- def hold(self):
- """
- Using the Hold command places the Tetrimino in play into the Hold Queue.
- The previously held Tetrimino (if one exists) will then start falling
- from the top of the Matrix,
- beginning from its generation position and North Facing orientation.
- Only one Tetrimino may be held at a time.
- A Lock Down must take place between Holds.
- For example, at the beginning, the first Tetrimino is generated and begins to fall.
- The player decides to hold this Tetrimino.
- Immediately the Next Tetrimino is generated from the Next Queue and begins to fall.
- The player must first Lock Down this Tetrimino before holding another Tetrimino.
- In other words, you may not Hold the same Tetrimino more than once.
- """
-
- if not self.hold_enabled:
- return
-
- piece = self.hold_queue.piece
- if piece:
- self.hold_queue.insert(self.matrix.piece)
- self.matrix.insert(piece)
- self.update()
- else:
- self.hold_queue.insert(self.matrix.piece)
- self.new_piece()
- self.hold_enabled = False
-
- def game_over(self):
- self.matrix.fall_timer.stop()
- self.stats.clock.stop()
- self.matrix.auto_repeat_timer.stop()
- self.music.stop()
- self.playing = False
- self.matrix.game_over = True
- msgbox = QtWidgets.QMessageBox(self)
- msgbox.setWindowTitle(self.tr("Game over"))
- msgbox.setIcon(QtWidgets.QMessageBox.Information)
- if self.stats.score_total == self.stats.high_score:
- msgbox.setText(
- self.tr(
- "Congratulations!\nYou have the high score: {}"
- ).format(
-
- locale.format("%i", self.stats.high_score, grouping=True, monetary=True)
- )
- )
- qsettings.setValue(self.tr("High score"), self.stats.high_score)
- else:
- msgbox.setText(
- self.tr(
- "Score: {}\nHigh score: {}"
- ).format(
- locale.format("%i", self.stats.score_total, grouping=True, monetary=True),
- locale.format("%i", self.stats.high_score, grouping=True, monetary=True)
- )
- )
- msgbox.setDetailedText(self.stats.text(full_stats=True))
- msgbox.exec_()
\ No newline at end of file
diff --git a/game_gui.py b/game_gui.py
new file mode 100644
index 0000000..c0b42ab
--- /dev/null
+++ b/game_gui.py
@@ -0,0 +1,1473 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+import ctypes
+import collections
+import itertools
+import locale
+import os
+import time
+
+import consts
+from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE
+from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia
+from __version__ import __title__, __author__, __version__
+from block import Block
+from point import Point
+from tetromino import Tetromino, GhostPiece
+
+
+class Grid(QtWidgets.QWidget):
+ """
+ Mother class of Hold queue, Matrix, Next piece, and Next queue
+ """
+
+ ROWS = consts.GRID_DEFAULT_ROWS + consts.GRID_INVISIBLE_ROWS
+ COLUMNS = consts.GRID_DEFAULT_COLUMNS
+ STARTING_POSITION = Point(
+ consts.GRID_DEFAULT_COLUMNS // 2,
+ consts.GRID_DEFAULT_ROWS // 2 + consts.GRID_INVISIBLE_ROWS
+ )
+ GRIDLINE_COLOR = consts.GRID_GRIDLINE_COLOR
+ HARD_DROP_MOVEMENT = consts.GRID_HARD_DROP_MOVEMENT
+ SPOTLIGHT = Point(*consts.GRID_SPOTLIGHT )
+
+ def __init__(self, frames):
+ super().__init__(frames)
+ self.setStyleSheet("background-color: transparent")
+ self.frames = frames
+ self.spotlight = self.SPOTLIGHT
+ self.piece = None
+
+ def insert(self, piece, position=None):
+ """
+ Add a Tetromino to self
+ Update its coordinates
+ """
+ piece.insert_into(self, position or self.STARTING_POSITION)
+ self.piece = piece
+ self.update()
+
+ def resizeEvent(self, event):
+ self.bottom = Block.side * self.ROWS
+ self.grid_top = consts.GRID_INVISIBLE_ROWS * Block.side
+ width = Block.side * self.COLUMNS
+ self.left = (self.width() - width) // 2
+ self.right = width + self.left
+ self.top_left_corner = Point(self.left, 0)
+
+ def paintEvent(self, event=None):
+ painter = QtGui.QPainter(self)
+ painter.setRenderHint(QtGui.QPainter.Antialiasing)
+
+ self.paint_grid(painter)
+
+ if (not self.frames.paused or not self.frames.playing) and self.piece:
+ self.paint_piece(painter, self.piece)
+
+ def paint_grid(self, painter):
+ painter.setPen(self.GRIDLINE_COLOR)
+ for x in (self.left + i * Block.side for i in range(self.COLUMNS + 1)):
+ painter.drawLine(x, self.grid_top, x, self.bottom)
+ for y in (j * Block.side for j in range(consts.GRID_INVISIBLE_ROWS, self.ROWS + 1)):
+ painter.drawLine(self.left, y, self.right, y)
+
+ def paint_piece(self, painter, piece):
+ for mino in piece.minoes:
+ mino.paint(painter, self.top_left_corner, self.spotlight)
+
+
+class Matrix(Grid):
+ """
+ The rectangular arrangement of cells creating the active game area.
+ Tetriminos fall from the top-middle just above the Skyline (off-screen) to the bottom.
+ """
+
+ ROWS = consts.MATRIX_ROWS + consts.GRID_INVISIBLE_ROWS
+ COLUMNS = consts.MATRIX_COLUMNS
+ STARTING_POSITION = Point(COLUMNS // 2, consts.GRID_INVISIBLE_ROWS - 1)
+ TEXT_COLOR = consts.MATRIX_TEXT_COLOR
+
+ drop_signal = QtCore.Signal(int)
+ lock_signal = QtCore.Signal(int, str)
+
+ def __init__(self, frames):
+ super().__init__(frames)
+
+ self.load_sfx()
+
+ self.game_over = False
+ self.text = ""
+ self.temporary_texts = []
+
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+ self.auto_repeat_delay = 0
+ self.auto_repeat_timer = QtCore.QTimer()
+ self.auto_repeat_timer.timeout.connect(self.auto_repeat)
+ self.fall_timer = QtCore.QTimer()
+ self.fall_timer.timeout.connect(self.fall)
+
+ self.cells = []
+
+ def load_sfx(self):
+ self.wall_sfx = QtMultimedia.QSoundEffect(self)
+ url = QtCore.QUrl.fromLocalFile(consts.WALL_SFX_PATH)
+ self.wall_sfx.setSource(url)
+
+ self.rotate_sfx = QtMultimedia.QSoundEffect(self)
+ url = QtCore.QUrl.fromLocalFile(consts.ROTATE_SFX_PATH)
+ self.rotate_sfx.setSource(url)
+
+ self.hard_drop_sfx = QtMultimedia.QSoundEffect(self)
+ url = QtCore.QUrl.fromLocalFile(consts.HARD_DROP_SFX_PATH)
+ self.hard_drop_sfx.setSource(url)
+
+ def new_game(self):
+ self.game_over = False
+ self.lock_delay = consts.LOCK_DELAY
+ self.cells = [self.empty_row() for y in range(self.ROWS)]
+ self.setFocus()
+ self.actions_to_repeat = []
+ self.wall_hit = False
+
+ def new_level(self, level):
+ self.show_temporary_text(self.tr("Level\n") + str(level))
+ self.speed = consts.INITIAL_SPEED * (0.8 - ((level - 1) * 0.007)) ** (level - 1)
+ self.fall_timer.start(self.speed)
+ if level > 15:
+ self.lock_delay *= 0.9
+
+ def empty_row(self):
+ return [None for x in range(self.COLUMNS)]
+
+ def is_empty_cell(self, coord):
+ x, y = coord.x(), coord.y()
+ return (
+ 0 <= x < self.COLUMNS
+ and y < self.ROWS
+ and not (0 <= y and self.cells[y][x])
+ )
+
+ def keyPressEvent(self, event):
+ if event.isAutoRepeat():
+ return
+
+ if not self.frames.playing:
+ return
+
+ try:
+ action = self.keys[event.key()]
+ except KeyError:
+ return
+
+ self.do(action)
+ if action in (s.MOVE_LEFT, s.MOVE_RIGHT, s.SOFT_DROP):
+ if action not in self.actions_to_repeat:
+ self.actions_to_repeat.append(action)
+ self.auto_repeat_wait()
+
+ def keyReleaseEvent(self, event):
+ if event.isAutoRepeat():
+ return
+
+ if not self.frames.playing:
+ return
+
+ try:
+ self.actions_to_repeat.remove(self.keys[event.key()])
+ except (KeyError, ValueError):
+ pass
+ else:
+ self.auto_repeat_wait()
+
+ if not self.actions_to_repeat:
+ for mino in self.piece.minoes:
+ mino.fade()
+ self.update()
+
+ def auto_repeat_wait(self):
+ self.auto_repeat_delay = (
+ time.time() + settings[s.DELAYS][s.AUTO_SHIFT_DELAY] / 1000
+ )
+
+ def auto_repeat(self):
+ """
+ Tapping the move button allows a single cell movement of the Tetrimino
+ in the direction pressed.
+ Holding down the move button triggers an Auto-Repeat movement
+ that allows the player to move a Tetrimino from one side of the Matrix
+ to the other in about 0.5 seconds.
+ This is essential on higher levels when the Fall Speed of a Tetrimino is very fast.
+ There is a slight delay between the time the move button is pressed
+ and the time when Auto-Repeat kicks in : s.AUTO_SHIFT_DELAY.
+ This delay prevents unwanted extra movement of a Tetrimino.
+ Auto-Repeat only affects Left/Right movement.
+ Auto-Repeat continues to the Next Tetrimino (after Lock Down)
+ as long as the move button remains pressed.
+ In addition, when Auto-Repeat begins,
+ and the player then holds the opposite direction button,
+ the Tetrimino then begins moving the opposite direction with the initial delay.
+ When any single button is then released,
+ the Tetrimino should again move in the direction still held,
+ with the Auto-Shift delay applied once more.
+ """
+ if (
+ not self.frames.playing
+ or self.frames.paused
+ or time.time() < self.auto_repeat_delay
+ ):
+ return
+
+ if self.actions_to_repeat:
+ self.do(self.actions_to_repeat[-1])
+
+ def do(self, action):
+ """The player can move, rotate, Soft Drop, Hard Drop,
+ and Hold the falling Tetrimino (i.e., the Tetrimino in play).
+ """
+ if action == s.PAUSE:
+ self.frames.pause(not self.frames.paused)
+
+ if not self.frames.playing or self.frames.paused or not self.piece:
+ return
+
+ for mino in self.piece.minoes:
+ mino.shine(0)
+
+ if action == s.MOVE_LEFT:
+ if self.piece.move(L, 0):
+ self.lock_wait()
+ self.wall_hit = False
+ elif not self.wall_hit:
+ self.wall_hit = True
+ self.wall_sfx.play()
+
+ elif action == s.MOVE_RIGHT:
+ if self.piece.move(R, 0):
+ self.rotate_sfx.play()
+ self.lock_wait()
+ self.wall_hit = False
+ elif not self.wall_hit:
+ self.wall_hit = True
+ self.wall_sfx.play()
+
+ elif action == s.ROTATE_CLOCKWISE:
+ if self.piece.rotate(direction=CLOCKWISE):
+ self.rotate_sfx.play()
+ self.lock_wait()
+ self.wall_hit = False
+ elif not self.wall_hit:
+ self.wall_hit = True
+ self.wall_sfx.play()
+
+ elif action == s.ROTATE_COUNTERCLOCKWISE:
+ if self.piece.rotate(direction=COUNTERCLOCKWISE):
+ self.lock_wait()
+ self.wall_hit = False
+ elif not self.wall_hit:
+ self.wall_hit = True
+ self.wall_sfx.play()
+
+ elif action == s.SOFT_DROP:
+ if self.piece.soft_drop():
+ self.drop_signal.emit(1)
+ self.wall_hit = False
+ elif not self.wall_hit:
+ self.wall_hit = True
+ self.wall_sfx.play()
+
+ elif action == s.HARD_DROP:
+ trail = self.piece.hard_drop()
+ self.top_left_corner += Point(0, self.HARD_DROP_MOVEMENT * Block.side)
+ self.drop_signal.emit(2 * trail)
+ QtCore.QTimer.singleShot(consts.ANIMATION_DELAY, self.after_hard_drop)
+ self.hard_drop_sfx.play()
+ self.lock_phase()
+
+ elif action == s.HOLD:
+ self.frames.hold()
+
+ def after_hard_drop(self):
+ """ Reset the animation movement of the Matrix on a hard drop """
+ self.top_left_corner -= Point(0, self.HARD_DROP_MOVEMENT * Block.side)
+
+ def lock_wait(self):
+ self.fall_delay = time.time() + (self.speed + self.lock_delay) / 1000
+
+ def fall(self):
+ """
+ Once a Tetrimino is generated,
+ it immediately drops one row (if no existing Block is in its path).
+ From here, it begins its descent to the bottom of the Matrix.
+ The Tetrimino will fall at its normal Fall Speed
+ whether or not it is being manipulated by the player.
+ """
+ if self.piece:
+ if self.piece.move(0, 1):
+ self.lock_wait()
+ else:
+ if time.time() >= self.fall_delay:
+ self.lock_phase()
+
+ def lock_phase(self):
+ """
+ The player can perform the same actions on a Tetrimino in this phase
+ as he/she can in the Falling Phase,
+ as long as the Tetrimino is not yet Locked Down.
+ A Tetrimino that is Hard Dropped Locks Down immediately.
+ However, if a Tetrimino naturally falls or Soft Drops onto a landing Surface,
+ it is given a delay (self.fall_delay) on a Lock Down Timer
+ before it actually Locks Down.
+ """
+
+ self.wall_sfx.play()
+
+ # Enter minoes into the matrix
+ for mino in self.piece.minoes:
+ if mino.coord.y() >= 0:
+ self.cells[mino.coord.y()][mino.coord.x()] = mino
+ mino.shine(glowing=2, delay=consts.ANIMATION_DELAY)
+ self.update()
+
+ if all(mino.coord.y() < consts.GRID_INVISIBLE_ROWS for mino in self.piece.minoes):
+ self.frames.game_over()
+ return
+
+ """
+ In this phase,
+ the engine looks for patterns made from Locked Down Blocks in the Matrix.
+ Once a pattern has been matched,
+ it can trigger any number of Tetris variant-related effects.
+ The classic pattern is the Line Clear pattern.
+ This pattern is matched when one or more rows of 10 horizontally aligned
+ Matrix cells are occupied by Blocks.
+ The matching Blocks are then marked for removal on a hit list.
+ Blocks on the hit list are cleared from the Matrix at a later time
+ in the Eliminate Phase.
+ """
+ # Dectect complete lines
+ self.complete_lines = []
+ for y, row in enumerate(self.cells):
+ if all(cell for cell in row):
+ self.complete_lines.append(y)
+ for block in row:
+ block.shine()
+ self.spotlight = row[self.COLUMNS // 2].coord
+ self.auto_repeat_timer.stop()
+ self.lock_signal.emit(len(self.complete_lines), self.piece.t_spin)
+
+ if self.complete_lines:
+ self.fall_timer.stop()
+ QtCore.QTimer.singleShot(consts.LINE_CLEAR_DELAY, self.eliminate_phase)
+ else:
+ self.frames.new_piece()
+
+
+ def eliminate_phase(self):
+ """
+ Any Minos marked for removal, i.e., on the hit list,
+ are cleared from the Matrix in this phase.
+ If this results in one or more complete 10-cell rows in the Matrix
+ becoming unoccupied by Minos,
+ then all Minos above that row(s) collapse,
+ or fall by the number of complete rows cleared from the Matrix.
+ """
+ for y in self.complete_lines:
+ del self.cells[y]
+ self.cells.insert(0, self.empty_row())
+
+ for y, row in enumerate(self.cells):
+ for x, block in enumerate(row):
+ if block:
+ block.coord.setX(x)
+ block.coord.setY(y)
+
+ self.update()
+ self.auto_repeat_wait()
+ self.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
+
+ self.frames.new_piece()
+
+ def paintEvent(self, event):
+ """
+ Draws grid, actual piece, blocks in the Matrix and show texts
+ """
+ painter = QtGui.QPainter(self)
+ painter.setRenderHint(QtGui.QPainter.Antialiasing)
+
+ self.paint_grid(painter)
+
+ if not self.frames.paused or self.game_over:
+ if self.piece:
+ if settings[s.OTHER][s.GHOST]:
+ self.ghost = GhostPiece(self.piece)
+ self.spotlight = self.ghost.minoes[0].coord
+ self.paint_piece(painter, self.ghost)
+ self.paint_piece(painter, self.piece)
+
+ # Blocks in matrix
+ for row in self.cells:
+ for block in row:
+ if block:
+ block.paint(painter, self.top_left_corner, self.spotlight)
+
+ if self.frames.playing and self.frames.paused:
+ painter.setFont(QtGui.QFont("Maassslicer", 0.75 * Block.side))
+ painter.setPen(self.TEXT_COLOR)
+ painter.drawText(
+ self.rect(),
+ QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
+ self.tr("PAUSE\n\nPress %s\nto resume") % settings[s.KEYBOARD][s.PAUSE],
+ )
+ if self.game_over:
+ painter.setFont(QtGui.QFont("Maassslicer", Block.side))
+ painter.setPen(self.TEXT_COLOR)
+ painter.drawText(
+ self.rect(),
+ QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
+ self.tr("GAME\nOVER"),
+ )
+ if self.temporary_texts:
+ painter.setFont(self.temporary_text_font)
+ painter.setPen(self.TEXT_COLOR)
+ painter.drawText(
+ self.rect(),
+ QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap,
+ "\n\n\n" + "\n\n".join(self.temporary_texts),
+ )
+
+ def resizeEvent(self, event):
+ super().resizeEvent(event)
+ self.temporary_text_font = QtGui.QFont(consts.MATRIX_FONT_NAME, Block.side)
+
+ def show_temporary_text(self, text):
+ self.temporary_texts.append(text.upper())
+ self.font = self.temporary_text_font
+ self.update()
+ QtCore.QTimer.singleShot(consts.TEMPORARY_TEXT_DURATION, self.delete_text)
+
+ def delete_text(self):
+ del self.temporary_texts[0]
+ self.update()
+
+ def focusOutEvent(self, event):
+ if self.frames.playing:
+ self.frames.pause(True)
+
+
+class HoldQueue(Grid):
+ """
+ The Hold Queue allows the player to “hold” a falling Tetrimino for as long as they wish.
+ Holding a Tetrimino releases the Tetrimino already in the Hold Queue (if one exists).
+ """
+
+ def paintEvent(self, event):
+ if not settings[s.OTHER][s.HOLD_ENABLED]:
+ return
+
+ super().paintEvent(event)
+
+
+class NextQueue(Grid):
+ """
+ The Next Queue allows the player to see the Next Tetrimino that will be generated
+ and put into play.
+ """
+
+ ROWS = consts.NEXT_QUEUE_ROWS
+ COLUMNS = consts.NEXT_QUEUE_COLUMNS
+
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.pieces = []
+
+ def new_piece(self):
+ self.pieces = self.pieces[1:] + [Tetromino()]
+ self.insert_pieces()
+
+ def insert_pieces(self):
+ for y, piece in enumerate(self.pieces):
+ piece.insert_into(self, Point(3, 3 * y + 1))
+
+ def paintEvent(self, event=None):
+ if not settings[s.OTHER][s.SHOW_NEXT_QUEUE]:
+ return
+
+ painter = QtGui.QPainter(self)
+ painter.setRenderHint(QtGui.QPainter.Antialiasing)
+
+ if not self.frames.paused:
+ for piece in self.pieces:
+ self.paint_piece(painter, piece)
+
+
+class AspectRatioWidget(QtWidgets.QWidget):
+ """
+ Keeps aspect ratio of child widget on resize
+ https://stackoverflow.com/questions/48043469/how-to-lock-aspect-ratio-while-resizing-the-window
+ """
+
+ def __init__(self, widget, parent):
+ super().__init__(parent)
+ self.aspect_ratio = widget.size().width() / widget.size().height()
+ self.setLayout(QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight, self))
+ # add spacer, then widget, then spacer
+ self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
+ self.layout().addWidget(widget)
+ self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
+
+ def resizeEvent(self, e):
+ w = e.size().width()
+ h = e.size().height()
+
+ if w / h > self.aspect_ratio: # too wide
+ self.layout().setDirection(QtWidgets.QBoxLayout.LeftToRight)
+ widget_stretch = h * self.aspect_ratio
+ outer_stretch = (w - widget_stretch) / 2 + 0.5
+ else: # too tall
+ self.layout().setDirection(QtWidgets.QBoxLayout.TopToBottom)
+ widget_stretch = w / self.aspect_ratio
+ outer_stretch = (h - widget_stretch) / 2 + 0.5
+
+ self.layout().setStretch(0, outer_stretch)
+ self.layout().setStretch(1, widget_stretch)
+ self.layout().setStretch(2, outer_stretch)
+
+class Stats(QtWidgets.QWidget):
+ """
+ Show informations relevant to the game being played is displayed on-screen.
+ Looks for patterns made from Locked Down Blocks in the Matrix and calculate score.
+ """
+
+ ROWS = consts.STATS_ROWS
+ COLUMNS = consts.STATS_COLUMNS
+ TEXT_COLOR = consts.STATS_TEXT_COLOR
+
+ temporary_text = QtCore.Signal(str)
+
+ def __init__(self, frames):
+ super().__init__(frames)
+ self.frames = frames
+ self.setStyleSheet("background-color: transparent")
+
+ self.load_sfx()
+
+ self.setSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
+ )
+ self.text_options = QtGui.QTextOption(QtCore.Qt.AlignRight)
+ self.text_options.setWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
+
+ self.clock = QtCore.QTimer()
+ self.clock.timeout.connect(self.tick)
+
+ self.high_score = int(qsettings.value(self.tr("High score"), 0))
+
+ def load_sfx(self):
+ self.line_clear_sfx = QtMultimedia.QSoundEffect(self)
+ url = QtCore.QUrl.fromLocalFile(consts.LINE_CLEAR_SFX_PATH)
+ self.line_clear_sfx.setSource(url)
+
+ self.tetris_sfx = QtMultimedia.QSoundEffect(self)
+ url = QtCore.QUrl.fromLocalFile(consts.TETRIS_SFX_PATH)
+ self.tetris_sfx.setSource(url)
+
+ def new_game(self):
+ self.level -= 1
+ self.goal = 0
+ self.complete_lines_total = 0
+ self.score_total = 1
+ self.t_spin_total = 0
+ self.mini_t_spin_total = 0
+ self.nb_back_to_back = 0
+ self.back_to_back_scores = None
+ self.combo = -1
+ self.combos_total = 0
+ self.max_combo = 0
+ self.chronometer = 0
+ self.nb_tetro = 0
+ self.clock.start(1000)
+ self.lines_stats = [0, 0, 0, 0, 0]
+
+ def new_level(self):
+ self.level += 1
+ self.goal += 5 * self.level
+ return self.level
+
+ def update_score(self, nb_complete_lines, t_spin):
+ """
+ The player scores points by performing Single, Double, Triple,
+ and Tetris Line Clears, as well as T-Spins and Mini T-Spins.
+ Soft and Hard Drops also award points.
+ There is a special bonus for Back-to-Backs,
+ which is when two actions such as a Tetris and T-Spin Double take place
+ without a Single, Double, or Triple Line Clear occurring between them.
+ Scoring for Line Clears, T-Spins, and Mini T-Spins are level dependent,
+ while Hard and Soft Drop point values remain constant.
+ """
+ self.nb_tetro += 1
+ if nb_complete_lines:
+ self.complete_lines_total += nb_complete_lines
+ self.lines_stats[nb_complete_lines] += 1
+ if t_spin == "T-Spin":
+ self.t_spin_total += 1
+ elif t_spin == "Mini T-Spin":
+ self.mini_t_spin_total += 1
+
+ score = consts.SCORES[nb_complete_lines][t_spin]
+
+ if score:
+ text = " ".join((t_spin, consts.SCORES[nb_complete_lines]["name"]))
+ if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
+ self.tetris_sfx.play()
+ elif t_spin or nb_complete_lines:
+ self.line_clear_sfx.play()
+
+ self.goal -= score
+ score = 100 * self.level * score
+ self.score_total += score
+
+ self.temporary_text.emit(text + "\n{:n}".format(score))
+
+# ==============================================================================
+# Combo
+# Bonus for complete lines on each consecutive lock downs
+# if nb_complete_lines:
+# ==============================================================================
+ if nb_complete_lines:
+ self.combo += 1
+ if self.combo > 0:
+ if nb_complete_lines == 1:
+ combo_score = 20 * self.combo * self.level
+ else:
+ combo_score = 50 * self.combo * self.level
+ self.score_total += combo_score
+ self.max_combo = max(self.max_combo, self.combo)
+ self.combos_total += 1
+ self.temporary_text.emit(
+ self.tr("COMBO x{:n}\n{:n}").format(self.combo, combo_score)
+ )
+ else:
+ self.combo = -1
+
+# ==============================================================================
+# Back-to_back sequence
+# Two major bonus actions, such as two Tetrises, performed without
+# a Single, Double, or Triple Line Clear occurring between them.
+# Bonus for Tetrises, T-Spin Line Clears, and Mini T-Spin Line Clears
+# performed consecutively in a B2B sequence.
+# ==============================================================================
+ if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
+ if self.back_to_back_scores is not None:
+ self.back_to_back_scores.append(score // 2)
+ else:
+ # The first Line Clear in the Back-to-Back sequence
+ # does not receive the Back-to-Back Bonus.
+ self.back_to_back_scores = []
+ elif nb_complete_lines and not t_spin:
+ # A Back-to-Back sequence is only broken by a Single, Double, or Triple Line Clear.
+ # Locking down a Tetrimino without clearing a line
+ # or holding a Tetrimino does not break the Back-to-Back sequence.
+ # T-Spins and Mini T-Spins that do not clear any lines
+ # do not receive the Back-to-Back Bonus; instead they are scored as normal.
+ # They also cannot start a Back-to-Back sequence, however,
+ # they do not break an existing Back-to-Back sequence.
+ if self.back_to_back_scores:
+ b2b_score = sum(self.back_to_back_scores)
+ self.score_total += b2b_score
+ self.nb_back_to_back += 1
+ self.temporary_text.emit(
+ self.tr("BACK TO BACK\n{:n}").format(b2b_score)
+ )
+ self.back_to_back_scores = None
+
+ self.high_score = max(self.score_total, self.high_score)
+ self.update()
+
+ def update_drop_score(self, n):
+ """ Tetrimino is Soft Dropped for n lines or Hard Dropped for (n/2) lines"""
+ self.score_total += n
+ self.high_score = max(self.score_total, self.high_score)
+ self.update()
+
+ def tick(self):
+ self.chronometer += 1
+ self.update()
+
+ def paintEvent(self, event):
+ if not self.frames.playing and not self.frames.matrix.game_over:
+ return
+
+ painter = QtGui.QPainter(self)
+ painter.setFont(self.font)
+ painter.setPen(self.TEXT_COLOR)
+
+ painter.drawText(
+ QtCore.QRectF(self.rect()), self.text(sep="\n\n"), self.text_options
+ )
+
+ def text(self, full_stats=False, sep="\n"):
+ text = (
+ self.tr("Score: ")
+ + locale.format("%i", self.score_total, grouping=True, monetary=True)
+ + sep
+ + self.tr("High score: ")
+ + locale.format("%i", self.high_score, grouping=True, monetary=True)
+ + sep
+ + self.tr("Time: {}\n").format(
+ time.strftime("%H:%M:%S", time.gmtime(self.chronometer))
+ )
+ + sep
+ + self.tr("Level: ")
+ + locale.format("%i", self.level, grouping=True, monetary=True)
+ + sep
+ + self.tr("Goal: ")
+ + locale.format("%i", self.goal, grouping=True, monetary=True)
+ + sep
+ + self.tr("Lines: ")
+ + locale.format(
+ "%i", self.complete_lines_total, grouping=True, monetary=True
+ )
+ + sep
+ + self.tr("Mini T-Spins: ")
+ + locale.format("%i", self.mini_t_spin_total, grouping=True, monetary=True)
+ + sep
+ + self.tr("T-Spins: ")
+ + locale.format("%i", self.t_spin_total, grouping=True, monetary=True)
+ + sep
+ + self.tr("Back-to-back: ")
+ + locale.format("%i", self.nb_back_to_back, grouping=True, monetary=True)
+ + sep
+ + self.tr("Max combo: ")
+ + locale.format("%i", self.max_combo, grouping=True, monetary=True)
+ + sep
+ + self.tr("Combos: ")
+ + locale.format("%i", self.combos_total, grouping=True, monetary=True)
+ )
+ if full_stats:
+ minutes = self.chronometer / 60
+ text += (
+ "\n"
+ + sep
+ + self.tr("Lines per minute: {:.1f}").format(
+ self.complete_lines_total / minutes
+ )
+ + sep
+ + self.tr("Tetrominos locked down: ")
+ + locale.format("%i", self.nb_tetro, grouping=True, monetary=True)
+ + sep
+ + self.tr("Tetrominos per minute: {:.1f}").format(
+ self.nb_tetro / minutes
+ )
+ + sep
+ )
+ text += sep.join(
+ score_type["name"]
+ + self.tr(": ")
+ + locale.format("%i", nb, grouping=True, monetary=True)
+ for score_type, nb in tuple(zip(consts.SCORES, self.lines_stats))[1:]
+ )
+ return text
+
+ def resizeEvent(self, event):
+ self.font = QtGui.QFont(consts.STATS_FONT_NAME, Block.side / 3.5)
+
+
+class Frames(QtWidgets.QWidget):
+ """
+ Display Hold queue, Matrix, Next piece, Next queue and Stats.
+ Manage interactions between them.
+ """
+
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.setSizePolicy(
+ QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
+ )
+
+ self.playing = False
+ self.paused = False
+ self.load_music()
+
+ self.hold_queue = HoldQueue(self)
+ self.matrix = Matrix(self)
+ self.next_piece = Grid(self)
+ self.stats = Stats(self)
+ self.next_queue = NextQueue(self)
+
+ self.matrices = (self.hold_queue, self.matrix, self.next_piece)
+ self.columns = sum(matrix.COLUMNS + 2 for matrix in self.matrices)
+ self.rows = self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS
+
+ w = QtWidgets.QWidget(self)
+ w.setStyleSheet("background-color: transparent")
+ grid = QtWidgets.QGridLayout()
+ for x in range(self.rows):
+ grid.setRowStretch(x, 1)
+ for y in range(self.columns):
+ grid.setColumnStretch(y, 1)
+ grid.setSpacing(0)
+ x, y = 0, 0
+ grid.addWidget(
+ self.hold_queue, y, x, self.hold_queue.ROWS + 1, self.hold_queue.COLUMNS + 2
+ )
+ x += self.hold_queue.COLUMNS + 2
+ grid.addWidget(self.matrix, y, x, self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS, self.matrix.COLUMNS + 2)
+ x += self.matrix.COLUMNS + 3
+ grid.addWidget(
+ self.next_piece, y, x, self.next_piece.ROWS + 1, self.next_piece.COLUMNS + 2
+ )
+ x, y = 0, self.hold_queue.ROWS + 2
+ grid.addWidget(self.stats, y, x, self.stats.ROWS, self.stats.COLUMNS + 1)
+ x += self.stats.COLUMNS + self.matrix.COLUMNS + 5
+ grid.addWidget(
+ self.next_queue, y, x, self.next_queue.ROWS, self.next_queue.COLUMNS + 2
+ )
+ w.setLayout(grid)
+ w.resize(self.columns, self.rows)
+ asw = AspectRatioWidget(w, self)
+ layout = QtWidgets.QGridLayout()
+ layout.addWidget(asw)
+ self.setLayout(layout)
+
+ self.stats.temporary_text.connect(self.matrix.show_temporary_text)
+ self.matrix.drop_signal.connect(self.stats.update_drop_score)
+ self.matrix.lock_signal.connect(self.stats.update_score)
+
+ self.set_background(os.path.join(consts.BG_IMAGE_DIR, consts.START_BG_IMAGE_NAME))
+
+ self.apply_settings()
+
+ def load_music(self):
+ playlist = QtMultimedia.QMediaPlaylist(self)
+ for entry in os.scandir(consts.MUSICS_DIR):
+ path = os.path.join(consts.MUSICS_DIR, entry.name)
+ url = QtCore.QUrl.fromLocalFile(path)
+ music = QtMultimedia.QMediaContent(url)
+ playlist.addMedia(music)
+ playlist.setPlaybackMode(QtMultimedia.QMediaPlaylist.Loop)
+ self.music = QtMultimedia.QMediaPlayer(self)
+ self.music.setAudioRole(QtMultimedia.QAudio.GameRole)
+ self.music.setPlaylist(playlist)
+ self.music.setVolume(settings[s.SOUND][s.MUSIC_VOLUME])
+
+ def apply_settings(self):
+ if self.music.volume() > 5 and self.playing:
+ self.music.play()
+ else:
+ self.music.pause()
+
+ if self.playing:
+ self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
+ self.pause(False)
+
+ self.matrix.keys = {
+ getattr(QtCore.Qt, "Key_" + name): action
+ for action, name in settings[s.KEYBOARD].items()
+ }
+ self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
+ self.matrix.spotlight = Matrix.SPOTLIGHT
+
+ for sfx in (
+ self.matrix.rotate_sfx, self.matrix.wall_sfx,
+ self.stats.line_clear_sfx, self.stats.tetris_sfx
+ ):
+ sfx.setVolume(settings[s.SOUND][s.SFX_VOLUME])
+
+ def resizeEvent(self, event):
+ Block.side = 0.9 * min(self.width() // self.columns, self.height() // self.rows)
+ self.resize_bg_image()
+
+ def reset_backgrounds(self):
+ backgrounds_paths = (
+ os.path.join(consts.BG_IMAGE_DIR, entry.name)
+ for entry in os.scandir(consts.BG_IMAGE_DIR)
+ )
+ self.backgrounds_cycle = itertools.cycle(backgrounds_paths)
+
+ def set_background(self, path):
+ self.bg_image = QtGui.QImage(path)
+ self.resize_bg_image()
+
+ def resize_bg_image(self):
+ self.resized_bg_image = QtGui.QPixmap.fromImage(self.bg_image)
+ self.resized_bg_image = self.resized_bg_image.scaled(
+ self.size(),
+ QtCore.Qt.KeepAspectRatioByExpanding,
+ QtCore.Qt.SmoothTransformation
+ )
+ self.resized_bg_image = self.resized_bg_image.copy(
+ (self.resized_bg_image.width() - self.width()) // 2,
+ (self.resized_bg_image.height() - self.height()) // 2,
+ self.width(),
+ self.height()
+ )
+ self.update()
+
+ def paintEvent(self, event):
+ painter = QtGui.QPainter(self)
+ painter.drawPixmap(
+ self.rect(),
+ self.resized_bg_image)
+
+ def new_game(self):
+ if self.playing:
+ answer = QtWidgets.QMessageBox.question(
+ self,
+ self.tr("New game"),
+ self.tr("A game is in progress.\n" "Do you want to abord it?"),
+ QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
+ QtWidgets.QMessageBox.Cancel,
+ )
+ if answer == QtWidgets.QMessageBox.Cancel:
+ self.pause(False)
+ return
+ self.music.stop()
+
+ self.reset_backgrounds()
+ self.stats.level, ok = QtWidgets.QInputDialog.getInt(
+ self,
+ self.tr("New game"),
+ self.tr("Start level:"),
+ 1,
+ 1,
+ 15,
+ flags=QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint,
+ )
+ if not ok:
+ return
+
+ self.playing = True
+ self.load_music()
+ self.music.play()
+ self.hold_queue.piece = None
+ self.stats.new_game()
+ self.matrix.new_game()
+ self.next_queue.pieces = [Tetromino() for _ in range(5)]
+ self.next_queue.insert_pieces()
+ self.next_piece.insert(Tetromino())
+ self.pause(False)
+ self.new_level()
+ self.new_piece()
+
+ def new_level(self):
+ self.set_background(next(self.backgrounds_cycle))
+ level = self.stats.new_level()
+ self.matrix.new_level(level)
+
+ def new_piece(self):
+ if self.stats.goal <= 0:
+ self.new_level()
+ self.matrix.insert(self.next_piece.piece)
+ self.matrix.lock_wait()
+ self.next_piece.insert(self.next_queue.pieces[0])
+ self.next_queue.new_piece()
+ self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
+ self.update()
+
+ if not self.matrix.piece.move(0, 0):
+ self.game_over()
+ return
+
+ self.matrix.fall_timer.start(self.matrix.speed)
+
+ def pause(self, paused):
+ if not self.playing:
+ return
+
+ if paused:
+ self.paused = True
+ self.update()
+ self.matrix.fall_timer.stop()
+ self.stats.clock.stop()
+ self.matrix.auto_repeat_timer.stop()
+ self.music.pause()
+ else:
+ self.matrix.text = ""
+ self.update()
+ QtCore.QTimer.singleShot(1000, self.resume)
+
+ def resume(self):
+ self.paused = False
+ self.update()
+ self.matrix.fall_timer.start(self.matrix.speed)
+ self.stats.clock.start(1000)
+ self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
+ self.music.play()
+
+ def hold(self):
+ """
+ Using the Hold command places the Tetrimino in play into the Hold Queue.
+ The previously held Tetrimino (if one exists) will then start falling
+ from the top of the Matrix,
+ beginning from its generation position and North Facing orientation.
+ Only one Tetrimino may be held at a time.
+ A Lock Down must take place between Holds.
+ For example, at the beginning, the first Tetrimino is generated and begins to fall.
+ The player decides to hold this Tetrimino.
+ Immediately the Next Tetrimino is generated from the Next Queue and begins to fall.
+ The player must first Lock Down this Tetrimino before holding another Tetrimino.
+ In other words, you may not Hold the same Tetrimino more than once.
+ """
+
+ if not self.hold_enabled:
+ return
+
+ piece = self.hold_queue.piece
+ if piece:
+ self.hold_queue.insert(self.matrix.piece)
+ self.matrix.insert(piece)
+ self.update()
+ else:
+ self.hold_queue.insert(self.matrix.piece)
+ self.new_piece()
+ self.hold_enabled = False
+
+ def game_over(self):
+ self.matrix.fall_timer.stop()
+ self.stats.clock.stop()
+ self.matrix.auto_repeat_timer.stop()
+ self.music.stop()
+ self.playing = False
+ self.matrix.game_over = True
+ msgbox = QtWidgets.QMessageBox(self)
+ msgbox.setWindowTitle(self.tr("Game over"))
+ msgbox.setIcon(QtWidgets.QMessageBox.Information)
+ if self.stats.score_total == self.stats.high_score:
+ msgbox.setText(
+ self.tr(
+ "Congratulations!\nYou have the high score: {}"
+ ).format(
+
+ locale.format("%i", self.stats.high_score, grouping=True, monetary=True)
+ )
+ )
+ qsettings.setValue(self.tr("High score"), self.stats.high_score)
+ else:
+ msgbox.setText(
+ self.tr(
+ "Score: {}\nHigh score: {}"
+ ).format(
+ locale.format("%i", self.stats.score_total, grouping=True, monetary=True),
+ locale.format("%i", self.stats.high_score, grouping=True, monetary=True)
+ )
+ )
+ msgbox.setDetailedText(self.stats.text(full_stats=True))
+ msgbox.exec_()
+
+
+class SettingStrings(QtCore.QObject):
+ """
+ Setting string for translation
+ """
+
+ def __init__(self):
+ super().__init__()
+
+ self.KEYBOARD = self.tr("Keyboard settings")
+ self.MOVE_LEFT = self.tr("Move left")
+ self.MOVE_RIGHT = self.tr("Move right")
+ self.ROTATE_CLOCKWISE = self.tr("Rotate clockwise")
+ self.ROTATE_COUNTERCLOCKWISE = self.tr("Rotate counterclockwise")
+ self.SOFT_DROP = self.tr("Soft drop")
+ self.HARD_DROP = self.tr("Hard drop")
+ self.HOLD = self.tr("Hold")
+ self.PAUSE = self.tr("Pause")
+ self.OTHER = self.tr("Other settings")
+
+ self.DELAYS = self.tr("Delays")
+ self.AUTO_SHIFT_DELAY = self.tr("Auto-shift delay")
+ self.AUTO_REPEAT_RATE = self.tr("Auto-repeat rate")
+
+ self.SOUND = self.tr("Sound")
+ self.MUSIC_VOLUME = self.tr("Music volume")
+ self.SFX_VOLUME = self.tr("Effects volume")
+
+ self.GHOST = self.tr("Show ghost piece")
+ self.SHOW_NEXT_QUEUE = self.tr("Show next queue")
+ self.HOLD_ENABLED = self.tr("Hold enabled")
+
+
+class KeyButton(QtWidgets.QPushButton):
+ """ Button widget capturing key name on focus """
+
+ names = {
+ value: name.replace("Key_", "")
+ for name, value in QtCore.Qt.__dict__.items()
+ if "Key_" in name
+ }
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+ def keyPressEvent(self, event):
+ key = event.key()
+ self.setText(self.names[key])
+
+
+class SettingsGroup(QtWidgets.QGroupBox):
+ """ Group box of a type of settings """
+
+ def __init__(self, group, parent, cls):
+ super().__init__(group, parent)
+ layout = QtWidgets.QFormLayout(self)
+ self.widgets = {}
+ for setting, value in settings[group].items():
+ if cls == KeyButton:
+ widget = KeyButton(value)
+ elif cls == QtWidgets.QCheckBox:
+ widget = QtWidgets.QCheckBox(setting)
+ widget.setChecked(value)
+ elif cls == QtWidgets.QSpinBox:
+ widget = QtWidgets.QSpinBox()
+ widget.setRange(0, 1000)
+ widget.setValue(value)
+ widget.setSuffix(" ms")
+ elif cls == QtWidgets.QSlider:
+ widget = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+ widget.setValue(value)
+ if cls == QtWidgets.QCheckBox:
+ layout.addRow(widget)
+ else:
+ layout.addRow(setting, widget)
+ self.widgets[setting] = widget
+ self.setLayout(layout)
+
+
+class SettingsDialog(QtWidgets.QDialog):
+ """ Show settings dialog """
+
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.setWindowTitle(self.tr("Settings"))
+ self.setModal(True)
+
+ layout = QtWidgets.QGridLayout()
+
+ self.groups = {}
+ self.groups[s.KEYBOARD] = SettingsGroup(s.KEYBOARD, self, KeyButton)
+ self.groups[s.DELAYS] = SettingsGroup(s.DELAYS, self, QtWidgets.QSpinBox)
+ self.groups[s.SOUND] = SettingsGroup(s.SOUND, self, QtWidgets.QSlider)
+ self.groups[s.OTHER] = SettingsGroup(s.OTHER, self, QtWidgets.QCheckBox)
+
+ layout.addWidget(self.groups[s.KEYBOARD], 0, 0, 3, 1)
+ layout.addWidget(self.groups[s.DELAYS], 0, 1)
+ layout.addWidget(self.groups[s.SOUND], 1, 1)
+ layout.addWidget(self.groups[s.OTHER], 2, 1)
+
+ buttons = QtWidgets.QDialogButtonBox(
+ QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
+ )
+ buttons.accepted.connect(self.ok)
+ buttons.rejected.connect(self.close)
+ layout.addWidget(buttons, 3, 0, 1, 2)
+
+ self.setLayout(layout)
+
+ self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].valueChanged.connect(
+ parent.frames.music.setVolume
+ )
+ self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderPressed.connect(
+ parent.frames.music.play)
+ self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderReleased.connect(
+ parent.frames.music.pause)
+
+ self.groups[s.SOUND].widgets[s.SFX_VOLUME].sliderReleased.connect(
+ parent.frames.stats.line_clear_sfx.play)
+
+ self.show()
+
+ def ok(self):
+ """ Save settings """
+
+ for group, elements in self.groups.items():
+ for setting, widget in elements.widgets.items():
+ if isinstance(widget, KeyButton):
+ value = widget.text()
+ elif isinstance(widget, QtWidgets.QCheckBox):
+ value = widget.isChecked()
+ elif isinstance(widget, QtWidgets.QSpinBox):
+ value = widget.value()
+ elif isinstance(widget, QtWidgets.QSlider):
+ value = widget.value()
+ settings[group][setting] = value
+ qsettings.setValue(group + "/" + setting, value)
+ self.close()
+
+
+class Window(QtWidgets.QMainWindow):
+ """ Main window """
+
+ def __init__(self):
+ splash_screen = QtWidgets.QSplashScreen(
+ QtGui.QPixmap(consts.SPLASH_SCREEN_PATH)
+ )
+ splash_screen.show()
+
+ self.set_locale()
+
+ self.load_settings()
+
+ super().__init__()
+ self.setWindowTitle(__title__.upper())
+ self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+
+ self.setWindowIcon(QtGui.QIcon(consts.ICON_PATH))
+ # Windows' taskbar icon
+ try:
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
+ ".".join((__author__, __title__, __version__))
+ )
+ except AttributeError:
+ pass
+
+ # Stylesheet
+ try:
+ import qdarkstyle
+ except ImportError:
+ pass
+ else:
+ self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment())
+
+ for font_path in consts.STATS_FONT_PATH, consts.MATRIX_FONT_PATH:
+ QtGui.QFontDatabase.addApplicationFont(font_path)
+
+ self.frames = Frames(self)
+ self.setCentralWidget(self.frames)
+ self.hold_queue = self.frames.hold_queue
+ self.matrix = self.frames.matrix
+ self.stats = self.frames.stats
+
+ self.menu = self.menuBar()
+
+ geometry = qsettings.value("WindowGeometry")
+ if geometry:
+ self.restoreGeometry(geometry)
+ else:
+ self.resize(*consts.DEFAULT_WINDOW_SIZE)
+ self.setWindowState(
+ QtCore.Qt.WindowStates(
+ int(qsettings.value("WindowState", QtCore.Qt.WindowActive))
+ )
+ )
+
+ splash_screen.finish(self);
+
+ def set_locale(self):
+ app = QtWidgets.QApplication.instance()
+
+ # Set appropriate thounsand separator characters
+ locale.setlocale(locale.LC_ALL, "")
+ # Qt
+ language = QtCore.QLocale.system().name()[:2]
+
+ qt_translator = QtCore.QTranslator(app)
+ qt_translation_path = QtCore.QLibraryInfo.location(
+ QtCore.QLibraryInfo.TranslationsPath
+ )
+ if qt_translator.load("qt_" + language, qt_translation_path):
+ app.installTranslator(qt_translator)
+
+ tetris2000_translator = QtCore.QTranslator(app)
+ if tetris2000_translator.load(language, consts.LOCALE_PATH):
+ app.installTranslator(tetris2000_translator)
+
+ def load_settings(self):
+ global s
+ s = SettingStrings()
+ global qsettings
+ qsettings = QtCore.QSettings(__author__, __title__)
+ global settings
+ settings = collections.OrderedDict(
+ [
+ (
+ s.KEYBOARD,
+ collections.OrderedDict(
+ [
+ (
+ s.MOVE_LEFT,
+ qsettings.value(s.KEYBOARD + "/" + s.MOVE_LEFT, consts.DEFAULT_MOVE_LEFT_KEY),
+ ),
+ (
+ s.MOVE_RIGHT,
+ qsettings.value(
+ s.KEYBOARD + "/" + s.MOVE_RIGHT, consts.DEFAULT_MOVE_RIGHT_KEY
+ ),
+ ),
+ (
+ s.ROTATE_CLOCKWISE,
+ qsettings.value(
+ s.KEYBOARD + "/" + s.ROTATE_CLOCKWISE, consts.DEFAULT_ROTATE_CLOCKWISE_KEY
+ ),
+ ),
+ (
+ s.ROTATE_COUNTERCLOCKWISE,
+ qsettings.value(
+ s.KEYBOARD + "/" + s.ROTATE_COUNTERCLOCKWISE,
+ consts.DEFAULT_ROTATE_COUNTERCLOCKWISE_KEY,
+ ),
+ ),
+ (
+ s.SOFT_DROP,
+ qsettings.value(s.KEYBOARD + "/" + s.SOFT_DROP, consts.DEFAULT_SOFT_DROP_KEY),
+ ),
+ (
+ s.HARD_DROP,
+ qsettings.value(
+ s.KEYBOARD + "/" + s.HARD_DROP, consts.DEFAULT_HARD_DROP_KEY
+ ),
+ ),
+ (
+ s.HOLD,
+ qsettings.value(s.KEYBOARD + "/" + s.HOLD, consts.DEFAULT_HOLD_KEY),
+ ),
+ (
+ s.PAUSE,
+ qsettings.value(s.KEYBOARD + "/" + s.PAUSE, consts.DEFAULT_PAUSE_KEY),
+ ),
+ ]
+ ),
+ ),
+ (
+ s.DELAYS,
+ collections.OrderedDict(
+ [
+ (
+ s.AUTO_SHIFT_DELAY,
+ int(
+ qsettings.value(
+ s.DELAYS + "/" + s.AUTO_SHIFT_DELAY, consts.DEFAULT_AUTO_SHIFT_DELAY
+ )
+ ),
+ ),
+ (
+ s.AUTO_REPEAT_RATE,
+ int(
+ qsettings.value(
+ s.DELAYS + "/" + s.AUTO_REPEAT_RATE, consts.DEFAULT_AUTO_REPEAT_RATE
+ )
+ ),
+ ),
+ ]
+ ),
+ ),
+ (
+ s.SOUND,
+ collections.OrderedDict(
+ [
+ (
+ s.MUSIC_VOLUME,
+ int(
+ qsettings.value(s.SOUND + "/" + s.MUSIC_VOLUME, consts.DEFAUT_MUSIC_VOLUME)
+ ),
+ ),
+ (
+ s.SFX_VOLUME,
+ int(
+ qsettings.value(
+ s.SOUND + "/" + s.SFX_VOLUME, consts.DEFAULT_SFX_VOLUME
+ )
+ ),
+ ),
+ ]
+ ),
+ ),
+ (
+ s.OTHER,
+ collections.OrderedDict(
+ [
+ (
+ s.GHOST,
+ bool(qsettings.value(s.OTHER + "/" + s.GHOST, consts.DEFAULT_SHOW_GHOST)),
+ ),
+ (
+ s.SHOW_NEXT_QUEUE,
+ bool(
+ qsettings.value(
+ s.OTHER + "/" + s.SHOW_NEXT_QUEUE, consts.DEFAULT_SHOW_NEXT_QUEUE
+ )
+ ),
+ ),
+ (
+ s.HOLD_ENABLED,
+ bool(
+ qsettings.value(
+ s.OTHER + "/" + s.HOLD_ENABLED, consts.DEFAULT_HOLD_ENABLED
+ )
+ ),
+ ),
+ ]
+ ),
+ ),
+ ]
+ )
+
+ def menuBar(self):
+ menu = super().menuBar()
+
+ new_game_action = QtWidgets.QAction(self.tr("&New game"), self)
+ new_game_action.triggered.connect(self.frames.new_game)
+ menu.addAction(new_game_action)
+
+ settings_action = QtWidgets.QAction(self.tr("&Settings"), self)
+ settings_action.triggered.connect(self.show_settings_dialog)
+ menu.addAction(settings_action)
+
+ about_action = QtWidgets.QAction(self.tr("&About"), self)
+ about_action.triggered.connect(self.about)
+ menu.addAction(about_action)
+ return menu
+
+ def show_settings_dialog(self):
+ SettingsDialog(self).exec_()
+
+ self.frames.apply_settings()
+
+ def about(self):
+ QtWidgets.QMessageBox.about(
+ self,
+ __title__,
+ self.tr(
+"""Tetris® clone by Adrien Malingrey
+
+Tetris Game Design by Alekseï Pajitnov
+Graphism inspired by Tetris Effect
+Window style sheet: qdarkstyle by Colin Duquesnoy
+Fonts by Markus Koellmann, Peter Wiegel
+Images from:
+OpenGameArt.org by beren77, Duion
+Pexels.com by Min An, Jaymantri, Felix Mittermeier
+Pixabay.com by LoganArt
+Pixnio.com by Adrian Pelletier
+Unsplash.com by Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja
+StockSnap.io by Nathan Anderson, José Ignacio Pompé
+Musics from ocremix.org by:
+CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS
+Sound effects made with voc-one by Simple-Media"""
+ ),
+ )
+ if self.frames.playing:
+ self.frames.pause(False)
+
+ def closeEvent(self, event):
+ if self.frames.playing:
+ answer = QtWidgets.QMessageBox.question(
+ self,
+ self.tr("Quit game?"),
+ self.tr("A game is in progress.\nDo you want to abord it?"),
+ QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
+ QtWidgets.QMessageBox.Cancel,
+ )
+ if answer == QtWidgets.QMessageBox.Cancel:
+ event.ignore()
+ self.frames.pause(False)
+ return
+
+ self.frames.music.stop()
+
+ # Save settings
+ qsettings.setValue(self.tr("High score"), self.stats.high_score)
+ qsettings.setValue("WindowGeometry", self.saveGeometry())
+ qsettings.setValue("WindowState", int(self.windowState()))
\ No newline at end of file
diff --git a/grids.py b/grids.py
deleted file mode 100644
index 1f785a3..0000000
--- a/grids.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-import consts
-from qt5 import QtWidgets, QtGui
-from block import Block
-from point import Point
-from settings import s, settings
-from tetromino import Tetromino
-
-
-class Grid(QtWidgets.QWidget):
- """
- Mother class of Hold queue, Matrix, Next piece, and Next queue
- """
-
- ROWS = consts.GRID_DEFAULT_ROWS + consts.GRID_INVISIBLE_ROWS
- COLUMNS = consts.GRID_DEFAULT_COLUMNS
- STARTING_POSITION = Point(
- consts.GRID_DEFAULT_COLUMNS // 2,
- consts.GRID_DEFAULT_ROWS // 2 + consts.GRID_INVISIBLE_ROWS
- )
- GRIDLINE_COLOR = consts.GRID_GRIDLINE_COLOR
- HARD_DROP_MOVEMENT = consts.GRID_HARD_DROP_MOVEMENT
- SPOTLIGHT = Point(*consts.GRID_SPOTLIGHT )
-
- def __init__(self, frames):
- super().__init__(frames)
- self.setStyleSheet("background-color: transparent")
- self.frames = frames
- self.spotlight = self.SPOTLIGHT
- self.piece = None
-
- def insert(self, piece, position=None):
- """
- Add a Tetromino to self
- Update its coordinates
- """
- piece.insert_into(self, position or self.STARTING_POSITION)
- self.piece = piece
- self.update()
-
- def resizeEvent(self, event):
- self.bottom = Block.side * self.ROWS
- self.grid_top = consts.GRID_INVISIBLE_ROWS * Block.side
- width = Block.side * self.COLUMNS
- self.left = (self.width() - width) // 2
- self.right = width + self.left
- self.top_left_corner = Point(self.left, 0)
-
- def paintEvent(self, event=None):
- painter = QtGui.QPainter(self)
- painter.setRenderHint(QtGui.QPainter.Antialiasing)
-
- self.paint_grid(painter)
-
- if (not self.frames.paused or not self.frames.playing) and self.piece:
- self.paint_piece(painter, self.piece)
-
- def paint_grid(self, painter):
- painter.setPen(self.GRIDLINE_COLOR)
- for x in (self.left + i * Block.side for i in range(self.COLUMNS + 1)):
- painter.drawLine(x, self.grid_top, x, self.bottom)
- for y in (j * Block.side for j in range(consts.GRID_INVISIBLE_ROWS, self.ROWS + 1)):
- painter.drawLine(self.left, y, self.right, y)
-
- def paint_piece(self, painter, piece):
- for mino in piece.minoes:
- mino.paint(painter, self.top_left_corner, self.spotlight)
-
-
-class HoldQueue(Grid):
- """
- The Hold Queue allows the player to “hold” a falling Tetrimino for as long as they wish.
- Holding a Tetrimino releases the Tetrimino already in the Hold Queue (if one exists).
- """
-
- def paintEvent(self, event):
- if not settings[s.OTHER][s.HOLD_ENABLED]:
- return
-
- super().paintEvent(event)
-
-
-class NextQueue(Grid):
- """
- The Next Queue allows the player to see the Next Tetrimino that will be generated
- and put into play.
- """
-
- ROWS = consts.NEXT_QUEUE_ROWS
- COLUMNS = consts.NEXT_QUEUE_COLUMNS
-
- def __init__(self, parent):
- super().__init__(parent)
- self.pieces = []
-
- def new_piece(self):
- self.pieces = self.pieces[1:] + [Tetromino()]
- self.insert_pieces()
-
- def insert_pieces(self):
- for y, piece in enumerate(self.pieces):
- piece.insert_into(self, Point(3, 3 * y + 1))
-
- def paintEvent(self, event=None):
- if not settings[s.OTHER][s.SHOW_NEXT_QUEUE]:
- return
-
- painter = QtGui.QPainter(self)
- painter.setRenderHint(QtGui.QPainter.Antialiasing)
-
- if not self.frames.paused:
- for piece in self.pieces:
- self.paint_piece(painter, piece)
\ No newline at end of file
diff --git a/locale/Tetris2000.ts b/locale/Tetris2000.ts
index 8f89ae9..8d1486a 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 1110bfa..fff7efc 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/matrix.py b/matrix.py
deleted file mode 100644
index 53e29d7..0000000
--- a/matrix.py
+++ /dev/null
@@ -1,393 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-import time
-
-import consts
-from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE
-from qt5 import QtGui, QtCore, QtMultimedia
-from grids import Grid
-from point import Point
-from block import Block
-from tetromino import GhostPiece
-from settings import s, settings
-
-
-class Matrix(Grid):
- """
- The rectangular arrangement of cells creating the active game area.
- Tetriminos fall from the top-middle just above the Skyline (off-screen) to the bottom.
- """
-
- ROWS = consts.MATRIX_ROWS + consts.GRID_INVISIBLE_ROWS
- COLUMNS = consts.MATRIX_COLUMNS
- STARTING_POSITION = Point(COLUMNS // 2, consts.GRID_INVISIBLE_ROWS - 1)
- TEXT_COLOR = consts.MATRIX_TEXT_COLOR
-
- drop_signal = QtCore.Signal(int)
- lock_signal = QtCore.Signal(int, str)
-
- def __init__(self, frames):
- super().__init__(frames)
-
- self.load_sfx()
-
- self.game_over = False
- self.text = ""
- self.temporary_texts = []
-
- self.setFocusPolicy(QtCore.Qt.StrongFocus)
-
- self.auto_repeat_delay = 0
- self.auto_repeat_timer = QtCore.QTimer()
- self.auto_repeat_timer.timeout.connect(self.auto_repeat)
- self.fall_timer = QtCore.QTimer()
- self.fall_timer.timeout.connect(self.fall)
-
- self.cells = []
-
- def load_sfx(self):
- self.wall_sfx = QtMultimedia.QSoundEffect(self)
- url = QtCore.QUrl.fromLocalFile(consts.WALL_SFX_PATH)
- self.wall_sfx.setSource(url)
-
- self.rotate_sfx = QtMultimedia.QSoundEffect(self)
- url = QtCore.QUrl.fromLocalFile(consts.ROTATE_SFX_PATH)
- self.rotate_sfx.setSource(url)
-
- self.hard_drop_sfx = QtMultimedia.QSoundEffect(self)
- url = QtCore.QUrl.fromLocalFile(consts.HARD_DROP_SFX_PATH)
- self.hard_drop_sfx.setSource(url)
-
- def new_game(self):
- self.game_over = False
- self.lock_delay = consts.LOCK_DELAY
- self.cells = [self.empty_row() for y in range(self.ROWS)]
- self.setFocus()
- self.actions_to_repeat = []
- self.wall_hit = False
-
- def new_level(self, level):
- self.show_temporary_text(self.tr("Level\n") + str(level))
- self.speed = consts.INITIAL_SPEED * (0.8 - ((level - 1) * 0.007)) ** (level - 1)
- self.fall_timer.start(self.speed)
- if level > 15:
- self.lock_delay *= 0.9
-
- def empty_row(self):
- return [None for x in range(self.COLUMNS)]
-
- def is_empty_cell(self, coord):
- x, y = coord.x(), coord.y()
- return (
- 0 <= x < self.COLUMNS
- and y < self.ROWS
- and not (0 <= y and self.cells[y][x])
- )
-
- def keyPressEvent(self, event):
- if event.isAutoRepeat():
- return
-
- if not self.frames.playing:
- return
-
- try:
- action = self.keys[event.key()]
- except KeyError:
- return
-
- self.do(action)
- if action in (s.MOVE_LEFT, s.MOVE_RIGHT, s.SOFT_DROP):
- if action not in self.actions_to_repeat:
- self.actions_to_repeat.append(action)
- self.auto_repeat_wait()
-
- def keyReleaseEvent(self, event):
- if event.isAutoRepeat():
- return
-
- if not self.frames.playing:
- return
-
- try:
- self.actions_to_repeat.remove(self.keys[event.key()])
- except (KeyError, ValueError):
- pass
- else:
- self.auto_repeat_wait()
-
- if not self.actions_to_repeat:
- for mino in self.piece.minoes:
- mino.fade()
- self.update()
-
- def auto_repeat_wait(self):
- self.auto_repeat_delay = (
- time.time() + settings[s.DELAYS][s.AUTO_SHIFT_DELAY] / 1000
- )
-
- def auto_repeat(self):
- """
- Tapping the move button allows a single cell movement of the Tetrimino
- in the direction pressed.
- Holding down the move button triggers an Auto-Repeat movement
- that allows the player to move a Tetrimino from one side of the Matrix
- to the other in about 0.5 seconds.
- This is essential on higher levels when the Fall Speed of a Tetrimino is very fast.
- There is a slight delay between the time the move button is pressed
- and the time when Auto-Repeat kicks in : s.AUTO_SHIFT_DELAY.
- This delay prevents unwanted extra movement of a Tetrimino.
- Auto-Repeat only affects Left/Right movement.
- Auto-Repeat continues to the Next Tetrimino (after Lock Down)
- as long as the move button remains pressed.
- In addition, when Auto-Repeat begins,
- and the player then holds the opposite direction button,
- the Tetrimino then begins moving the opposite direction with the initial delay.
- When any single button is then released,
- the Tetrimino should again move in the direction still held,
- with the Auto-Shift delay applied once more.
- """
- if (
- not self.frames.playing
- or self.frames.paused
- or time.time() < self.auto_repeat_delay
- ):
- return
-
- if self.actions_to_repeat:
- self.do(self.actions_to_repeat[-1])
-
- def do(self, action):
- """The player can move, rotate, Soft Drop, Hard Drop,
- and Hold the falling Tetrimino (i.e., the Tetrimino in play).
- """
- if action == s.PAUSE:
- self.frames.pause(not self.frames.paused)
-
- if not self.frames.playing or self.frames.paused or not self.piece:
- return
-
- for mino in self.piece.minoes:
- mino.shine(0)
-
- if action == s.MOVE_LEFT:
- if self.piece.move(L, 0):
- self.lock_wait()
- self.wall_hit = False
- elif not self.wall_hit:
- self.wall_hit = True
- self.wall_sfx.play()
-
- elif action == s.MOVE_RIGHT:
- if self.piece.move(R, 0):
- self.rotate_sfx.play()
- self.lock_wait()
- self.wall_hit = False
- elif not self.wall_hit:
- self.wall_hit = True
- self.wall_sfx.play()
-
- elif action == s.ROTATE_CLOCKWISE:
- if self.piece.rotate(direction=CLOCKWISE):
- self.rotate_sfx.play()
- self.lock_wait()
- self.wall_hit = False
- elif not self.wall_hit:
- self.wall_hit = True
- self.wall_sfx.play()
-
- elif action == s.ROTATE_COUNTERCLOCKWISE:
- if self.piece.rotate(direction=COUNTERCLOCKWISE):
- self.lock_wait()
- self.wall_hit = False
- elif not self.wall_hit:
- self.wall_hit = True
- self.wall_sfx.play()
-
- elif action == s.SOFT_DROP:
- if self.piece.soft_drop():
- self.drop_signal.emit(1)
- self.wall_hit = False
- elif not self.wall_hit:
- self.wall_hit = True
- self.wall_sfx.play()
-
- elif action == s.HARD_DROP:
- trail = self.piece.hard_drop()
- self.top_left_corner += Point(0, self.HARD_DROP_MOVEMENT * Block.side)
- self.drop_signal.emit(2 * trail)
- QtCore.QTimer.singleShot(consts.ANIMATION_DELAY, self.after_hard_drop)
- self.hard_drop_sfx.play()
- self.lock_phase()
-
- elif action == s.HOLD:
- self.frames.hold()
-
- def after_hard_drop(self):
- """ Reset the animation movement of the Matrix on a hard drop """
- self.top_left_corner -= Point(0, self.HARD_DROP_MOVEMENT * Block.side)
-
- def lock_wait(self):
- self.fall_delay = time.time() + (self.speed + self.lock_delay) / 1000
-
- def fall(self):
- """
- Once a Tetrimino is generated,
- it immediately drops one row (if no existing Block is in its path).
- From here, it begins its descent to the bottom of the Matrix.
- The Tetrimino will fall at its normal Fall Speed
- whether or not it is being manipulated by the player.
- """
- if self.piece:
- if self.piece.move(0, 1):
- self.lock_wait()
- else:
- if time.time() >= self.fall_delay:
- self.lock_phase()
-
- def lock_phase(self):
- """
- The player can perform the same actions on a Tetrimino in this phase
- as he/she can in the Falling Phase,
- as long as the Tetrimino is not yet Locked Down.
- A Tetrimino that is Hard Dropped Locks Down immediately.
- However, if a Tetrimino naturally falls or Soft Drops onto a landing Surface,
- it is given a delay (self.fall_delay) on a Lock Down Timer
- before it actually Locks Down.
- """
-
- self.wall_sfx.play()
-
- # Enter minoes into the matrix
- for mino in self.piece.minoes:
- if mino.coord.y() >= 0:
- self.cells[mino.coord.y()][mino.coord.x()] = mino
- mino.shine(glowing=2, delay=consts.ANIMATION_DELAY)
- self.update()
-
- if all(mino.coord.y() < consts.GRID_INVISIBLE_ROWS for mino in self.piece.minoes):
- self.frames.game_over()
- return
-
- """
- In this phase,
- the engine looks for patterns made from Locked Down Blocks in the Matrix.
- Once a pattern has been matched,
- it can trigger any number of Tetris variant-related effects.
- The classic pattern is the Line Clear pattern.
- This pattern is matched when one or more rows of 10 horizontally aligned
- Matrix cells are occupied by Blocks.
- The matching Blocks are then marked for removal on a hit list.
- Blocks on the hit list are cleared from the Matrix at a later time
- in the Eliminate Phase.
- """
- # Dectect complete lines
- self.complete_lines = []
- for y, row in enumerate(self.cells):
- if all(cell for cell in row):
- self.complete_lines.append(y)
- for block in row:
- block.shine()
- self.spotlight = row[self.COLUMNS // 2].coord
- self.auto_repeat_timer.stop()
- self.lock_signal.emit(len(self.complete_lines), self.piece.t_spin)
-
- if self.complete_lines:
- self.fall_timer.stop()
- QtCore.QTimer.singleShot(consts.LINE_CLEAR_DELAY, self.eliminate_phase)
- else:
- self.frames.new_piece()
-
-
- def eliminate_phase(self):
- """
- Any Minos marked for removal, i.e., on the hit list,
- are cleared from the Matrix in this phase.
- If this results in one or more complete 10-cell rows in the Matrix
- becoming unoccupied by Minos,
- then all Minos above that row(s) collapse,
- or fall by the number of complete rows cleared from the Matrix.
- """
- for y in self.complete_lines:
- del self.cells[y]
- self.cells.insert(0, self.empty_row())
-
- for y, row in enumerate(self.cells):
- for x, block in enumerate(row):
- if block:
- block.coord.setX(x)
- block.coord.setY(y)
-
- self.update()
- self.auto_repeat_wait()
- self.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
-
- self.frames.new_piece()
-
- def paintEvent(self, event):
- """
- Draws grid, actual piece, blocks in the Matrix and show texts
- """
- painter = QtGui.QPainter(self)
- painter.setRenderHint(QtGui.QPainter.Antialiasing)
-
- self.paint_grid(painter)
-
- if not self.frames.paused or self.game_over:
- if self.piece:
- if settings[s.OTHER][s.GHOST]:
- self.ghost = GhostPiece(self.piece)
- self.spotlight = self.ghost.minoes[0].coord
- self.paint_piece(painter, self.ghost)
- self.paint_piece(painter, self.piece)
-
- # Blocks in matrix
- for row in self.cells:
- for block in row:
- if block:
- block.paint(painter, self.top_left_corner, self.spotlight)
-
- if self.frames.playing and self.frames.paused:
- painter.setFont(QtGui.QFont("Maassslicer", 0.75 * Block.side))
- painter.setPen(self.TEXT_COLOR)
- painter.drawText(
- self.rect(),
- QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
- self.tr("PAUSE\n\nPress %s\nto resume") % settings[s.KEYBOARD][s.PAUSE],
- )
- if self.game_over:
- painter.setFont(QtGui.QFont("Maassslicer", Block.side))
- painter.setPen(self.TEXT_COLOR)
- painter.drawText(
- self.rect(),
- QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
- self.tr("GAME\nOVER"),
- )
- if self.temporary_texts:
- painter.setFont(self.temporary_text_font)
- painter.setPen(self.TEXT_COLOR)
- painter.drawText(
- self.rect(),
- QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap,
- "\n\n\n" + "\n\n".join(self.temporary_texts),
- )
-
- def resizeEvent(self, event):
- super().resizeEvent(event)
- self.temporary_text_font = QtGui.QFont(consts.MATRIX_FONT_NAME, Block.side)
-
- def show_temporary_text(self, text):
- self.temporary_texts.append(text.upper())
- self.font = self.temporary_text_font
- self.update()
- QtCore.QTimer.singleShot(consts.TEMPORARY_TEXT_DURATION, self.delete_text)
-
- def delete_text(self):
- del self.temporary_texts[0]
- self.update()
-
- def focusOutEvent(self, event):
- if self.frames.playing:
- self.frames.pause(True)
\ No newline at end of file
diff --git a/settings.py b/settings.py
deleted file mode 100644
index 4c55179..0000000
--- a/settings.py
+++ /dev/null
@@ -1,277 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-import collections
-
-import consts
-from __version__ import __author__, __title__
-from qt5 import QtWidgets, QtCore
-
-
-class SettingStrings(QtCore.QObject):
- """
- Setting string for translation
- """
-
- def __init__(self):
- super().__init__()
-
- self.KEYBOARD = self.tr("Keyboard settings")
- self.MOVE_LEFT = self.tr("Move left")
- self.MOVE_RIGHT = self.tr("Move right")
- self.ROTATE_CLOCKWISE = self.tr("Rotate clockwise")
- self.ROTATE_COUNTERCLOCKWISE = self.tr("Rotate counterclockwise")
- self.SOFT_DROP = self.tr("Soft drop")
- self.HARD_DROP = self.tr("Hard drop")
- self.HOLD = self.tr("Hold")
- self.PAUSE = self.tr("Pause")
- self.OTHER = self.tr("Other settings")
-
- self.DELAYS = self.tr("Delays")
- self.AUTO_SHIFT_DELAY = self.tr("Auto-shift delay")
- self.AUTO_REPEAT_RATE = self.tr("Auto-repeat rate")
-
- self.SOUND = self.tr("Sound")
- self.MUSIC_VOLUME = self.tr("Music volume")
- self.SFX_VOLUME = self.tr("Effects volume")
-
- self.GHOST = self.tr("Show ghost piece")
- self.SHOW_NEXT_QUEUE = self.tr("Show next queue")
- self.HOLD_ENABLED = self.tr("Hold enabled")
-
-
-class KeyButton(QtWidgets.QPushButton):
- """ Button widget capturing key name on focus """
-
- names = {
- value: name.replace("Key_", "")
- for name, value in QtCore.Qt.__dict__.items()
- if "Key_" in name
- }
-
- def __init__(self, *args):
- super().__init__(*args)
-
- def keyPressEvent(self, event):
- key = event.key()
- self.setText(self.names[key])
-
-
-class SettingsGroup(QtWidgets.QGroupBox):
- """ Group box of a type of settings """
-
- def __init__(self, group, parent, cls):
- super().__init__(group, parent)
- layout = QtWidgets.QFormLayout(self)
- self.widgets = {}
- for setting, value in settings[group].items():
- if cls == KeyButton:
- widget = KeyButton(value)
- elif cls == QtWidgets.QCheckBox:
- widget = QtWidgets.QCheckBox(setting)
- widget.setChecked(value)
- elif cls == QtWidgets.QSpinBox:
- widget = QtWidgets.QSpinBox()
- widget.setRange(0, 1000)
- widget.setValue(value)
- widget.setSuffix(" ms")
- elif cls == QtWidgets.QSlider:
- widget = QtWidgets.QSlider(QtCore.Qt.Horizontal)
- widget.setValue(value)
- if cls == QtWidgets.QCheckBox:
- layout.addRow(widget)
- else:
- layout.addRow(setting, widget)
- self.widgets[setting] = widget
- self.setLayout(layout)
-
-
-class SettingsDialog(QtWidgets.QDialog):
- """ Show settings dialog """
-
- def __init__(self, parent):
- super().__init__(parent)
- self.setWindowTitle(self.tr("Settings"))
- self.setModal(True)
-
- layout = QtWidgets.QGridLayout()
-
- self.groups = {}
- self.groups[s.KEYBOARD] = SettingsGroup(s.KEYBOARD, self, KeyButton)
- self.groups[s.DELAYS] = SettingsGroup(s.DELAYS, self, QtWidgets.QSpinBox)
- self.groups[s.SOUND] = SettingsGroup(s.SOUND, self, QtWidgets.QSlider)
- self.groups[s.OTHER] = SettingsGroup(s.OTHER, self, QtWidgets.QCheckBox)
-
- layout.addWidget(self.groups[s.KEYBOARD], 0, 0, 3, 1)
- layout.addWidget(self.groups[s.DELAYS], 0, 1)
- layout.addWidget(self.groups[s.SOUND], 1, 1)
- layout.addWidget(self.groups[s.OTHER], 2, 1)
-
- buttons = QtWidgets.QDialogButtonBox(
- QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
- )
- buttons.accepted.connect(self.ok)
- buttons.rejected.connect(self.close)
- layout.addWidget(buttons, 3, 0, 1, 2)
-
- self.setLayout(layout)
-
- self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].valueChanged.connect(
- parent.frames.music.setVolume
- )
- self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderPressed.connect(
- parent.frames.music.play)
- self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderReleased.connect(
- parent.frames.music.pause)
-
- self.groups[s.SOUND].widgets[s.SFX_VOLUME].sliderReleased.connect(
- parent.frames.stats.line_clear_sfx.play)
-
- self.show()
-
- def ok(self):
- """ Save settings """
-
- for group, elements in self.groups.items():
- for setting, widget in elements.widgets.items():
- if isinstance(widget, KeyButton):
- value = widget.text()
- elif isinstance(widget, QtWidgets.QCheckBox):
- value = widget.isChecked()
- elif isinstance(widget, QtWidgets.QSpinBox):
- value = widget.value()
- elif isinstance(widget, QtWidgets.QSlider):
- value = widget.value()
- settings[group][setting] = value
- qsettings.setValue(group + "/" + setting, value)
- self.close()
-
-
-s = SettingStrings()
-
-qsettings = QtCore.QSettings(__author__, __title__)
-
-settings = collections.OrderedDict(
- [
- (
- s.KEYBOARD,
- collections.OrderedDict(
- [
- (
- s.MOVE_LEFT,
- qsettings.value(s.KEYBOARD + "/" + s.MOVE_LEFT, consts.DEFAULT_MOVE_LEFT_KEY),
- ),
- (
- s.MOVE_RIGHT,
- qsettings.value(
- s.KEYBOARD + "/" + s.MOVE_RIGHT, consts.DEFAULT_MOVE_RIGHT_KEY
- ),
- ),
- (
- s.ROTATE_CLOCKWISE,
- qsettings.value(
- s.KEYBOARD + "/" + s.ROTATE_CLOCKWISE, consts.DEFAULT_ROTATE_CLOCKWISE_KEY
- ),
- ),
- (
- s.ROTATE_COUNTERCLOCKWISE,
- qsettings.value(
- s.KEYBOARD + "/" + s.ROTATE_COUNTERCLOCKWISE,
- consts.DEFAULT_ROTATE_COUNTERCLOCKWISE_KEY,
- ),
- ),
- (
- s.SOFT_DROP,
- qsettings.value(s.KEYBOARD + "/" + s.SOFT_DROP, consts.DEFAULT_SOFT_DROP_KEY),
- ),
- (
- s.HARD_DROP,
- qsettings.value(
- s.KEYBOARD + "/" + s.HARD_DROP, consts.DEFAULT_HARD_DROP_KEY
- ),
- ),
- (
- s.HOLD,
- qsettings.value(s.KEYBOARD + "/" + s.HOLD, consts.DEFAULT_HOLD_KEY),
- ),
- (
- s.PAUSE,
- qsettings.value(s.KEYBOARD + "/" + s.PAUSE, consts.DEFAULT_PAUSE_KEY),
- ),
- ]
- ),
- ),
- (
- s.DELAYS,
- collections.OrderedDict(
- [
- (
- s.AUTO_SHIFT_DELAY,
- int(
- qsettings.value(
- s.DELAYS + "/" + s.AUTO_SHIFT_DELAY, consts.DEFAULT_AUTO_SHIFT_DELAY
- )
- ),
- ),
- (
- s.AUTO_REPEAT_RATE,
- int(
- qsettings.value(
- s.DELAYS + "/" + s.AUTO_REPEAT_RATE, consts.DEFAULT_AUTO_REPEAT_RATE
- )
- ),
- ),
- ]
- ),
- ),
- (
- s.SOUND,
- collections.OrderedDict(
- [
- (
- s.MUSIC_VOLUME,
- int(
- qsettings.value(s.SOUND + "/" + s.MUSIC_VOLUME, consts.DEFAUT_MUSIC_VOLUME)
- ),
- ),
- (
- s.SFX_VOLUME,
- int(
- qsettings.value(
- s.SOUND + "/" + s.SFX_VOLUME, consts.DEFAULT_SFX_VOLUME
- )
- ),
- ),
- ]
- ),
- ),
- (
- s.OTHER,
- collections.OrderedDict(
- [
- (
- s.GHOST,
- bool(qsettings.value(s.OTHER + "/" + s.GHOST, consts.DEFAULT_SHOW_GHOST)),
- ),
- (
- s.SHOW_NEXT_QUEUE,
- bool(
- qsettings.value(
- s.OTHER + "/" + s.SHOW_NEXT_QUEUE, consts.DEFAULT_SHOW_NEXT_QUEUE
- )
- ),
- ),
- (
- s.HOLD_ENABLED,
- bool(
- qsettings.value(
- s.OTHER + "/" + s.HOLD_ENABLED, consts.DEFAULT_HOLD_ENABLED
- )
- ),
- ),
- ]
- ),
- ),
- ]
-)
\ No newline at end of file
diff --git a/stats.py b/stats.py
deleted file mode 100644
index 46c073a..0000000
--- a/stats.py
+++ /dev/null
@@ -1,252 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-import locale
-import time
-
-import consts
-from qt5 import QtWidgets, QtGui, QtCore, QtMultimedia
-from settings import qsettings
-from block import Block
-
-
-
-class Stats(QtWidgets.QWidget):
- """
- Show informations relevant to the game being played is displayed on-screen.
- Looks for patterns made from Locked Down Blocks in the Matrix and calculate score.
- """
-
- ROWS = consts.STATS_ROWS
- COLUMNS = consts.STATS_COLUMNS
- TEXT_COLOR = consts.STATS_TEXT_COLOR
-
- temporary_text = QtCore.Signal(str)
-
- def __init__(self, frames):
- super().__init__(frames)
- self.frames = frames
- self.setStyleSheet("background-color: transparent")
-
- self.load_sfx()
-
- self.setSizePolicy(
- QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
- )
- self.text_options = QtGui.QTextOption(QtCore.Qt.AlignRight)
- self.text_options.setWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
-
- self.clock = QtCore.QTimer()
- self.clock.timeout.connect(self.tick)
-
- self.high_score = int(qsettings.value(self.tr("High score"), 0))
-
- def load_sfx(self):
- self.line_clear_sfx = QtMultimedia.QSoundEffect(self)
- url = QtCore.QUrl.fromLocalFile(consts.LINE_CLEAR_SFX_PATH)
- self.line_clear_sfx.setSource(url)
-
- self.tetris_sfx = QtMultimedia.QSoundEffect(self)
- url = QtCore.QUrl.fromLocalFile(consts.TETRIS_SFX_PATH)
- self.tetris_sfx.setSource(url)
-
- def new_game(self):
- self.level -= 1
- self.goal = 0
- self.complete_lines_total = 0
- self.score_total = 1
- self.t_spin_total = 0
- self.mini_t_spin_total = 0
- self.nb_back_to_back = 0
- self.back_to_back_scores = None
- self.combo = -1
- self.combos_total = 0
- self.max_combo = 0
- self.chronometer = 0
- self.nb_tetro = 0
- self.clock.start(1000)
- self.lines_stats = [0, 0, 0, 0, 0]
-
- def new_level(self):
- self.level += 1
- self.goal += 5 * self.level
- return self.level
-
- def update_score(self, nb_complete_lines, t_spin):
- """
- The player scores points by performing Single, Double, Triple,
- and Tetris Line Clears, as well as T-Spins and Mini T-Spins.
- Soft and Hard Drops also award points.
- There is a special bonus for Back-to-Backs,
- which is when two actions such as a Tetris and T-Spin Double take place
- without a Single, Double, or Triple Line Clear occurring between them.
- Scoring for Line Clears, T-Spins, and Mini T-Spins are level dependent,
- while Hard and Soft Drop point values remain constant.
- """
- self.nb_tetro += 1
- if nb_complete_lines:
- self.complete_lines_total += nb_complete_lines
- self.lines_stats[nb_complete_lines] += 1
- if t_spin == "T-Spin":
- self.t_spin_total += 1
- elif t_spin == "Mini T-Spin":
- self.mini_t_spin_total += 1
-
- score = consts.SCORES[nb_complete_lines][t_spin]
-
- if score:
- text = " ".join((t_spin, consts.SCORES[nb_complete_lines]["name"]))
- if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
- self.tetris_sfx.play()
- elif t_spin or nb_complete_lines:
- self.line_clear_sfx.play()
-
- self.goal -= score
- score = 100 * self.level * score
- self.score_total += score
-
- self.temporary_text.emit(text + "\n{:n}".format(score))
-
-# ==============================================================================
-# Combo
-# Bonus for complete lines on each consecutive lock downs
-# if nb_complete_lines:
-# ==============================================================================
- if nb_complete_lines:
- self.combo += 1
- if self.combo > 0:
- if nb_complete_lines == 1:
- combo_score = 20 * self.combo * self.level
- else:
- combo_score = 50 * self.combo * self.level
- self.score_total += combo_score
- self.max_combo = max(self.max_combo, self.combo)
- self.combos_total += 1
- self.temporary_text.emit(
- self.tr("COMBO x{:n}\n{:n}").format(self.combo, combo_score)
- )
- else:
- self.combo = -1
-
-# ==============================================================================
-# Back-to_back sequence
-# Two major bonus actions, such as two Tetrises, performed without
-# a Single, Double, or Triple Line Clear occurring between them.
-# Bonus for Tetrises, T-Spin Line Clears, and Mini T-Spin Line Clears
-# performed consecutively in a B2B sequence.
-# ==============================================================================
- if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
- if self.back_to_back_scores is not None:
- self.back_to_back_scores.append(score // 2)
- else:
- # The first Line Clear in the Back-to-Back sequence
- # does not receive the Back-to-Back Bonus.
- self.back_to_back_scores = []
- elif nb_complete_lines and not t_spin:
- # A Back-to-Back sequence is only broken by a Single, Double, or Triple Line Clear.
- # Locking down a Tetrimino without clearing a line
- # or holding a Tetrimino does not break the Back-to-Back sequence.
- # T-Spins and Mini T-Spins that do not clear any lines
- # do not receive the Back-to-Back Bonus; instead they are scored as normal.
- # They also cannot start a Back-to-Back sequence, however,
- # they do not break an existing Back-to-Back sequence.
- if self.back_to_back_scores:
- b2b_score = sum(self.back_to_back_scores)
- self.score_total += b2b_score
- self.nb_back_to_back += 1
- self.temporary_text.emit(
- self.tr("BACK TO BACK\n{:n}").format(b2b_score)
- )
- self.back_to_back_scores = None
-
- self.high_score = max(self.score_total, self.high_score)
- self.update()
-
- def update_drop_score(self, n):
- """ Tetrimino is Soft Dropped for n lines or Hard Dropped for (n/2) lines"""
- self.score_total += n
- self.high_score = max(self.score_total, self.high_score)
- self.update()
-
- def tick(self):
- self.chronometer += 1
- self.update()
-
- def paintEvent(self, event):
- if not self.frames.playing and not self.frames.matrix.game_over:
- return
-
- painter = QtGui.QPainter(self)
- painter.setFont(self.font)
- painter.setPen(self.TEXT_COLOR)
-
- painter.drawText(
- QtCore.QRectF(self.rect()), self.text(sep="\n\n"), self.text_options
- )
-
- def text(self, full_stats=False, sep="\n"):
- text = (
- self.tr("Score: ")
- + locale.format("%i", self.score_total, grouping=True, monetary=True)
- + sep
- + self.tr("High score: ")
- + locale.format("%i", self.high_score, grouping=True, monetary=True)
- + sep
- + self.tr("Time: {}\n").format(
- time.strftime("%H:%M:%S", time.gmtime(self.chronometer))
- )
- + sep
- + self.tr("Level: ")
- + locale.format("%i", self.level, grouping=True, monetary=True)
- + sep
- + self.tr("Goal: ")
- + locale.format("%i", self.goal, grouping=True, monetary=True)
- + sep
- + self.tr("Lines: ")
- + locale.format(
- "%i", self.complete_lines_total, grouping=True, monetary=True
- )
- + sep
- + self.tr("Mini T-Spins: ")
- + locale.format("%i", self.mini_t_spin_total, grouping=True, monetary=True)
- + sep
- + self.tr("T-Spins: ")
- + locale.format("%i", self.t_spin_total, grouping=True, monetary=True)
- + sep
- + self.tr("Back-to-back: ")
- + locale.format("%i", self.nb_back_to_back, grouping=True, monetary=True)
- + sep
- + self.tr("Max combo: ")
- + locale.format("%i", self.max_combo, grouping=True, monetary=True)
- + sep
- + self.tr("Combos: ")
- + locale.format("%i", self.combos_total, grouping=True, monetary=True)
- )
- if full_stats:
- minutes = self.chronometer / 60
- text += (
- "\n"
- + sep
- + self.tr("Lines per minute: {:.1f}").format(
- self.complete_lines_total / minutes
- )
- + sep
- + self.tr("Tetrominos locked down: ")
- + locale.format("%i", self.nb_tetro, grouping=True, monetary=True)
- + sep
- + self.tr("Tetrominos per minute: {:.1f}").format(
- self.nb_tetro / minutes
- )
- + sep
- )
- text += sep.join(
- score_type["name"]
- + self.tr(": ")
- + locale.format("%i", nb, grouping=True, monetary=True)
- for score_type, nb in tuple(zip(consts.SCORES, self.lines_stats))[1:]
- )
- return text
-
- def resizeEvent(self, event):
- self.font = QtGui.QFont(consts.STATS_FONT_NAME, Block.side / 3.5)
\ No newline at end of file
diff --git a/window.py b/window.py
deleted file mode 100644
index 337c984..0000000
--- a/window.py
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-import locale
-import ctypes
-
-import consts
-from qt5 import QtWidgets, QtCore, QtGui
-from __version__ import __title__, __author__, __version__
-from settings import SettingsDialog, qsettings
-from frames import Frames
-
-
-class Window(QtWidgets.QMainWindow):
- """ Main window """
-
- def __init__(self):
- splash_screen = QtWidgets.QSplashScreen(
- QtGui.QPixmap(consts.SPLASH_SCREEN_PATH)
- )
- splash_screen.show()
-
- self.set_locale()
-
- super().__init__()
- self.setWindowTitle(__title__.upper())
- self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
-
- self.setWindowIcon(QtGui.QIcon(consts.ICON_PATH))
- # Windows' taskbar icon
- try:
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
- ".".join((__author__, __title__, __version__))
- )
- except AttributeError:
- pass
-
- # Stylesheet
- try:
- import qdarkstyle
- except ImportError:
- pass
- else:
- self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment())
-
- for font_path in consts.STATS_FONT_PATH, consts.MATRIX_FONT_PATH:
- QtGui.QFontDatabase.addApplicationFont(font_path)
-
- self.frames = Frames(self)
- self.setCentralWidget(self.frames)
- self.hold_queue = self.frames.hold_queue
- self.matrix = self.frames.matrix
- self.stats = self.frames.stats
-
- self.menu = self.menuBar()
-
- geometry = qsettings.value("WindowGeometry")
- if geometry:
- self.restoreGeometry(geometry)
- else:
- self.resize(*consts.DEFAULT_WINDOW_SIZE)
- self.setWindowState(
- QtCore.Qt.WindowStates(
- int(qsettings.value("WindowState", QtCore.Qt.WindowActive))
- )
- )
-
- splash_screen.finish(self);
-
- def set_locale(self):
- app = QtWidgets.QApplication.instance()
-
- # Set appropriate thounsand separator characters
- locale.setlocale(locale.LC_ALL, "")
- # Qt
- language = QtCore.QLocale.system().name()[:2]
-
- qt_translator = QtCore.QTranslator(app)
- qt_translation_path = QtCore.QLibraryInfo.location(
- QtCore.QLibraryInfo.TranslationsPath
- )
- if qt_translator.load("qt_" + language, qt_translation_path):
- app.installTranslator(qt_translator)
-
- tetris2000_translator = QtCore.QTranslator(app)
- if tetris2000_translator.load(language, consts.LOCALE_PATH):
- app.installTranslator(tetris2000_translator)
-
- def menuBar(self):
- menu = super().menuBar()
-
- new_game_action = QtWidgets.QAction(self.tr("&New game"), self)
- new_game_action.triggered.connect(self.frames.new_game)
- menu.addAction(new_game_action)
-
- settings_action = QtWidgets.QAction(self.tr("&Settings"), self)
- settings_action.triggered.connect(self.show_settings_dialog)
- menu.addAction(settings_action)
-
- about_action = QtWidgets.QAction(self.tr("&About"), self)
- about_action.triggered.connect(self.about)
- menu.addAction(about_action)
- return menu
-
- def show_settings_dialog(self):
- SettingsDialog(self).exec_()
-
- self.frames.apply_settings()
-
- def about(self):
- QtWidgets.QMessageBox.about(
- self,
- __title__,
- self.tr(
-"""Tetris® clone by Adrien Malingrey
-
-Tetris Game Design by Alekseï Pajitnov
-Graphism inspired by Tetris Effect
-Window style sheet: qdarkstyle by Colin Duquesnoy
-Fonts by Markus Koellmann, Peter Wiegel
-Images from:
-OpenGameArt.org by beren77, Duion
-Pexels.com by Min An, Jaymantri, Felix Mittermeier
-Pixabay.com by LoganArt
-Pixnio.com by Adrian Pelletier
-Unsplash.com by Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja
-StockSnap.io by Nathan Anderson, José Ignacio Pompé
-Musics from ocremix.org by:
-CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS
-Sound effects made with voc-one by Simple-Media"""
- ),
- )
- if self.frames.playing:
- self.frames.pause(False)
-
- def closeEvent(self, event):
- if self.frames.playing:
- answer = QtWidgets.QMessageBox.question(
- self,
- self.tr("Quit game?"),
- self.tr("A game is in progress.\nDo you want to abord it?"),
- QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
- QtWidgets.QMessageBox.Cancel,
- )
- if answer == QtWidgets.QMessageBox.Cancel:
- event.ignore()
- self.frames.pause(False)
- return
-
- self.frames.music.stop()
-
- # Save settings
- qsettings.setValue(self.tr("High score"), self.stats.high_score)
- qsettings.setValue("WindowGeometry", self.saveGeometry())
- qsettings.setValue("WindowState", int(self.windowState()))
\ No newline at end of file