Jetris

Being generally very interested in games I recently stumbled across a Tetris clone, Quadrapassel. It really cought my attention and after playing for a long time I decided to program a Tetris clone myself.
There may still be a few inconsistencies but all in all I think it works very well.
Your game score will be shown in the window’s caption along with the info if the game is over yet.
To see further details about Tetris, visit this Wikipedia entry.
Jetris 16

Controls

  • Left arrow key to move the brick to the left
  • Right arrow key to move brick to the right
  • Up arrow key to rotate the brick (clockwise)
  • Down arrow to increase the brick’s fall speed
  • Space to bring the brick to the far bottom
  • Escape to pause and unpause the game
  • Tab to toggle if a fake brick is shown where the brick will land
  • Right shift to toggle brick looks
  • Backspace to start a new game

Jetris 6 Jetris 8 Jetris 11


# Python 2.7.7 Code
# Pygame 1.9.1 (for Python 2.7.7)
# Jonathan Frech

# Version 1.0
#    * 30th of May, 2015
#    * 31st of May, 2015
#    * first instance

# Version 1.1
#   * 31st of May, 2015
#   * better key control
#   * more variables used
#   * more rows deleted at once yield more points
#   * -> changed scoring system
#   * added new looks
#   * worked on COM
#   * tried to fix row deletion
#   * commenting

# Version 1.2
#   * 1st of June, 2015
#   * 2nd of June, 2015
#   * 3rd of June, 2015
#   * finally fixed row deletion bugs (hopefully all)
#   * optimized scoring
#   * fixed turning bug (the game freezing at certain conditions)
#   * optimized turning
#   * better tile look handling
#   * changed game over and pause visualization
#   * brick is now randomly turned at spawn
#   * tried to work on COM (failed)
#   * fixed (yet) another row deletion bug
#   * commenting
#   * banned COM idea

""" VERSION """
VERSION_NAME = "Jetris"
VERSION_NUMBER = 1.2
VERSION_CAPTION = VERSION_NAME + " v. " + str(VERSION_NUMBER)

""" MODULES """
# importing needed modules
import pygame, sys, time, math, os, random, re

""" CLASSES """
# dummy class for global variables
class dummy():
	pass

# tile class (for 1x1 tiles)
# partially used in brick class
class tile():
	# init
	def __init__(self, _pos, _color = None):
		self.pos = _pos
		self.color = _color
		
		# if no color is specified, pick one at random
		if self.color == None:
			self.color = [random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)]
		
		self.dead = False
		self.shouldFall = 0
	
	# render function
	def render(self, _surface):
		# variables
		x, y = self.pos[0] * main.TILESIZE[0], self.pos[1] * main.TILESIZE[1]
		_x, _y = main.TILESIZE[0] / 2, main.TILESIZE[1] / 2
		color = self.color
		
		if main.LOOK > len(main.LOOKFUNCTIONS) - 1:
			main.LOOK = 0
		
		main.LOOKFUNCTIONS[main.LOOK](_surface, x, y, _x, _y, color)
		
	
	# checks the row (if it needs to get deleted or not)
	def checkRow(self):
		# debug to see if row check works
		#self.color = [100, 150, 200]
		
		# make sure self. is not dead
		if not self.dead:
			# if self is the last tile in the checked row
			if self.pos[0] >= main.TILEQUANTITY[0] - 1:
				# row gets deleted
				for _ in main.TILES:
					if _.pos[1] == self.pos[1]:
						_.dead = True
				
				# tiles above get moved down
				for _ in main.TILES:
					if _.pos[1]  main.KEYHOLDDELAY:
			if main.KEY_BRICKLEFTPRESSED % main.KEYREPEAT == 0:
				# move left
				if main.KEY_BRICKLEFT in main.KEYSDOWN:
					self.move("left")
		
		if main.KEY_BRICKRIGHTPRESSED > main.KEYHOLDDELAY:
			if main.KEY_BRICKRIGHTPRESSED % main.KEYREPEAT == 0:
				# move right
				if main.KEY_BRICKRIGHT in main.KEYSDOWN:
					self.move("right")
		
		movedDown = False
		if main.BRICKMOVETICKS % main.KEYREPEAT == 0:
			# move down
			if main.KEY_BRICKDOWN in main.KEYSDOWN:
				self.move("down")
				movedDown = True
		
		# if user did not move the brick
		# downward, do it
		if not movedDown:
			if main.TICKS % main.BRICKFALLTIME == 0:
				self.move("down")
	
	# handles events
	def handleEvent(self, _event):
		if _event.type == pygame.KEYDOWN:
			if _event.key == main.KEY_BRICKTURN:
				self.turn()
			
			elif _event.key == main.KEY_BRICKLEFT:
				self.move("left")
				main.KEY_BRICKLEFTPRESSED = 0
			
			elif _event.key == main.KEY_BRICKRIGHT:
				self.move("right")
				main.KEY_BRICKRIGHTPRESSED = 0
			
			elif _event.key == main.KEY_BRICKBOTTOM:
				self.moveToBottom()
		
	# render function
	def render(self, _surface):
		# calculate tiles:
		self.calculateTiles()
		
		for _ in self.tiles:
			_.render(_surface)
	
	# turn shape
	def turn(self):
		rawshape = []
		y = -1
		for _y in self.shape:
			x = -1; y += 1
			for _x in _y:
				x += 1
				X, Y = len(self.shape) - y - 1, x
				
				while len(rawshape) < Y+1:
					rawshape.append([])
				while len(rawshape[Y])  main.TILEQUANTITY[0] - 1:
						# that tile is outside the right bound
						pos[0] -= 1
					
					# bottom bound
					while pos[1] + y > main.TILEQUANTITY[1] - 1:
						# that tile is outside the bottom bound
						pos[1] -= 1
		
		# check if shape interferes with any tiles
		valid = True
		y = -1
		for _y in shape:
			y += 1; x = -1
			for _x in _y:
				x += 1
				if _x == "#":
					# there is a tile at (x, y)
					
					# check all tiles
					for _ in main.TILES:
						if _.pos[0] == pos[0] + x and _.pos[1] == pos[1] + y:
							valid = False
							break
		
		# if everything is correct, take on shape and position
		if valid:
			self.shape = shape
			self.pos = pos
		
	# get solid
	def getSolid(self):
		self.solid = True
		if self.generateTiles:
			for _ in self.tiles:
				main.TILES.append(_)
	
	# calculate the tiles needed to create self.shape
	def calculateTiles(self):
		self.tiles = []
		y = -1
		for _y in self.shape:
			y += 1; x = -1
			for _x in _y:
				x += 1
				if _x == "#":
					self.tiles.append(tile([self.pos[0] + x, self.pos[1] + y], self.color))
	
	# move as far as it can
	def moveToBottom(self):
		while not self.solid:
			self.move("down")
	
	# move brick
	def move(self, _dir):
		# calculate tiles:
		self.calculateTiles()
		
		""" UP """
		if _dir == "up":
			# tile bound
			for mTile in main.TILES:
				for sTile in self.tiles:
					if mTile.pos[0] == sTile.pos[0]:
						# x value the same
						if mTile.pos[1] == sTile.pos[1] - 1:
							# cannot move
							return False
			
			# board bound			
			for sTile in self.tiles:
				if sTile.pos[1] = main.TILEQUANTITY[1] - 1:
					# cannot move
					self.getSolid()
					return False
					
			# move
			self.pos[1] += 1
				
		""" LEFT """
		if _dir == "left":
			# tile bound
			for mTile in main.TILES:
				for sTile in self.tiles:
					if mTile.pos[1] == sTile.pos[1]:
						# y value the same
						if mTile.pos[0] == sTile.pos[0] - 1:
							# cannot move
							return False
			
			# board bound			
			for sTile in self.tiles:
				if sTile.pos[0] = main.TILEQUANTITY[0] - 1:
					# cannot move
					return False
					
			# move
			self.pos[0] += 1
		
		# succesfully moved
		return True

""" LOOK FUNCTIONS """
# simple rectangles
def look0(_surface, x, y, _x, _y, color):
	pygame.draw.rect(_surface, color, [x, y, main.TILESIZE[0], main.TILESIZE[1]])

# smaller rectangles
def look1(_surface, x, y, _x, _y, color):
	n = 2
	pygame.draw.rect(_surface, color, [x + n, y + n, main.TILESIZE[0] - n*2, main.TILESIZE[1] - n*2])

# two differntly colored rectangles
def look2(_surface, x, y, _x, _y, color):
	p1 = [ x, y, main.TILESIZE[0], main.TILESIZE[1] ]
	n = 2
	p2 = [x + n, y + n, main.TILESIZE[0] - n*2, main.TILESIZE[1] - n*2]
	n = 50
	c = [colorValid(color[0] - n), colorValid(color[1] - n), colorValid(color[2] - n)]
	
	pygame.draw.rect(_surface, color, p1)
	pygame.draw.rect(_surface, c, p2)

# two colored rectangles with white rectangle
def look3(_surface, x, y, _x, _y, color):
	p1 = [x, y, main.TILESIZE[0], main.TILESIZE[1]]
	n = 2
	p2 = [x + n, y + n, main.TILESIZE[0] - n*2, main.TILESIZE[1] - n*2]
	p3 = [x + 2*n, y + 2*n, 4*n, 4*n]
	n = 50
	c = [colorValid(color[0] - n), colorValid(color[1] - n), colorValid(color[2] - n)]
	
	pygame.draw.rect(_surface, color, p1)
	pygame.draw.rect(_surface, c, p2)
	pygame.draw.rect(_surface, [255, 255, 255], p3)

# two small rectangles
# on at the top left, on at the bottom right
def look4(_surface, x, y, _x, _y, color):
	n = 2
	sX, sY = (main.TILESIZE[0] - n*3) / 2, (main.TILESIZE[1] - n*3) / 2
	p1 = [x + n, y + n, sX, sY]
	p2 = [x + n + sX, y + n + sY, sX, sY]
	
	pygame.draw.rect(_surface, color, p1)
	pygame.draw.rect(_surface, color, p2)

# four smaller rectangles
def look5(_surface, x, y, _x, _y, color):
	n = 2
	sX, sY = (main.TILESIZE[0] / 2) - 3*n, (main.TILESIZE[1] / 2) - 3*n
	p1 = [x + n, y + n, sX, sY]
	p2 = [x + n, y + n + sY + n, sX, sY]
	p3 = [x + n + sX + n, y + n, sX, sY]
	p4 = [x + n + sX + n, y + n + sY + n, sX, sY]
	
	pygame.draw.rect(_surface, color, p1)
	pygame.draw.rect(_surface, color, p2)
	pygame.draw.rect(_surface, color, p3)
	pygame.draw.rect(_surface, color, p4)

# circles
def look6(_surface, x, y, _x, _y, color):
	pygame.draw.circle(_surface, color, [x + _x, y + _y], _y)

# circles II
def look7(_surface, x, y, _x, _y, color):
	for _ in range(0, _y):
		c = [
			colorValid(color[0] - _ * 3),
			colorValid(color[1] - _ * 4),
			colorValid(color[2] - _ * 5)
		]
		pygame.draw.circle(_surface, c, [x + _x, y + _y], _y - _)

# crosses
def look8(_surface, x, y, _x, _y, color):
	pygame.draw.line(_surface, color, [x + _x, y], [x + _x, y + main.TILESIZE[1]], 10)
	pygame.draw.line(_surface, color, [x, y + _y], [x + main.TILESIZE[0], y + _y], 10)

# initialize the look functions
def initLook():
	main.LOOKFUNCTIONS = [
		look0,
		look1,
		look2,
		look3#,
		#look4,
		#look5,
		#look6,
		#look7,
		#look8
	]

""" FUNCTIONS """
# validates color integer
# extra feature: _min and _max implementation
def colorValid(_color, _min = 0, _max = 255):
	newColor = math.fabs(_color)
	n = _max - _min
	if newColor > n:
		if int(newColor / n) % 2 == 0:
			newColor = newColor % n
		else:
			newColor = n - (newColor % n)
	
	return int(newColor) + _min

# quits the program
def quit():
	sys.exit()

# generate new brick
def newBrick():
	# brick types
	bricktypes = [
		[ "####",     main.COLOR_ROD    ], #rod
		[ "##; #; #", main.COLOR_HOOK1  ], #hook1
		[ "##;# ;#",  main.COLOR_HOOK2  ], #hook2
		[ " # ;###",  main.COLOR_T      ], #T
		[ " #;##;#",  main.COLOR_STAIR1 ], #stair1
		[ "# ;##; #", main.COLOR_STAIR2 ], #stair2
		[ "##;##",    main.COLOR_BLOCK  ] #block
	]
	
	# choose one at random
	bricktype = bricktypes[random.randint(0, len(bricktypes) - 1)]
	
	# create brick
	main.BRICK = brick(strToShape( bricktype[0] ), [main.TILEQUANTITY[0] / 2, 0], bricktype[1])
	
	# randomly turn brick
	for _ in range(0, random.randint(0, 2)):
		main.BRICK.turn()
	
	# count brick
	main.BRICKCOUNT += 1

# calculates score when a line is broken
# (multiply lines at once now make a difference!)
def score(_lines):
	# calculate score
	score = main.BRICKCOUNT * _lines * 13
	
	# update score and caption
	main.SCORE += score
	pygame.display.set_caption(main.CAPTION + " (score " + str(main.SCORE) + ")")

# turns a string into a shape
def strToShape(_str):
	shape = []
	for _ in re.split(";", _str):
		shape.append(_)
	return shape

""" TICK; RENDER """
# tick function
def tick():
	# handle events
	for event in pygame.event.get():
		# quit
		if event.type == pygame.QUIT:
			quit()
		
		# keyup
		if event.type == pygame.KEYUP:
			# handle 'main.KEYSDOWN'
			if event.key in main.KEYSDOWN:
				main.KEYSDOWN.remove(event.key)
		
		# keydown
		if event.type == pygame.KEYDOWN:
			# handle 'main.KEYSDOWN'
			if event.key not in main.KEYSDOWN:
				main.KEYSDOWN.append(event.key)
			
			# quit
			if event.key == pygame.K_q:
				if pygame.key.get_mods() == 2**10:
					quit()
			
			# reset game
			if event.key == main.KEY_RESET:
				resetGame()
			
			# pause game
			if event.key == main.KEY_PAUSE:
				if main.PAUSED:
					main.PAUSED = False
				else:
					main.PAUSED = True
			
			# change look
			if event.key == main.KEY_LOOK:
				main.LOOK += 1
			
			# toggle if land brick is shown
			if event.key == main.KEY_LANDING:
				if main.LANDBRICK:
					main.LANDBRICK = False
				else:
					main.LANDBRICK = True
		
		# if game is not paused
		if not main.PAUSED:
			# handle brick
			if not main.GAMEOVER:
				main.BRICK.handleEvent(event)
	
	# ticking the game
	# if game is not paused
	if not main.PAUSED:
		
		# tick individual tick counts
		main.BRICKFALLTIMETICKS += 1
		main.BRICKMOVETICKS += 1
		
		# speed up bricks
		if main.BRICKFALLTIMETICKS >= 30*60: # every half a minute
			main.BRICKFALLTIMETICKS = 0
			if main.BRICKFALLTIME - 5 > 15:
				main.BRICKFALLTIME -= 5
			else:
				main.BRICKFALLTIME = 15
		
		# tick and create the brick
		if not main.GAMEOVER:
			if main.BRICK.solid:
				newBrick()
			else:
				main.BRICK.tick()
		
		# check tiles
		for _ in main.TILES:
			# unused gameover visualization
			"""
			if main.GAMEOVER:
				if random.randint(0, 10) == 0:
					if random.randint(0, 1) == 0:
						_.color = main.COLOR_GAMEOVER1
					else:
						_.color = main.COLOR_GAMEOVER2
			"""
			
			# check every tile to the far left
			if not _.dead and _.pos[0]  0:
				# if tile is already at bottom it cannot fall
				if _.pos[1] >= main.TILEQUANTITY[1] - 1:
					_.shouldFall = 0
				
				# tile is not at bottom
				else:
					# canFall defines if the tile can fall
					canFall = True
					
					# check oher tiles
					for __ in main.TILES:
						if __.pos[0] == _.pos[0] and __.pos[1] == _.pos[1] + 1:
							# tile __ is directly under tile _
							if __.shouldFall > 0:
								# tile __ is still falling, tile _'s way is obstructed
								canFall = False
					
					# if tile can fall, move it
					if canFall:
						_.pos[1] += 1
						_.shouldFall -= 1
		
		# update score
		if main.ROWSDELETEDINTICK > 0:
			# debug
			#print "<<>> row(s) deleted!"
			
			# score
			score(main.ROWSDELETEDINTICK)
			
			# score variables reset
			main.ROWSDELETEDINTICK = 0
		
		# check game over
		for _ in main.TILES:
			if _.pos[1]  main.WIDTH:
				n = main.HEIGHT
			else:
				n = main.WIDTH
			for _ in range(0, n*2, 30):
				pygame.draw.line(main.SURF, main.COLOR_PAUSE, [_, 0], [0, _])
	
	# blit and flip
	main.SCREEN.blit(main.SURF, [0, 0])
	pygame.display.flip()
	
	# update icon
	# using the current game surface as icon and have a border around it
	if main.ICONTYPE == "surfaceWidthBorder":
		c = colorValid(main.TICKS, 50)
		color = [c, c, c]
		pygame.draw.line(main.SURF, color, [0, 0], [main.WIDTH, 0], 30)
		pygame.draw.line(main.SURF, color, [0, main.HEIGHT], [main.WIDTH, main.HEIGHT], 30)
		pygame.draw.line(main.SURF, color, [0, 0], [0, main.HEIGHT], 30)
		pygame.draw.line(main.SURF, color, [main.WIDTH, 0], [main.WIDTH, main.HEIGHT], 30)
		
	# using the current game surface as icon
	elif main.ICONTYPE == "surface":
		pygame.display.set_icon(main.SURF)
	
	# displaying pixels with random color (only colors used in the game)
	elif main.ICONTYPE == "pixels":
		if main.TICKS % 60 == 0 or main.TICKS = 1:
			ticks += 1
			main.TICKS += 1
			tick()
			delta -= 1
			shouldRender = True
		
		if shouldRender:
			frames += 1
			render()
		
		if time.time() * 1000 - lastTimer >= 1000:
			lastTimer += 1000
			
			# debug
			#print("Frames: " + str(frames) + ", ticks: " + str(ticks))
			
			frames = 0
			ticks = 0

# main variable
main = dummy()
init()

# start program
run()
Advertisements

2 thoughts on “Jetris

  1. Pingback: Jong | J-Blog

  2. Pingback: Jetris CE – J-Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s