# coding: utf-8 """ A simple tetris game. (c) Julian Habrock , 8/2009 bytemuehle.de """ import random import pygame, os import pygame.locals as loc WIDTH = 13 HEIGHT = 25 STONE_SIZE = 20 BORDER_WIDTH = 3 COLORS = {"background":(0, 0, 0), "foreground":(255, 255, 255), "board_background":(150, 160, 170), "board_foreground":(0, 0, 0), "stone_border": (255, 255, 255), "board_border_topleft":(100, 100, 100), "board_border_bottomright":(50, 50, 50)} STONE_COLORS = [ (130, 0, 130), # mangenta (220, 110, 0), # orange (0, 150, 0), # green (0, 0, 220), # blue (190, 0, 0), # red (220, 220, 0), # yellow (0, 220, 220) # cyan ] random.shuffle(STONE_COLORS) def rotate(stone): """matrix rotation to the right""" width, height = len(stone), len(stone[0]) new_stone = [[-1] * width for dummy in xrange(height)] for col in xrange(width): for row in xrange(height): new_stone[height-1-row][col] = stone[col][row] return new_stone def blit_matrix(matrix, surf, pos): """ blit a 2d array on a surface at the given position """ for row in xrange(len(matrix)): for col in xrange(len(matrix[0])): if matrix[row][col]: surf.fill(STONE_COLORS[matrix[row][col]-1], (col*STONE_SIZE+pos[0], row*STONE_SIZE+pos[1], STONE_SIZE, STONE_SIZE)) pygame.draw.rect(surf, COLORS["stone_border"], (col*STONE_SIZE+pos[0], row*STONE_SIZE+pos[1], STONE_SIZE, STONE_SIZE), 1) FORMS = [ [ [1, 1, 1], [0, 1, 0] ], [ [1, 1], [1, 1] ], [ [1, 0], [1, 0], [1, 1] ], [ [0, 1], [0, 1], [1, 1] ], [ [1, 1, 1, 1] ], [ [0, 1, 1], [1, 1, 0] ], [ [1, 1, 0], [0, 1, 1] ], ] STONES = [] def build_stones(): """build colored stones in all rotations""" for index, form in enumerate(FORMS): stone = [[[col and index+1 for col in row] for row in form]] for dummy in xrange(3): stone.append(rotate(stone[-1])) STONES.append(stone) build_stones() TITLE = [ [1, 1, 1, 0, 2, 2, 0, 3, 3, 3, 0, 4, 4, 0, 5, 0, 0, 6, 6], [0, 1, 0, 0, 2, 7, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0], [0, 1, 0, 0, 2, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 6, 6, 0]] class Stone(object): """A stone that can be moved and rotated by the player while falling down""" def __init__(self, board): """Initialize a stone with a random form""" self.form = random.randint(0, 6) self.rotation = 0 self.board = board self.position = [(WIDTH-self.get_width())/2, 0] def _validate(self, x_add=0, y_add=0, rot_add=0): """internal function to move or rotate the stone if possible""" new_rot = (self.rotation + rot_add) % 4 new_pos = self.position[0] + x_add, self.position[1] + y_add if not self.board.check_position(STONES[self.form][new_rot], new_pos): return False self.position = new_pos self.rotation = new_rot return True def turn_left(self): """if possible, rotate the stone to the left""" return self._validate(rot_add=1) def turn_right(self): """if possible, rotate the stone to the right""" return self._validate(rot_add=3) def move_left(self): """if possible, move the stone one field to the left""" return self._validate(x_add=-1) def move_right(self): """if possible, move the stone one field to the right""" return self._validate(x_add=1) def move_down(self): """if possible, move the stone one field down""" return self._validate(y_add=1) def fall_down(self): """move the stone down as far as possible""" fallen = False while self.move_down(): fallen = True if fallen: self.board.reset_counter() def update(self, events): """handle events""" for event in events: if event.type == loc.KEYDOWN: if event.key == loc.K_LEFT: self.move_left() elif event.key == loc.K_RIGHT: self.move_right() elif event.key == loc.K_UP: self.turn_left() elif event.key == loc.K_DOWN: self.turn_right() elif event.key == loc.K_SPACE: self.move_down() elif event.key in (loc.K_RETURN, loc.K_KP_ENTER): self.fall_down() def get_width(self): """return the width of the stone""" return len(self()[0]) def get_height(self): """return the height of the stone""" return len(self()) def __call__(self): """return the stone as raw data (2d-array)""" return STONES[self.form][self.rotation] def __getitem__(self, value): """ make the stone data accessible directly """ return self()[value] class Board(object): """Main game object that manages the stones and the rest of the game play""" def __init__(self, screen, pos): """Initialize everything""" self.font = pygame.font.Font(None, 26) self.screen = screen self.speed = 10 self.blit_if_not_running = False self.status = "running" self.fields = None self.active_stone = self.next_stone = None self.counter = self.removed_lines = 0 left, top = pos self.rect = pygame.Rect((left, top, STONE_SIZE*WIDTH, STONE_SIZE*HEIGHT)) #draw border bdw = BORDER_WIDTH right, bottom = left+STONE_SIZE*WIDTH, top+STONE_SIZE*HEIGHT pygame.draw.polygon(screen, COLORS["board_border_topleft"], [(left, top), (right, top), (right+bdw, top-bdw), (left-bdw, top-bdw), (left-bdw, bottom+bdw), (left, bottom)]) pygame.draw.polygon(screen, COLORS["board_border_bottomright"], [(right, bottom), (left, bottom), (left-bdw, bottom+bdw), (right+bdw, bottom+bdw), (right+bdw, top-bdw), (right, top)]) self.init_game() def init_game(self): """(re)set all variables to start a new game""" self.fields = [[0] * WIDTH for dummy in xrange(HEIGHT)] self.active_stone = Stone(self) self.next_stone = Stone(self) self.counter = 0 self.removed_lines = 0 self.status = "running" def clear_lines(self): """remove full lines""" self.fields = [row for row in self.fields if 0 in row] for dummy in xrange(HEIGHT-len(self.fields)): self.fields.insert(0, [0]*WIDTH) self.removed_lines += 1 def new_stone(self): """ add the current active stone to the field and brings a new stone up to the screen """ # embed active stone to the board assert self.check_position(self.active_stone(), self.active_stone.position) pos = self.active_stone.position for row in xrange(self.active_stone.get_height()): board_row = row + pos[1] for col in xrange(self.active_stone.get_width()): if self.active_stone[row][col]: board_col = col + pos[0] self.fields[board_row][board_col] = self.active_stone[ row][col] # check for full lines self.clear_lines() # renew active stone self.active_stone = self.next_stone self.next_stone = Stone(self) if not self.check_position(self.active_stone(), self.active_stone.position): self.status = "gameover" self.blit_text("GameOver") def check_position(self, stone, pos): """ check whether ``pos`` is a valid position for ``stone`` ``Parameters`` stone : 2d array stone form pos : (x, y) tuple position of the stone """ x_pos, y_pos = pos # validate board position if not (0 <= x_pos <= WIDTH-len(stone[0]) and 0 <= y_pos <= HEIGHT-len(stone)): return False # check for collisions with other stones for row in xrange(len(stone)): board_row = row + pos[1] for col in xrange(len(stone[0])): board_col = col + pos[0] if stone[row][col] and self.fields[board_row][board_col]: return False return True def update(self, events): """Event processing and stone handling""" for event in events: if event.type == loc.KEYDOWN: if event.key == loc.K_p: if self.status == "running": self.status = "paused" self.blit_text("paused") elif self.status == "paused": self.status = "running" if (event.key in (loc.K_RETURN, loc.K_SPACE) and self.status == "gameover"): self.init_game() if self.status == "running": self.active_stone.update(events) self.counter += 1 self.counter %= max(self.speed - (self.removed_lines / 5), 1) if not self.counter: if not self.active_stone.move_down(): self.clear_lines() self.new_stone() def blit(self): """ if neccessary, blit the board, next stone and information text and return a list of dirty rects """ if self.status != "running": if self.blit_if_not_running: self.blit_if_not_running = False return self.rect return [] xpos, ypos = self.rect.topleft text_surf = self.font.render(" lines: %i | level: %i " % ( self.removed_lines, self.removed_lines/5+1), 1, COLORS["foreground"], COLORS["background"]) text_rect = text_surf.get_rect(centerx=self.rect.centerx, bottom=ypos+HEIGHT*STONE_SIZE+BORDER_WIDTH+30) drects = [ self.screen.fill(COLORS["board_background"], self.rect), self.screen.fill(COLORS["background"], (xpos, ypos-100, STONE_SIZE*WIDTH, 100-BORDER_WIDTH)), text_rect] self.screen.blit(text_surf, text_rect) blit_matrix(self.next_stone(), self.screen, (xpos+100, ypos-80-BORDER_WIDTH)) blit_matrix(self.fields, self.screen, self.rect.topleft) stone_x, stone_y = self.active_stone.position blit_matrix(self.active_stone(), self.screen, (stone_x*STONE_SIZE+xpos, stone_y*STONE_SIZE+ypos, )) return drects def blit_text(self, text): """fade the board and blit a text above the board, centered""" surf = pygame.Surface(self.rect.size).convert_alpha() surf.fill(COLORS["board_background"]+(200,)) self.screen.blit(surf, self.rect) text_surf = self.font.render(text, 1, COLORS["board_foreground"]) text_rect = text_surf.get_rect(center=self.rect.center) self.screen.blit(text_surf, text_rect) self.blit_if_not_running = True def reset_counter(self): """reset the internal counter""" self.counter = 1 def main(): """main game function with the mainloop.""" # Initialize Everything os.environ["SDL_VIDEO_WINDOW_POS"] = "center" pygame.init() icon = pygame.Surface((32, 32)) icon.set_colorkey((0, 0, 0)) offset = (32 - min(STONE_SIZE, 30)) / 2 rect = (offset, offset, STONE_SIZE, STONE_SIZE) icon.fill((0, 150, 0), rect) pygame.draw.rect(icon, COLORS["stone_border"], rect, 1) pygame.display.set_icon(icon) pygame.key.set_repeat(150, 30) screen = pygame.display.set_mode((500, 750)) screen.fill(COLORS["background"]) pygame.display.set_caption('T3TR!5 - by jug') # create game objects clock = pygame.time.Clock() board = Board(screen, (120, 200)) # blit the title blit_matrix(TITLE, screen, (60, 20)) pygame.display.flip() while 1: clock.tick(40) events = pygame.event.get() for event in events: if ((event.type == loc.KEYDOWN and event.key == loc.K_ESCAPE) or event.type == loc.QUIT): return board.update(events) pygame.display.update(board.blit()) if __name__ == '__main__': main()