Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # want to join a pygame community? visit fb.com/groups/pygame
- import math, random, sys, time
- import pygame
- import pygame.gfxdraw
- from pygame.locals import *
- # define some colors
- FUCHSIA = (255, 0, 255)
- PURPLE = (128, 0, 128)
- TEAL = ( 0, 128, 128)
- LIME = ( 0, 255, 0)
- GREEN = ( 0, 128, 0)
- OLIVE = (128, 128, 0)
- YELLOW = (255, 255, 0)
- ORANGE = (255, 165, 0)
- RED = (255, 0, 0)
- MAROON = (128, 0, 0)
- SILVER = (192, 192, 192)
- GREY = (128, 128, 128)
- NIGHT = (32, 32, 32)
- BLUE = ( 0, 0, 255)
- NAVY = ( 0, 0, 128)
- AQUA = ( 0, 255, 255)
- WHITE = (255, 255, 255)
- BLACK = ( 0, 0, 0)
- # define some data
- BLOCKS = {1: {'startingY' : [0, 2, 0, 2], 'block' : [[(-1, 0), (0, 0), (1, 0), (2, 0)], [(1, -2), (1, -1), (1, 0), (1, 1)], [(-1, 0), (0, 0), (1, 0), (2, 0)], [(0, -2), (0, -1), (0, 0), (0, 1)]]}, # long one
- 2: {'startingY' : [0, 1, 0, 1], 'block' : [[(-1, 0), (0, 0), (1, 0), (0, 1)], [(-1, 0), (0, -1), (0, 0), (0, 1)], [(0, 0), (-1, 1), (0, 1), (1, 1)], [(0, -1), (0, 0), (0, 1), (1, 0)]]}, # T shape
- 3: {'startingY' : [0, 1, 0, 1], 'block' : [[(-1, 1), (-1, 0), (0, 0), (1, 0)], [(-1, -1), (0, -1), (0, 0), (0, 1)], [(1, 0), (-1, 1), (0, 1), (1, 1)], [(0, -1), (0, 0), (0, 1), (1, 1)]]}, # L left
- 4: {'startingY' : [0, 1, 0, 1], 'block' : [[(-1, 0), (0, 0), (1, 0), (1, 1)], [(0, -1), (0, 0), (0, 1), (-1, 1)], [(-1, 0), (-1, 1), (0, 1), (1, 1)], [(0, -1), (0, 0), (0, 1), (1, -1)]]}, # L right
- 5: {'startingY' : [0, 1, 0, 1], 'block' : [[(-1, 0), (0, 0), (0, 1), (1, 1)], [(1, -1), (1, 0), (0, 0), (0, 1)], [(-1, 0), (0, 0), (0, 1), (1, 1)], [(0, -1), (0, 0), (-1, 0), (-1, 1)]]}, # lightening 1
- 6: {'startingY' : [0, 1, 0, 1], 'block' : [[(-1, 1), (0, 1), (0, 0), (1, 0)], [(0, -1), (0, 0), (1, 0), (1, 1)], [(-1, 1), (0, 1), (0, 0), (1, 0)], [(-1, -1), (-1, 0), (0, 0), (0, 1)]]}, # lightening 2
- 7: {'startingY' : [1], 'block' : [[(-1, -1), (0, -1), (-1, 0), (0, 0)]]}, # square
- }
- BLOCK_COLORS = [BLACK, RED, TEAL, ORANGE, NAVY, PURPLE, GREEN, YELLOW]
- SCORES = [40, 100, 300, 1200]
- tetrisFont = {
- 'width' : 10,
- 'height' : 14,
- 'space' : 4,
- 'totalWidth' : 14,
- 'a' : [[0, 5, 5, 0], [5, 0, 10, 5], [10, 5, 10, 14], [0, 7, 10, 7], [0, 14, 0, 5]],
- 'b' : [[0, 0, 5, 0], [5, 0, 10, 4], [10, 4, 5, 7], [5, 7, 10, 10], [10, 10, 5, 14], [5, 14, 0, 14], [0, 14, 0, 0]],
- 'c' : [[0, 0, 10, 0], [0, 0, 0, 14], [0, 14, 10, 14]],
- 'd' : [[0, 0, 5, 0], [5, 0, 10, 5], [10, 5, 10, 9], [10, 9, 5, 14], [5, 14, 0, 14], [0, 14, 0, 0]],
- 'e' : [[0, 0, 10, 0], [0, 0, 0, 14], [0, 7, 8, 7], [0, 14, 10, 14]],
- 'f' : [[0, 0, 10, 0], [0, 0, 0, 14], [0, 7, 8, 7]],
- 'g' : [[0, 0, 10, 0], [0, 0, 0, 14], [0, 14, 10, 14], [10, 14, 10, 9], [10, 9, 8, 7]],
- 'h' : [[0, 0, 0, 14], [0, 7, 10, 7], [10, 0, 10, 14]],
- 'i' : [[0, 0, 10, 0], [5, 0, 5, 14], [0, 14, 10, 14]],
- 'j' : [[0, 9, 5, 14], [5, 14, 10, 14], [10, 14, 10, 0]],
- 'k' : [[0, 0, 0, 14], [0, 7, 10, 0], [0, 7, 10,14]],
- 'l' : [[0, 0, 0, 14], [0, 14, 10, 14]],
- 'm' : [[0, 14, 0, 0], [0, 0, 5, 5], [5, 5, 10, 0], [10, 0, 10, 14]],
- 'n' : [[0, 14, 0, 0], [0, 0, 10, 14], [10, 14, 10, 0]],
- 'o' : [[0, 0, 10, 0], [10, 0, 10, 14], [10, 14, 0, 14], [0, 14, 0, 0]],
- 'p' : [[0, 14, 0, 0], [0, 0, 10, 0], [10, 0, 10, 7], [10, 7, 0, 9]],
- 'q' : [[0, 0, 10, 0], [10, 0, 10, 9], [10, 9, 0, 14], [0, 14, 0, 0], [5, 9, 10, 14]],
- 'r' : [[0, 14, 0, 0], [0, 0, 10, 0], [10, 0, 10, 7], [10, 7, 0, 9], [5, 8, 10, 14]],
- 's' : [[0, 12, 3, 14], [3, 14, 10, 14], [10, 14, 10, 9], [10, 9, 0, 5], [0, 5, 0, 0], [0, 0, 7, 0], [7, 0, 10, 2]],
- 't' : [[0, 0, 10, 0], [5, 0, 5, 14]],
- 'u' : [[0, 0, 0, 9], [0, 9, 5, 14], [5, 14, 10,9], [10, 9, 10, 0]],
- 'v' : [[0, 0, 5, 14], [5, 14, 10, 0]],
- 'w' : [[0, 0, 3, 14], [3, 14, 5, 9], [5, 9, 7, 14], [7, 14, 10, 0]],
- 'x' : [[0, 0, 10, 14], [0, 14, 10, 0]],
- 'y' : [[0, 0, 5, 7], [10, 0, 5, 7], [5, 7, 5, 14]],
- 'z' : [[0, 0, 10, 0], [10, 0, 0, 14], [0, 14, 10, 14]],
- '0' : [[0, 0, 10, 0], [10, 0, 10, 14], [10, 14, 0, 14], [0, 14, 0, 0], [0, 14, 10, 0]],
- '1' : [[3, 3, 5, 0], [5, 0, 5, 14]],
- '2' : [[0, 0, 10, 0], [10, 0, 10, 5], [10, 5, 0, 9], [0, 9, 0, 14], [0, 14, 10, 14]],
- '3' : [[0, 0, 10, 0], [10, 0, 10, 14], [10, 14, 0, 14], [0, 7, 10, 7]],
- '4' : [[0, 0, 0, 7], [0, 7, 10, 7], [10, 0, 10, 14]],
- '5' : [[10, 0, 0, 0], [0, 0, 0, 5], [0, 5, 10, 9], [10, 9, 10, 14], [10, 14, 0, 14]],
- '6' : [[0, 0, 0, 14], [0, 14, 10, 14], [10, 14, 10, 9], [10, 9, 0, 5]],
- '7' : [[0, 0, 10, 0], [10, 0, 10, 7], [10, 7, 5, 14]],
- '8' : [[0, 0, 10, 0], [10, 0, 10, 14], [10, 14, 0, 14], [0, 14, 0, 0], [0, 7, 10, 7]],
- '9' : [[10, 14, 10, 0], [10, 0, 0, 0], [0, 0, 0, 5], [0, 5, 10, 9]],
- ':' : [[5, 3, 5, 5], [5, 9, 5, 11]],
- '.' : [[5, 11, 5, 14]],
- '!' : [[5, 0, 5, 7], [5, 11, 5, 14]],
- }
- # exit the program
- def quit():
- for event in pygame.event.get():
- if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
- return True
- return False
- # this function creates a surface using a line "vector" font. the background is transparent
- def fontSurface(font, text, scale, fgColor):
- width = math.ceil(len(text) * (font['totalWidth'] * scale))
- height = math.ceil(font['height'] * scale)
- surface = pygame.Surface((width, height + 1))
- offset = 0
- for char in text:
- if char != ' ':
- lines = font[char]
- for line in lines:
- sx, sy, ex, ey = (line[0] * scale) + offset, (line[1] * scale), (line[2] * scale) + offset, (line[3] * scale)
- pygame.draw.line(surface, fgColor, (sx, sy), (ex, ey), 1)
- offset += font['totalWidth'] * scale
- surface.set_colorkey(0)
- return surface
- # this function draws the block preview above the score and level
- def miniBlock(x, y, blockID):
- global BLOCK_COLORS, BLOCKS
- surface = pygame.display.get_surface() # get the primary display surface
- block = BLOCKS[blockID]['block'][0] # create a shortcut to the next block, rotation 0
- for bit in block:
- pygame.draw.rect(surface, BLOCK_COLORS[blockID], (x + bit[0] * 7, y + bit[1] * 7, 7, 7), 0) # draw block bits to display
- # draw a patterned tile
- # this class creates a tile of diagonal lines and can fill the entire display with it
- # i was going to add a function that drew the tiles in definable area but didn't need it for this project
- class tiles:
- def __init__(self):
- global NIGHT, W, H
- self.tileSize = 32
- self.tile = pygame.Surface((self.tileSize, self.tileSize))
- # draw the lines
- for linePos in range(0, self.tileSize * 2, 8):
- pygame.draw.line(self.tile, NIGHT, (linePos, 0), (linePos - self.tileSize, self.tileSize), 1)
- # get the width and height of the display in tiles
- self.displayWidthInTiles = (W / self.tileSize) * self.tileSize
- if W % self.tileSize > 0: self.displayWidthInTiles += self.tileSize
- self.displayHeightInTiles = (H / self.tileSize) * self.tileSize
- if H % self.tileSize > 0: self.displayHeightInTiles += self.tileSize
- # fill the display with tiles
- def fillDisplay(self, displaySurf):
- global AREAS, W, H
- for tilePosY in range(0, self.displayHeightInTiles, self.tileSize):
- for tilePosX in range(0, self.displayWidthInTiles, self.tileSize):
- displaySurf.blit(self.tile, (tilePosX, tilePosY))
- AREAS.append((0, 0, W, H)) # add the entire display to the update list for pygame.display.update(list) in the main loop
- # this class is responsible for the "zone" in which the blocks appear and fall
- class blockZone:
- def __init__(self):
- global BLOCKS, W, H
- #define the area in which the blocks fall and stack in
- self.height = (int(H * 0.9722222222222222) / 20) * 20
- self.topMargin = (H - self.height) / 2
- self.blockSize = int(float(self.height) / 20)
- self.width = self.blockSize * 10
- self.leftMargin = (W - self.width) / 2
- # the display surface for the blocks to be draw to
- self.bzSurf = pygame.Surface((self.width, self.height))
- # this array is to hold block information 0 = no block, 1+ = block
- self.stack = [[0 for x in range(10)] for y in range(20)]
- # starting variables
- self.currentBlock = random.randint(1, 7)
- self.rotation = 0
- self.blockPosX = 5
- self.blockPosY = BLOCKS[self.currentBlock]['startingY'][self.rotation]
- self.nextBlock = random.randint(1, 7)
- # this functions blits the block zone surface to the primary display
- def blitToDisplay(self, displaySurf):
- global AREAS
- displaySurf.blit(self.bzSurf, (self.leftMargin, self.topMargin))
- AREAS.append((self.leftMargin, self.topMargin, self.width, self.height)) # add the zone rect to the dislay update list
- # draw stack takes the information from the stack array and draws a colored rectangle at x * blocksize and y * blocksize
- # on to the block zone surface
- def drawStack(self):
- global BLOCK_COLORS
- for x in range(10):
- for y in range(20):
- pygame.draw.rect(self.bzSurf, BLOCK_COLORS[self.stack[y][x]], (x * self.blockSize, y * self.blockSize, self.blockSize, self.blockSize), 0)
- # draw blocks is probably mislabelled as it doesn't draw the block to any surface but writes the blocks ID to the stack
- # if the delete parameter is true then the current block is removed from the stack by filling the stack with 0's
- def drawBlock(self, delete = False):
- global BLOCKS, BLOCK_COLORS
- block = BLOCKS[self.currentBlock]['block'][self.rotation]
- if delete:
- blockID = 0
- else:
- blockID = self.currentBlock
- for bit in block:
- y = self.blockPosY + bit[1]
- self.stack[y][self.blockPosX + bit[0]] = blockID
- # fall moves the block down one square
- # because falling can trigger collisions the function checks for these in three ways:
- # falling: does blocky+1 encounters stacked blocks or the block zone floor?
- # full lines: if a block joins the stack and completes lines
- # spawn: if a block spawn into the stack (game over)
- def fall(self):
- global BLOCKS, previousScore, gameOver
- self.drawBlock(True) # delete the current block from the stack so it doesn't detect itself
- if self.collisionOnFall(): # if block y + 1 encounters the stack
- self.drawBlock() # redraw the block onto the stack
- # test for complete lines
- self.testForFullLines()
- # set up new block to spawn
- self.currentBlock = self.nextBlock
- self.nextBlock = random.randint(1, 7)
- self.rotation = 0
- self.blockPosY = BLOCKS[self.currentBlock]['startingY'][0]
- self.blockPosX = 5
- # test for spawn collision (game over)
- gameOver = self.collisionOnMove(False)
- self.drawBlock() # draw newly spawn block to the stack
- # because calling the fall() function is fairly low down in the main
- # loop the drawStack has to be called otherwise the newly spawned block
- # isn't shown
- if gameOver:
- self.drawStack()
- # make previous score different to the actual to trigger a display update of the score
- previousScore = -1
- else:
- # if no collision has taken place then it's ok to fall
- self.blockPosY += 1
- self.drawBlock()
- # collision on fall tests for collision if the current blocks y pos is incremented by one
- def collisionOnFall(self):
- global BLOCKS
- block = BLOCKS[self.currentBlock]['block'][self.rotation]
- tempY = self.blockPosY + 1 # no need to change the real blockPosY as it's just a test
- # scroll through the current block's bit to see if they "hit" a block on the stack
- for bit in block:
- x = self.blockPosX + bit[0]
- y = tempY + bit[1]
- if y > 19: return True # has the block reached the bottom of the block zone
- if self.stack[y][x] > 0: return True # a hit has occurred
- # no collision has taken place
- return False
- # this function does two things. It temporarily rotates the block and/or
- # performs a collision test at the block's current location
- # if the rotate parameter is False no the block will not be rotated before testing for collision
- def collisionOnMove(self, rotate = True):
- global BLOCKS
- if rotate:
- block = BLOCKS[self.currentBlock]['block'][(self.rotation + 1) % len(BLOCKS[self.currentBlock]['block'])]
- else:
- block = BLOCKS[self.currentBlock]['block'][self.rotation]
- for bit in block:
- x = self.blockPosX + bit[0]
- y = self.blockPosY + bit[1]
- if x > 9 or x < 0 or y > 19 or y < 0: return True # check if the block is outside of the block zone
- if self.stack[y][x] > 0: return True # collision with stacked blocks
- # no collision
- return False
- # when a block has settled onto the stack, the stack is tested for complete lines
- # to do this a loop searches for a 0 in the x array of the stack: if 0 in stack[y]
- # for example [1, 2, 2, 1, 5, 0, 0, 0, 0, 1] would not be a complete line because it has a zero in it
- def testForFullLines(self):
- global SCORES, score, level, lines, FPS, SPF
- popList = [] # this is to keep track of the complete lines that need removing from the stack
- for y in range(19, 0, -1): # start at the bottom of the stack and work upwards
- if 0 in self.stack[y]: # ignore lines with 0 in
- continue
- popList.append(y) # add the line number to the popList
- # if lines need deleting then they can't be deleted while iterating through the list
- # because it messes with the list's indexing so to counteract this a second for loop
- # goes through the popList and "pop"s them of the stack
- if popList:
- for y in popList: # pop the complete lines off the stack
- self.stack.pop(y)
- for y in popList: # insert the same number of blank lines at the beginning of the stack, index 0
- self.stack.insert(0, [0 for x in range(10)])
- # update the score and the completed line count. every 10 completed lines increases the level
- # and speed of the falling blocks
- score += SCORES[len(popList) - 1] * level
- lines += len(popList)
- if lines >= 10: # next level?
- lines %= 10
- level += 1
- FPS += 0.3 # increase the frame rate by 0.3fps
- # rotates the current block if there's no collision in doing so
- def rotateBlock(self):
- global BLOCKS
- if not self.collisionOnMove(): # test for collision
- self.rotation += 1
- self.rotation %= len(BLOCKS[self.currentBlock]['block'])
- # define display surface
- W, H = 1280, 720
- HW, HH = W / 2, H / 2
- # initialise display
- pygame.init()
- FONT = pygame.font.SysFont(None, 72)
- DS = pygame.display.set_mode((W, H))
- pygame.display.set_caption("Tetris Clone")
- FPS = 1
- SPF = 1.00 / FPS
- # the areas of the display that need updating
- AREAS = []
- # create the background tiles instance
- TILES = tiles()
- TILES.fillDisplay(DS)
- BZ = blockZone()
- BZ.drawBlock()
- # draw the tetris logo against the side of the stack window
- logo = fontSurface(tetrisFont, "tetris", 8, WHITE)
- verticalLogo = pygame.transform.rotate(logo, 90)
- DS.blit(verticalLogo, (BZ.leftMargin - 10 - verticalLogo.get_rect().width, 10))
- # copy a section of the background for the score and level
- scorePosX = BZ.leftMargin + BZ.width + 10
- scoreRect = (scorePosX, BZ.topMargin, 48, BZ.height)
- scoreBackground = pygame.Surface(scoreRect[2:4])
- scoreBackground.blit(DS, (0, 0), scoreRect)
- # track the score
- previousScore = -1
- score = 0
- # track the level
- level = 1
- # track number of lines completed
- lines = 0
- # monitor key held
- keyHeld = False
- # is it curtains?
- gameOver = False
- # start FPS monitoring
- FPSTime = time.time()
- # main loop
- while not quit() and not gameOver: # esc key or x triggers a game over
- k = pygame.key.get_pressed() # get the pressed keys
- if k[K_DOWN]:
- SPF = 0.05 # increase the frame rate to speed up the falling block
- else:
- SPF = 1.00 / FPS # set the frame rate of the falling block
- # because the main loop is running at full frames per second, hold a key down and moving
- # a block or rotating it would be ridiculously fast so the action can't be repeated until
- # the player and released the key and pressed it again. keyHeld needs to be False
- if k[K_LEFT] and not keyHeld:
- BZ.drawBlock(True) # delete the current block from the stack
- BZ.blockPosX -= 1 # move the current block left
- if BZ.collisionOnMove(False): # check for collision
- BZ.blockPosX += 1 # if collision return position to original location before pressing left
- BZ.drawBlock() # redraw block to the stack
- # same as above but to the right
- if k[K_RIGHT] and not keyHeld:
- BZ.drawBlock(True)
- BZ.blockPosX += 1
- if BZ.collisionOnMove(False):
- BZ.blockPosX -= 1
- BZ.drawBlock()
- # rotate the current block
- if k[K_UP] and not keyHeld:
- BZ.drawBlock(True)
- BZ.rotateBlock()
- BZ.drawBlock()
- # save a snapshot of the game and exit (for debugging really)
- if k[K_RETURN]:
- pygame.image.save(DS, "screenshot.png")
- pygame.quit()
- sys.exit()
- # when a key is pressed it has to be released and pressed again to repeat the action.
- # when calling pygame.key.get_pressed() a list is returned representing all the keys on the keyboard.
- # each key on the keyboard has it's own place in the list. spacebar for instance is at index 32,
- # so k[32] will give you the value 1 if space is pressed or 0 if it isn't. because the only keys
- # used in the game are the arrow keys we can test them all at once to see if they're pressed.
- # k[273:277] represent all 4 arrow keys in the list. If any of them are pressed then the keyHeld is
- # set to True and no other action can be taken by the player until they release the key and press it again.
- if True in k[273:277]:
- keyHeld = True
- else:
- keyHeld = False
- # draw the stack the block zone surface
- BZ.drawStack()
- BZ.blitToDisplay(DS) # blit the block zone surface to the primary display
- # if the score has changed then cover the score area with tiled background we grabbed before the main loop
- # create the score and level surface, rotate it and blit it to the primary display.
- # draw the mini block preview to the primary display and add the score area to the AREAS list so it's
- # updated when pygame.display.update() is called.
- if score != previousScore:
- previousScore = score
- scoreSurf = fontSurface(tetrisFont, "score: {0} level: {1}".format(score, level), 2, WHITE)
- DS.blit(scoreBackground, (scorePosX, BZ.topMargin))
- DS.blit(pygame.transform.rotate(scoreSurf, -90), (scorePosX + 10, BZ.topMargin + 48))
- miniBlock(scorePosX + 17, BZ.topMargin + 10, BZ.nextBlock)
- AREAS.append(scoreRect)
- # this limits the falling blocks to a frame rate:
- if time.time() > FPSTime + SPF:
- BZ.fall()
- FPSTime = time.time()
- # only update the areas of the display listed in the AREAS list (saves CPU)
- pygame.display.update(AREAS)
- AREAS = []
- # game over section, display "game over" and "press esc to quit" over the block zone
- gameOverScale = float(BZ.width) / 140
- gameOverSurface = fontSurface(tetrisFont, "game over!", gameOverScale, WHITE)
- DS.blit(gameOverSurface, (BZ.leftMargin + 10, BZ.topMargin + 10))
- pressEscapeScale = float(BZ.width) / 238
- pressEscapeSurface = fontSurface(tetrisFont, "press esc to quit", pressEscapeScale, WHITE)
- DS.blit(pressEscapeSurface, (BZ.leftMargin, BZ.topMargin + 20 + gameOverSurface.get_rect().height))
- # update the block zone area
- pygame.display.update([(BZ.leftMargin, BZ.topMargin + 10, BZ.width, BZ.height)])
- # wait until esc is pressed
- while not quit():
- pass
- # blit the block zone over the "game over" and "escape to quit" text for the screenshot
- DS.blit(BZ.bzSurf, (BZ.leftMargin, BZ.topMargin))
- # save the display surface to disk using a timestamp for the filename. use png in this case
- # for smaller files sizes
- pygame.image.save(DS, "screenshot-{0}.png".format(time.strftime("%Y%m%d%H%M%S")))
- # quit
- pygame.quit()
- sys.exit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement