1#!/usr/bin/env python 2 3 4############################################################################# 5## 6## Copyright (C) 2014 Riverbank Computing Limited. 7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). 8## All rights reserved. 9## 10## This file is part of the examples of PyQt. 11## 12## $QT_BEGIN_LICENSE:BSD$ 13## You may use this file under the terms of the BSD license as follows: 14## 15## "Redistribution and use in source and binary forms, with or without 16## modification, are permitted provided that the following conditions are 17## met: 18## * Redistributions of source code must retain the above copyright 19## notice, this list of conditions and the following disclaimer. 20## * Redistributions in binary form must reproduce the above copyright 21## notice, this list of conditions and the following disclaimer in 22## the documentation and/or other materials provided with the 23## distribution. 24## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor 25## the names of its contributors may be used to endorse or promote 26## products derived from this software without specific prior written 27## permission. 28## 29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 40## $QT_END_LICENSE$ 41## 42############################################################################# 43 44 45import copy 46import random 47 48from PyQt5.QtCore import pyqtSignal, QBasicTimer, QSize, Qt 49from PyQt5.QtGui import QColor, QPainter, QPixmap 50from PyQt5.QtWidgets import (QApplication, QFrame, QGridLayout, QLabel, 51 QLCDNumber, QPushButton, QWidget) 52 53 54NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape = range(8) 55 56 57class TetrixWindow(QWidget): 58 def __init__(self): 59 super(TetrixWindow, self).__init__() 60 61 self.board = TetrixBoard() 62 63 nextPieceLabel = QLabel() 64 nextPieceLabel.setFrameStyle(QFrame.Box | QFrame.Raised) 65 nextPieceLabel.setAlignment(Qt.AlignCenter) 66 self.board.setNextPieceLabel(nextPieceLabel) 67 68 scoreLcd = QLCDNumber(5) 69 scoreLcd.setSegmentStyle(QLCDNumber.Filled) 70 levelLcd = QLCDNumber(2) 71 levelLcd.setSegmentStyle(QLCDNumber.Filled) 72 linesLcd = QLCDNumber(5) 73 linesLcd.setSegmentStyle(QLCDNumber.Filled) 74 75 startButton = QPushButton("&Start") 76 startButton.setFocusPolicy(Qt.NoFocus) 77 quitButton = QPushButton("&Quit") 78 quitButton.setFocusPolicy(Qt.NoFocus) 79 pauseButton = QPushButton("&Pause") 80 pauseButton.setFocusPolicy(Qt.NoFocus) 81 82 startButton.clicked.connect(self.board.start) 83 pauseButton.clicked.connect(self.board.pause) 84 quitButton.clicked.connect(QApplication.instance().quit) 85 self.board.scoreChanged.connect(scoreLcd.display) 86 self.board.levelChanged.connect(levelLcd.display) 87 self.board.linesRemovedChanged.connect(linesLcd.display) 88 89 layout = QGridLayout() 90 layout.addWidget(self.createLabel("NEXT"), 0, 0) 91 layout.addWidget(nextPieceLabel, 1, 0) 92 layout.addWidget(self.createLabel("LEVEL"), 2, 0) 93 layout.addWidget(levelLcd, 3, 0) 94 layout.addWidget(startButton, 4, 0) 95 layout.addWidget(self.board, 0, 1, 6, 1) 96 layout.addWidget(self.createLabel("SCORE"), 0, 2) 97 layout.addWidget(scoreLcd, 1, 2) 98 layout.addWidget(self.createLabel("LINES REMOVED"), 2, 2) 99 layout.addWidget(linesLcd, 3, 2) 100 layout.addWidget(quitButton, 4, 2) 101 layout.addWidget(pauseButton, 5, 2) 102 self.setLayout(layout) 103 104 self.setWindowTitle("Tetrix") 105 self.resize(550, 370) 106 107 def createLabel(self, text): 108 lbl = QLabel(text) 109 lbl.setAlignment(Qt.AlignHCenter | Qt.AlignBottom) 110 return lbl 111 112 113class TetrixBoard(QFrame): 114 BoardWidth = 10 115 BoardHeight = 22 116 117 scoreChanged = pyqtSignal(int) 118 119 levelChanged = pyqtSignal(int) 120 121 linesRemovedChanged = pyqtSignal(int) 122 123 def __init__(self, parent=None): 124 super(TetrixBoard, self).__init__(parent) 125 126 self.timer = QBasicTimer() 127 self.nextPieceLabel = None 128 self.isWaitingAfterLine = False 129 self.curPiece = TetrixPiece() 130 self.nextPiece = TetrixPiece() 131 self.curX = 0 132 self.curY = 0 133 self.numLinesRemoved = 0 134 self.numPiecesDropped = 0 135 self.score = 0 136 self.level = 0 137 self.board = None 138 139 self.setFrameStyle(QFrame.Panel | QFrame.Sunken) 140 self.setFocusPolicy(Qt.StrongFocus) 141 self.isStarted = False 142 self.isPaused = False 143 self.clearBoard() 144 145 self.nextPiece.setRandomShape() 146 147 def shapeAt(self, x, y): 148 return self.board[(y * TetrixBoard.BoardWidth) + x] 149 150 def setShapeAt(self, x, y, shape): 151 self.board[(y * TetrixBoard.BoardWidth) + x] = shape 152 153 def timeoutTime(self): 154 return 1000 / (1 + self.level) 155 156 def squareWidth(self): 157 return self.contentsRect().width() / TetrixBoard.BoardWidth 158 159 def squareHeight(self): 160 return self.contentsRect().height() / TetrixBoard.BoardHeight 161 162 def setNextPieceLabel(self, label): 163 self.nextPieceLabel = label 164 165 def sizeHint(self): 166 return QSize(TetrixBoard.BoardWidth * 15 + self.frameWidth() * 2, 167 TetrixBoard.BoardHeight * 15 + self.frameWidth() * 2) 168 169 def minimumSizeHint(self): 170 return QSize(TetrixBoard.BoardWidth * 5 + self.frameWidth() * 2, 171 TetrixBoard.BoardHeight * 5 + self.frameWidth() * 2) 172 173 def start(self): 174 if self.isPaused: 175 return 176 177 self.isStarted = True 178 self.isWaitingAfterLine = False 179 self.numLinesRemoved = 0 180 self.numPiecesDropped = 0 181 self.score = 0 182 self.level = 1 183 self.clearBoard() 184 185 self.linesRemovedChanged.emit(self.numLinesRemoved) 186 self.scoreChanged.emit(self.score) 187 self.levelChanged.emit(self.level) 188 189 self.newPiece() 190 self.timer.start(self.timeoutTime(), self) 191 192 def pause(self): 193 if not self.isStarted: 194 return 195 196 self.isPaused = not self.isPaused 197 if self.isPaused: 198 self.timer.stop() 199 else: 200 self.timer.start(self.timeoutTime(), self) 201 202 self.update() 203 204 def paintEvent(self, event): 205 super(TetrixBoard, self).paintEvent(event) 206 207 painter = QPainter(self) 208 rect = self.contentsRect() 209 210 if self.isPaused: 211 painter.drawText(rect, Qt.AlignCenter, "Pause") 212 return 213 214 boardTop = rect.bottom() - TetrixBoard.BoardHeight * self.squareHeight() 215 216 for i in range(TetrixBoard.BoardHeight): 217 for j in range(TetrixBoard.BoardWidth): 218 shape = self.shapeAt(j, TetrixBoard.BoardHeight - i - 1) 219 if shape != NoShape: 220 self.drawSquare(painter, 221 rect.left() + j * self.squareWidth(), 222 boardTop + i * self.squareHeight(), shape) 223 224 if self.curPiece.shape() != NoShape: 225 for i in range(4): 226 x = self.curX + self.curPiece.x(i) 227 y = self.curY - self.curPiece.y(i) 228 self.drawSquare(painter, rect.left() + x * self.squareWidth(), 229 boardTop + (TetrixBoard.BoardHeight - y - 1) * self.squareHeight(), 230 self.curPiece.shape()) 231 232 def keyPressEvent(self, event): 233 if not self.isStarted or self.isPaused or self.curPiece.shape() == NoShape: 234 super(TetrixBoard, self).keyPressEvent(event) 235 return 236 237 key = event.key() 238 if key == Qt.Key_Left: 239 self.tryMove(self.curPiece, self.curX - 1, self.curY) 240 elif key == Qt.Key_Right: 241 self.tryMove(self.curPiece, self.curX + 1, self.curY) 242 elif key == Qt.Key_Down: 243 self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY) 244 elif key == Qt.Key_Up: 245 self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY) 246 elif key == Qt.Key_Space: 247 self.dropDown() 248 elif key == Qt.Key_D: 249 self.oneLineDown() 250 else: 251 super(TetrixBoard, self).keyPressEvent(event) 252 253 def timerEvent(self, event): 254 if event.timerId() == self.timer.timerId(): 255 if self.isWaitingAfterLine: 256 self.isWaitingAfterLine = False 257 self.newPiece() 258 self.timer.start(self.timeoutTime(), self) 259 else: 260 self.oneLineDown() 261 else: 262 super(TetrixBoard, self).timerEvent(event) 263 264 def clearBoard(self): 265 self.board = [NoShape for i in range(TetrixBoard.BoardHeight * TetrixBoard.BoardWidth)] 266 267 def dropDown(self): 268 dropHeight = 0 269 newY = self.curY 270 while newY > 0: 271 if not self.tryMove(self.curPiece, self.curX, newY - 1): 272 break 273 newY -= 1 274 dropHeight += 1 275 276 self.pieceDropped(dropHeight) 277 278 def oneLineDown(self): 279 if not self.tryMove(self.curPiece, self.curX, self.curY - 1): 280 self.pieceDropped(0) 281 282 def pieceDropped(self, dropHeight): 283 for i in range(4): 284 x = self.curX + self.curPiece.x(i) 285 y = self.curY - self.curPiece.y(i) 286 self.setShapeAt(x, y, self.curPiece.shape()) 287 288 self.numPiecesDropped += 1 289 if self.numPiecesDropped % 25 == 0: 290 self.level += 1 291 self.timer.start(self.timeoutTime(), self) 292 self.levelChanged.emit(self.level) 293 294 self.score += dropHeight + 7 295 self.scoreChanged.emit(self.score) 296 self.removeFullLines() 297 298 if not self.isWaitingAfterLine: 299 self.newPiece() 300 301 def removeFullLines(self): 302 numFullLines = 0 303 304 for i in range(TetrixBoard.BoardHeight - 1, -1, -1): 305 lineIsFull = True 306 307 for j in range(TetrixBoard.BoardWidth): 308 if self.shapeAt(j, i) == NoShape: 309 lineIsFull = False 310 break 311 312 if lineIsFull: 313 numFullLines += 1 314 for k in range(TetrixBoard.BoardHeight - 1): 315 for j in range(TetrixBoard.BoardWidth): 316 self.setShapeAt(j, k, self.shapeAt(j, k + 1)) 317 318 for j in range(TetrixBoard.BoardWidth): 319 self.setShapeAt(j, TetrixBoard.BoardHeight - 1, NoShape) 320 321 if numFullLines > 0: 322 self.numLinesRemoved += numFullLines 323 self.score += 10 * numFullLines 324 self.linesRemovedChanged.emit(self.numLinesRemoved) 325 self.scoreChanged.emit(self.score) 326 327 self.timer.start(500, self) 328 self.isWaitingAfterLine = True 329 self.curPiece.setShape(NoShape) 330 self.update() 331 332 def newPiece(self): 333 self.curPiece = copy.deepcopy(self.nextPiece) 334 self.nextPiece.setRandomShape() 335 self.showNextPiece() 336 self.curX = TetrixBoard.BoardWidth // 2 + 1 337 self.curY = TetrixBoard.BoardHeight - 1 + self.curPiece.minY() 338 339 if not self.tryMove(self.curPiece, self.curX, self.curY): 340 self.curPiece.setShape(NoShape) 341 self.timer.stop() 342 self.isStarted = False 343 344 def showNextPiece(self): 345 if self.nextPieceLabel is None: 346 return 347 348 dx = self.nextPiece.maxX() - self.nextPiece.minX() + 1 349 dy = self.nextPiece.maxY() - self.nextPiece.minY() + 1 350 351 pixmap = QPixmap(dx * self.squareWidth(), dy * self.squareHeight()) 352 painter = QPainter(pixmap) 353 painter.fillRect(pixmap.rect(), self.nextPieceLabel.palette().window()) 354 355 for i in range(4): 356 x = self.nextPiece.x(i) - self.nextPiece.minX() 357 y = self.nextPiece.y(i) - self.nextPiece.minY() 358 self.drawSquare(painter, x * self.squareWidth(), 359 y * self.squareHeight(), self.nextPiece.shape()) 360 361 painter.end() 362 363 self.nextPieceLabel.setPixmap(pixmap) 364 365 def tryMove(self, newPiece, newX, newY): 366 for i in range(4): 367 x = newX + newPiece.x(i) 368 y = newY - newPiece.y(i) 369 if x < 0 or x >= TetrixBoard.BoardWidth or y < 0 or y >= TetrixBoard.BoardHeight: 370 return False 371 if self.shapeAt(x, y) != NoShape: 372 return False 373 374 self.curPiece = newPiece 375 self.curX = newX 376 self.curY = newY 377 self.update() 378 return True 379 380 def drawSquare(self, painter, x, y, shape): 381 colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC, 382 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00] 383 384 color = QColor(colorTable[shape]) 385 painter.fillRect(x + 1, y + 1, self.squareWidth() - 2, 386 self.squareHeight() - 2, color) 387 388 painter.setPen(color.lighter()) 389 painter.drawLine(x, y + self.squareHeight() - 1, x, y) 390 painter.drawLine(x, y, x + self.squareWidth() - 1, y) 391 392 painter.setPen(color.darker()) 393 painter.drawLine(x + 1, y + self.squareHeight() - 1, 394 x + self.squareWidth() - 1, y + self.squareHeight() - 1) 395 painter.drawLine(x + self.squareWidth() - 1, 396 y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1) 397 398 399class TetrixPiece(object): 400 coordsTable = ( 401 ((0, 0), (0, 0), (0, 0), (0, 0)), 402 ((0, -1), (0, 0), (-1, 0), (-1, 1)), 403 ((0, -1), (0, 0), (1, 0), (1, 1)), 404 ((0, -1), (0, 0), (0, 1), (0, 2)), 405 ((-1, 0), (0, 0), (1, 0), (0, 1)), 406 ((0, 0), (1, 0), (0, 1), (1, 1)), 407 ((-1, -1), (0, -1), (0, 0), (0, 1)), 408 ((1, -1), (0, -1), (0, 0), (0, 1)) 409 ) 410 411 def __init__(self): 412 self.coords = [[0, 0] for _ in range(4)] 413 self.pieceShape = NoShape 414 415 self.setShape(NoShape) 416 417 def shape(self): 418 return self.pieceShape 419 420 def setShape(self, shape): 421 table = TetrixPiece.coordsTable[shape] 422 for i in range(4): 423 for j in range(2): 424 self.coords[i][j] = table[i][j] 425 426 self.pieceShape = shape 427 428 def setRandomShape(self): 429 self.setShape(random.randint(1, 7)) 430 431 def x(self, index): 432 return self.coords[index][0] 433 434 def y(self, index): 435 return self.coords[index][1] 436 437 def setX(self, index, x): 438 self.coords[index][0] = x 439 440 def setY(self, index, y): 441 self.coords[index][1] = y 442 443 def minX(self): 444 m = self.coords[0][0] 445 for i in range(4): 446 m = min(m, self.coords[i][0]) 447 448 return m 449 450 def maxX(self): 451 m = self.coords[0][0] 452 for i in range(4): 453 m = max(m, self.coords[i][0]) 454 455 return m 456 457 def minY(self): 458 m = self.coords[0][1] 459 for i in range(4): 460 m = min(m, self.coords[i][1]) 461 462 return m 463 464 def maxY(self): 465 m = self.coords[0][1] 466 for i in range(4): 467 m = max(m, self.coords[i][1]) 468 469 return m 470 471 def rotatedLeft(self): 472 if self.pieceShape == SquareShape: 473 return self 474 475 result = TetrixPiece() 476 result.pieceShape = self.pieceShape 477 for i in range(4): 478 result.setX(i, self.y(i)) 479 result.setY(i, -self.x(i)) 480 481 return result 482 483 def rotatedRight(self): 484 if self.pieceShape == SquareShape: 485 return self 486 487 result = TetrixPiece() 488 result.pieceShape = self.pieceShape 489 for i in range(4): 490 result.setX(i, -self.y(i)) 491 result.setY(i, self.x(i)) 492 493 return result 494 495 496if __name__ == '__main__': 497 498 import sys 499 500 app = QApplication(sys.argv) 501 window = TetrixWindow() 502 window.show() 503 random.seed(None) 504 sys.exit(app.exec_()) 505