-
Notifications
You must be signed in to change notification settings - Fork 51
/
Copy pathtetris.py
346 lines (315 loc) · 14.1 KB
/
tetris.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#!/usr/bin/env python3
# File: tetris.py
# Description: Main file with tetris game.
# Author: Pavel Benáček <[email protected]>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pygame
import pdb
import random
import math
import block
import constants
class Tetris(object):
"""
The class with implementation of tetris game logic.
"""
def __init__(self,bx,by):
"""
Initialize the tetris object.
Parameters:
- bx - number of blocks in x
- by - number of blocks in y
"""
# Compute the resolution of the play board based on the required number of blocks.
self.resx = bx*constants.BWIDTH+2*constants.BOARD_HEIGHT+constants.BOARD_MARGIN
self.resy = by*constants.BHEIGHT+2*constants.BOARD_HEIGHT+constants.BOARD_MARGIN
# Prepare the pygame board objects (white lines)
self.board_up = pygame.Rect(0,constants.BOARD_UP_MARGIN,self.resx,constants.BOARD_HEIGHT)
self.board_down = pygame.Rect(0,self.resy-constants.BOARD_HEIGHT,self.resx,constants.BOARD_HEIGHT)
self.board_left = pygame.Rect(0,constants.BOARD_UP_MARGIN,constants.BOARD_HEIGHT,self.resy)
self.board_right = pygame.Rect(self.resx-constants.BOARD_HEIGHT,constants.BOARD_UP_MARGIN,constants.BOARD_HEIGHT,self.resy)
# List of used blocks
self.blk_list = []
# Compute start indexes for tetris blocks
self.start_x = math.ceil(self.resx/2.0)
self.start_y = constants.BOARD_UP_MARGIN + constants.BOARD_HEIGHT + constants.BOARD_MARGIN
# Blocka data (shapes and colors). The shape is encoded in the list of [X,Y] points. Each point
# represents the relative position. The true/false value is used for the configuration of rotation where
# False means no rotate and True allows the rotation.
self.block_data = (
([[0,0],[1,0],[2,0],[3,0]],constants.RED,True), # I block
([[0,0],[1,0],[0,1],[-1,1]],constants.GREEN,True), # S block
([[0,0],[1,0],[2,0],[2,1]],constants.BLUE,True), # J block
([[0,0],[0,1],[1,0],[1,1]],constants.ORANGE,False), # O block
([[-1,0],[0,0],[0,1],[1,1]],constants.GOLD,True), # Z block
([[0,0],[1,0],[2,0],[1,1]],constants.PURPLE,True), # T block
([[0,0],[1,0],[2,0],[0,1]],constants.CYAN,True), # J block
)
# Compute the number of blocks. When the number of blocks is even, we can use it directly but
# we have to decrese the number of blocks in line by one when the number is odd (because of the used margin).
self.blocks_in_line = bx if bx%2 == 0 else bx-1
self.blocks_in_pile = by
# Score settings
self.score = 0
# Remember the current speed
self.speed = 1
# The score level threshold
self.score_level = constants.SCORE_LEVEL
def apply_action(self):
"""
Get the event from the event queue and run the appropriate
action.
"""
# Take the event from the event queue.
for ev in pygame.event.get():
# Check if the close button was fired.
if ev.type == pygame.QUIT or (ev.type == pygame.KEYDOWN and ev.unicode == 'q'):
self.done = True
# Detect the key evevents for game control.
if ev.type == pygame.KEYDOWN:
if ev.key == pygame.K_DOWN:
self.active_block.move(0,constants.BHEIGHT)
if ev.key == pygame.K_LEFT:
self.active_block.move(-constants.BWIDTH,0)
if ev.key == pygame.K_RIGHT:
self.active_block.move(constants.BWIDTH,0)
if ev.key == pygame.K_SPACE:
self.active_block.rotate()
if ev.key == pygame.K_p:
self.pause()
# Detect if the movement event was fired by the timer.
if ev.type == constants.TIMER_MOVE_EVENT:
self.active_block.move(0,constants.BHEIGHT)
def pause(self):
"""
Pause the game and draw the string. This function
also calls the flip function which draws the string on the screen.
"""
# Draw the string to the center of the screen.
self.print_center(["PAUSE","Press \"p\" to continue"])
pygame.display.flip()
while True:
for ev in pygame.event.get():
if ev.type == pygame.KEYDOWN and ev.key == pygame.K_p:
return
def set_move_timer(self):
"""
Setup the move timer to the
"""
# Setup the time to fire the move event. Minimal allowed value is 1
speed = math.floor(constants.MOVE_TICK / self.speed)
speed = max(1,speed)
pygame.time.set_timer(constants.TIMER_MOVE_EVENT,speed)
def run(self):
# Initialize the game (pygame, fonts)
pygame.init()
pygame.font.init()
self.myfont = pygame.font.SysFont(pygame.font.get_default_font(),constants.FONT_SIZE)
self.screen = pygame.display.set_mode((self.resx,self.resy))
pygame.display.set_caption("Tetris")
# Setup the time to fire the move event every given time
self.set_move_timer()
# Control variables for the game. The done signal is used
# to control the main loop (it is set by the quit action), the game_over signal
# is set by the game logic and it is also used for the detection of "game over" drawing.
# Finally the new_block variable is used for the requesting of new tetris block.
self.done = False
self.game_over = False
self.new_block = True
# Print the initial score
self.print_status_line()
while not(self.done) and not(self.game_over):
# Get the block and run the game logic
self.get_block()
self.game_logic()
self.draw_game()
# Display the game_over and wait for a keypress
if self.game_over:
self.print_game_over()
# Disable the pygame stuff
pygame.font.quit()
pygame.display.quit()
def print_status_line(self):
"""
Print the current state line
"""
string = ["SCORE: {0} SPEED: {1}x".format(self.score,self.speed)]
self.print_text(string,constants.POINT_MARGIN,constants.POINT_MARGIN)
def print_game_over(self):
"""
Print the game over string.
"""
# Print the game over text
self.print_center(["Game Over","Press \"q\" to exit"])
# Draw the string
pygame.display.flip()
# Wait untill the space is pressed
while True:
for ev in pygame.event.get():
if ev.type == pygame.QUIT or (ev.type == pygame.KEYDOWN and ev.unicode == 'q'):
return
def print_text(self,str_lst,x,y):
"""
Print the text on the X,Y coordinates.
Parameters:
- str_lst - list of strings to print. Each string is printed on new line.
- x - X coordinate of the first string
- y - Y coordinate of the first string
"""
prev_y = 0
for string in str_lst:
size_x,size_y = self.myfont.size(string)
txt_surf = self.myfont.render(string,False,(255,255,255))
self.screen.blit(txt_surf,(x,y+prev_y))
prev_y += size_y
def print_center(self,str_list):
"""
Print the string in the center of the screen.
Parameters:
- str_lst - list of strings to print. Each string is printed on new line.
"""
max_xsize = max([tmp[0] for tmp in map(self.myfont.size,str_list)])
self.print_text(str_list,self.resx/2-max_xsize/2,self.resy/2)
def block_colides(self):
"""
Check if the block colides with any other block.
The function returns True if the collision is detected.
"""
for blk in self.blk_list:
# Check if the block is not the same
if blk == self.active_block:
continue
# Detect situations
if(blk.check_collision(self.active_block.shape)):
return True
return False
def game_logic(self):
"""
Implementation of the main game logic. This function detects colisions
and insertion of new tetris blocks.
"""
# Remember the current configuration and try to
# apply the action
self.active_block.backup()
self.apply_action()
# Border logic, check if we colide with down border or any
# other border. This check also includes the detection with other tetris blocks.
down_board = self.active_block.check_collision([self.board_down])
any_border = self.active_block.check_collision([self.board_left,self.board_up,self.board_right])
block_any = self.block_colides()
# Restore the configuration if any collision was detected
if down_board or any_border or block_any:
self.active_block.restore()
# So far so good, sample the previous state and try to move down (to detect the colision with other block).
# After that, detect the the insertion of new block. The block new block is inserted if we reached the boarder
# or we cannot move down.
self.active_block.backup()
self.active_block.move(0,constants.BHEIGHT)
can_move_down = not self.block_colides()
self.active_block.restore()
# We end the game if we are on the respawn and we cannot move --> bang!
if not can_move_down and (self.start_x == self.active_block.x and self.start_y == self.active_block.y):
self.game_over = True
# The new block is inserted if we reached down board or we cannot move down.
if down_board or not can_move_down:
# Request new block
self.new_block = True
# Detect the filled line and possibly remove the line from the
# screen.
self.detect_line()
def detect_line(self):
"""
Detect if the line is filled. If yes, remove the line and
move with remaining bulding blocks to new positions.
"""
# Get each shape block of the non-moving tetris block and try
# to detect the filled line. The number of bulding blocks is passed to the class
# in the init function.
for shape_block in self.active_block.shape:
tmp_y = shape_block.y
tmp_cnt = self.get_blocks_in_line(tmp_y)
# Detect if the line contains the given number of blocks
if tmp_cnt != self.blocks_in_line:
continue
# Ok, the full line is detected!
self.remove_line(tmp_y)
# Update the score.
self.score += self.blocks_in_line * constants.POINT_VALUE
# Check if we need to speed up the game. If yes, change control variables
if self.score > self.score_level:
self.score_level *= constants.SCORE_LEVEL_RATIO
self.speed *= constants.GAME_SPEEDUP_RATIO
# Change the game speed
self.set_move_timer()
def remove_line(self,y):
"""
Remove the line with given Y coordinates. Blocks below the filled
line are untouched. The rest of blocks (yi > y) are moved one level done.
Parameters:
- y - Y coordinate to remove.
"""
# Iterate over all blocks in the list and remove blocks with the Y coordinate.
for block in self.blk_list:
block.remove_blocks(y)
# Setup new block list (not needed blocks are removed)
self.blk_list = [blk for blk in self.blk_list if blk.has_blocks()]
def get_blocks_in_line(self,y):
"""
Get the number of shape blocks on the Y coordinate.
Parameters:
- y - Y coordinate to scan.
"""
# Iteraveovel all block's shape list and increment the counter
# if the shape block equals to the Y coordinate.
tmp_cnt = 0
for block in self.blk_list:
for shape_block in block.shape:
tmp_cnt += (1 if y == shape_block.y else 0)
return tmp_cnt
def draw_board(self):
"""
Draw the white board.
"""
pygame.draw.rect(self.screen,constants.WHITE,self.board_up)
pygame.draw.rect(self.screen,constants.WHITE,self.board_down)
pygame.draw.rect(self.screen,constants.WHITE,self.board_left)
pygame.draw.rect(self.screen,constants.WHITE,self.board_right)
# Update the score
self.print_status_line()
def get_block(self):
"""
Generate new block into the game if is required.
"""
if self.new_block:
# Get the block and add it into the block list(static for now)
tmp = random.randint(0,len(self.block_data)-1)
data = self.block_data[tmp]
self.active_block = block.Block(data[0],self.start_x,self.start_y,self.screen,data[1],data[2])
self.blk_list.append(self.active_block)
self.new_block = False
def draw_game(self):
"""
Draw the game screen.
"""
# Clean the screen, draw the board and draw
# all tetris blocks
self.screen.fill(constants.BLACK)
self.draw_board()
for blk in self.blk_list:
blk.draw()
# Draw the screen buffer
pygame.display.flip()
if __name__ == "__main__":
Tetris(16,30).run()
#Special add to try pull requests