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