Sideways Tetris prototype


This is a little prototype game that I made to answer the question: What if tetris was played on a hill?

Tetris theme is as performed by me on a synth. You can download the game here (requires python and pygame), or view the hackish source code after the break.

#!/usr/bin/env python
#
# Sideways Tetris Proof of Concept
# By Matt Mets, completed in 2009
#
# This code is released into the public domain.  Attribution is appreciated.
 
import sys, pygame, random
import numpy
from pygame.locals import *
 
pygame.init()
 
# Seed the random number generator using the system time
random.seed()
 
# Set up the game screen
size = width, height = 1024,768
screen = pygame.display.set_mode(size, DOUBLEBUF)
pygame.display.set_caption('Sideways Tetris')
 
# Start the background music
pygame.mixer.init(22050, -16, 2, 3072)
sound = pygame.mixer.Sound('theme.ogg')
sound.play(-1)
 
 
# This class abstracts drawing the pieces on the board
class boardDrawer():
    def __init__(self,screen):
        self.diamondSprites = []
 
        self.diamondSprites.append(pygame.image.load("sprites/jewel_blue.png").convert_alpha())
        self.diamondSprites.append(pygame.image.load("sprites/jewel_red.png").convert_alpha())
        self.diamondSprites.append(pygame.image.load("sprites/jewel_green.png").convert_alpha())
        self.diamondSprites.append(pygame.image.load("sprites/jewel_yellow.png").convert_alpha())
        self.diamondSprites.append(pygame.image.load("sprites/jewel_purple.png").convert_alpha())
        self.diamondSprites.append(pygame.image.load("sprites/jewel_orange.png").convert_alpha())
        self.diamondSprites.append(pygame.image.load("sprites/jewel_gray.png").convert_alpha())
 
        self.screen = screen
        # drawing specific things
        self.xOrigin = 520
        self.yOrigin = 727
        self.xOffset = 20
        self.yOffset = 20
 
    def drawDiamond(self, part, color):
        self.screen.blit(self.diamondSprites[color - 1], self.getCoords(part))
 
 
    def getCoords(self, part):
        xPiecePos = self.xOrigin + self.yOffset*part[1] - self.xOffset*part[0]
        yPiecePos = self.yOrigin - self.yOffset*part[1] - self.xOffset*part[0]
 
        return [xPiecePos, yPiecePos]
 
 
# This class holds all the loose parts that are not part of the player piece
class looseParts():
    def __init__(self, boardDrawer):
        self.boardDrawer = boardDrawer
 
        self.xMax = 8
        self.yMax = 8
        self.height = 20
 
        self.partArrayA = self.xMax + self.yMax + 1
        self.partArrayB = self.height
        # Note: The extra 1 is because we are cheating.  The extra bit is to avoid
        #       indexing off of the right hand side of the board when a part is
        #       next to it.
        self.partArray = numpy.zeros([self.partArrayA + 1, self.partArrayB], dtype=numpy.int)
 
 
    def draw(self):
        for a in range(0, self.partArrayA):
            for b in range(0, self.partArrayB):
                if self.partArray[a,b] != 0:
                    self.boardDrawer.drawDiamond(self.matrixToBoard(a,b), self.partArray[a,b])
 
    def addParts(self, newParts):
        for part in newParts:
            self.partArray[self.boardToMatrix(part[0], part[1])] = part[2]
 
        self.checkLineCompleted()
 
    def checkLineCompleted(self):
        # Check if a line has been completed, and remove it.
 
        # Translation array.  We store where things are going, then move
        # them at the end.
        translateArray = numpy.zeros([self.partArrayA, self.partArrayB], dtype=numpy.int)
        foundCompleted = False
 
        # Check for full Vs
        for b in range(0, self.partArrayB):
            valid = True
            for a in range(0, self.partArrayA):
                if (self.partArray[a, b] == 0):
                    valid = False
            if (valid):
                foundCompleted = True
                print "found v to remove: ", b
                # Mark all rows above this one so they can fall down
                for B in range(b + 1, self.partArrayB):
                    for A in range(0, self.partArrayA):
                        translateArray[A,B] += 1
 
        # Now, if there were any collisions, go through the board and do all the translations
        if (foundCompleted):
            for b in range(0, self.partArrayB):
                for a in range(0, self.partArrayA):
                    if (translateArray[a,b] > 0):
                        self.partArray[a,b-translateArray[a,b]]=self.partArray[a,b]
                        self.partArray[a,b] = 0
 
 
    def collides(self, part):
        collides = False
 
        # Check left hand side of board
        if (part[0] <= self.xMax):
            if (part[1] < 0):
                collides = True
        else:
            if (part[1] < (part[0] - self.xMax)):
                collides = True
 
        # And right hand side
        if (part[1] <= self.yMax):
            if (part[0] < 0):
                collides = True
        else:
            if (part[0] < (part[1] - self.yMax)):
                collides = True
 
        if (collides == False):
            # Check if it collides with any pieces on the board
            overlapX = part[0] % 1
            overlapY = part[1] % 1
 
            if (overlapX and overlapY):
                if (self.partArray[self.boardToMatrix(part[0]-overlapX, part[1]-overlapY)] != 0) or \
                   (self.partArray[self.boardToMatrix(part[0]-overlapX, part[1]+overlapY)] != 0) or \
                   (self.partArray[self.boardToMatrix(part[0]+overlapX, part[1]-overlapY)] != 0) or \
                   (self.partArray[self.boardToMatrix(part[0]+overlapX, part[1]+overlapY)] != 0):
                    collides = True
            elif (overlapX):
                if (self.partArray[self.boardToMatrix(part[0]-overlapX, part[1])] != 0) or \
                   (self.partArray[self.boardToMatrix(part[0]+overlapX, part[1])] != 0):
                    collides = True
            elif (overlapY):
                if (self.partArray[self.boardToMatrix(part[0], part[1]-overlapY)] != 0) or \
                   (self.partArray[self.boardToMatrix(part[0], part[1]+overlapY)] != 0):
                    collides = True
            else:
                if (self.partArray[self.boardToMatrix(part[0], part[1])] != 0):
                    collides = True
 
        return collides
 
    def boardToMatrix(self,x,y):
        # Convert board coordinates to partition matrix coordinates
        if (x >= y):
            return self.xMax - x + y, y
        else:
            return self.xMax - x + y, x
 
    def matrixToBoard(self,a,b):
        # Convert partition matrix coordinates to board coordinates
        if (a <= self.xMax):
            return self.xMax - a + b, b
        else:
            return b, a - self.xMax + b
 
 
class tetrisBlock():
    def __init__(self, boardDrawer, looseParts):
        self.boardDrawer = boardDrawer
        self.grid = looseParts
 
        self.orientation = 0    # 0=0 degrees, 1=90 degrees, etc
        self.xPos = 18          # Center of the piece by weight, in board coordinates
        self.yPos = 18
 
        self.parts = None       # Array of part coordinates, filled in by a subclass.
        self.color = 1
 
        self.updatesPerDrop = 20 # Number of times update() must be called before the piece falls
        self.updateCount = 0    # Counter for above.
 
        self.valid = 1          # 1=valid, 0=invalid.  Invalid parts are stuck and should be replaced.
 
    def tryMove(self, rightKey, leftKey, upKey, downKey):
        xChange = 0
        yChange = 0
        oChange = 0
 
        # only one of these can happen
        if (rightKey and downKey):
            xChange = -1
        elif (leftKey and downKey):
            yChange = -1
        elif (rightKey):
            xChange = -.5
            yChange = .5
        elif (leftKey):
            xChange = .5
            yChange = -.5
        elif (downKey):
            xChange = -.5
            yChange = -.5
        elif (upKey):
            oChange = 1
 
        # If there was a move, check that it is valid
        if (self.moveValid(xChange, yChange, oChange)):
            self.xPos += xChange
            self.yPos += yChange
            self.orientation = (self.orientation + oChange) %4
 
 
    def moveValid(self, xChange, yChange, oChange):
        for part in self.parts:
            if (self.grid.collides(self.translatePartTest(part, self.xPos+ xChange, \
                                                                self.yPos + yChange,
                                                                (self.orientation + oChange)%4))):
               return 0
 
        return 1
 
    def translatePart(self, part):
        return self.translatePartTest(part, self.xPos, self.yPos, self.orientation)
 
    def translatePartTest(self, part, xPos, yPos, orientation):
        if (orientation == 0):
            newPart = (part[0], part[1])
        elif (orientation == 1):
            newPart = (-part[1], part[0])
        elif (orientation == 2):
            newPart = (-part[0], -part[1])
        else:
            newPart = (part[1], -part[0])
 
        return (newPart[0] + xPos, newPart[1] + yPos)
 
    def update(self):
        # Check if the part should be dropped.
        self.updateCount += 1
        if (self.updateCount >= self.updatesPerDrop):
            self.updateCount = 0
 
            # If the part can be moved down, then do so; otherwise, stick it to the
            # board and make a new part.  If it is stuck out of bounds, the game is over.
 
            if (self.moveValid(-.5,-.5,0)):
                self.xPos -= .5
                self.yPos -= .5
            else:
                self.valid = 0
 
    def getParts(self):
        # Translate the relative coordinates into world coordinates and return that list.
        translated = []
 
        for part in self.parts:
            translatedPart = self.translatePart(part)
            translated.append((translatedPart[0], translatedPart[1], self.color))
 
        return translated
 
    def draw(self):
        for part in self.parts:
            self.boardDrawer.drawDiamond(self.translatePart(part), self.color)
 
 
class squareBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (0,0),(0,-1),(-1,-1),(-1,0)    # Blocks in the part
        self.color = 1
 
class longBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (0,-2),(0,-1),(0,0),(0,1)    # Blocks in the part
        self.color = 2
 
class teeBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (0,-1),(0,0),(0,1),(1,0)    # Blocks in the part
        self.color = 3
 
class rightSBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (0,-1),(0,0),(1,0),(1,1)    # Blocks in the part
        self.color = 4
 
class leftSBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (-1,0),(0,0),(0,1),(1,1)    # Blocks in the part
        self.color = 5
 
class rightLBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (0,1),(0,0),(0,-1),(-1,-1)    # Blocks in the part
        self.color = 6
 
class leftLBlock(tetrisBlock):
    def __init__(self, board, grid):
        tetrisBlock.__init__(self, board, grid)
        self.parts = (0,1),(0,0),(0,-1),(1,-1)    # Blocks in the part
        self.color = 7
 
def newBlock(board, parts):
    a = random.randint(0,7)
    if (a == 0):
        return longBlock(board, parts)
    elif (a == 1):
        return squareBlock(board, parts)
    elif (a == 2):
        return teeBlock(board, parts)
    elif (a == 3):
        return leftSBlock(board, parts)
    elif (a == 4):
        return rightSBlock(board, parts)
    elif (a == 5):
        return rightLBlock(board, parts)
    else:
        return leftLBlock(board, parts)
 
clock = pygame.time.Clock()
 
background = pygame.image.load("sprites/background.png").convert()
 
board = boardDrawer(screen)
parts = looseParts(board)
block = newBlock(board, parts)
 
# Make the keyboard do autorepeat
pygame.key.set_repeat(100,30)
 
# for screencap
#loopcount = 0
 
while 1:
    # Check our event queue
 
    keyPressed = 0    # Just record that any game key was pressed, sort them out later.
 
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if (hasattr(event, 'key') and event.type == KEYDOWN):
            if (event.key == K_RIGHT):  keyPressed = 1
            elif event.key == K_LEFT:   keyPressed = 1
            elif event.key == K_UP:     keyPressed = 1
            elif event.key == K_DOWN:   keyPressed = 1
            elif event.key == K_ESCAPE: sys.exit(0)
 
    if (keyPressed):
        # Ignore the event at this point, and just grab whatever keys are
        # are pressed down.  The event seems to only repeat the last pressed
        # key for some reason.
        keystate = pygame.key.get_pressed()
        block.tryMove(keystate[K_RIGHT], keystate[K_LEFT], keystate[K_UP], keystate[K_DOWN])
 
    # Update (drop) the block, if necessary
    block.update()
 
    # Check if the block got stuck.  If it did, add the parts to the loose
    # parts list and then make a new one.
    if (not block.valid):
        parts.addParts(block.getParts())
 
        # Check if there is a line completion
        block = newBlock(board, parts)
 
    # Draw the background
    screen.blit(background, [0,0])
 
    # Then the parts at the bottom of the screen
    parts.draw()
 
    # Then the current block
    block.draw()
 
    # for screencap
#    pygame.image.save(screen, ("screen_cap/no_%04i.jpg" % loopcount))
#    loopcount += 1
 
    # And display
    pygame.display.flip()
 
    # Enforce a max of 30fps
    clock.tick_busy_loop(30)
This entry was posted in Journal, Ridiculous. Bookmark the permalink.

5 Responses to Sideways Tetris prototype

  1. Robin J. says:

    Not Tetris! I have nightmares playing that game where the walls keep getting moved out on me. I don’t need to have the nighttime stress of playing it sideways.

  2. Nancy says:

    The sideways tetris board looks like it would be a stylish necktie!

  3. mahto says:

    @Robin: Oh no! I didn’t mean to give anyone nightmares. Pretend it is pokemon?

    @Nancy: Yes, yes indeed!

  4. chris coleman says:

    Very slick! How long do you think it took you to write that?

  5. mahto says:

    @chris coleman
    I’m not sure how long the game took to write, I think a few long nights of work.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>