Pentris Python Computer Game

July 2012   Bianchini

Project Documents

  pentris.py

I made this advanced version of Tetris as a project for 15.112 Fundamentals of Programming as a part of Carnegie Mellon University's precollege program. Using Python 2 and TkInter to produce the simple GUI, I made Pentris like Tetris, but where all pieces have 5 blocks in them instead of 4. Additional features include mirroring pieces, piece previews, hard drops, and high score tables.

Play the game!

Before explaining how it works and how I made it, you should try it for yourself!  You might want to refer back to the controls here.  I made this game as one of my first exposures to programming, so enjoy the simple, retro, and slightly janky feel.

 

The Pentris GUI, complete with a live score at the top as well as a next piece preview in the upper left corner.  You can see your high scores at the end of each round.

How to play

Download the file listed under "Project Documents" and put it in a directory that you'll remember (downloads works).  Open up a terminal window, and write/paste the following:

python2 ~/path/to/file/pentris.py

...where ~/path/to/file/ is the directory that contains the file.  For example:  python2 ~/Downloads/pentris.py

After you type that in, look for the python window that opens (it will not pop to the front of your screen).  It is possible that you will have to download python2 in order to get the file to open, which you can do here.

Controls

Action Key
pause p
restart a new game r
move piece left, right, down left, right, down arrows
drop piece spacebar
rotate piece clockwise s
rotate piece counter-clockwise a
mirror piece m

 

Features

Pentris is a more complex version of Tetris, starting with the falling pieces, which are made up of 5 blocks each instead of the traditional 4.  This also results in more types of pieces (12 instead of 5, when excluding mirrored duplicates in the original 7 pieces) for less predictable gameplay.  The game board is larger to accommodate the bulkier geometries.  The falling pieces can be rotated in either direction as well as mirrored about a vertical axis.  The other controls and features are standard:  move left, right, and down; hard drop; preview of the next piece; score display.

The 12 possible game pieces in Pentris.  Each is built out of 5 blocks instead of the classic 4 blocks in Tetris.

Implementation

I made Pentris for an open-ended assignment in Carnegie Mellon University's 15.112 Fundamentals of Programming class when I was in high school.  This was my first exposure to programming, so making a game was an easy way for me to visualize some of the logic I was learning to implement.  Coded in Python 2, I made the GUI using TkInter.

Here is the full script if you want to browse without downloading the file.  But first, here are some practices I've picked up since making this game, so don't judge me too hard by the code that follows:

  • Commenting throughout the code makes it easier for others (and yourself after you forget a lot) to follow.
  • Classes are a thing.
  • Separating code into separate files in a way that makes sense is much cleaner than one insanely long script.
#Pentris!
#Elizabeth Bianchini, 2012

#Added:
    #five-piece tetrominoes instead of four-piece
    #a mirrorPiece function, using "m"-key
    #a drawHighScore function that resets when the program quits
    #dual rotation:  "a" for counter clockwise, "s" for clockwise
    #a pause feature
    #a hard drop:  spacebar
    #piece preview
    #an instructions screen before the start of first game

from Tkinter import *
import random

def drawGame(canvas):
    drawBoard(canvas)
    if (canvas.data["isGameOver"] == False):
        drawFallingPiece(canvas)

def drawBoard(canvas):
    for row in xrange(canvas.data["rows"]):
        for col in xrange(canvas.data["cols"]):
            drawCell(canvas, row, col, canvas.data["board"][row][col])

def drawCell(canvas, row, col, color):
    topLeftX = 30 + col * 20
    topLeftY = 30 + row * 20
    bottomRightX = topLeftX + 20
    bottomRightY = topLeftY + 20
    canvas.create_rectangle((topLeftX, topLeftY), (bottomRightX,bottomRightY), fill = "white")
    colorTopX = topLeftX + 2
    colorTopY = topLeftY + 2
    colorBottomX = bottomRightX - 2
    colorBottomY = bottomRightY - 2
    canvas.create_rectangle((colorTopX, colorTopY), (colorBottomX,colorBottomY), fill = color)
        
def timerFired(canvas):
    canvas.data["firstGame"] = False
    if canvas.data["isGameOver"]:
        gameOverScreen(canvas)
        canvas.after(1000, timerFired, canvas)
    elif canvas.data["paused"]:
        pauseScreen(canvas)
        canvas.after(1000, timerFired, canvas)
    elif moveFallingPiece(canvas, 1, 0):
        delay = 1000
        canvas.after(delay, timerFired, canvas)
        redrawAll(canvas)
    else:
        placeFallingPiece(canvas)
        removeFullRows(canvas)
        canvas.data["fallingPiece"] = canvas.data["nextFallingPiece"]
        canvas.data["fallingPieceColor"] = canvas.data["nextFallingPieceColor"]
        newFallingPiece(canvas)
        if (moveFallingPiece(canvas, 0, 0) == False):
            canvas.data["isGameOver"] = True
            timerFired(canvas)
        else:
            redrawAll(canvas)
            canvas.after(1000, timerFired, canvas)

def gameOverScreen(canvas):
    canvas.data["isGameOver"] = True
    textX = canvas.data["canvasWidth"]/2
    textY = canvas.data["canvasHeight"]/6
    canvas.create_text((textX, textY), text = "Game Over!", font = ("Arial", 28, "bold"), fill = "white")
    canvas.create_text((textX, textY + 30), text = "Press 'r' to restart", font = ("Arial", 18, "bold"), fill = "white")

def drawHighScores(canvas):
    textX = canvas.data["canvasWidth"]/2
    textY = canvas.data["canvasHeight"]/2.5 - 25
    highScores = canvas.data["highScores"]
    currentScore = canvas.data["score"]
    if (canvas.data["highScoresPrinted"] == False):
        if (currentScore > highScores[-1]):
            highScores[-1] = currentScore
            for x in xrange(1, 5):
                if (highScores[-x] > highScores[-(x + 1)]):
                    swapIndices(highScores, -x, -(x + 1))
    canvas.create_text((textX, textY), text = "High Scores:", font = ("Arial", 34, "bold"), fill = "white")
    for x in xrange(5):
        score = highScores[x]
        canvas.create_text((textX, textY + 50 + 40 * x), text = score, font = ("Arial", 34, "bold"), fill = "white")
    canvas.data["highScoresPrinted"] = True

def swapIndices(highScores, index1, index2):
    holdValue = highScores[index1]
    highScores[index1] = highScores[index2]
    highScores[index2] = holdValue

def placeFallingPiece(canvas):
    fallingPieceRow = canvas.data["fallingPieceRow"]
    fallingPieceCol = canvas.data["fallingPieceCol"]
    piece = canvas.data["fallingPiece"]
    color = canvas.data["fallingPieceColor"]
    board = canvas.data["board"]
    for y in xrange(len(piece)):
        for x in xrange(len(piece[0])):
            if piece[y][x]:
                board[y + fallingPieceRow][x + fallingPieceCol] = color
    canvas.after(1000, redrawAll, canvas)

def newFallingPiece(canvas):
    fallingPiece = canvas.data["nextFallingPiece"]
    fallingPieceColor = canvas.data["nextFallingPieceColor"]
    newNextFallingPiece(canvas)
    canvas.data["fallingPiece"] = fallingPiece
    canvas.data["fallingPieceColor"] = fallingPieceColor
    fallingPieceRow = 0
    fallingPieceCol = canvas.data["cols"]/2 - len(fallingPiece[0])/2
    canvas.data["fallingPieceRow"] = fallingPieceRow
    canvas.data["fallingPieceCol"] = fallingPieceCol
    if (moveFallingPiece(canvas, 0, 0)):
        redrawAll(canvas)
    else:
        canvas.data["isGameOver"] = True
        gameOverScreen(canvas)

def drawFallingPiece(canvas):
    drawNextFallingPiece(canvas)
    piece = canvas.data["fallingPiece"]
    color = canvas.data["fallingPieceColor"]
    TLrow = canvas.data["fallingPieceRow"]
    TLcol = canvas.data["fallingPieceCol"]
    for y in xrange(len(piece)):
        for x in xrange(len(piece[y])):
            if piece[y][x]:
                TLX = 30 + (TLcol + x) * 20
                TLY = 30 + (TLrow + y) * 20
                BRX = TLX + 20
                BRY = TLY + 20
                canvas.create_rectangle((TLX, TLY), (BRX, BRY), fill = "white")
                TLXColor = TLX + 2
                TLYColor = TLY + 2
                BRXColor = BRX - 2
                BRYColor = BRY - 2
                canvas.create_rectangle((TLXColor, TLYColor), (BRXColor,BRYColor), fill = color)

def newNextFallingPiece(canvas):
    x = random.randint(0, 11)
    nextFallingPiece = canvas.data["tetrisPieces"][x]
    nextFallingPieceColor = canvas.data["tetrisPieceColors"][x]
    canvas.data["nextFallingPiece"] = nextFallingPiece
    canvas.data["nextFallingPieceColor"] = nextFallingPieceColor

def drawNextFallingPiece(canvas):
    piece = canvas.data["nextFallingPiece"]
    color = canvas.data["nextFallingPieceColor"]
    TLPointX = 10
    TLPointY = 10
    for y in xrange(len(piece)):
        for x in xrange(len(piece[y])):
            if piece[y][x]:
                TLX = 10 + x * 5
                TLY = 10 + y * 5
                BRX = TLX + 5
                BRY = TLY + 5
                canvas.create_rectangle((TLX, TLY), (BRX, BRY), fill = color)

def dropFallingPiece(canvas):
    while fallingPieceIsLegal(canvas):
        canvas.data["fallingPieceRow"] += 1
    canvas.data["fallingPieceRow"] -= 1
    redrawAll(canvas)

def moveFallingPiece(canvas, drow, dcol):
    canvas.data["fallingPieceRow"] += drow
    canvas.data["fallingPieceCol"] += dcol
    if fallingPieceIsLegal(canvas):
        piece = canvas.data["fallingPiece"]
        color = canvas.data["fallingPieceColor"]
        for y in xrange(len(piece)):
            for x in xrange(len(piece[y])):
                if piece[y][x]:
                    tlx = canvas.data["fallingPieceRow"]
                    tly = canvas.data["fallingPieceCol"]
                    brx = canvas.data["fallingPieceRow"] + 20
                    bry = canvas.data["fallingPieceCol"] + 20
                    canvas.create_rectangle((tlx, tly), (brx, bry), fill = "white")
                    canvas.create_rectangle((tlx + 2, tly + 2), (brx - 2, bry - 2), fill = color)
        return True
    else:
        canvas.data["fallingPieceRow"] -= drow
        canvas.data["fallingPieceCol"] -= dcol
        redrawAll(canvas)
        return False

def fallingPieceIsLegal(canvas):
    board = canvas.data["board"]
    piece = canvas.data["fallingPiece"]
    row = canvas.data["fallingPieceRow"]
    col = canvas.data["fallingPieceCol"]
    emptyColor = canvas.data["emptyColor"]
    for y in xrange(len(piece)):
        for x in xrange(len(piece[y])):
            if piece[y][x]:
                if ((row + y) >= canvas.data["rows"]):  return False
                elif ((col + x) >= canvas.data["cols"]):  return False
                elif ((row + y) < 0):  return False
                if ((col + x) < 0):  return False
                elif (board[row + y][col + x] != emptyColor):  return False
    return True

def mirrorFallingPiece(canvas):
    oldPiece = canvas.data["fallingPiece"]
    rows = len(oldPiece)
    cols = len(oldPiece[0])
    newPiece = []
    for x in xrange(rows):
        newPiece.append([False] * cols)
    for y in xrange(len(oldPiece)):
        for x in xrange(len(oldPiece[y])):
            if (oldPiece[y][x]):
                newPiece[y][-(x + 1)] = True
    canvas.data["fallingPiece"] = newPiece
    if fallingPieceIsLegal(canvas):
        redrawAll(canvas)
    else:
        canvas.data["fallingPiece"] = oldPiece

def rotateFallingPieceCC(canvas):
    oldPiece = canvas.data["fallingPiece"]
    oldRows = len(oldPiece)
    oldCols = len(oldPiece[0])
    oldCenterRow = fallingPieceCenter(canvas)[0]
    oldCenterCol = fallingPieceCenter(canvas)[1]
    newRows = oldCols
    newCols = oldRows
    newPiece = []
    for x in xrange(newRows):
        newPiece.append([False] * newCols)
    for y in xrange(len(oldPiece)):
        for x in xrange(len(oldPiece[y])):
            if (oldPiece[y][x]):
                newPiece[-(x + 1)][y] = True
    canvas.data["fallingPiece"] = newPiece
    newCenterRow = fallingPieceCenter(canvas)[0]
    newCenterCol = fallingPieceCenter(canvas)[1]
    canvas.data["fallingPieceRow"] += (oldCenterRow - newCenterRow)
    canvas.data["fallingPieceCol"] += (oldCenterCol - newCenterCol)
    if fallingPieceIsLegal(canvas):
        redrawAll(canvas)
    else:
        canvas.data["fallingPiece"] = oldPiece
        canvas.data["fallingPieceRow"] -= (oldCenterRow - newCenterRow)
        canvas.data["fallingPieceCol"] -= (oldCenterCol - newCenterCol)

def rotateFallingPieceC(canvas):
    oldPiece = canvas.data["fallingPiece"]
    oldRows = len(oldPiece)
    oldCols = len(oldPiece[0])
    oldCenterRow = fallingPieceCenter(canvas)[0]
    oldCenterCol = fallingPieceCenter(canvas)[1]
    newRows = oldCols
    newCols = oldRows
    newPiece = []
    for x in xrange(newRows):
        newPiece.append([False] * newCols)
    for y in xrange(len(oldPiece)):
        for x in xrange(len(oldPiece[y])):
            if (oldPiece[y][x]):
                newPiece[x][-(y + 1)] = True
    canvas.data["fallingPiece"] = newPiece
    newCenterRow = fallingPieceCenter(canvas)[0]
    newCenterCol = fallingPieceCenter(canvas)[1]
    canvas.data["fallingPieceRow"] += (oldCenterRow - newCenterRow)
    canvas.data["fallingPieceCol"] += (oldCenterCol - newCenterCol)
    if fallingPieceIsLegal(canvas):
        redrawAll(canvas)
    else:
        canvas.data["fallingPiece"] = oldPiece
        canvas.data["fallingPieceRow"] -= (oldCenterRow - newCenterRow)
        canvas.data["fallingPieceCol"] -= (oldCenterCol - newCenterCol)

def fallingPieceCenter(canvas):
    piece = canvas.data["fallingPiece"]
    TLRow = canvas.data["fallingPieceRow"]
    TLCol = canvas.data["fallingPieceCol"]
    centerRow = TLRow + len(piece)/2
    centerCol = TLCol + len(piece[0])/2
    return (centerRow, centerCol)

def drawScore(canvas):
    score = canvas.data["score"]
    textX = canvas.data["canvasWidth"]/2
    textY = 30/2
    canvas.create_rectangle((textX - 30 * 2, textY - 15), (textX + 30 * 2, textY + 15), fill = "white")
    canvas.create_text((textX, textY), text = ("Score:", score), font = ("Arial", 16, "bold"))

def removeFullRows(canvas):
    emptyColor = canvas.data["emptyColor"]
    boardRows = canvas.data["rows"]
    boardCols = canvas.data["cols"]
    board = canvas.data["board"]
    oldRow = boardRows - 1
    newRow = boardRows - 1
    while (oldRow > -1):
        for x in xrange(boardCols):
            board[newRow][x] = board[oldRow][x]
        if (emptyColor in board[oldRow]):
            newRow -= 1
        oldRow -= 1
    canvas.data["score"] += (oldRow - newRow)**2
    for x in xrange(newRow - oldRow):
        for y in xrange(boardCols):
            board[x][y] = "black"

def instructions(canvas):
    width = canvas.data["canvasWidth"]
    height = canvas.data["canvasHeight"]
    textX = width/2
    textY = height/6
    canvas.create_text((textX, textY), text = "Pentris Instructions:", font = ("Arial", 24, "bold"), fill = "white")
    canvas.create_text((textX, textY + 35), text = "Use arrow keys to move right, left, and down", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 65), text = "'a' key rotates the falling piece counter clockwise", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 95), text = "'s' key rotates the falling piece clockwise", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 125), text = "'m' key makes a mirror of the falling piece", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 155), text = "The spacebar hard drops the falling piece", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 185), text = "Look in upper left corner for next piece", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 215), text = "'p' key pauses the current game", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, textY + 245), text = "'r' key starts a new game", font = ("Arial", 12, "bold"), fill = "white")
    canvas.create_text((textX, height * 5/6), text = "Push 'r' for New Game!", font = ("Arial", 18, "bold"), fill = "white")

def pauseScreen(canvas):
    textX = canvas.data["canvasWidth"]/2
    textY = canvas.data["canvasHeight"]/2
    canvas.create_text((textX, textY), text = "Paused", font = ("Arial", 32, "bold"), fill = "white")
    canvas.create_text((textX, textY + 50), text = "Press 'p' to resume", font = ("Arial", 16, "bold"), fill = "white")
    canvas.create_text((textX, textY + 70), text = "or press 'r' to restart", font = ("Arial", 16, "bold"), fill = "white")

def keyPressed(event):
    canvas = event.widget.canvas
    if canvas.data["isGameOver"]:
        if (event.keysym == "r"):  init(canvas)
    elif canvas.data["paused"]:
        if (event.keysym == "p"):  canvas.data["paused"] = False
        elif (event.keysym == "r"):
            canvas.data["paused"] = False
            init(canvas)
    else:
        if (event.keysym == 'Down'):
            moveFallingPiece(canvas, 1, 0)
            redrawAll(canvas)
        elif (event.keysym == "Left"):
            moveFallingPiece(canvas, 0, -1)
            redrawAll(canvas)
        elif (event.keysym == "Right"):
            moveFallingPiece(canvas, 0, 1)
            redrawAll(canvas)
        elif (event.keysym == "a"):  rotateFallingPieceCC(canvas)
        elif (event.keysym == "s"):  rotateFallingPieceC(canvas)
        elif (event.keysym == "m"):  mirrorFallingPiece(canvas)
        elif (event.keysym == "space"):  dropFallingPiece(canvas)
        elif (event.keysym == "r"):  init(canvas)
        elif (event.keysym == "p"):
            if canvas.data["paused"]:  canvas.data["paused"] = False
            else:  canvas.data["paused"] = True

def redrawAll(canvas):
    canvas.delete(ALL)
    drawGame(canvas)
    drawScore(canvas)
    if canvas.data["isGameOver"]:
        gameOverScreen(canvas)
        drawHighScores(canvas)

def init(canvas):
    canvas.data["highScoresPrinted"] = False
    canvas.data["isGameOver"] = False
    emptyColor = ("black")
    board = []
    rows = canvas.data["rows"]
    cols = canvas.data["cols"]
    canvas.data["score"] = 0
    for x in xrange(rows):
        board.append([emptyColor] * cols)
    canvas.data["board"] = board
    canvas.data["emptyColor"] = emptyColor
    pieceA = [[True, True, True, True, True]]
    pieceB = [[True, False, False, False],
              [True, True, True, True]]
    pieceC = [[True, True, False, False],
              [False, True, True, True]]
    pieceD = [[True, True, False],
              [True, True, True]]
    pieceE = [[True, False, True],
              [True, True, True]]
    pieceF = [[True, True, True, True],
              [False, False, True, False]]
    pieceG = [[False, True, False],
              [False, True, False],
              [True, True, True]]
    pieceH = [[True, False, False],
              [True, False, False],
              [True, True, True]]
    pieceI = [[True, True, False],
              [False, True, True],
              [False, False, True]]
    pieceJ = [[True, False, False],
               [True, True, True],
               [False, False, True]]
    pieceK = [[True, False, False],
               [True, True, True],
               [False, True, False]]
    pieceL = [[False, True, False],
               [True, True, True],
               [False, True, False]]
    tetrisPieces = [pieceA, pieceB, pieceC, pieceD, pieceE, pieceF, pieceG, pieceH, pieceI, pieceJ, pieceK, pieceL]
    tetrisPieceColors = ["YellowGreen", "red", "yellow", "magenta", "pink", "cyan", "green", "orange", "VioletRed", "SeaGreen", "PowderBlue", "PeachPuff"]
    canvas.data["tetrisPieces"] = tetrisPieces
    canvas.data["tetrisPieceColors"] = tetrisPieceColors
    newNextFallingPiece(canvas)
    newFallingPiece(canvas)
    redrawAll(canvas)
    if canvas.data["firstGame"]:  canvas.after(1000, timerFired, canvas)

def run(rows, cols):
    root = Tk()
    canvas = Canvas(root, width = cols * 20 + 2 * 30, height = rows * 20 + 2 * 30, bg = "black")
    canvas.pack()
    root.resizable(width = 0, height = 0)
    root.canvas = canvas.canvas = canvas
    canvas.data = {}
    canvas.data["rows"] = rows
    canvas.data["cols"] = cols
    canvas.data["canvasWidth"] = cols * 20 + 2 * 30
    canvas.data["canvasHeight"] = rows * 20 + 2 * 30
    canvas.data["canvasColor"] = "black"
    highScores = [0, 0, 0, 0, 0]
    canvas.data["highScores"] = highScores
    canvas.data["isGameOver"] = True
    canvas.data["firstGame"] = True
    canvas.data["paused"] = False
    instructions(canvas)
    root.bind_all("<KeyPress>", keyPressed)
    root.mainloop()

run(18, 12)

Make your own customizations

If you download the file, I encourage you to change some things to customize the game!  Here are several things that would be easy to add/change:

  • Change the size of the gameboard.  You can do that by changing the last line to be run(height, width) where height and width correspond to the number of blocks for the gameboard.
  • Add/remove/change the game pieces.  In the init function, the game piece shapes and colors are defined.  Make sure the list of colors is at least as long as the list of pieces, or there could be indexing errors.
  • Change all the colors.
  • Change the speed.  I didn't originally write the code to make changing the speed simple, so you'll have to change delay times multiple places in the code.
  • Anything you want!

Back