8
\$\begingroup\$

README.md

Tetris game

I am working towards making a Tetris game where you can challenge an AI. Multiple parts have been finished, but a lot is still under construction. However I would like an intermediate review. The code I will post is a fully working 1 player Tetris game made with python3.6 & tkinter. If you are interested, Check the full github online.

Controls

  • To change rotation of the current Piece w
  • To move Left a
  • To move Right d
  • Hard drop (The piece will fall down instantly) s

Features

  • See the next piece
  • Hard drop
  • Multiple rows cleared at once give more score
  • [! Missing] Update in speed after certain amount of time/blocks

Code

#!/usr/bin/python3
from tkinter import Canvas, Label, Tk, StringVar, Button, LEFT
from random import choice, randint
class GameCanvas(Canvas):
 def clean_line(self, boxes_to_delete):
 for box in boxes_to_delete:
 self.delete(box)
 self.update()
 def drop_boxes(self, boxes_to_drop):
 for box in boxes_to_drop:
 self.move(box, 0, Tetris.BOX_SIZE)
 self.update()
 def completed_lines(self, y_coords):
 cleaned_lines = 0
 y_coords = sorted(y_coords)
 for y in y_coords:
 if sum(1 for box in self.find_withtag('game') if self.coords(box)[3] == y) == \
 ((Tetris.GAME_WIDTH - 20) // Tetris.BOX_SIZE):
 self.clean_line([box
 for box in self.find_withtag('game')
 if self.coords(box)[3] == y])
 self.drop_boxes([box
 for box in self.find_withtag('game')
 if self.coords(box)[3] < y])
 cleaned_lines += 1
 return cleaned_lines
 def game_board(self):
 board = [[0] * ((Tetris.GAME_WIDTH - 20) // Tetris.BOX_SIZE)\
 for _ in range(Tetris.GAME_HEIGHT // Tetris.BOX_SIZE)]
 for box in self.find_withtag('game'):
 x, y, _, _ = self.coords(box)
 board[int(y // Tetris.BOX_SIZE)][int(x // Tetris.BOX_SIZE)] = 1
 return board
 def boxes(self):
 return self.find_withtag('game') == self.find_withtag(fill="blue")
class Shape():
 def __init__(self, coords = None):
 if not coords:
 self.__coords = choice(Tetris.SHAPES)
 else:
 self.__coords = coords
 @property
 def coords(self):
 return self.__coords
 def rotate(self): 
 self.__coords = self.__rotate()
 def rotate_directions(self):
 rotated = self.__rotate()
 directions = [(rotated[i][0] - self.__coords[i][0],
 rotated[i][1] - self.__coords[i][1]) for i in range(len(self.__coords))]
 return directions
 @property
 def matrix(self):
 return [[1 if (j, i) in self.__coords else 0 \
 for j in range(max(self.__coords, key=lambda x: x[0])[0] + 1)] \
 for i in range(max(self.__coords, key=lambda x: x[1])[1] + 1)]
 def drop(self, board, offset):
 # print("\n\n\n")
 # print('\n'.join(''.join(map(str, b)) for b in board))
 # print("\n\n\n")
 off_x, off_y = offset
 # print(off_x,off_y)
 last_level = len(board) - len(self.matrix) + 1
 for level in range(off_y, last_level):
 for i in range(len(self.matrix)):
 for j in range(len(self.matrix[0])):
 if board[level+i][off_x+j] == 1 and self.matrix[i][j] == 1:
 return level - 1
 return last_level - 1 
 def __rotate(self):
 max_x = max(self.__coords, key=lambda x:x[0])[0]
 new_original = (max_x, 0)
 rotated = [(new_original[0] - coord[1],
 new_original[1] + coord[0]) for coord in self.__coords]
 min_x = min(rotated, key=lambda x:x[0])[0]
 min_y = min(rotated, key=lambda x:x[1])[1]
 return [(coord[0] - min_x, coord[1] - min_y) for coord in rotated]
class Piece():
 def __init__(self, canvas, start_point, shape = None):
 self.__shape = shape
 if not shape:
 self.__shape = Shape()
 self.canvas = canvas
 self.boxes = self.__create_boxes(start_point)
 @property
 def shape(self):
 return self.__shape
 def move(self, direction):
 if all(self.__can_move(self.canvas.coords(box), direction) for box in self.boxes):
 x, y = direction
 for box in self.boxes:
 self.canvas.move(box,
 x * Tetris.BOX_SIZE,
 y * Tetris.BOX_SIZE)
 return True
 return False
 def rotate(self):
 directions = self.__shape.rotate_directions()
 if all(self.__can_move(self.canvas.coords(self.boxes[i]), directions[i]) for i in range(len(self.boxes))):
 self.__shape.rotate()
 for i in range(len(self.boxes)):
 x, y = directions[i]
 self.canvas.move(self.boxes[i],
 x * Tetris.BOX_SIZE,
 y * Tetris.BOX_SIZE)
 @property
 def offset(self):
 return (min(int(self.canvas.coords(box)[0]) // Tetris.BOX_SIZE for box in self.boxes),
 min(int(self.canvas.coords(box)[1]) // Tetris.BOX_SIZE for box in self.boxes))
 def predict_movement(self, board):
 level = self.__shape.drop(board, self.offset)
 min_y = min([self.canvas.coords(box)[1] for box in self.boxes])
 return (0, level - (min_y // Tetris.BOX_SIZE))
 def predict_drop(self, board):
 level = self.__shape.drop(board, self.offset)
 self.remove_predicts()
 min_y = min([self.canvas.coords(box)[1] for box in self.boxes])
 for box in self.boxes:
 x1, y1, x2, y2 = self.canvas.coords(box)
 box = self.canvas.create_rectangle(x1,
 level * Tetris.BOX_SIZE + (y1 - min_y),
 x2,
 (level + 1) * Tetris.BOX_SIZE + (y1 - min_y),
 fill="yellow",
 tags = "predict")
 def remove_predicts(self):
 for i in self.canvas.find_withtag('predict'):
 self.canvas.delete(i) 
 self.canvas.update()
 def __create_boxes(self, start_point):
 boxes = []
 off_x, off_y = start_point
 for coord in self.__shape.coords:
 x, y = coord
 box = self.canvas.create_rectangle(x * Tetris.BOX_SIZE + off_x,
 y * Tetris.BOX_SIZE + off_y,
 x * Tetris.BOX_SIZE + Tetris.BOX_SIZE + off_x,
 y * Tetris.BOX_SIZE + Tetris.BOX_SIZE + off_y,
 fill="blue",
 tags="game")
 boxes += [box]
 return boxes
 def __can_move(self, box_coords, new_pos):
 x, y = new_pos
 x = x * Tetris.BOX_SIZE
 y = y * Tetris.BOX_SIZE
 x_left, y_up, x_right, y_down = box_coords
 overlap = set(self.canvas.find_overlapping((x_left + x_right) / 2 + x, 
 (y_up + y_down) / 2 + y, 
 (x_left + x_right) / 2 + x,
 (y_up + y_down) / 2 + y))
 other_items = set(self.canvas.find_withtag('game')) - set(self.boxes)
 if y_down + y > Tetris.GAME_HEIGHT or \
 x_left + x < 0 or \
 x_right + x > Tetris.GAME_WIDTH or \
 overlap & other_items:
 # print("y_down + y > Tetris.GAME_HEIGHT : {}".format(y_down + y > Tetris.GAME_HEIGHT))
 # print("x_left + x < 0 : {}".format(x_left + x < 0))
 # print("x_right + x > Tetris.GAME_WIDTH : {}".format(x_right + x > Tetris.GAME_WIDTH))
 # print("overlap & other_items : {}".format(overlap & other_items))
 return False
 return True 
class Tetris():
 SHAPES = ([(0, 0), (1, 0), (0, 1), (1, 1)], # Square
 [(0, 0), (1, 0), (2, 0), (3, 0)], # Line
 [(2, 0), (0, 1), (1, 1), (2, 1)], # Right L
 [(0, 0), (0, 1), (1, 1), (2, 1)], # Left L
 [(0, 1), (1, 1), (1, 0), (2, 0)], # Right Z
 [(0, 0), (1, 0), (1, 1), (2, 1)], # Left Z
 [(1, 0), (0, 1), (1, 1), (2, 1)]) # T
 BOX_SIZE = 20
 GAME_WIDTH = 300
 GAME_HEIGHT = 500
 GAME_START_POINT = GAME_WIDTH / 2 / BOX_SIZE * BOX_SIZE - BOX_SIZE
 def __init__(self, predictable = False):
 self._level = 1
 self._score = 0
 self._blockcount = 0
 self.speed = 500
 self.predictable = predictable
 self.root = Tk()
 self.root.geometry("500x550") 
 self.root.title('Tetris')
 self.root.bind("<Key>", self.game_control)
 self.__game_canvas()
 self.__level_score_label()
 self.__next_piece_canvas()
 def game_control(self, event):
 if event.char in ["a", "A", "\uf702"]:
 self.current_piece.move((-1, 0))
 self.update_predict()
 elif event.char in ["d", "D", "\uf703"]:
 self.current_piece.move((1, 0))
 self.update_predict()
 elif event.char in ["s", "S", "\uf701"]:
 self.hard_drop()
 elif event.char in ["w", "W", "\uf700"]:
 self.current_piece.rotate()
 self.update_predict()
 def new_game(self):
 self.level = 1
 self.score = 0
 self.blockcount = 0
 self.speed = 500
 self.canvas.delete("all")
 self.next_canvas.delete("all")
 self.__draw_canvas_frame()
 self.__draw_next_canvas_frame()
 self.current_piece = None
 self.next_piece = None 
 self.game_board = [[0] * ((Tetris.GAME_WIDTH - 20) // Tetris.BOX_SIZE)\
 for _ in range(Tetris.GAME_HEIGHT // Tetris.BOX_SIZE)]
 self.update_piece()
 def update_piece(self):
 if not self.next_piece:
 self.next_piece = Piece(self.next_canvas, (20,20))
 self.current_piece = Piece(self.canvas, (Tetris.GAME_START_POINT, 0), self.next_piece.shape)
 self.next_canvas.delete("all")
 self.__draw_next_canvas_frame()
 self.next_piece = Piece(self.next_canvas, (20,20))
 self.update_predict()
 def start(self):
 self.new_game()
 self.root.after(self.speed, None)
 self.drop()
 self.root.mainloop()
 def drop(self):
 if not self.current_piece.move((0,1)):
 self.current_piece.remove_predicts()
 self.completed_lines()
 self.game_board = self.canvas.game_board()
 self.update_piece()
 if self.is_game_over():
 return
 else:
 self._blockcount += 1
 self.score += 1
 self.root.after(self.speed, self.drop)
 def hard_drop(self):
 self.current_piece.move(self.current_piece.predict_movement(self.game_board))
 def update_predict(self):
 if self.predictable:
 self.current_piece.predict_drop(self.game_board)
 def update_status(self):
 self.status_var.set(f"Level: {self.level}, Score: {self.score}")
 self.status.update()
 def is_game_over(self):
 if not self.current_piece.move((0,1)):
 self.play_again_btn = Button(self.root, text="Play Again", command=self.play_again)
 self.quit_btn = Button(self.root, text="Quit", command=self.quit) 
 self.play_again_btn.place(x = Tetris.GAME_WIDTH + 10, y = 200, width=100, height=25)
 self.quit_btn.place(x = Tetris.GAME_WIDTH + 10, y = 300, width=100, height=25)
 return True
 return False
 def play_again(self):
 self.play_again_btn.destroy()
 self.quit_btn.destroy()
 self.start()
 def quit(self):
 self.root.quit() 
 def completed_lines(self):
 y_coords = [self.canvas.coords(box)[3] for box in self.current_piece.boxes]
 completed_line = self.canvas.completed_lines(y_coords)
 if completed_line == 1:
 self.score += 400
 elif completed_line == 2:
 self.score += 1000
 elif completed_line == 3:
 self.score += 3000
 elif completed_line >= 4:
 self.score += 12000
 def __game_canvas(self):
 self.canvas = GameCanvas(self.root, 
 width = Tetris.GAME_WIDTH, 
 height = Tetris.GAME_HEIGHT)
 self.canvas.pack(padx=5 , pady=10, side=LEFT)
 def __level_score_label(self):
 self.status_var = StringVar() 
 self.status = Label(self.root, 
 textvariable=self.status_var, 
 font=("Helvetica", 10, "bold"))
 #self.status.place(x = Tetris.GAME_WIDTH + 10, y = 100, width=100, height=25)
 self.status.pack()
 def __next_piece_canvas(self):
 self.next_canvas = Canvas(self.root,
 width = 100,
 height = 100)
 self.next_canvas.pack(padx=5 , pady=10)
 def __draw_canvas_frame(self):
 self.canvas.create_line(10, 0, 10, self.GAME_HEIGHT, fill = "red", tags = "line")
 self.canvas.create_line(self.GAME_WIDTH-10, 0, self.GAME_WIDTH-10, self.GAME_HEIGHT, fill = "red", tags = "line")
 self.canvas.create_line(10, self.GAME_HEIGHT, self.GAME_WIDTH-10, self.GAME_HEIGHT, fill = "red", tags = "line")
 def __draw_next_canvas_frame(self):
 self.next_canvas.create_rectangle(10, 10, 90, 90, tags="frame")
 #set & get
 def __get_level(self):
 return self._level
 def __set_level(self, level):
 self.speed = 500 - (level - 1) * 25
 self._level = level
 self.update_status()
 def __get_score(self):
 return self._score
 def __set_score(self, score):
 self._score = score
 self.update_status()
 def __get_blockcount(self):
 return self._blockcount
 def __set_blockcount(self, blockcount):
 self.level = blockcount // 5 + 1
 self._blockcount = blockcount
 level = property(__get_level, __set_level)
 score = property(__get_score, __set_score)
 blockcount = property(__get_blockcount, __set_blockcount)
if __name__ == '__main__':
 game = Tetris(predictable = True)
 game.start()
asked May 2, 2018 at 19:35
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

This is a quick answer, maybe I will add more things later (but I do not promise).

My suggestion for improvements is rather from the UX standpoint:

  • While your program seems doing the job, it has been hard for me to adapt my mind to the 4 control keys you use. It is more natural to use the arrow direction keys , , and in place of s, d and a respectively.
  • For the rotation key, I would also prefer the arrow direction instead of w. This is good in case you decide to opt for the previous suggestion.
  • The yellow indicator is is a little noisy (and to be honest, it irritates me because somehow it confuses me). It would be good to remove it totally: after all, it is a factor that will impact the gamer who will have to focus where he has to place the corresponding shape.
  • When the shape is a line, I noticed it is drown a little out of the box (and cuts its right wall) where you display the levels and scores.
  • I think you can still improve the colors you used: that red container is a little bit .... how to say ... even the blue and the background. I know this is subjective, but subjectivity is a part of the user experience, especially when it comes to games.

But the overall is good, even better when we see you managed to do that in tkinter. Good continuation.

answered May 3, 2018 at 12:19
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I think I can agree on most points. I am not a game designer, so the UX is really flawed (I know). But, the predictable drop can be disabled by game = Tetris(predictable = False). False is also the default. Secondly, maybe it is preference, but wasd are my goto gaming keys :) \$\endgroup\$ Commented May 3, 2018 at 12:26
  • 1
    \$\begingroup\$ Yeah, I am sure you are going to focus more on the AI aspect of the game, UX is too subjective and you can not do it alone. For the keys, I understand what you mean, but coming back to the UX world, I live in France, so my keyboard is AZERTY, not QWERTY, so it is not very easy for me to press w :) \$\endgroup\$ Commented May 3, 2018 at 12:32
  • 1
    \$\begingroup\$ The different layout of the keys didn't even cross my mind. Very good point, I will do a UX update eventually, but not that high on the priority list. Great answer nonetheless. \$\endgroup\$ Commented May 3, 2018 at 12:33

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.