Revert back merge error 4h ago

I nearly lost my mind
This commit is contained in:
adrienmalin 2018-08-06 23:00:06 +02:00 committed by GitHub
parent f2b5a32b35
commit a264f0cae0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2411 additions and 1023 deletions

View File

@ -2,9 +2,17 @@
# -*- coding: utf-8 -*-
"""
Another TETRIS® clone
Tetris Game Design by Alexey Pajitnov.
Parts of comments issued from 2009 Tetris Design Guideline
"""
import sys
from qt5 import QtWidgets
from game_gui import Window
from window import Window

130
block.py Normal file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import consts
from consts import U, D
from point import Point
from qt5 import QtCore, QtGui
class Block:
"""
Mino or block
Mino : A single square-shaped building block of a shape called a Tetrimino.
Four Minos arranged into any of their various connected patterns is known as a Tetrimino
Block : A single block locked in a cell in the Grid
"""
# Colors
BORDER_COLOR = consts.BLOCK_BORDER_COLOR
FILL_COLOR = consts.BLOCK_FILL_COLOR
GLOWING_BORDER_COLOR = consts.BLOCK_GLOWING_BORDER_COLOR
GLOWING_FILL_COLOR = consts.BLOCK_GLOWING_FILL_COLOR
LIGHT_COLOR = consts.BLOCK_LIGHT_COLOR
TRANSPARENT = consts.BLOCK_TRANSPARENT
GLOWING = consts.BLOCK_GLOWING
side = consts.BLOCK_INITIAL_SIDE
def __init__(self, coord, trail=0):
self.coord = coord
self.trail = trail
self.border_color = self.BORDER_COLOR
self.fill_color = self.FILL_COLOR
self.glowing = self.GLOWING
def paint(self, painter, top_left_corner, spotlight):
p = top_left_corner + self.coord * Block.side
block_center = Point(Block.side/2, Block.side/2)
self.center = p + block_center
spotlight = top_left_corner + Block.side * spotlight + block_center
self.glint = 0.15 * spotlight + 0.85 * self.center
if self.trail:
start = (
top_left_corner + (self.coord + Point(0, self.trail * U)) * Block.side
)
stop = top_left_corner + (self.coord + Point(0, 2 * D)) * Block.side
fill = QtGui.QLinearGradient(start, stop)
fill.setColorAt(0, self.LIGHT_COLOR)
fill.setColorAt(1, self.GLOWING_FILL_COLOR)
painter.setBrush(fill)
painter.setPen(QtCore.Qt.NoPen)
painter.drawRoundedRect(
start.x(),
start.y(),
Block.side,
Block.side * (1 + self.trail),
20,
20,
QtCore.Qt.RelativeSize,
)
if self.glowing:
fill = QtGui.QRadialGradient(self.center, self.glowing * Block.side)
fill.setColorAt(0, self.TRANSPARENT)
fill.setColorAt(0.5 / self.glowing, self.LIGHT_COLOR)
fill.setColorAt(1, self.TRANSPARENT)
painter.setBrush(QtGui.QBrush(fill))
painter.setPen(QtCore.Qt.NoPen)
painter.drawEllipse(
self.center.x() - self.glowing * Block.side,
self.center.y() - self.glowing * Block.side,
2 * self.glowing * Block.side,
2 * self.glowing * Block.side,
)
painter.setBrush(self.brush())
painter.setPen(self.pen())
painter.drawRoundedRect(
p.x() + 1,
p.y() + 1,
Block.side - 2,
Block.side - 2,
20,
20,
QtCore.Qt.RelativeSize,
)
def brush(self):
if self.fill_color is None:
return QtCore.Qt.NoBrush
fill = QtGui.QRadialGradient(self.glint, 1.5 * Block.side)
fill.setColorAt(0, self.fill_color.lighter())
fill.setColorAt(1, self.fill_color)
return QtGui.QBrush(fill)
def pen(self):
if self.border_color is None:
return QtCore.Qt.NoPen
border = QtGui.QRadialGradient(self.glint, Block.side)
border.setColorAt(0, self.border_color.lighter())
border.setColorAt(1, self.border_color.darker())
return QtGui.QPen(QtGui.QBrush(border), 1, join=QtCore.Qt.RoundJoin)
def shine(self, glowing=2, delay=None):
self.border_color = Block.GLOWING_BORDER_COLOR
self.fill_color = Block.GLOWING_FILL_COLOR
self.glowing = glowing
if delay:
QtCore.QTimer.singleShot(delay, self.fade)
def fade(self):
self.border_color = Block.BORDER_COLOR
self.fill_color = Block.FILL_COLOR
self.glowing = 0
self.trail = 0
class GhostBlock(Block):
"""
Mino of the ghost piece
"""
BORDER_COLOR = consts.GHOST_BLOCK_BORDER_COLOR
FILL_COLOR = consts.GHOST_BLOCK_FILL_COLOR
GLOWING_FILL_COLOR = consts.GHOST_BLOCK_GLOWING_FILL_COLOR
GLOWING = consts.GHOST_BLOCK_GLOWING

333
frames.py Normal file
View File

@ -0,0 +1,333 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import itertools
import consts
from qt5 import QtWidgets, QtCore, QtGui, QtMultimedia
from settings import s, qsettings, settings
from block import Block
from tetromino import Tetromino
from grids import Grid, HoldQueue, NextQueue
from matrix import Matrix
from stats import Stats
class AspectRatioWidget(QtWidgets.QWidget):
"""
Keeps aspect ratio of child widget on resize
https://stackoverflow.com/questions/48043469/how-to-lock-aspect-ratio-while-resizing-the-window
"""
def __init__(self, widget, parent):
super().__init__(parent)
self.aspect_ratio = widget.size().width() / widget.size().height()
self.setLayout(QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight, self))
# add spacer, then widget, then spacer
self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
self.layout().addWidget(widget)
self.layout().addItem(QtWidgets.QSpacerItem(0, 0))
def resizeEvent(self, e):
w = e.size().width()
h = e.size().height()
if w / h > self.aspect_ratio: # too wide
self.layout().setDirection(QtWidgets.QBoxLayout.LeftToRight)
widget_stretch = h * self.aspect_ratio
outer_stretch = (w - widget_stretch) / 2 + 0.5
else: # too tall
self.layout().setDirection(QtWidgets.QBoxLayout.TopToBottom)
widget_stretch = w / self.aspect_ratio
outer_stretch = (h - widget_stretch) / 2 + 0.5
self.layout().setStretch(0, outer_stretch)
self.layout().setStretch(1, widget_stretch)
self.layout().setStretch(2, outer_stretch)
class Frames(QtWidgets.QWidget):
"""
Display Hold queue, Matrix, Next piece, Next queue and Stats.
Manage interactions between them.
"""
def __init__(self, parent):
super().__init__(parent)
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.playing = False
self.paused = False
self.load_music()
self.hold_queue = HoldQueue(self)
self.matrix = Matrix(self)
self.next_piece = Grid(self)
self.stats = Stats(self)
self.next_queue = NextQueue(self)
self.matrices = (self.hold_queue, self.matrix, self.next_piece)
self.columns = sum(matrix.COLUMNS + 2 for matrix in self.matrices)
self.rows = self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS
w = QtWidgets.QWidget(self)
w.setStyleSheet("background-color: transparent")
grid = QtWidgets.QGridLayout()
for x in range(self.rows):
grid.setRowStretch(x, 1)
for y in range(self.columns):
grid.setColumnStretch(y, 1)
grid.setSpacing(0)
x, y = 0, 0
grid.addWidget(
self.hold_queue, y, x, self.hold_queue.ROWS + 1, self.hold_queue.COLUMNS + 2
)
x += self.hold_queue.COLUMNS + 2
grid.addWidget(self.matrix, y, x, self.matrix.ROWS + consts.GRID_INVISIBLE_ROWS, self.matrix.COLUMNS + 2)
x += self.matrix.COLUMNS + 3
grid.addWidget(
self.next_piece, y, x, self.next_piece.ROWS + 1, self.next_piece.COLUMNS + 2
)
x, y = 0, self.hold_queue.ROWS + 2
grid.addWidget(self.stats, y, x, self.stats.ROWS, self.stats.COLUMNS + 1)
x += self.stats.COLUMNS + self.matrix.COLUMNS + 5
grid.addWidget(
self.next_queue, y, x, self.next_queue.ROWS, self.next_queue.COLUMNS + 2
)
w.setLayout(grid)
w.resize(self.columns, self.rows)
asw = AspectRatioWidget(w, self)
layout = QtWidgets.QGridLayout()
layout.addWidget(asw)
self.setLayout(layout)
self.stats.temporary_text.connect(self.matrix.show_temporary_text)
self.matrix.drop_signal.connect(self.stats.update_drop_score)
self.matrix.lock_signal.connect(self.stats.update_score)
self.set_background(os.path.join(consts.BG_IMAGE_DIR, consts.START_BG_IMAGE_NAME))
self.apply_settings()
def load_music(self):
playlist = QtMultimedia.QMediaPlaylist(self)
for entry in os.scandir(consts.MUSICS_DIR):
path = os.path.join(consts.MUSICS_DIR, entry.name)
url = QtCore.QUrl.fromLocalFile(path)
music = QtMultimedia.QMediaContent(url)
playlist.addMedia(music)
playlist.setPlaybackMode(QtMultimedia.QMediaPlaylist.Loop)
self.music = QtMultimedia.QMediaPlayer(self)
self.music.setAudioRole(QtMultimedia.QAudio.GameRole)
self.music.setPlaylist(playlist)
self.music.setVolume(settings[s.SOUND][s.MUSIC_VOLUME])
def apply_settings(self):
if self.music.volume() > 5 and self.playing:
self.music.play()
else:
self.music.pause()
if self.playing:
self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
self.pause(False)
self.matrix.keys = {
getattr(QtCore.Qt, "Key_" + name): action
for action, name in settings[s.KEYBOARD].items()
}
self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
self.matrix.spotlight = Matrix.SPOTLIGHT
for sfx in (
self.matrix.rotate_sfx, self.matrix.wall_sfx,
self.stats.line_clear_sfx, self.stats.tetris_sfx
):
sfx.setVolume(settings[s.SOUND][s.SFX_VOLUME])
def resizeEvent(self, event):
Block.side = 0.9 * min(self.width() // self.columns, self.height() // self.rows)
self.resize_bg_image()
def reset_backgrounds(self):
backgrounds_paths = (
os.path.join(consts.BG_IMAGE_DIR, entry.name)
for entry in os.scandir(consts.BG_IMAGE_DIR)
)
self.backgrounds_cycle = itertools.cycle(backgrounds_paths)
def set_background(self, path):
self.bg_image = QtGui.QImage(path)
self.resize_bg_image()
def resize_bg_image(self):
self.resized_bg_image = QtGui.QPixmap.fromImage(self.bg_image)
self.resized_bg_image = self.resized_bg_image.scaled(
self.size(),
QtCore.Qt.KeepAspectRatioByExpanding,
QtCore.Qt.SmoothTransformation
)
self.resized_bg_image = self.resized_bg_image.copy(
(self.resized_bg_image.width() - self.width()) // 2,
(self.resized_bg_image.height() - self.height()) // 2,
self.width(),
self.height()
)
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(
self.rect(),
self.resized_bg_image)
def new_game(self):
if self.playing:
answer = QtWidgets.QMessageBox.question(
self,
self.tr("New game"),
self.tr("A game is in progress.\n" "Do you want to abord it?"),
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
QtWidgets.QMessageBox.Cancel,
)
if answer == QtWidgets.QMessageBox.Cancel:
self.pause(False)
return
self.music.stop()
self.reset_backgrounds()
self.stats.level, ok = QtWidgets.QInputDialog.getInt(
self,
self.tr("New game"),
self.tr("Start level:"),
1,
1,
15,
flags=QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint,
)
if not ok:
return
self.playing = True
self.load_music()
self.music.play()
self.hold_queue.piece = None
self.stats.new_game()
self.matrix.new_game()
self.next_queue.pieces = [Tetromino() for _ in range(5)]
self.next_queue.insert_pieces()
self.next_piece.insert(Tetromino())
self.pause(False)
self.new_level()
self.new_piece()
def new_level(self):
self.set_background(next(self.backgrounds_cycle))
level = self.stats.new_level()
self.matrix.new_level(level)
def new_piece(self):
if self.stats.goal <= 0:
self.new_level()
self.matrix.insert(self.next_piece.piece)
self.matrix.lock_wait()
self.next_piece.insert(self.next_queue.pieces[0])
self.next_queue.new_piece()
self.hold_enabled = settings[s.OTHER][s.HOLD_ENABLED]
self.update()
if not self.matrix.piece.move(0, 0):
self.game_over()
return
self.matrix.fall_timer.start(self.matrix.speed)
def pause(self, paused):
if not self.playing:
return
if paused:
self.paused = True
self.update()
self.matrix.fall_timer.stop()
self.stats.clock.stop()
self.matrix.auto_repeat_timer.stop()
self.music.pause()
else:
self.matrix.text = ""
self.update()
QtCore.QTimer.singleShot(1000, self.resume)
def resume(self):
self.paused = False
self.update()
self.matrix.fall_timer.start(self.matrix.speed)
self.stats.clock.start(1000)
self.matrix.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
self.music.play()
def hold(self):
"""
Using the Hold command places the Tetrimino in play into the Hold Queue.
The previously held Tetrimino (if one exists) will then start falling
from the top of the Matrix,
beginning from its generation position and North Facing orientation.
Only one Tetrimino may be held at a time.
A Lock Down must take place between Holds.
For example, at the beginning, the first Tetrimino is generated and begins to fall.
The player decides to hold this Tetrimino.
Immediately the Next Tetrimino is generated from the Next Queue and begins to fall.
The player must first Lock Down this Tetrimino before holding another Tetrimino.
In other words, you may not Hold the same Tetrimino more than once.
"""
if not self.hold_enabled:
return
piece = self.hold_queue.piece
if piece:
self.hold_queue.insert(self.matrix.piece)
self.matrix.insert(piece)
self.update()
else:
self.hold_queue.insert(self.matrix.piece)
self.new_piece()
self.hold_enabled = False
def game_over(self):
self.matrix.fall_timer.stop()
self.stats.clock.stop()
self.matrix.auto_repeat_timer.stop()
self.music.stop()
self.playing = False
self.matrix.game_over = True
msgbox = QtWidgets.QMessageBox(self)
msgbox.setWindowTitle(self.tr("Game over"))
msgbox.setIcon(QtWidgets.QMessageBox.Information)
if self.stats.score_total == self.stats.high_score:
msgbox.setText(
self.tr(
"Congratulations!\nYou have the high score: {}"
).format(
locale.format("%i", self.stats.high_score, grouping=True, monetary=True)
)
)
qsettings.setValue(self.tr("High score"), self.stats.high_score)
else:
msgbox.setText(
self.tr(
"Score: {}\nHigh score: {}"
).format(
locale.format("%i", self.stats.score_total, grouping=True, monetary=True),
locale.format("%i", self.stats.high_score, grouping=True, monetary=True)
)
)
msgbox.setDetailedText(self.stats.text(full_stats=True))
msgbox.exec_()

View File

@ -19,7 +19,7 @@ class Grid(QtWidgets.QWidget):
COLUMNS = consts.GRID_DEFAULT_COLUMNS
STARTING_POSITION = Point(
consts.GRID_DEFAULT_COLUMNS // 2,
consts.GRID_DEFAULT_ROWS // 2 + 2
consts.GRID_DEFAULT_ROWS // 2 + consts.GRID_INVISIBLE_ROWS
)
GRIDLINE_COLOR = consts.GRID_GRIDLINE_COLOR
HARD_DROP_MOVEMENT = consts.GRID_HARD_DROP_MOVEMENT

View File

@ -3,39 +3,39 @@
<context>
<name>Frames</name>
<message>
<location filename="../game_gui.py" line="930"/>
<location filename="../frames.py" line="205"/>
<source>New game</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="917"/>
<location filename="../frames.py" line="192"/>
<source>A game is in progress.
Do you want to abord it?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="930"/>
<location filename="../frames.py" line="205"/>
<source>Start level:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1048"/>
<location filename="../frames.py" line="323"/>
<source>High score</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1037"/>
<location filename="../frames.py" line="312"/>
<source>Game over</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1040"/>
<location filename="../frames.py" line="315"/>
<source>Congratulations!
You have the high score: {}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1050"/>
<location filename="../frames.py" line="325"/>
<source>Score: {}
High score: {}</source>
<translation type="unfinished"></translation>
@ -44,13 +44,13 @@ High score: {}</source>
<context>
<name>Matrix</name>
<message>
<location filename="../game_gui.py" line="135"/>
<location filename="../matrix.py" line="73"/>
<source>Level
</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="418"/>
<location filename="../matrix.py" line="356"/>
<source>PAUSE
Press %s
@ -58,7 +58,7 @@ to resume</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="426"/>
<location filename="../matrix.py" line="364"/>
<source>GAME
OVER</source>
<translation type="unfinished"></translation>
@ -67,97 +67,97 @@ OVER</source>
<context>
<name>SettingStrings</name>
<message>
<location filename="../game_gui.py" line="1177"/>
<location filename="../settings.py" line="20"/>
<source>Keyboard settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1178"/>
<location filename="../settings.py" line="21"/>
<source>Move left</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1179"/>
<location filename="../settings.py" line="22"/>
<source>Move right</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1180"/>
<location filename="../settings.py" line="23"/>
<source>Rotate clockwise</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1181"/>
<location filename="../settings.py" line="24"/>
<source>Rotate counterclockwise</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1182"/>
<location filename="../settings.py" line="25"/>
<source>Soft drop</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1183"/>
<location filename="../settings.py" line="26"/>
<source>Hard drop</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1184"/>
<location filename="../settings.py" line="27"/>
<source>Hold</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1185"/>
<location filename="../settings.py" line="28"/>
<source>Pause</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1186"/>
<location filename="../settings.py" line="29"/>
<source>Other settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1188"/>
<location filename="../settings.py" line="31"/>
<source>Delays</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1189"/>
<location filename="../settings.py" line="32"/>
<source>Auto-shift delay</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1190"/>
<location filename="../settings.py" line="33"/>
<source>Auto-repeat rate</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1192"/>
<location filename="../settings.py" line="35"/>
<source>Sound</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1193"/>
<location filename="../settings.py" line="36"/>
<source>Music volume</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1194"/>
<location filename="../settings.py" line="37"/>
<source>Effects volume</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1196"/>
<location filename="../settings.py" line="39"/>
<source>Show ghost piece</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1197"/>
<location filename="../settings.py" line="40"/>
<source>Show next queue</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1198"/>
<location filename="../settings.py" line="41"/>
<source>Hold enabled</source>
<translation type="unfinished"></translation>
</message>
@ -165,7 +165,7 @@ OVER</source>
<context>
<name>SettingsDialog</name>
<message>
<location filename="../game_gui.py" line="1113"/>
<location filename="../settings.py" line="95"/>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
@ -173,95 +173,95 @@ OVER</source>
<context>
<name>Stats</name>
<message>
<location filename="../game_gui.py" line="533"/>
<location filename="../stats.py" line="43"/>
<source>High score</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="616"/>
<location filename="../stats.py" line="126"/>
<source>COMBO x{:n}
{:n}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="648"/>
<location filename="../stats.py" line="158"/>
<source>BACK TO BACK
{:n}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Score: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>High score: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Time: {}
</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Level: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Goal: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Lines: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Mini T-Spins: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>T-Spins: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Back-to-back: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Max combo: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Combos: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="718"/>
<location filename="../stats.py" line="228"/>
<source>Lines per minute: {:.1f}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="718"/>
<location filename="../stats.py" line="228"/>
<source>Tetrominos locked down: </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="718"/>
<location filename="../stats.py" line="228"/>
<source>Tetrominos per minute: {:.1f}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="733"/>
<location filename="../stats.py" line="243"/>
<source>: </source>
<translation type="unfinished"></translation>
</message>
@ -269,38 +269,38 @@ OVER</source>
<context>
<name>Window</name>
<message>
<location filename="../game_gui.py" line="1411"/>
<location filename="../window.py" line="93"/>
<source>&amp;New game</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1415"/>
<location filename="../window.py" line="97"/>
<source>&amp;Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1419"/>
<location filename="../window.py" line="101"/>
<source>&amp;About</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1457"/>
<location filename="../window.py" line="139"/>
<source>Quit game?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1457"/>
<location filename="../window.py" line="139"/>
<source>A game is in progress.
Do you want to abord it?</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1472"/>
<location filename="../window.py" line="154"/>
<source>High score</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../game_gui.py" line="1430"/>
<location filename="../window.py" line="112"/>
<source>Tetris® clone by Adrien Malingrey
Tetris Game Design by Alekseï Pajitnov

View File

@ -4,41 +4,41 @@
<context>
<name>Frames</name>
<message>
<location filename="../game_gui.py" line="930"/>
<location filename="../frames.py" line="205"/>
<source>New game</source>
<translation>Nouvelle partie</translation>
</message>
<message>
<location filename="../game_gui.py" line="917"/>
<location filename="../frames.py" line="192"/>
<source>A game is in progress.
Do you want to abord it?</source>
<translation>Une partie est en cours.
Voulez-vous l&apos;abandonner ?</translation>
</message>
<message>
<location filename="../game_gui.py" line="930"/>
<location filename="../frames.py" line="205"/>
<source>Start level:</source>
<translation>Commencer au niveau :</translation>
</message>
<message>
<location filename="../game_gui.py" line="1048"/>
<location filename="../frames.py" line="323"/>
<source>High score</source>
<translation>Meilleur score</translation>
</message>
<message>
<location filename="../game_gui.py" line="1037"/>
<location filename="../frames.py" line="312"/>
<source>Game over</source>
<translation>Partie terminée</translation>
</message>
<message>
<location filename="../game_gui.py" line="1040"/>
<location filename="../frames.py" line="315"/>
<source>Congratulations!
You have the high score: {}</source>
<translation>Bravo !
Vous avez atteint le meilleur score : {}</translation>
</message>
<message>
<location filename="../game_gui.py" line="1050"/>
<location filename="../frames.py" line="325"/>
<source>Score: {}
High score: {}</source>
<translation>Score : {}
@ -48,14 +48,14 @@ Meilleur score : {}</translation>
<context>
<name>Matrix</name>
<message>
<location filename="../game_gui.py" line="135"/>
<location filename="../matrix.py" line="73"/>
<source>Level
</source>
<translation>Niveau
</translation>
</message>
<message>
<location filename="../game_gui.py" line="418"/>
<location filename="../matrix.py" line="356"/>
<source>PAUSE
Press %s
@ -67,7 +67,7 @@ Appuyez sur
pour reprendre</translation>
</message>
<message>
<location filename="../game_gui.py" line="426"/>
<location filename="../matrix.py" line="364"/>
<source>GAME
OVER</source>
<translation>PARTIE
@ -77,97 +77,97 @@ TERMINÉE</translation>
<context>
<name>SettingStrings</name>
<message>
<location filename="../game_gui.py" line="1177"/>
<location filename="../settings.py" line="20"/>
<source>Keyboard settings</source>
<translation>Configuration du clavier</translation>
</message>
<message>
<location filename="../game_gui.py" line="1178"/>
<location filename="../settings.py" line="21"/>
<source>Move left</source>
<translation>Déplacer à gauche</translation>
</message>
<message>
<location filename="../game_gui.py" line="1179"/>
<location filename="../settings.py" line="22"/>
<source>Move right</source>
<translation>Déplacer à droite</translation>
</message>
<message>
<location filename="../game_gui.py" line="1180"/>
<location filename="../settings.py" line="23"/>
<source>Rotate clockwise</source>
<translation>Tourner dans le sens horaire</translation>
</message>
<message>
<location filename="../game_gui.py" line="1181"/>
<location filename="../settings.py" line="24"/>
<source>Rotate counterclockwise</source>
<translation>Tourner dans le sens anti-horaire</translation>
</message>
<message>
<location filename="../game_gui.py" line="1182"/>
<location filename="../settings.py" line="25"/>
<source>Soft drop</source>
<translation>Chute lente</translation>
</message>
<message>
<location filename="../game_gui.py" line="1183"/>
<location filename="../settings.py" line="26"/>
<source>Hard drop</source>
<translation>Chute rapide</translation>
</message>
<message>
<location filename="../game_gui.py" line="1184"/>
<location filename="../settings.py" line="27"/>
<source>Hold</source>
<translation>Réserve</translation>
</message>
<message>
<location filename="../game_gui.py" line="1185"/>
<location filename="../settings.py" line="28"/>
<source>Pause</source>
<translation>Pause</translation>
</message>
<message>
<location filename="../game_gui.py" line="1186"/>
<location filename="../settings.py" line="29"/>
<source>Other settings</source>
<translation>Autres paramètres</translation>
</message>
<message>
<location filename="../game_gui.py" line="1188"/>
<location filename="../settings.py" line="31"/>
<source>Delays</source>
<translation>Temporisation</translation>
</message>
<message>
<location filename="../game_gui.py" line="1189"/>
<location filename="../settings.py" line="32"/>
<source>Auto-shift delay</source>
<translation>Délai avant répétition</translation>
</message>
<message>
<location filename="../game_gui.py" line="1190"/>
<location filename="../settings.py" line="33"/>
<source>Auto-repeat rate</source>
<translation>Vitesse de répétition</translation>
</message>
<message>
<location filename="../game_gui.py" line="1192"/>
<location filename="../settings.py" line="35"/>
<source>Sound</source>
<translation>Son</translation>
</message>
<message>
<location filename="../game_gui.py" line="1193"/>
<location filename="../settings.py" line="36"/>
<source>Music volume</source>
<translation>Volume de la musique</translation>
</message>
<message>
<location filename="../game_gui.py" line="1194"/>
<location filename="../settings.py" line="37"/>
<source>Effects volume</source>
<translation>Volume des effets sonores</translation>
</message>
<message>
<location filename="../game_gui.py" line="1196"/>
<location filename="../settings.py" line="39"/>
<source>Show ghost piece</source>
<translation>Afficher la pièce fantôme</translation>
</message>
<message>
<location filename="../game_gui.py" line="1197"/>
<location filename="../settings.py" line="40"/>
<source>Show next queue</source>
<translation>Afficher les 6 prochaines pièces</translation>
</message>
<message>
<location filename="../game_gui.py" line="1198"/>
<location filename="../settings.py" line="41"/>
<source>Hold enabled</source>
<translation>Activer la réserve</translation>
</message>
@ -175,7 +175,7 @@ TERMINÉE</translation>
<context>
<name>SettingsDialog</name>
<message>
<location filename="../game_gui.py" line="1113"/>
<location filename="../settings.py" line="95"/>
<source>Settings</source>
<translation>Préférences</translation>
</message>
@ -183,98 +183,98 @@ TERMINÉE</translation>
<context>
<name>Stats</name>
<message>
<location filename="../game_gui.py" line="533"/>
<location filename="../stats.py" line="43"/>
<source>High score</source>
<translation>Meilleur score</translation>
</message>
<message>
<location filename="../game_gui.py" line="616"/>
<location filename="../stats.py" line="126"/>
<source>COMBO x{:n}
{:n}</source>
<translation>COMBO x{:n}
{:n}</translation>
</message>
<message>
<location filename="../game_gui.py" line="648"/>
<location filename="../stats.py" line="158"/>
<source>BACK TO BACK
{:n}</source>
<translation>BACK TO BACK
{:n}</translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Time: {}
</source>
<translation>Temps : {}
</translation>
</message>
<message>
<location filename="../game_gui.py" line="718"/>
<location filename="../stats.py" line="228"/>
<source>Lines per minute: {:.1f}</source>
<translation>Lignes par minute : {:.1f}</translation>
</message>
<message>
<location filename="../game_gui.py" line="718"/>
<location filename="../stats.py" line="228"/>
<source>Tetrominos per minute: {:.1f}</source>
<translation>Tétrominos par minute : {:.1f}</translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Score: </source>
<translation>Score : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>High score: </source>
<translation>Meilleur score : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Level: </source>
<translation>Niveau : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Goal: </source>
<translation>Objectif : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Lines: </source>
<translation>Lignes : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Mini T-Spins: </source>
<translation>Mini T-Spins : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>T-Spins: </source>
<translation>T-Spins : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Back-to-back: </source>
<translation>Back-to-back : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Max combo: </source>
<translation>Combo max : </translation>
</message>
<message>
<location filename="../game_gui.py" line="679"/>
<location filename="../stats.py" line="189"/>
<source>Combos: </source>
<translation>Combos : </translation>
</message>
<message>
<location filename="../game_gui.py" line="718"/>
<location filename="../stats.py" line="228"/>
<source>Tetrominos locked down: </source>
<translation>Tétrominos bloqués : </translation>
</message>
<message>
<location filename="../game_gui.py" line="733"/>
<location filename="../stats.py" line="243"/>
<source>: </source>
<translation> : </translation>
</message>
@ -282,39 +282,39 @@ TERMINÉE</translation>
<context>
<name>Window</name>
<message>
<location filename="../game_gui.py" line="1472"/>
<location filename="../window.py" line="154"/>
<source>High score</source>
<translation>Meilleur score</translation>
</message>
<message>
<location filename="../game_gui.py" line="1411"/>
<location filename="../window.py" line="93"/>
<source>&amp;New game</source>
<translation>&amp;Nouvelle partie</translation>
</message>
<message>
<location filename="../game_gui.py" line="1415"/>
<location filename="../window.py" line="97"/>
<source>&amp;Settings</source>
<translation>&amp;Préférences</translation>
</message>
<message>
<location filename="../game_gui.py" line="1419"/>
<location filename="../window.py" line="101"/>
<source>&amp;About</source>
<translation>&amp;À propos</translation>
</message>
<message>
<location filename="../game_gui.py" line="1457"/>
<location filename="../window.py" line="139"/>
<source>A game is in progress.
Do you want to abord it?</source>
<translation>Une partie est en cours.
Voulez-vous l&apos;abandonner ?</translation>
</message>
<message>
<location filename="../game_gui.py" line="1457"/>
<location filename="../window.py" line="139"/>
<source>Quit game?</source>
<translation>Quitter la partie ?</translation>
</message>
<message>
<location filename="../game_gui.py" line="1430"/>
<location filename="../window.py" line="112"/>
<source>Tetris® clone by Adrien Malingrey
Tetris Game Design by Alekseï Pajitnov

View File

@ -1,2 +1,3 @@
for /F %%n in ('dir /B *.ts') do pylupdate5 ..\game_gui.py -ts %%n
for /F %%n in ('dir /B *.ts') do pylupdate5 -verbose ..\window.py ..\settings.py ..\stats.py ..\matrix.py ..\frames.py -ts %%n
echo You may need to edit *.ts files with a text editor to correct special characters
pause

View File

@ -1,2 +1,3 @@
for /F %%n in ('dir /B *.ts') do pylupdate5 ..\game_gui.py -ts -noobsolete %%n
for /F %%n in ('dir /B *.ts') do pylupdate5 -verbose ..\window.py ..\settings.py ..\stats.py ..\matrix.py ..\frames.py -ts -noobsolete %%n
echo You may need to edit *.ts files with a text editor to correct special characters
pause

393
matrix.py Normal file
View File

@ -0,0 +1,393 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import consts
from consts import L, R, CLOCKWISE, COUNTERCLOCKWISE
from qt5 import QtGui, QtCore, QtMultimedia
from grids import Grid
from point import Point
from block import Block
from tetromino import GhostPiece
from settings import s, settings
class Matrix(Grid):
"""
The rectangular arrangement of cells creating the active game area.
Tetriminos fall from the top-middle just above the Skyline (off-screen) to the bottom.
"""
ROWS = consts.MATRIX_ROWS + consts.GRID_INVISIBLE_ROWS
COLUMNS = consts.MATRIX_COLUMNS
STARTING_POSITION = Point(COLUMNS // 2, consts.GRID_INVISIBLE_ROWS - 1)
TEXT_COLOR = consts.MATRIX_TEXT_COLOR
drop_signal = QtCore.Signal(int)
lock_signal = QtCore.Signal(int, str)
def __init__(self, frames):
super().__init__(frames)
self.load_sfx()
self.game_over = False
self.text = ""
self.temporary_texts = []
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.auto_repeat_delay = 0
self.auto_repeat_timer = QtCore.QTimer()
self.auto_repeat_timer.timeout.connect(self.auto_repeat)
self.fall_timer = QtCore.QTimer()
self.fall_timer.timeout.connect(self.fall)
self.cells = []
def load_sfx(self):
self.wall_sfx = QtMultimedia.QSoundEffect(self)
url = QtCore.QUrl.fromLocalFile(consts.WALL_SFX_PATH)
self.wall_sfx.setSource(url)
self.rotate_sfx = QtMultimedia.QSoundEffect(self)
url = QtCore.QUrl.fromLocalFile(consts.ROTATE_SFX_PATH)
self.rotate_sfx.setSource(url)
self.hard_drop_sfx = QtMultimedia.QSoundEffect(self)
url = QtCore.QUrl.fromLocalFile(consts.HARD_DROP_SFX_PATH)
self.hard_drop_sfx.setSource(url)
def new_game(self):
self.game_over = False
self.lock_delay = consts.LOCK_DELAY
self.cells = [self.empty_row() for y in range(self.ROWS)]
self.setFocus()
self.actions_to_repeat = []
self.wall_hit = False
def new_level(self, level):
self.show_temporary_text(self.tr("Level\n") + str(level))
self.speed = consts.INITIAL_SPEED * (0.8 - ((level - 1) * 0.007)) ** (level - 1)
self.fall_timer.start(self.speed)
if level > 15:
self.lock_delay *= 0.9
def empty_row(self):
return [None for x in range(self.COLUMNS)]
def is_empty_cell(self, coord):
x, y = coord.x(), coord.y()
return (
0 <= x < self.COLUMNS
and y < self.ROWS
and not (0 <= y and self.cells[y][x])
)
def keyPressEvent(self, event):
if event.isAutoRepeat():
return
if not self.frames.playing:
return
try:
action = self.keys[event.key()]
except KeyError:
return
self.do(action)
if action in (s.MOVE_LEFT, s.MOVE_RIGHT, s.SOFT_DROP):
if action not in self.actions_to_repeat:
self.actions_to_repeat.append(action)
self.auto_repeat_wait()
def keyReleaseEvent(self, event):
if event.isAutoRepeat():
return
if not self.frames.playing:
return
try:
self.actions_to_repeat.remove(self.keys[event.key()])
except (KeyError, ValueError):
pass
else:
self.auto_repeat_wait()
if not self.actions_to_repeat:
for mino in self.piece.minoes:
mino.fade()
self.update()
def auto_repeat_wait(self):
self.auto_repeat_delay = (
time.time() + settings[s.DELAYS][s.AUTO_SHIFT_DELAY] / 1000
)
def auto_repeat(self):
"""
Tapping the move button allows a single cell movement of the Tetrimino
in the direction pressed.
Holding down the move button triggers an Auto-Repeat movement
that allows the player to move a Tetrimino from one side of the Matrix
to the other in about 0.5 seconds.
This is essential on higher levels when the Fall Speed of a Tetrimino is very fast.
There is a slight delay between the time the move button is pressed
and the time when Auto-Repeat kicks in : s.AUTO_SHIFT_DELAY.
This delay prevents unwanted extra movement of a Tetrimino.
Auto-Repeat only affects Left/Right movement.
Auto-Repeat continues to the Next Tetrimino (after Lock Down)
as long as the move button remains pressed.
In addition, when Auto-Repeat begins,
and the player then holds the opposite direction button,
the Tetrimino then begins moving the opposite direction with the initial delay.
When any single button is then released,
the Tetrimino should again move in the direction still held,
with the Auto-Shift delay applied once more.
"""
if (
not self.frames.playing
or self.frames.paused
or time.time() < self.auto_repeat_delay
):
return
if self.actions_to_repeat:
self.do(self.actions_to_repeat[-1])
def do(self, action):
"""The player can move, rotate, Soft Drop, Hard Drop,
and Hold the falling Tetrimino (i.e., the Tetrimino in play).
"""
if action == s.PAUSE:
self.frames.pause(not self.frames.paused)
if not self.frames.playing or self.frames.paused or not self.piece:
return
for mino in self.piece.minoes:
mino.shine(0)
if action == s.MOVE_LEFT:
if self.piece.move(L, 0):
self.lock_wait()
self.wall_hit = False
elif not self.wall_hit:
self.wall_hit = True
self.wall_sfx.play()
elif action == s.MOVE_RIGHT:
if self.piece.move(R, 0):
self.rotate_sfx.play()
self.lock_wait()
self.wall_hit = False
elif not self.wall_hit:
self.wall_hit = True
self.wall_sfx.play()
elif action == s.ROTATE_CLOCKWISE:
if self.piece.rotate(direction=CLOCKWISE):
self.rotate_sfx.play()
self.lock_wait()
self.wall_hit = False
elif not self.wall_hit:
self.wall_hit = True
self.wall_sfx.play()
elif action == s.ROTATE_COUNTERCLOCKWISE:
if self.piece.rotate(direction=COUNTERCLOCKWISE):
self.lock_wait()
self.wall_hit = False
elif not self.wall_hit:
self.wall_hit = True
self.wall_sfx.play()
elif action == s.SOFT_DROP:
if self.piece.soft_drop():
self.drop_signal.emit(1)
self.wall_hit = False
elif not self.wall_hit:
self.wall_hit = True
self.wall_sfx.play()
elif action == s.HARD_DROP:
trail = self.piece.hard_drop()
self.top_left_corner += Point(0, self.HARD_DROP_MOVEMENT * Block.side)
self.drop_signal.emit(2 * trail)
QtCore.QTimer.singleShot(consts.ANIMATION_DELAY, self.after_hard_drop)
self.hard_drop_sfx.play()
self.lock_phase()
elif action == s.HOLD:
self.frames.hold()
def after_hard_drop(self):
""" Reset the animation movement of the Matrix on a hard drop """
self.top_left_corner -= Point(0, self.HARD_DROP_MOVEMENT * Block.side)
def lock_wait(self):
self.fall_delay = time.time() + (self.speed + self.lock_delay) / 1000
def fall(self):
"""
Once a Tetrimino is generated,
it immediately drops one row (if no existing Block is in its path).
From here, it begins its descent to the bottom of the Matrix.
The Tetrimino will fall at its normal Fall Speed
whether or not it is being manipulated by the player.
"""
if self.piece:
if self.piece.move(0, 1):
self.lock_wait()
else:
if time.time() >= self.fall_delay:
self.lock_phase()
def lock_phase(self):
"""
The player can perform the same actions on a Tetrimino in this phase
as he/she can in the Falling Phase,
as long as the Tetrimino is not yet Locked Down.
A Tetrimino that is Hard Dropped Locks Down immediately.
However, if a Tetrimino naturally falls or Soft Drops onto a landing Surface,
it is given a delay (self.fall_delay) on a Lock Down Timer
before it actually Locks Down.
"""
self.wall_sfx.play()
# Enter minoes into the matrix
for mino in self.piece.minoes:
if mino.coord.y() >= 0:
self.cells[mino.coord.y()][mino.coord.x()] = mino
mino.shine(glowing=2, delay=consts.ANIMATION_DELAY)
self.update()
if all(mino.coord.y() < consts.GRID_INVISIBLE_ROWS for mino in self.piece.minoes):
self.frames.game_over()
return
"""
In this phase,
the engine looks for patterns made from Locked Down Blocks in the Matrix.
Once a pattern has been matched,
it can trigger any number of Tetris variant-related effects.
The classic pattern is the Line Clear pattern.
This pattern is matched when one or more rows of 10 horizontally aligned
Matrix cells are occupied by Blocks.
The matching Blocks are then marked for removal on a hit list.
Blocks on the hit list are cleared from the Matrix at a later time
in the Eliminate Phase.
"""
# Dectect complete lines
self.complete_lines = []
for y, row in enumerate(self.cells):
if all(cell for cell in row):
self.complete_lines.append(y)
for block in row:
block.shine()
self.spotlight = row[self.COLUMNS // 2].coord
self.auto_repeat_timer.stop()
self.lock_signal.emit(len(self.complete_lines), self.piece.t_spin)
if self.complete_lines:
self.fall_timer.stop()
QtCore.QTimer.singleShot(consts.LINE_CLEAR_DELAY, self.eliminate_phase)
else:
self.frames.new_piece()
def eliminate_phase(self):
"""
Any Minos marked for removal, i.e., on the hit list,
are cleared from the Matrix in this phase.
If this results in one or more complete 10-cell rows in the Matrix
becoming unoccupied by Minos,
then all Minos above that row(s) collapse,
or fall by the number of complete rows cleared from the Matrix.
"""
for y in self.complete_lines:
del self.cells[y]
self.cells.insert(0, self.empty_row())
for y, row in enumerate(self.cells):
for x, block in enumerate(row):
if block:
block.coord.setX(x)
block.coord.setY(y)
self.update()
self.auto_repeat_wait()
self.auto_repeat_timer.start(settings[s.DELAYS][s.AUTO_REPEAT_RATE])
self.frames.new_piece()
def paintEvent(self, event):
"""
Draws grid, actual piece, blocks in the Matrix and show texts
"""
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
self.paint_grid(painter)
if not self.frames.paused or self.game_over:
if self.piece:
if settings[s.OTHER][s.GHOST]:
self.ghost = GhostPiece(self.piece)
self.spotlight = self.ghost.minoes[0].coord
self.paint_piece(painter, self.ghost)
self.paint_piece(painter, self.piece)
# Blocks in matrix
for row in self.cells:
for block in row:
if block:
block.paint(painter, self.top_left_corner, self.spotlight)
if self.frames.playing and self.frames.paused:
painter.setFont(QtGui.QFont("Maassslicer", 0.75 * Block.side))
painter.setPen(self.TEXT_COLOR)
painter.drawText(
self.rect(),
QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
self.tr("PAUSE\n\nPress %s\nto resume") % settings[s.KEYBOARD][s.PAUSE],
)
if self.game_over:
painter.setFont(QtGui.QFont("Maassslicer", Block.side))
painter.setPen(self.TEXT_COLOR)
painter.drawText(
self.rect(),
QtCore.Qt.AlignCenter | QtCore.Qt.TextWordWrap,
self.tr("GAME\nOVER"),
)
if self.temporary_texts:
painter.setFont(self.temporary_text_font)
painter.setPen(self.TEXT_COLOR)
painter.drawText(
self.rect(),
QtCore.Qt.AlignHCenter | QtCore.Qt.TextWordWrap,
"\n\n\n" + "\n\n".join(self.temporary_texts),
)
def resizeEvent(self, event):
super().resizeEvent(event)
self.temporary_text_font = QtGui.QFont(consts.MATRIX_FONT_NAME, Block.side)
def show_temporary_text(self, text):
self.temporary_texts.append(text.upper())
self.font = self.temporary_text_font
self.update()
QtCore.QTimer.singleShot(consts.TEMPORARY_TEXT_DURATION, self.delete_text)
def delete_text(self):
del self.temporary_texts[0]
self.update()
def focusOutEvent(self, event):
if self.frames.playing:
self.frames.pause(True)

View File

@ -10,6 +10,17 @@ class Point(QtCore.QPoint):
Point of coordinates (x, y)
"""
def rotate(self, center, direction=CLOCKWISE):
""" Returns the Point image of the rotation of self
through 90° CLOKWISE or COUNTERCLOCKWISE around center"""
if self == center:
return self
p = self - center
p = Point(-direction * p.y(), direction * p.x())
p += center
return p
def __add__(self, o):
return Point(self.x() + o.x(), self.y() + o.y())
@ -27,17 +38,6 @@ class Point(QtCore.QPoint):
__rmul__ = __mul__
__rtruediv__ = __truediv__
def rotate(self, center, direction=CLOCKWISE):
""" Returns the Point image of the rotation of self
through 90° CLOKWISE or COUNTERCLOCKWISE around center"""
if self == center:
return self
p = self - center
p = Point(-direction * p.y(), direction * p.x())
p += center
return p
def __repr__(self):
return "Point({}, {})".format(self.x(), self.y())

277
settings.py Normal file
View File

@ -0,0 +1,277 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import collections
import consts
from __version__ import __author__, __title__
from qt5 import QtWidgets, QtCore
class SettingStrings(QtCore.QObject):
"""
Setting string for translation
"""
def __init__(self):
super().__init__()
self.KEYBOARD = self.tr("Keyboard settings")
self.MOVE_LEFT = self.tr("Move left")
self.MOVE_RIGHT = self.tr("Move right")
self.ROTATE_CLOCKWISE = self.tr("Rotate clockwise")
self.ROTATE_COUNTERCLOCKWISE = self.tr("Rotate counterclockwise")
self.SOFT_DROP = self.tr("Soft drop")
self.HARD_DROP = self.tr("Hard drop")
self.HOLD = self.tr("Hold")
self.PAUSE = self.tr("Pause")
self.OTHER = self.tr("Other settings")
self.DELAYS = self.tr("Delays")
self.AUTO_SHIFT_DELAY = self.tr("Auto-shift delay")
self.AUTO_REPEAT_RATE = self.tr("Auto-repeat rate")
self.SOUND = self.tr("Sound")
self.MUSIC_VOLUME = self.tr("Music volume")
self.SFX_VOLUME = self.tr("Effects volume")
self.GHOST = self.tr("Show ghost piece")
self.SHOW_NEXT_QUEUE = self.tr("Show next queue")
self.HOLD_ENABLED = self.tr("Hold enabled")
class KeyButton(QtWidgets.QPushButton):
""" Button widget capturing key name on focus """
names = {
value: name.replace("Key_", "")
for name, value in QtCore.Qt.__dict__.items()
if "Key_" in name
}
def __init__(self, *args):
super().__init__(*args)
def keyPressEvent(self, event):
key = event.key()
self.setText(self.names[key])
class SettingsGroup(QtWidgets.QGroupBox):
""" Group box of a type of settings """
def __init__(self, group, parent, cls):
super().__init__(group, parent)
layout = QtWidgets.QFormLayout(self)
self.widgets = {}
for setting, value in settings[group].items():
if cls == KeyButton:
widget = KeyButton(value)
elif cls == QtWidgets.QCheckBox:
widget = QtWidgets.QCheckBox(setting)
widget.setChecked(value)
elif cls == QtWidgets.QSpinBox:
widget = QtWidgets.QSpinBox()
widget.setRange(0, 1000)
widget.setValue(value)
widget.setSuffix(" ms")
elif cls == QtWidgets.QSlider:
widget = QtWidgets.QSlider(QtCore.Qt.Horizontal)
widget.setValue(value)
if cls == QtWidgets.QCheckBox:
layout.addRow(widget)
else:
layout.addRow(setting, widget)
self.widgets[setting] = widget
self.setLayout(layout)
class SettingsDialog(QtWidgets.QDialog):
""" Show settings dialog """
def __init__(self, parent):
super().__init__(parent)
self.setWindowTitle(self.tr("Settings"))
self.setModal(True)
layout = QtWidgets.QGridLayout()
self.groups = {}
self.groups[s.KEYBOARD] = SettingsGroup(s.KEYBOARD, self, KeyButton)
self.groups[s.DELAYS] = SettingsGroup(s.DELAYS, self, QtWidgets.QSpinBox)
self.groups[s.SOUND] = SettingsGroup(s.SOUND, self, QtWidgets.QSlider)
self.groups[s.OTHER] = SettingsGroup(s.OTHER, self, QtWidgets.QCheckBox)
layout.addWidget(self.groups[s.KEYBOARD], 0, 0, 3, 1)
layout.addWidget(self.groups[s.DELAYS], 0, 1)
layout.addWidget(self.groups[s.SOUND], 1, 1)
layout.addWidget(self.groups[s.OTHER], 2, 1)
buttons = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
buttons.accepted.connect(self.ok)
buttons.rejected.connect(self.close)
layout.addWidget(buttons, 3, 0, 1, 2)
self.setLayout(layout)
self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].valueChanged.connect(
parent.frames.music.setVolume
)
self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderPressed.connect(
parent.frames.music.play)
self.groups[s.SOUND].widgets[s.MUSIC_VOLUME].sliderReleased.connect(
parent.frames.music.pause)
self.groups[s.SOUND].widgets[s.SFX_VOLUME].sliderReleased.connect(
parent.frames.stats.line_clear_sfx.play)
self.show()
def ok(self):
""" Save settings """
for group, elements in self.groups.items():
for setting, widget in elements.widgets.items():
if isinstance(widget, KeyButton):
value = widget.text()
elif isinstance(widget, QtWidgets.QCheckBox):
value = widget.isChecked()
elif isinstance(widget, QtWidgets.QSpinBox):
value = widget.value()
elif isinstance(widget, QtWidgets.QSlider):
value = widget.value()
settings[group][setting] = value
qsettings.setValue(group + "/" + setting, value)
self.close()
s = SettingStrings()
qsettings = QtCore.QSettings(__author__, __title__)
settings = collections.OrderedDict(
[
(
s.KEYBOARD,
collections.OrderedDict(
[
(
s.MOVE_LEFT,
qsettings.value(s.KEYBOARD + "/" + s.MOVE_LEFT, consts.DEFAULT_MOVE_LEFT_KEY),
),
(
s.MOVE_RIGHT,
qsettings.value(
s.KEYBOARD + "/" + s.MOVE_RIGHT, consts.DEFAULT_MOVE_RIGHT_KEY
),
),
(
s.ROTATE_CLOCKWISE,
qsettings.value(
s.KEYBOARD + "/" + s.ROTATE_CLOCKWISE, consts.DEFAULT_ROTATE_CLOCKWISE_KEY
),
),
(
s.ROTATE_COUNTERCLOCKWISE,
qsettings.value(
s.KEYBOARD + "/" + s.ROTATE_COUNTERCLOCKWISE,
consts.DEFAULT_ROTATE_COUNTERCLOCKWISE_KEY,
),
),
(
s.SOFT_DROP,
qsettings.value(s.KEYBOARD + "/" + s.SOFT_DROP, consts.DEFAULT_SOFT_DROP_KEY),
),
(
s.HARD_DROP,
qsettings.value(
s.KEYBOARD + "/" + s.HARD_DROP, consts.DEFAULT_HARD_DROP_KEY
),
),
(
s.HOLD,
qsettings.value(s.KEYBOARD + "/" + s.HOLD, consts.DEFAULT_HOLD_KEY),
),
(
s.PAUSE,
qsettings.value(s.KEYBOARD + "/" + s.PAUSE, consts.DEFAULT_PAUSE_KEY),
),
]
),
),
(
s.DELAYS,
collections.OrderedDict(
[
(
s.AUTO_SHIFT_DELAY,
int(
qsettings.value(
s.DELAYS + "/" + s.AUTO_SHIFT_DELAY, consts.DEFAULT_AUTO_SHIFT_DELAY
)
),
),
(
s.AUTO_REPEAT_RATE,
int(
qsettings.value(
s.DELAYS + "/" + s.AUTO_REPEAT_RATE, consts.DEFAULT_AUTO_REPEAT_RATE
)
),
),
]
),
),
(
s.SOUND,
collections.OrderedDict(
[
(
s.MUSIC_VOLUME,
int(
qsettings.value(s.SOUND + "/" + s.MUSIC_VOLUME, consts.DEFAUT_MUSIC_VOLUME)
),
),
(
s.SFX_VOLUME,
int(
qsettings.value(
s.SOUND + "/" + s.SFX_VOLUME, consts.DEFAULT_SFX_VOLUME
)
),
),
]
),
),
(
s.OTHER,
collections.OrderedDict(
[
(
s.GHOST,
bool(qsettings.value(s.OTHER + "/" + s.GHOST, consts.DEFAULT_SHOW_GHOST)),
),
(
s.SHOW_NEXT_QUEUE,
bool(
qsettings.value(
s.OTHER + "/" + s.SHOW_NEXT_QUEUE, consts.DEFAULT_SHOW_NEXT_QUEUE
)
),
),
(
s.HOLD_ENABLED,
bool(
qsettings.value(
s.OTHER + "/" + s.HOLD_ENABLED, consts.DEFAULT_HOLD_ENABLED
)
),
),
]
),
),
]
)

252
stats.py Normal file
View File

@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import locale
import time
import consts
from qt5 import QtWidgets, QtGui, QtCore, QtMultimedia
from settings import qsettings
from block import Block
class Stats(QtWidgets.QWidget):
"""
Show informations relevant to the game being played is displayed on-screen.
Looks for patterns made from Locked Down Blocks in the Matrix and calculate score.
"""
ROWS = consts.STATS_ROWS
COLUMNS = consts.STATS_COLUMNS
TEXT_COLOR = consts.STATS_TEXT_COLOR
temporary_text = QtCore.Signal(str)
def __init__(self, frames):
super().__init__(frames)
self.frames = frames
self.setStyleSheet("background-color: transparent")
self.load_sfx()
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.text_options = QtGui.QTextOption(QtCore.Qt.AlignRight)
self.text_options.setWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere)
self.clock = QtCore.QTimer()
self.clock.timeout.connect(self.tick)
self.high_score = int(qsettings.value(self.tr("High score"), 0))
def load_sfx(self):
self.line_clear_sfx = QtMultimedia.QSoundEffect(self)
url = QtCore.QUrl.fromLocalFile(consts.LINE_CLEAR_SFX_PATH)
self.line_clear_sfx.setSource(url)
self.tetris_sfx = QtMultimedia.QSoundEffect(self)
url = QtCore.QUrl.fromLocalFile(consts.TETRIS_SFX_PATH)
self.tetris_sfx.setSource(url)
def new_game(self):
self.level -= 1
self.goal = 0
self.complete_lines_total = 0
self.score_total = 1
self.t_spin_total = 0
self.mini_t_spin_total = 0
self.nb_back_to_back = 0
self.back_to_back_scores = None
self.combo = -1
self.combos_total = 0
self.max_combo = 0
self.chronometer = 0
self.nb_tetro = 0
self.clock.start(1000)
self.lines_stats = [0, 0, 0, 0, 0]
def new_level(self):
self.level += 1
self.goal += 5 * self.level
return self.level
def update_score(self, nb_complete_lines, t_spin):
"""
The player scores points by performing Single, Double, Triple,
and Tetris Line Clears, as well as T-Spins and Mini T-Spins.
Soft and Hard Drops also award points.
There is a special bonus for Back-to-Backs,
which is when two actions such as a Tetris and T-Spin Double take place
without a Single, Double, or Triple Line Clear occurring between them.
Scoring for Line Clears, T-Spins, and Mini T-Spins are level dependent,
while Hard and Soft Drop point values remain constant.
"""
self.nb_tetro += 1
if nb_complete_lines:
self.complete_lines_total += nb_complete_lines
self.lines_stats[nb_complete_lines] += 1
if t_spin == "T-Spin":
self.t_spin_total += 1
elif t_spin == "Mini T-Spin":
self.mini_t_spin_total += 1
score = consts.SCORES[nb_complete_lines][t_spin]
if score:
text = " ".join((t_spin, consts.SCORES[nb_complete_lines]["name"]))
if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
self.tetris_sfx.play()
elif t_spin or nb_complete_lines:
self.line_clear_sfx.play()
self.goal -= score
score = 100 * self.level * score
self.score_total += score
self.temporary_text.emit(text + "\n{:n}".format(score))
# ==============================================================================
# Combo
# Bonus for complete lines on each consecutive lock downs
# if nb_complete_lines:
# ==============================================================================
if nb_complete_lines:
self.combo += 1
if self.combo > 0:
if nb_complete_lines == 1:
combo_score = 20 * self.combo * self.level
else:
combo_score = 50 * self.combo * self.level
self.score_total += combo_score
self.max_combo = max(self.max_combo, self.combo)
self.combos_total += 1
self.temporary_text.emit(
self.tr("COMBO x{:n}\n{:n}").format(self.combo, combo_score)
)
else:
self.combo = -1
# ==============================================================================
# Back-to_back sequence
# Two major bonus actions, such as two Tetrises, performed without
# a Single, Double, or Triple Line Clear occurring between them.
# Bonus for Tetrises, T-Spin Line Clears, and Mini T-Spin Line Clears
# performed consecutively in a B2B sequence.
# ==============================================================================
if (t_spin and nb_complete_lines) or nb_complete_lines == 4:
if self.back_to_back_scores is not None:
self.back_to_back_scores.append(score // 2)
else:
# The first Line Clear in the Back-to-Back sequence
# does not receive the Back-to-Back Bonus.
self.back_to_back_scores = []
elif nb_complete_lines and not t_spin:
# A Back-to-Back sequence is only broken by a Single, Double, or Triple Line Clear.
# Locking down a Tetrimino without clearing a line
# or holding a Tetrimino does not break the Back-to-Back sequence.
# T-Spins and Mini T-Spins that do not clear any lines
# do not receive the Back-to-Back Bonus; instead they are scored as normal.
# They also cannot start a Back-to-Back sequence, however,
# they do not break an existing Back-to-Back sequence.
if self.back_to_back_scores:
b2b_score = sum(self.back_to_back_scores)
self.score_total += b2b_score
self.nb_back_to_back += 1
self.temporary_text.emit(
self.tr("BACK TO BACK\n{:n}").format(b2b_score)
)
self.back_to_back_scores = None
self.high_score = max(self.score_total, self.high_score)
self.update()
def update_drop_score(self, n):
""" Tetrimino is Soft Dropped for n lines or Hard Dropped for (n/2) lines"""
self.score_total += n
self.high_score = max(self.score_total, self.high_score)
self.update()
def tick(self):
self.chronometer += 1
self.update()
def paintEvent(self, event):
if not self.frames.playing and not self.frames.matrix.game_over:
return
painter = QtGui.QPainter(self)
painter.setFont(self.font)
painter.setPen(self.TEXT_COLOR)
painter.drawText(
QtCore.QRectF(self.rect()), self.text(sep="\n\n"), self.text_options
)
def text(self, full_stats=False, sep="\n"):
text = (
self.tr("Score: ")
+ locale.format("%i", self.score_total, grouping=True, monetary=True)
+ sep
+ self.tr("High score: ")
+ locale.format("%i", self.high_score, grouping=True, monetary=True)
+ sep
+ self.tr("Time: {}\n").format(
time.strftime("%H:%M:%S", time.gmtime(self.chronometer))
)
+ sep
+ self.tr("Level: ")
+ locale.format("%i", self.level, grouping=True, monetary=True)
+ sep
+ self.tr("Goal: ")
+ locale.format("%i", self.goal, grouping=True, monetary=True)
+ sep
+ self.tr("Lines: ")
+ locale.format(
"%i", self.complete_lines_total, grouping=True, monetary=True
)
+ sep
+ self.tr("Mini T-Spins: ")
+ locale.format("%i", self.mini_t_spin_total, grouping=True, monetary=True)
+ sep
+ self.tr("T-Spins: ")
+ locale.format("%i", self.t_spin_total, grouping=True, monetary=True)
+ sep
+ self.tr("Back-to-back: ")
+ locale.format("%i", self.nb_back_to_back, grouping=True, monetary=True)
+ sep
+ self.tr("Max combo: ")
+ locale.format("%i", self.max_combo, grouping=True, monetary=True)
+ sep
+ self.tr("Combos: ")
+ locale.format("%i", self.combos_total, grouping=True, monetary=True)
)
if full_stats:
minutes = self.chronometer / 60
text += (
"\n"
+ sep
+ self.tr("Lines per minute: {:.1f}").format(
self.complete_lines_total / minutes
)
+ sep
+ self.tr("Tetrominos locked down: ")
+ locale.format("%i", self.nb_tetro, grouping=True, monetary=True)
+ sep
+ self.tr("Tetrominos per minute: {:.1f}").format(
self.nb_tetro / minutes
)
+ sep
)
text += sep.join(
score_type["name"]
+ self.tr(": ")
+ locale.format("%i", nb, grouping=True, monetary=True)
for score_type, nb in tuple(zip(consts.SCORES, self.lines_stats))[1:]
)
return text
def resizeEvent(self, event):
self.font = QtGui.QFont(consts.STATS_FONT_NAME, Block.side / 3.5)

View File

@ -4,172 +4,9 @@
import random
import consts
from consts import L, R, U, D, CLOCKWISE, COUNTERCLOCKWISE
from qt5 import QtCore, QtGui
class Point(QtCore.QPoint):
"""
Point of coordinates (x, y)
"""
def __init__(self, x, y):
super().__init__(x, y)
def __add__(self, o):
return Point(self.x() + o.x(), self.y() + o.y())
def __sub__(self, o):
return Point(self.x() - o.x(), self.y() - o.y())
def __mul__(self, k):
return Point(k * self.x(), k * self.y())
def __truediv__(self, k):
return Point(self.x() / k, self.y() / k)
__radd__ = __add__
__rsub__ = __sub__
__rmul__ = __mul__
__rtruediv__ = __truediv__
def rotate(self, center, direction=CLOCKWISE):
""" Returns the Point image of the rotation of self
through 90° CLOKWISE or COUNTERCLOCKWISE around center"""
if self == center:
return self
p = self - center
p = Point(-direction * p.y(), direction * p.x())
p += center
return p
def __repr__(self):
return "Point({}, {})".format(self.x(), self.y())
__str__ = __repr__
class Block:
"""
Mino or block
Mino : A single square-shaped building block of a shape called a Tetrimino.
Four Minos arranged into any of their various connected patterns is known as a Tetrimino
Block : A single block locked in a cell in the Grid
"""
# Colors
BORDER_COLOR = consts.BLOCK_BORDER_COLOR
FILL_COLOR = consts.BLOCK_FILL_COLOR
GLOWING_BORDER_COLOR = consts.BLOCK_GLOWING_BORDER_COLOR
GLOWING_FILL_COLOR = consts.BLOCK_GLOWING_FILL_COLOR
LIGHT_COLOR = consts.BLOCK_LIGHT_COLOR
TRANSPARENT = consts.BLOCK_TRANSPARENT
GLOWING = consts.BLOCK_GLOWING
side = consts.BLOCK_INITIAL_SIDE
def __init__(self, coord, trail=0):
self.coord = coord
self.trail = trail
self.border_color = self.BORDER_COLOR
self.fill_color = self.FILL_COLOR
self.glowing = self.GLOWING
def paint(self, painter, top_left_corner, spotlight):
p = top_left_corner + self.coord * Block.side
block_center = Point(Block.side/2, Block.side/2)
self.center = p + block_center
spotlight = top_left_corner + Block.side * spotlight + block_center
self.glint = 0.15 * spotlight + 0.85 * self.center
if self.trail:
start = (
top_left_corner + (self.coord + Point(0, self.trail * U)) * Block.side
)
stop = top_left_corner + (self.coord + Point(0, 2 * D)) * Block.side
fill = QtGui.QLinearGradient(start, stop)
fill.setColorAt(0, self.LIGHT_COLOR)
fill.setColorAt(1, self.GLOWING_FILL_COLOR)
painter.setBrush(fill)
painter.setPen(QtCore.Qt.NoPen)
painter.drawRoundedRect(
start.x(),
start.y(),
Block.side,
Block.side * (1 + self.trail),
20,
20,
QtCore.Qt.RelativeSize,
)
if self.glowing:
fill = QtGui.QRadialGradient(self.center, self.glowing * Block.side)
fill.setColorAt(0, self.TRANSPARENT)
fill.setColorAt(0.5 / self.glowing, self.LIGHT_COLOR)
fill.setColorAt(1, self.TRANSPARENT)
painter.setBrush(QtGui.QBrush(fill))
painter.setPen(QtCore.Qt.NoPen)
painter.drawEllipse(
self.center.x() - self.glowing * Block.side,
self.center.y() - self.glowing * Block.side,
2 * self.glowing * Block.side,
2 * self.glowing * Block.side,
)
painter.setBrush(self.brush())
painter.setPen(self.pen())
painter.drawRoundedRect(
p.x() + 1,
p.y() + 1,
Block.side - 2,
Block.side - 2,
20,
20,
QtCore.Qt.RelativeSize,
)
def brush(self):
if self.fill_color is None:
return QtCore.Qt.NoBrush
fill = QtGui.QRadialGradient(self.glint, 1.5 * Block.side)
fill.setColorAt(0, self.fill_color.lighter())
fill.setColorAt(1, self.fill_color)
return QtGui.QBrush(fill)
def pen(self):
if self.border_color is None:
return QtCore.Qt.NoPen
border = QtGui.QRadialGradient(self.glint, Block.side)
border.setColorAt(0, self.border_color.lighter())
border.setColorAt(1, self.border_color.darker())
return QtGui.QPen(QtGui.QBrush(border), 1, join=QtCore.Qt.RoundJoin)
def shine(self, glowing=2, delay=None):
self.border_color = Block.GLOWING_BORDER_COLOR
self.fill_color = Block.GLOWING_FILL_COLOR
self.glowing = glowing
if delay:
QtCore.QTimer.singleShot(delay, self.fade)
def fade(self):
self.border_color = Block.BORDER_COLOR
self.fill_color = Block.FILL_COLOR
self.glowing = 0
self.trail = 0
class GhostBlock(Block):
"""
Mino of the ghost piece
"""
BORDER_COLOR = consts.GHOST_BLOCK_BORDER_COLOR
FILL_COLOR = consts.GHOST_BLOCK_FILL_COLOR
GLOWING_FILL_COLOR = consts.GHOST_BLOCK_GLOWING_FILL_COLOR
GLOWING = consts.GHOST_BLOCK_GLOWING
from point import Point
from block import Block, GhostBlock
class MetaTetro(type):
@ -443,7 +280,7 @@ class TetroO(Tetromino, metaclass=MetaTetro):
return False
class Ghost(Tetromino):
class GhostPiece(Tetromino):
"""
A graphical representation of where the Tetrimino in play will come to rest
if it is dropped from its current position.

156
window.py Normal file
View File

@ -0,0 +1,156 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import locale
import ctypes
import consts
from qt5 import QtWidgets, QtCore, QtGui
from __version__ import __title__, __author__, __version__
from settings import SettingsDialog, qsettings
from frames import Frames
class Window(QtWidgets.QMainWindow):
""" Main window """
def __init__(self):
splash_screen = QtWidgets.QSplashScreen(
QtGui.QPixmap(consts.SPLASH_SCREEN_PATH)
)
splash_screen.show()
self.set_locale()
super().__init__()
self.setWindowTitle(__title__.upper())
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.setWindowIcon(QtGui.QIcon(consts.ICON_PATH))
# Windows' taskbar icon
try:
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
".".join((__author__, __title__, __version__))
)
except AttributeError:
pass
# Stylesheet
try:
import qdarkstyle
except ImportError:
pass
else:
self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment())
for font_path in consts.STATS_FONT_PATH, consts.MATRIX_FONT_PATH:
QtGui.QFontDatabase.addApplicationFont(font_path)
self.frames = Frames(self)
self.setCentralWidget(self.frames)
self.hold_queue = self.frames.hold_queue
self.matrix = self.frames.matrix
self.stats = self.frames.stats
self.menu = self.menuBar()
geometry = qsettings.value("WindowGeometry")
if geometry:
self.restoreGeometry(geometry)
else:
self.resize(*consts.DEFAULT_WINDOW_SIZE)
self.setWindowState(
QtCore.Qt.WindowStates(
int(qsettings.value("WindowState", QtCore.Qt.WindowActive))
)
)
splash_screen.finish(self);
def set_locale(self):
app = QtWidgets.QApplication.instance()
# Set appropriate thounsand separator characters
locale.setlocale(locale.LC_ALL, "")
# Qt
language = QtCore.QLocale.system().name()[:2]
qt_translator = QtCore.QTranslator(app)
qt_translation_path = QtCore.QLibraryInfo.location(
QtCore.QLibraryInfo.TranslationsPath
)
if qt_translator.load("qt_" + language, qt_translation_path):
app.installTranslator(qt_translator)
tetris2000_translator = QtCore.QTranslator(app)
if tetris2000_translator.load(language, consts.LOCALE_PATH):
app.installTranslator(tetris2000_translator)
def menuBar(self):
menu = super().menuBar()
new_game_action = QtWidgets.QAction(self.tr("&New game"), self)
new_game_action.triggered.connect(self.frames.new_game)
menu.addAction(new_game_action)
settings_action = QtWidgets.QAction(self.tr("&Settings"), self)
settings_action.triggered.connect(self.show_settings_dialog)
menu.addAction(settings_action)
about_action = QtWidgets.QAction(self.tr("&About"), self)
about_action.triggered.connect(self.about)
menu.addAction(about_action)
return menu
def show_settings_dialog(self):
SettingsDialog(self).exec_()
self.frames.apply_settings()
def about(self):
QtWidgets.QMessageBox.about(
self,
__title__,
self.tr(
"""Tetris® clone by Adrien Malingrey
Tetris Game Design by Alekseï Pajitnov
Graphism inspired by Tetris Effect
Window style sheet: qdarkstyle by Colin Duquesnoy
Fonts by Markus Koellmann, Peter Wiegel
Images from:
OpenGameArt.org by beren77, Duion
Pexels.com by Min An, Jaymantri, Felix Mittermeier
Pixabay.com by LoganArt
Pixnio.com by Adrian Pelletier
Unsplash.com by Aron, Patrick Fore, Ilnur Kalimullin, Gabriel Garcia Marengo, Adnanta Raharja
StockSnap.io by Nathan Anderson, José Ignacio Pompé
Musics from ocremix.org by:
CheDDer Nardz, djpretzel, MkVaff, Sir_NutS, R3FORGED, Sir_NutS
Sound effects made with voc-one by Simple-Media"""
),
)
if self.frames.playing:
self.frames.pause(False)
def closeEvent(self, event):
if self.frames.playing:
answer = QtWidgets.QMessageBox.question(
self,
self.tr("Quit game?"),
self.tr("A game is in progress.\nDo you want to abord it?"),
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
QtWidgets.QMessageBox.Cancel,
)
if answer == QtWidgets.QMessageBox.Cancel:
event.ignore()
self.frames.pause(False)
return
self.frames.music.stop()
# Save settings
qsettings.setValue(self.tr("High score"), self.stats.high_score)
qsettings.setValue("WindowGeometry", self.saveGeometry())
qsettings.setValue("WindowState", int(self.windowState()))