From 239338305364e076f4a23de0e869acb38e767b2e Mon Sep 17 00:00:00 2001 From: Leah Alpert Date: Fri, 26 Aug 2011 19:17:14 -0700 Subject: Updated tetris to display animation before and after game. Moved all necessary files into top-level diractory. Added handling of ESC to ddrinput. Tetris is now functional. --- README | 11 ++ ddrinput.py | 10 +- tetris.py | 376 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tetris_shape.py | 171 ++++++++++++++++++++++++++ 4 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 tetris.py create mode 100644 tetris_shape.py diff --git a/README b/README index e400562..c172172 100644 --- a/README +++ b/README @@ -15,4 +15,15 @@ to do that. This is going to be boss. +Tetris Instructions: +One or two players can play at once. +Press UP to join the game +Once all players have joined, press DOWN to start the game +Use LEFT and RIGHT to move your shape, UP to rotate, and DOWN to make it drop faster +When you complete a line it will disappear from the board and you gain one point. Your score is displayed in binary below your board. +If you clear two or more lines at once, n-1 incomplete lines will appear at the bottom of your opponent’s board. +When one player’s board reaches the top of the screen, they lose and the other player wins. +If neither player loses after 3 minutes, the player with the higher score wins. + +Press ESC to exit the program. diff --git a/ddrinput.py b/ddrinput.py index 92bc123..a1bf482 100644 --- a/ddrinput.py +++ b/ddrinput.py @@ -12,6 +12,8 @@ KEY_A = 97 KEY_S = 115 KEY_D = 100 KEY_W = 119 +KEY_SPACE = 32 +KEY_ESC = 27 DIRECTIONS = {0:'LEFT', 1:'RIGHT', 2:'UP', 3:'DOWN'} class DdrInput(object): @@ -23,7 +25,7 @@ class DdrInput(object): DEBUG MODE: - Use the arrow keys for player 1, asdw for player 0. + Use the arrow keys. Hold down a modifier (alt, control, etc.) to get player 2 """ def __init__(self, debug_mode=True): """ @@ -97,6 +99,12 @@ class DdrInput(object): elif event.key == KEY_W: player_index = 0 player_move = UP + elif event.key == KEY_ESC: + player_index = 2 + player_move = "DIE" + elif event.key == KEY_SPACE: + player_index = 1 + player_move = "DROP" if player_move != None: return (player_index, player_move) diff --git a/tetris.py b/tetris.py new file mode 100644 index 0000000..827255b --- /dev/null +++ b/tetris.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python +""" +Tetris Tk - A tetris clone written in Python using the Tkinter GUI library. + +Controls: + Left/a Move left + Right/d Move right + Up/w Rotate / add player + Down/s Move down / start game +""" + +from time import sleep, time +import random +import sys +from renderer import PygameRenderer +from tetris_shape import * +from ddrinput import DdrInput +from ddrinput import DIRECTIONS +import pygame + +MAXX = 10 +MAXY = 18 +NO_OF_LEVELS = 10 + +(LEFT, RIGHT, UP, DOWN) = range(4) + +COLORS = ["orange", "red", "green", "blue", "purple", "yellow", "magenta"] +LEVEL_COLORS = ["red", "orange red", "orange", "yellow", + "green yellow", "green", "turquoise", "blue", "blue violet", "purple"] +#COLORS = ["gray"] + +class Board(): + """ + The board represents the tetris playing area. A grid of x by y blocks. + Stores blocks that have landed. + """ + def __init__(self, max_x=10, max_y=20): + # blocks are stored in dict of (x,y)->"color" + self.landed = {} + self.max_x = max_x + self.max_y = max_y + + def clear(self): + self.landed = {} + + def receive_lines(self, num_lines): + #shift lines up + for y in range(self.max_y-num_lines): + for x in xrange(self.max_x): + block_color = self.landed.pop((x,y+num_lines),None) + if block_color: + self.landed[(x,y)] = block_color + #put in new lines + for j in range(num_lines): + for i in random.sample(xrange(self.max_x), random.choice([6,7])): + self.landed[(i,self.max_y-1-j)] = random.choice(COLORS) + + def check_for_complete_row( self, blocks ): + """ + Look for a complete row of blocks, from the bottom up until the top row + or until an empty row is reached. + """ + rows_deleted = 0 + + # Add the blocks to those in the grid that have already 'landed' + for block in blocks: + self.landed[ block.coord() ] = block.color + + empty_row = 0 + # find the first empty row + for y in xrange(self.max_y -1, -1, -1): + row_is_empty = True + for x in xrange(self.max_x): + if self.landed.get((x,y), None): + row_is_empty = False + break; + if row_is_empty: + empty_row = y + break + + # Now scan up and until a complete row is found. + y = self.max_y - 1 + while y > empty_row: + + complete_row = True + for x in xrange(self.max_x): + if self.landed.get((x,y), None) is None: + complete_row = False + break; + + if complete_row: + rows_deleted += 1 + + #delete the completed row + for x in xrange(self.max_x): + self.landed.pop((x,y)) + + # move all the rows above it down + for ay in xrange(y-1, empty_row, -1): + for x in xrange(self.max_x): + block_color = self.landed.pop((x,ay), None) + if block_color: + dx,dy = (0,1) + self.landed[(x+dx, ay+dy)] = block_color + + # move the empty row index down too + empty_row +=1 + # y stays same as row above has moved down. + else: + y -= 1 + + # return the number of rows deleted. + return rows_deleted + + def check_block( self, (x, y) ): + """ + Check if the x, y coordinate can have a block placed there. + That is; if there is a 'landed' block there or it is outside the + board boundary, then return False, otherwise return true. + """ + if x < 0 or x >= self.max_x or y < 0 or y >= self.max_y: + return False + elif self.landed.has_key( (x, y) ): + return False + else: + return True + +#represents a player. each player has a board, other player's board, +#current shape, score, etc +class Player(): + def __init__(self, player_id, gs, myBoard, otherBoard): + print "initialize player" + self.id = player_id + self.board = myBoard + self.other_board = otherBoard + self.score = 0 + self.gs = gs + self.shape = self.get_next_shape() + + def handle_move(self, direction): + #if you can't move then you've hit something + if self.shape: + if direction==UP: + self.shape.rotate(clockwise=False) + else: + if not self.shape.move( direction ): + # if you're heading down then the shape has 'landed' + if direction == DOWN: + points = self.board.check_for_complete_row( + self.shape.blocks) + #del self.shape + self.shape = self.get_next_shape() + + self.score += points + if self.gs.num_players == 2: + if points > 1: + self.other_board.receive_lines(points-1) + + # If the shape returned is None, then this indicates that + # that the check before creating it failed and the + # game is over! + if self.shape is None: + self.gs.state = "ending" #you lost! + print "ENDING GAME.... PLAYER",self.id,"LOST" + if self.gs.num_players == 2: + self.gs.winner = (self.id + 1) % 2 + else: + self.gs.winner = self.id + + + # do we go up a level? + if (self.gs.level < NO_OF_LEVELS and + self.score >= self.gs.thresholds[self.gs.level]): + self.gs.level+=1 + self.gs.delay-=100 + + # Signal that the shape has 'landed' + return False + return True + + def move_my_shape( self ): + if self.shape: + self.handle_move( DOWN ) + + def get_next_shape( self ): + #Randomly select which tetrominoe will be used next. + the_shape = self.gs.shapes[ random.randint(0,len(self.gs.shapes)-1) ] + return the_shape.check_and_create(self.board) + +#contains variables that are shared between the players: +#levels, delay time, etc +class GameState(): + def __init__(self, gui): + self.shapes = [square_shape, t_shape,l_shape, reverse_l_shape, + z_shape, s_shape,i_shape ] + self.num_players = 0 + self.level = 1 + self.delay = 800 + self.thresholds = range(10,100,10) + self.state = "waiting" #states: waiting (between games), playing, ending + self.winner = None #winning player id + + +#runs the overall game. initializes both player and any displays +class TetrisGame(object): + + #one-time initialization for gui etc + def __init__(self): + print "initialize tetris" + self.gui = PygameRenderer() + self.input = DdrInput() + while True: + self.init_game() + + #initializes each game + def init_game(self): + print "init next game" + self.boards = [Board(MAXX,MAXY), Board(MAXX,MAXY)] + self.players = [None,None] + self.gameState = GameState(self.gui) + self.board_animation(0,"up_arrow") + self.board_animation(1,"up_arrow") + self.update_gui() + self.handle_input() #this calls all other functions, such as add_player, start_game + + def add_player(self,num): # 0=left, 1=right + print "adding player",num + if self.players[num]==None: + self.boards[num].clear() + p = Player(num, self.gameState, self.boards[num], self.boards[(num+1)%2]) + self.players[num] = p + self.board_animation(num,"down_arrow") + self.gameState.num_players+=1 + self.update_gui() + + def start_game(self): + print "start game" + self.boards[0].clear() + self.boards[1].clear() + self.gameState.state = "playing" + self.update_gui() + self.gravity() + +#FIX THIS FOR GAME-OVER + def handle_input(self): + game_on = True + TIME_LIMIT = 3*60 + start_time = time() + drop_time = time() + while game_on: + if (self.gameState.state=="ending") or (self.gameState.state=="playing" and time()-start_time > TIME_LIMIT): + print "GAME OVER" + self.end_game() + game_on = False + return + if self.gameState.state=="playing" and time()-drop_time > self.gameState.delay/1000.0: + self.gravity() + drop_time = time() + if self.gameState.state != "ending": + self.update_gui() + + ev = self.input.poll() + if ev: + #print "EVENT",ev + player,direction = ev + #print "Player",player,direction + if direction == "DIE": + game_on = False + pygame.quit() + sys.exit() + if self.gameState.state=="playing": + if self.players[player]!=None: + #DROP is only for debugging purposes for now, to make the game end. + if direction == "DROP": + while self.players[player].handle_move( DOWN ): + pass + else: + self.players[player].handle_move(direction) + elif self.gameState.state == "waiting": + if direction==UP: + self.add_player(player) + elif direction==DOWN: + if self.players[player]!=None: + self.start_game() + if self.gameState.state != "ending": + self.update_gui() + + + def gravity(self): + for p in self.players: + if p: + p.move_my_shape() + + def update_gui(self): + self.gui.render_game(self.to_dict()) + + def end_game(self): + if self.gameState.winner!=None: + winner_id = self.gameState.winner + print "in end_game; player",winner_id,"wins" + else: + #CHANGE THISSSS + print "0 WINS BY DEFAULT FOR NOW..." + winner_id = 0 + self.animate_ending(winner_id) + + def board_animation(self, board_id, design): + b = self.boards[board_id] + d = self.create_shapes(design) + for coord in d: + b.landed[coord]="green" + + def animate_ending(self,winner_board): + print "game over, display animation" + self.board_animation(winner_board,"outline") + self.update_gui() + for i in range(250): + print i, + + def create_shapes(self,design): #in progress..... + shapes = {} + y = 4 + up_diags = [(1,y+4),(1,y+3),(2,y+3),(2,y+2),(3,y+2),(3,y+1), + (8,y+4),(8,y+3),(7,y+3),(7,y+2),(6,y+2),(6,y+1)] + down_diags = [(x0,10-y0+2*y) for (x0,y0) in up_diags] + line = [(i,j) for i in [4,5] for j in range(y,y+11)] + up_arrow = line[:] + for xy in up_diags: + up_arrow.append(xy) + down_arrow = line[:] + for xy in down_diags: + down_arrow.append(xy) + sides = [(i,j) for i in [0,9] for j in range(18)] + tb = [(i,j) for i in range(10) for j in [0,17]] + outline = tb + sides + + shapes["down_arrow"] = down_arrow + shapes["up_arrow"] = up_arrow + shapes["outline"] = outline + shapes["test"] = [(5,5)] + + return shapes[design] + + def to_dict(self): + d = {} + for n in range(2): + board = self.boards[n] + offset = n*MAXX + + #blocks + for (x,y) in board.landed: + d[(x+offset,y)] = board.landed[(x,y)] + + if self.players[n]!=None: + p = self.players[n] + #shapes + if p.shape: + blocks = p.shape.blocks + for b in blocks: + d[(b.x+offset*n,b.y)] = b.color + + #score + score = p.score + for i in range(10): + bit = score%2 + score = score>>1 + coord = (MAXX-1-i + offset, MAXY+1) + if bit: + d[coord] = "yellow" + else: + d[coord] = "gray" + return d + + +if __name__ == "__main__": + tetrisGame = TetrisGame() diff --git a/tetris_shape.py b/tetris_shape.py new file mode 100644 index 0000000..e81a0af --- /dev/null +++ b/tetris_shape.py @@ -0,0 +1,171 @@ +LEFT = "left" +RIGHT = "right" +DOWN = "down" +direction_d = { "left": (-1, 0), "right": (1, 0), "down": (0, 1) } + +class Block(object): + def __init__( self, (x, y), color): + self.color = color + self.x = x + self.y = y + + def coord( self ): + return (self.x, self.y) + +class shape(object): + """ + Shape is the Base class for the game pieces e.g. square, T, S, Z, L, + reverse L and I. Shapes are constructed of blocks. + """ + @classmethod + def check_and_create(cls, board, coords, color ): + """ + Check if the blocks that make the shape can be placed in empty coords + before creating and returning the shape instance. Otherwise, return + None. + """ + for coord in coords: + if not board.check_block( coord ): + return None + + return cls( board, coords, color) + + def __init__(self, board, coords, color ): + """ + Initialise the shape base. + """ + self.board = board + self.blocks = [] + + for coord in coords: + self.blocks.append( Block(coord,color) ) + + def move( self, direction ): + """ + Move the blocks in the direction indicated by adding (dx, dy) to the + current block coordinates + """ + d_x, d_y = direction_d[direction] + + for block in self.blocks: + x = block.x + d_x + y = block.y + d_y + if not self.board.check_block( (x, y) ): + return False + + for block in self.blocks: + block.x += d_x + block.y += d_y + + return True + + def rotate(self, clockwise = True): + """ + Rotate the blocks around the 'middle' block, 90-degrees. The + middle block is always the index 0 block in the list of blocks + that make up a shape. + """ + # TO DO: Refactor for DRY + middle = self.blocks[0] + rel = [] + for block in self.blocks: + rel.append( (block.x-middle.x, block.y-middle.y ) ) + + # to rotate 90-degrees (x,y) = (-y, x) + # First check that the there are no collisions or out of bounds moves. + for idx in xrange(len(self.blocks)): + rel_x, rel_y = rel[idx] + if clockwise: + x = middle.x+rel_y + y = middle.y-rel_x + else: + x = middle.x-rel_y + y = middle.y+rel_x + + if not self.board.check_block( (x, y) ): + return False + + for idx in xrange(len(self.blocks)): + rel_x, rel_y = rel[idx] + if clockwise: + x = middle.x+rel_y + y = middle.y-rel_x + else: + x = middle.x-rel_y + y = middle.y+rel_x + + self.blocks[idx].x = x + self.blocks[idx].y = y + + return True + +class shape_limited_rotate( shape ): + """ + This is a base class for the shapes like the S, Z and I that don't fully + rotate (which would result in the shape moving *up* one block on a 180). + Instead they toggle between 90 degrees clockwise and then back 90 degrees + anti-clockwise. + """ + def __init__( self, board, coords, color ): + self.clockwise = True + super(shape_limited_rotate, self).__init__(board, coords, color) + + def rotate(self, clockwise=True): + """ + Clockwise, is used to indicate if the shape should rotate clockwise + or back again anti-clockwise. It is toggled. + """ + super(shape_limited_rotate, self).rotate(clockwise=self.clockwise) + if self.clockwise: + self.clockwise=False + else: + self.clockwise=True + +class square_shape( shape ): + @classmethod + def check_and_create( cls, board ): + coords = [(4,0),(5,0),(4,1),(5,1)] + return super(square_shape, cls).check_and_create(board, coords, "red") + + def rotate(self, clockwise=True): + """ + Override the rotate method for the square shape to do exactly nothing! + """ + pass + +class t_shape( shape ): + @classmethod + def check_and_create( cls, board ): + coords = [(4,0),(3,0),(5,0),(4,1)] + return super(t_shape, cls).check_and_create(board, coords, "yellow" ) + +class l_shape( shape ): + @classmethod + def check_and_create( cls, board ): + coords = [(4,0),(3,0),(5,0),(3,1)] + return super(l_shape, cls).check_and_create(board, coords, "orange") + +class reverse_l_shape( shape ): + @classmethod + def check_and_create( cls, board ): + coords = [(5,0),(4,0),(6,0),(6,1)] + return super(reverse_l_shape, cls).check_and_create( + board, coords, "green") + +class z_shape( shape_limited_rotate ): + @classmethod + def check_and_create( cls, board ): + coords =[(5,0),(4,0),(5,1),(6,1)] + return super(z_shape, cls).check_and_create(board, coords, "purple") + +class s_shape( shape_limited_rotate ): + @classmethod + def check_and_create( cls, board ): + coords =[(5,1),(4,1),(5,0),(6,0)] + return super(s_shape, cls).check_and_create(board, coords, "magenta") + +class i_shape( shape_limited_rotate ): + @classmethod + def check_and_create( cls, board ): + coords =[(4,0),(3,0),(5,0),(6,0)] + return super(i_shape, cls).check_and_create(board, coords, "blue") -- cgit v1.2.3