333 lines
12 KiB
Python
333 lines
12 KiB
Python
#!/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_() |