Academic Integrity: tutoring, explanations, and feedback — we don’t complete graded work or submit on a student’s behalf.

Project 1: Tetris Think of three classes that would be used in an Tetris game. A

ID: 674869 • Letter: P

Question

Project 1: Tetris

Think of three classes that would be used in an Tetris game. At least one of these classes must have multiple instances in the game. For each of these classes, describe two attributes (instance variables) and two operations (methods). A one line, simple description is sufficient for each attribute and operation.  No C++ implementation please. The goal of this assignment is to concentrate on modeling the objects. Represent each class using the class diagram notation described during lecture. I hope you realize that there will be many "correct" answers.

If you are not familiar with Tetris, here is a brief description:
Tetris is an exciting action game. Typically, it is just a single player game, although it can be played with multiple players, each alternating turns.    Throughout the game, Tetris pieces fall from the top to the bottom of the playing area. Tetris pieces come in various shapes and sizes. You can manipulate a piece only when it is falling. Using certain keyboard commands, falling pieces may be rotated, moved horizontally, or dropped to the bottom of the playing area. When the Tetris pieces form a solid row of blocks across the playing area, that row vanishes. Because this is the only way to remove blocks, you should try to form solid rows whenever possible. The game ends when the pieces stack up to the top of the playing area. With each vanished row, a player's score increases. After a predetermined number of vanished rows, additional block waves occur and the speed of the game increases, making it harder and harder to build complete rows of pieces.

To help you, a screen shot of Tetris is shown below. Look at the items in the picture. Almost anything you see on the screen could be an object. Reread the above description of the game. Objects are frequently nouns in the specification of a program.  

If the game is quite unfamiliar to you, try playing the game by visiting the URL: http://www.bitstorm.org/tetris

Tetris has a very long history that spans many companies in the computer industry as well as many lawsuits. In 1985, inspired by a pentominoes game he had bought earlier, Alexey Pazhitnov creates Tetris for a Electronica 60 at the Moscow Academy of Science's Computer Center. Later that year, it is ported to the IBM PC by Vadim Gerasimov and starts spreading around Moscow. Eventually, its popularity makes it one of the most profitable games in history. For example, Nintendo bundled the game with its original GameBoy, selling over 30 million GameBoys in the 1990s. Atari also sold an arcade version, selling over 20,000 arcarde units.

Project 2: Better TrashCan

I have provided you with a sample class named TrashCan which has been diagrammed below and described in the online course content. You can acquire the source to the TrashCan class by clicking here (.NET 2012  XCode)

For this unit, I'd like you to identify additional member variables (data) that would be appropriate for the class TrashCan. I would also like you to identify additional methods (functions) that would be appropriate for the class TrashCan. Update and resubmit the .h file for TrashCan with the new possible methods and members you have identified commented and explained. YOU ARE NOT REQUIRED TO CODE UP ANYTHING MORE THAN THE .H FILE.

TrashCan Class

void setSize( int size );
int getSize( );
int getContents( );
void addItem( );
void empty( );
void cover( );
void uncover( );

void printCan( );

Driver Code

#include "TrashCan.h"

#include <iostream>
using namespace std;

int main( ) {

cout << "Welcome to Howie's TrashCan Program!" << endl;

TrashCan myCan;
TrashCan yourCan;

yourCan.setSize( 12 );
myCan.setSize( 12 );

yourCan.addItem( );
yourCan.addItem( );
myCan.addItem( );

myCan.printCan();
yourCan.printCan();

return( 0 );

}

Explanation / Answer

Some ideas behind the game:

The code consists of four classes: Tetris, Board, Tetrominoe and Shape. The Tetris class sets up the game. The Board is where the game logic is written. The Tetrominoe class contains names for all tetris pieces and the Shape class contains the code for a tetris piece.

#!/usr/bin/python

# -*- coding: utf-8 -*-

"""

ZetCode PyQt4 tutorial

This is a Tetris game clone.

author: Jan Bodnar

website: zetcode.com

last edited: October 2013

"""

import sys, random

from PyQt4 import QtCore, QtGui

class Tetris(QtGui.QMainWindow):

   

    def __init__(self):

        super(Tetris, self).__init__()

       

        self.initUI()

       

       

    def initUI(self):   

        self.tboard = Board(self)

        self.setCentralWidget(self.tboard)

        self.statusbar = self.statusBar()       

        self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)

       

        self.tboard.start()

       

        self.resize(180, 380)

        self.center()

        self.setWindowTitle('Tetris')       

        self.show()

       

    def center(self):

       

        screen = QtGui.QDesktopWidget().screenGeometry()

        size = self.geometry()

        self.move((screen.width()-size.width())/2,

            (screen.height()-size.height())/2)

       

class Board(QtGui.QFrame):

   

    msg2Statusbar = QtCore.pyqtSignal(str)

   

    BoardWidth = 10

    BoardHeight = 22

    Speed = 300

    def __init__(self, parent):

        super(Board, self).__init__(parent)

       

        self.initBoard()

       

       

    def initBoard(self):    

        self.timer = QtCore.QBasicTimer()

        self.isWaitingAfterLine = False

       

        self.curX = 0

        self.curY = 0

        self.numLinesRemoved = 0

      self.board = []

        self.setFocusPolicy(QtCore.Qt.StrongFocus)

        self.isStarted = False

        self.isPaused = False

        self.clearBoard()

       

       

    def shapeAt(self, x, y):

        return self.board[(y * Board.BoardWidth) + x]

       

    def setShapeAt(self, x, y, shape):

        self.board[(y * Board.BoardWidth) + x] = shape

       

    def squareWidth(self):

        return self.contentsRect().width() / Board.BoardWidth

       

    def squareHeight(self):

        return self.contentsRect().height() / Board.BoardHeight

       

    def start(self):

       

        if self.isPaused:

            return

        self.isStarted = True

        self.isWaitingAfterLine = False

        self.numLinesRemoved = 0

        self.clearBoard()

        self.msg2Statusbar.emit(str(self.numLinesRemoved))

        self.newPiece()

        self.timer.start(Board.Speed, self)

       

    def pause(self):

       

        if not self.isStarted:

            return

        self.isPaused = not self.isPaused

       

        if self.isPaused:

            self.timer.stop()

            self.msg2Statusbar.emit("paused")

           

        else:

            self.timer.start(Board.Speed, self)

            self.msg2Statusbar.emit(str(self.numLinesRemoved))

        self.update()

       

    def paintEvent(self, event):

       

        painter = QtGui.QPainter(self)

        rect = self.contentsRect()

        boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()

        for i in range(Board.BoardHeight):

            for j in range(Board.BoardWidth):

                shape = self.shapeAt(j, Board.BoardHeight - i - 1)

               

                if shape != Tetrominoe.NoShape:

                    self.drawSquare(painter,

                        rect.left() + j * self.squareWidth(),

                        boardTop + i * self.squareHeight(), shape)

        if self.curPiece.shape() != Tetrominoe.NoShape:

           

            for i in range(4):

               

                x = self.curX + self.curPiece.x(i)

                y = self.curY - self.curPiece.y(i)

                self.drawSquare(painter, rect.left() + x * self.squareWidth(),

                    boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),

                    self.curPiece.shape())

                   

    def keyPressEvent(self, event):

       

        if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:

            super(Board, self).keyPressEvent(event)

            return

        key = event.key()

       

        if key == QtCore.Qt.Key_P:

            self.pause()

            return

           

        if self.isPaused:

            return

               

        elif key == QtCore.Qt.Key_Left:

            self.tryMove(self.curPiece, self.curX - 1, self.curY)

           

        elif key == QtCore.Qt.Key_Right:

            self.tryMove(self.curPiece, self.curX + 1, self.curY)

           

        elif key == QtCore.Qt.Key_Down:

            self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY)

           

        elif key == QtCore.Qt.Key_Up:

            self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)

           

        elif key == QtCore.Qt.Key_Space:

            self.dropDown()

           

        elif key == QtCore.Qt.Key_D:

            self.oneLineDown()

           

        else:

            super(Board, self).keyPressEvent(event)

               

    def timerEvent(self, event):

       

        if event.timerId() == self.timer.timerId():

           

            if self.isWaitingAfterLine:

                self.isWaitingAfterLine = False

                self.newPiece()

            else:

                self.oneLineDown()

               

        else:

            super(Board, self).timerEvent(event)

           

    def clearBoard(self):

       

        for i in range(Board.BoardHeight * Board.BoardWidth):

            self.board.append(Tetrominoe.NoShape)

       

    def dropDown(self):

      

        newY = self.curY

       

        while newY > 0:

           

            if not self.tryMove(self.curPiece, self.curX, newY - 1):

                break

               

            newY -= 1

        self.pieceDropped()

       

    def oneLineDown(self):

       

        if not self.tryMove(self.curPiece, self.curX, self.curY - 1):

            self.pieceDropped()

           

    def pieceDropped(self):

       

        for i in range(4):

           

            x = self.curX + self.curPiece.x(i)

            y = self.curY - self.curPiece.y(i)

            self.setShapeAt(x, y, self.curPiece.shape())

        self.removeFullLines()

        if not self.isWaitingAfterLine:

            self.newPiece()

           

  def removeFullLines(self):

       

        numFullLines = 0

        rowsToRemove = []

        for i in range(Board.BoardHeight):

           

            n = 0

            for j in range(Board.BoardWidth):

                if not self.shapeAt(j, i) == Tetrominoe.NoShape:

                    n = n + 1

            if n == 10:

                rowsToRemove.append(i)

        rowsToRemove.reverse()

       

        for m in rowsToRemove:

           

            for k in range(m, Board.BoardHeight):

                for l in range(Board.BoardWidth):

                        self.setShapeAt(l, k, self.shapeAt(l, k + 1))

        numFullLines = numFullLines + len(rowsToRemove)

        if numFullLines > 0:

           

            self.numLinesRemoved = self.numLinesRemoved + numFullLines

            self.msg2Statusbar.emit(str(self.numLinesRemoved))

               

            self.isWaitingAfterLine = True

            self.curPiece.setShape(Tetrominoe.NoShape)

            self.update()

           

    def newPiece(self):

       

        self.curPiece = Shape()

        self.curPiece.setRandomShape()

        self.curX = Board.BoardWidth / 2 + 1

        self.curY = Board.BoardHeight - 1 + self.curPiece.minY()

       

        #print self.curY

        if not self.tryMove(self.curPiece, self.curX, self.curY):

           

            self.curPiece.setShape(Tetrominoe.NoShape)

            self.timer.stop()

            self.isStarted = False

            self.msg2Statusbar.emit("Game over")

    def tryMove(self, newPiece, newX, newY):

       

        for i in range(4):

           

            x = newX + newPiece.x(i)

            y = newY - newPiece.y(i)

           

            if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:

                return False

               

            if self.shapeAt(x, y) != Tetrominoe.NoShape:

                return False

        self.curPiece = newPiece

        self.curX = newX

        self.curY = newY

        self.update()

       

        return True

       

    def drawSquare(self, painter, x, y, shape):

       

        colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,

                      0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]

        color = QtGui.QColor(colorTable[shape])

        painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,

            self.squareHeight() - 2, color)

        painter.setPen(color.light())

        painter.drawLine(x, y + self.squareHeight() - 1, x, y)

        painter.drawLine(x, y, x + self.squareWidth() - 1, y)

        painter.setPen(color.dark())

        painter.drawLine(x + 1, y + self.squareHeight() - 1,

            x + self.squareWidth() - 1, y + self.squareHeight() - 1)

        painter.drawLine(x + self.squareWidth() - 1,

            y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)

class Tetrominoe(object):

   

    NoShape = 0

    ZShape = 1

    SShape = 2

    LineShape = 3

    TShape = 4

    SquareShape = 5

    LShape = 6

    MirroredLShape = 7

class Shape(object):

   

    coordsTable = (

        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),

        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),

        ((0, -1),    (0, 0),     (1, 0),     (1, 1)),

        ((0, -1),    (0, 0),     (0, 1),     (0, 2)),

        ((-1, 0),    (0, 0),     (1, 0),     (0, 1)),

        ((0, 0),     (1, 0),     (0, 1),     (1, 1)),

        ((-1, -1),   (0, -1),    (0, 0),     (0, 1)),

        ((1, -1),    (0, -1),    (0, 0),     (0, 1))

    )

    def __init__(self):

       

        self.coords = [[0,0] for i in range(4)]

        self.pieceShape = Tetrominoe.NoShape

        self.setShape(Tetrominoe.NoShape)

       

    def shape(self):

        return self.pieceShape

       

    def setShape(self, shape):

       

        table = Shape.coordsTable[shape]

       

        for i in range(4):

            for j in range(2):

                self.coords[i][j] = table[i][j]

        self.pieceShape = shape

       

    def setRandomShape(self):

        self.setShape(random.randint(1, 7))

       

    def x(self, index):

        return self.coords[index][0]

       

    def y(self, index):

        return self.coords[index][1]

       

    def setX(self, index, x):

        self.coords[index][0] = x

       

    def setY(self, index, y):

        self.coords[index][1] = y

       

    def minX(self):

       

        m = self.coords[0][0]

        for i in range(4):

            m = min(m, self.coords[i][0])

        return m

       

    def maxX(self):

       

        m = self.coords[0][0]

        for i in range(4):

            m = max(m, self.coords[i][0])

        return m

       

    def minY(self):

       

        m = self.coords[0][1]

        for i in range(4):

            m = min(m, self.coords[i][1])

        return m

       

    def maxY(self):

       

        m = self.coords[0][1]

        for i in range(4):

            m = max(m, self.coords[i][1])

        return m

       

    def rotateLeft(self):

       

        if self.pieceShape == Tetrominoe.SquareShape:

            return self

        result = Shape()

        result.pieceShape = self.pieceShape

       

        for i in range(4):

           

            result.setX(i, self.y(i))

            result.setY(i, -self.x(i))

        return result

       

    def rotateRight(self):

       

        if self.pieceShape == Tetrominoe.SquareShape:

            return self

        result = Shape()

        result.pieceShape = self.pieceShape

       

        for i in range(4):

           

            result.setX(i, -self.y(i))

            result.setY(i, self.x(i))

        return result

def main():

   

    app = QtGui.QApplication([])

    tetris = Tetris()   

    sys.exit(app.exec_())

if __name__ == '__main__':

    main()

The game is simplified a bit so that it is easier to understand. The game starts immediately after it is launched. We can pause the game by pressing the p key. The Space key will drop the tetris piece instantly to the bottom. The game goes at constant speed, no acceleration is implemented. The score is the number of lines that we have removed.

self.tboard = Board(self)

self.setCentralWidget(self.tboard)

An instance of the Board class is created and set to be the central widget of the application.

self.statusbar = self.statusBar()       

self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)

We create a statusbar where we will display messages. We will display three possible messages: the number of lines already removed, the paused message, or the game over message. The msg2Statusbaris a custom signal that is implemented in the Board class. The showMessage() is a built-in method that displays a message on a statusbar.

self.tboard.start()

This line initiates the game.

class Board(QtGui.QFrame):

   

    msg2Statusbar = QtCore.pyqtSignal(str)

...   

A custom signal is created. The msg2Statusbar is a signal that is emitted when we want to write a message or the score to the statusbar.

BoardWidth = 10

BoardHeight = 22

Speed = 300

These are Board's class variables. The BoardWidth and the BoardHeight define the size of the board in blocks. The Speed defines the speed of the game. Each 300 ms a new game cycle will start.

...

self.curX = 0

self.curY = 0

self.numLinesRemoved = 0

self.board = []

...

In the initBoard() method we initialize some important variables. The self.board variable is a list of numbers from 0 to 7. It represents the position of various shapes and remains of the shapes on the board.

def shapeAt(self, x, y):

    return self.board[(y * Board.BoardWidth) + x]

The shapeAt() method determines the type of a shape at a given block.

def squareWidth(self):

    return self.contentsRect().width() / Board.BoardWidth

The board can be dynamically resized. As a consequence, the size of a block may change. ThesquareWidth() calculates the width of the single square in pixels and returns it. The Board.BoardWidthis the size of the board in blocks.

for i in range(Board.BoardHeight):

    for j in range(Board.BoardWidth):

        shape = self.shapeAt(j, Board.BoardHeight - i - 1)

       

        if shape != Tetrominoe.NoShape:

            self.drawSquare(painter,

                rect.left() + j * self.squareWidth(),

                boardTop + i * self.squareHeight(), shape)

The painting of the game is divided into two steps. In the first step, we draw all the shapes, or remains of the shapes that have been dropped to the bottom of the board. All the squares are remembered in the self.board list variable. The variable is accessed using the shapeAt() method.

if self.curPiece.shape() != Tetrominoe.NoShape:

   

    for i in range(4):

       

        x = self.curX + self.curPiece.x(i)

        y = self.curY - self.curPiece.y(i)

        self.drawSquare(painter, rect.left() + x * self.squareWidth(),

            boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),

            self.curPiece.shape())

The next step is the drawing of the actual piece that is falling down.

elif key == QtCore.Qt.Key_Right:

    self.tryMove(self.curPiece, self.curX + 1, self.curY)

In the keyPressEvent() method we check for pressed keys. If we press the right arrow key, we try to move the piece to the right. We say try because the piece might not be able to move.

elif key == QtCore.Qt.Key_Up:

    self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)

The Up arrow key will rotate the falling piece to the left.

elif key == QtCore.Qt.Key_Space:

    self.dropDown()

The Space key will drop the falling piece instantly to the bottom.

elif key == QtCore.Qt.Key_D:

    self.oneLineDown()

Pressing the d key, the piece will go one block down. It can be used to accellerate the falling of a piece a bit.

def tryMove(self, newPiece, newX, newY):

   

    for i in range(4):

       

        x = newX + newPiece.x(i)

        y = newY - newPiece.y(i)

       

        if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:

            return False

           

        if self.shapeAt(x, y) != Tetrominoe.NoShape:

            return False

    self.curPiece = newPiece

    self.curX = newX

    self.curY = newY

    self.update()

    return True

In the tryMove() method we try to move our shapes. If the shape is at the edge of the board or is adjacent to some other piece, we return False. Otherwise we place the current falling piece to a new position.

def timerEvent(self, event):

   

    if event.timerId() == self.timer.timerId():

       

        if self.isWaitingAfterLine:

            self.isWaitingAfterLine = False

            self.newPiece()

        else:

            self.oneLineDown()

           

    else:

        super(Board, self).timerEvent(event)

In the timer event, we either create a new piece after the previous one was dropped to the bottom or we move a falling piece one line down.

def clearBoard(self):

   

    for i in range(Board.BoardHeight * Board.BoardWidth):

        self.board.append(Tetrominoe.NoShape)

The clearBoard() method clears the board by setting Tetrominoe.NoShape at each block of the board.

def removeFullLines(self):

   

    numFullLines = 0

    rowsToRemove = []

    for i in range(Board.BoardHeight):

       

        n = 0

        for j in range(Board.BoardWidth):

            if not self.shapeAt(j, i) == Tetrominoe.NoShape:

                n = n + 1

        if n == 10:

            rowsToRemove.append(i)

    rowsToRemove.reverse()

   

    for m in rowsToRemove:

       

        for k in range(m, Board.BoardHeight):

            for l in range(Board.BoardWidth):

                    self.setShapeAt(l, k, self.shapeAt(l, k + 1))

    numFullLines = numFullLines + len(rowsToRemove)

...

If the piece hits the bottom, we call the removeFullLines() method. We find out all full lines and remove them. We do it by moving all lines above the current full line to be removed one line down. Notice that we reverse the order of the lines to be removed. Otherwise, it would not work correctly. In our case we use a naive gravity. This means that the pieces may be floating above empty gaps.

def newPiece(self):

   

    self.curPiece = Shape()

    self.curPiece.setRandomShape()

    self.curX = Board.BoardWidth / 2 + 1

    self.curY = Board.BoardHeight - 1 + self.curPiece.minY()

   

    if not self.tryMove(self.curPiece, self.curX, self.curY):

       

        self.curPiece.setShape(Tetrominoe.NoShape)

        self.timer.stop()

        self.isStarted = False

        self.msg2Statusbar.emit("Game over")

The newPiece() method creates randomly a new tetris piece. If the piece cannot go into its initial position, the game is over.

class Tetrominoe(object):

   

    NoShape = 0

    ZShape = 1

    SShape = 2

    LineShape = 3

    TShape = 4

    SquareShape = 5

    LShape = 6

    MirroredLShape = 7

The Tetrominoe class holds names of all possible shapes. We have also a NoShape for an empty space.

The Shape class saves information about a tetris piece.

class Shape(object):

   

    coordsTable = (

        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),

        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),

        ...

    )

...   

The coordsTable tuple holds all possible coordinate values of our etris pieces. This is a template from which all pieces take their coordinate values.

self.coords = [[0,0] for i in range(4)]

Upon creation we create an empty coordinates list. The list will save the coordinates of the tetris piece.

Figure: Coordinates

The above image will help understand the coordinate values a bit more. For example, the tuples (0, -1), (0, 0), (-1, 0), (-1, -1) represent a Z-shape. The diagram illustrates the shape.

def rotateLeft(self):

   

    if self.pieceShape == Tetrominoe.SquareShape:

        return self

    result = Shape()

    result.pieceShape = self.pieceShape

   

    for i in range(4):

       

        result.setX(i, self.y(i))

        result.setY(i, -self.x(i))

    return result

The rotateLeft() method rotates a piece to the left. The square does not have to be rotated. That is why we simply return the reference to the current object. A new piece is created and its coordinates are set to the ones of the rotated piece.