TETRIS2000/frames.py
2018-08-06 12:23:47 +02:00

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_()