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