aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Leah Alpert <lalpert@mit.edu>2011-08-26 19:17:14 -0700
committerGravatar Leah Alpert <lalpert@mit.edu>2011-08-26 19:17:14 -0700
commit239338305364e076f4a23de0e869acb38e767b2e (patch)
tree6c7ae8e39abbc4a8a86e5daf91835a2a960a6743
parent37636974499803423dfad390a226f833d6389abd (diff)
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.
-rw-r--r--README11
-rw-r--r--ddrinput.py10
-rw-r--r--tetris.py376
-rw-r--r--tetris_shape.py171
4 files changed, 567 insertions, 1 deletions
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")