The Tetris Project

Just did the thing I talked about in the previous post.

It was really rather repetitive, 108 lines worth of code. Kinda bloated. It will probably be much easier with object oriented programming.

And the code:

from graphics import *

win = GraphWin('Rect', 1300, 500)
BLOCKSIZE = 40

def draw_i_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(4):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('blue')
           rect.setWidth(3)
    
draw_i_shape(20, 20, 'horizontal')


def draw_o_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(2):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('red')
           rect.setWidth(3)
        for i in range(2):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y + BLOCKSIZE), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE*2))
           rect.draw(win)
           rect.setFill('red')
           rect.setWidth(3)

draw_o_shape(20 + 13*BLOCKSIZE, 20, 'horizontal')


def draw_t_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(3):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('yellow')
           rect.setWidth(3)
        rect = Rectangle(Point(x + BLOCKSIZE, y + BLOCKSIZE), Point(x + BLOCKSIZE*(2), y + BLOCKSIZE*2))
        rect.draw(win)
        rect.setFill('yellow')
        rect.setWidth(3)

draw_t_shape(20 + 21*BLOCKSIZE, 20, 'horizontal')

def draw_j_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(3):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('orange')
           rect.setWidth(3)
        rect = Rectangle(Point(x + BLOCKSIZE*2, y + BLOCKSIZE), Point(x + BLOCKSIZE*(3), y + BLOCKSIZE*2))
        rect.draw(win)
        rect.setFill('orange')
        rect.setWidth(3)


draw_j_shape(20 + 4* BLOCKSIZE, 20, 'horizontal')

def draw_l_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(3):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('cyan')
           rect.setWidth(3)
        rect = Rectangle(Point(x, y + BLOCKSIZE), Point(x + BLOCKSIZE, y + BLOCKSIZE*2))
        rect.draw(win)
        rect.setFill('cyan')
        rect.setWidth(3)

draw_l_shape(20 + 9*BLOCKSIZE, 20, 'horizontal')
def draw_s_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(2):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('lawn green')
           rect.setWidth(3)
        for i in range(2):
           rect = Rectangle(Point(x + BLOCKSIZE*(i - 1), y + BLOCKSIZE), Point(x + BLOCKSIZE*i, y + BLOCKSIZE*2))
           rect.draw(win)
           rect.setFill('lawn green')
           rect.setWidth(3)

draw_s_shape(20 + 18*BLOCKSIZE, 20, 'horizontal')


def draw_z_shape(x, y, rotation):
    if rotation == 'horizontal':
        for i in range(2):
           rect = Rectangle(Point(x + BLOCKSIZE*i, y), Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE))
           rect.draw(win)
           rect.setFill('deep pink')
           rect.setWidth(3)
        for i in range(2):
           rect = Rectangle(Point(x + BLOCKSIZE*(i + 1), y + BLOCKSIZE), Point(x + BLOCKSIZE*(i + 2), y + BLOCKSIZE*2))
           rect.draw(win)
           rect.setFill('deep pink')
           rect.setWidth(3)

draw_z_shape(20 + 25*BLOCKSIZE, 20, 'horizontal')

win.mainloop()

Here is a picture of some of the tools I used to help program:

2 Likes

Is anyone working on Section 1 & 2 of 6.189 Final Project – Tetris! handout?

This involves adding Python code to the tetris_template.py file, which, by the time we have completed Section 11. Game Over, will provide us with a working Tetris game.

But, first, it would be a good idea to complete the Codecademy Introduction to Classes and Classes exercises. The Tetris file is completely object oriented, and the Codecademy exercises provide concepts the pervade that implementation of the project.

For this phase of the project, I don’t think the best strategy would be to paste in the same code that we have previously written for Tetris. It would be much more effective to arrive with the concepts we have learned from our previous work, and then to implement them with the structure and guidance provided by the handout instructions and the template.

Hi again!

I somehow cannot find the original code for tetris.py from Week One (I think at least). What is the URL address to find it? Thank you in advance,

Rose K.

I don’t think there was any downloaded code for Week One of the Tetris project. There is the template that Glenn mentioned in his post, but I don’t think that’s what you meant.

Hi, @Rose_Ketring,

As @Cedar_Mora mentioned, there was no download regarding the Tetris project in Week One. During the early weeks, the MMOOC essentially asked us to try printing some Tetris shapes composed of text characters. For example, to make a J shape, we could do this …

J_shape = "_|"
print J_shape

Various participants in the MMOOC did some web searching, and came up with means of printing characters in different colors.

Ultimately, though, as the organizers of the MMOOC undoubtedly expected would happen, it became obvious that we needed a means of drawing actual shapes on the screen. That’s when they gave us a graphics.py file to download. We could then write a Python program that began with the statement …

from graphics import *

… and save it in the same folder as the graphics.py file. That enabled us to use some of the objects defined in the graphics.py in our programs to draw and color shapes. Many of the posts in this thread are focused on how to do that.

However, in the Tetris game, we need to be able to have new randomly chosen shapes appear at the top of the game board, have them gradually drop to the bottom of the board, and enable the player of the game to move and rotate the shapes. Shapes are not allowed to overlap or move off the board. There’s a lot of programming required to make all of this happen. So, we were given a second code download, tetris_template.py, which is a skeleton of a program that we need to fill in with some functionality, along with a handout: 6.189 Final Project – Tetris!. We need to save tetris_template.py in the same folder as graphics.py, and follow the instructions in the handout to complete the game. So, that’s where we are now.

The tetris_template.py file makes extensive use of object oriented programming techniques. So, before attempting to do anything with that file, we should complete the following two Codecademy units:

In fact, object oriented programming is important enough to merit some attention. So, let’s start a new discussion on that topic.

1 Like

Well, I finished the basics of the Tetris Project. I finished it a couple of days ago because my college is on hold for now due to a technicality and I was very bored, so I thought I should do something productive besides web surf my whole life. The whole code is too long for me to include here, so I’ll just include my implementations of the functions. You have the template, of course. This isn’t complete code, so it may not be quite complete in itself. When included with the template, it works. If you would like whole version, I can include it.

class Block(Rectangle):
    def can_move(self, board, dx, dy):
        if board.can_move(self.x + dx, self.y + dy):
            return True
        else:
            return False

    def move(self, dx, dy):
        self.x += dx
        self.y += dy

        Rectangle.move(self, dx*Block.BLOCK_SIZE, dy*Block.BLOCK_SIZE)

class Shape():
    def get_blocks(self):
        return self.blocks

    def draw(self, win):
        for block in self.blocks:
            block.draw(win)

    def move(self, dx, dy):
        for block in self.blocks:
            block.move(dx, dy)

    def can_move(self, board, dx, dy):
        for block in self.blocks:
            if not block.can_move(board, dx, dy):
                return False
        return True

    def can_rotate(self, board):
        dir_ = self.get_rotation_dir()
        center = self.center_block
        for block in self.blocks:
            if not board.can_move(center.x - dir_*center.y + dir_*block.y,
                                  center.y + dir_*center.x - dir_*block.x):
                return False
        return True

    def rotate(self, board):
        center = self.center_block
        dir_ = self.get_rotation_dir()
        if self.can_rotate(board):
            for block in self.blocks:
                block.move(center.x - dir_*center.y + dir_*block.y - block.x, center.y + dir_*center.x - dir_*block.x - block.y)

        if self.shift_rotation_dir:
            self.rotation_dir *= -1

class Board():
    def draw_shape(self, shape):
        if shape.can_move(self, 0, 0):
            shape.draw(self.canvas)
            return True
        return False

    def can_move(self, x, y):
        if (0 <= x < self.width) and (0 <= y < self.height) and (x, y) not in self.grid:
            return True
        else:
            return False

    def add_shape(self, shape):
        blocks = shape.get_blocks()
        for block in blocks:
            self.grid[(block.x, block.y)] = block

    def delete_row(self, y):
        for i in range(0, self.width):
            self.grid[i, y].undraw()
            del(self.grid[i, y])

    def is_row_complete(self, y):
        for i in range(0, self.width):
            if (i, y) not in self.grid:
                return False
        return True

    def move_down_rows(self, y_start):
        for j in range(y_start, 0, -1):
            for i in range(0, self.width):
                if (i, j) in self.grid:
                    self.grid[i,j].move(0, 1)
                    self.grid[i, j + 1] = self.grid[i,j]
                    del(self.grid[i,j])

    def remove_complete_rows(self):
        for y in range(0, self.height):
            if self.is_row_complete(y):
                self.delete_row(y)
                self.move_down_rows(y - 1)

    def game_over(self):
        self.youjustlostthegame = Text(Point(self.width * Block.BLOCK_SIZE / 2, self.height * Block.BLOCK_SIZE / 8), "You just lost the game.")
        self.youjustlostthegame.setFill("blue")
        self.youjustlostthegame.draw(self.canvas)

class Tetris():
    def create_new_shape(self):
        new_shape = Tetris.SHAPES[random.randint(0,6)](Point(int(self.BOARD_WIDTH/2), 0))
        return new_shape

    def animate_shape(self):
        self.do_move('Down')
        self.win.after(self.delay, self.animate_shape)

    def do_move(self, direction):
        dx = self.DIRECTION[direction][0]
        dy = self.DIRECTION[direction][1]
        if self.current_shape.can_move(self.board, dx, dy):
            self.current_shape.move(dx, dy)
            return True
        elif direction == 'Down':
            self.board.add_shape(self.current_shape)
            self.board.remove_complete_rows()
            self.current_shape = self.create_new_shape()
            if not self.board.draw_shape(self.current_shape):
                self.board.game_over()
            return False
        else:
            return False

    def do_rotate(self):
        if self.current_shape.can_rotate(self.board):
            self.current_shape.rotate(self.board)

    def key_pressed(self, event):
        key = event.keysym
        if key == 'space':
            while self.do_move('Down'):
                pass
        elif key == 'Up':
            self.do_rotate()
        else:
            self.do_move(key)
        print key

win = Window("Tetris")
game = Tetris(win)
win.mainloop()

Some fun details about this have been:
  • It took me forever to implement Block.can_move() because I had forgotten that you can get position by adding the derivative. The arguments they gave seemed odd to me. It seems like x and y instead dx and dy would have been easier to work with, but I wasn't sure of that.
  • I still don't understand however the Shape.shift_rotation_direction works or what it is in there for.
  • Shape.can_move() was fun too. It took me a bit to realize that I had to check if a block couldn't move, not if it could.
  • I don't really understand why Shape.get_rotation_dir() was necessary. We could have just accessed the attribute directly. Maybe it's something to do with public/private, I don't know.
  • Shape.rotate() really messed with me, since I had to provide the derivative of the block for Block.move, while the formula they gave us gave only the position.
  • It was hard to figure out exactly what the bounds of Board.can_move() should be. Finally figured it out mostly by testing and debugging more than hard thought, but it makes sense now.
  • It was really weird thinking through the whole thing how what is actually shown and what is under the hood are completely separate things. It felt redundant at times, but it makes sense.
  • Board.remove_complete_rows() really messed with me. On my first implementation, if you cleared multiple rows at once, it only cleared the bottom row. After the next shape joined the grid, the next leftover complete row would be cleared. They should all have cleared at once. After a laborious hour or so I figured out that my algorithm checked if rows were complete from bottom to top. That way, after it cleared a row, the next complete row became the current row, but unfortunately, it checked the next row, since it thought the current row was already cleared. In other words, instead of range(0, self.height), I had range(self.height, 0, -1). As a separate problem, I didn't have the -1 originally, which meant it checked zero rows, but that got fixed.
  • I wanted to indicate the location of the Board.game_over() by the coordinate system instead of pixel location, since it would have been simpler to write, but I don't think it works that way.
  • Tetris.create_new_shape() was a very dense one liner that took me some time to figure out how to access a class via a dictionary. I didn't know that was possible before.
  • When I went to write this out, I discovered that I had implemented the rotate method in the wrong place. Even though it worked, I had to go back to refactor that.
  • blablabblablab Thanks for reading this far maybe.

Have fun with your version. Any advice is welcome bla bla bla bla

1 Like

Great work, @Cedar_Mora!

It’s lucky that your college offers some “bored” time, :wink: so you can get some real work done.

Some of my code is similar to yours; some of it is different.

I also had to put a lot of time into Board.remove_complete_rows(self):, and did lots of testing on move_down_rows(self, y_start):. This was to make sure that unusual cases got handled correctly. For example, as illustrated in the attached images, two rows that are not adjacent to each other may need to get removed at the same time. Note that when the falling upside-down “L” on the right side of the first image fell into place, the 5th and 7th rows from the bottom both became complete, but the sixth did not, because it still had a missing block in the 3rd column from the right.

The code passed the test, though.

            

Scoring has also been implemented.

Below is code for some of the methods.

    def move_down_rows(self, y_start):
        for y in range(y_start, -1, -1):
            for x in range(self.width):
                if (x, y) in self.grid:
                    block = self.grid[x, y]
                    block.undraw()
                    del self.grid[x, y]
                    block.move(0, 1)
                    self.grid[block.x, block.y] = block
                    block.draw(self.canvas)
    def remove_complete_rows(self):
        num_removed = 0
        for y in range(self.height):
            if self.is_row_complete(y):
                self.delete_row(y)
                self.move_down_rows(y - 1)
                num_removed += 1
        return num_removed

Following is code for a ScoreBoard class.

class ScoreBoard():
    def __init__(self, win, width, height, score):
        self.width = width
        self.height = height
        self.score = score
        self.canvas = CanvasFrame(win, self.width, self.height)
        self.canvas.setBackground('white')
        self.score_message = Text(Point(150, 25), "Score: " + str(self.score))
        self.score_message.setSize(20)
        self.score_message.setTextColor("black")
        self.score_message.draw(self.canvas)
    def set_score(self, score):
        self.score_message.setText("<p>ause / score: " + str(score))
    def set_final_score(self, score):
        self.score_message.setText("final score: " + str(score))
    def pause_score(self, score):
        self.score_message.setText("un<p>ause / score: " + str(score))

So as not to be a “spoiler”, I have refrained from posting all my code at this time, but will post it in the coming weeks, if anyone would like to see it.

If anyone else has code, whether complete, incomplete, or in need of debugging, it would be great to see it.

Thank you, @Glenn for being here and participating. This course has become such a ghost town. I guess that’s the way of the MOOC though.

I almost wish that this Gentle Introduction to Python wasn’t so gentle. They didn’t really teach how to make stuff without a template and instructions.

I have my college coming up so I will learn some of this stuff, but are some more intermediate games programming courses online? I’m looking to become a professional games developer. Or maybe an iOS developer. Or maybe just whatever will get me work haha. Anyways, any resources that come to mind?

I don’t actually know a lot about game development, but a good way to get a sense of the “landscape” would be to browse through some of the subreddits that relate to the topic. A few examples are listed below:

You can then click links and use Google to follow leads that look promising.

Preparing to become a professional games developer will give you an opportunity to get into lots of interesting topics, such as graphics, analysis of algorithms, data structures, artificial intelligence, GUI design, and game theory.

1 Like

ScoreBoard class implemented, with levels and score displayed. As levels go on, pieces drop faster, but clearing rows gets more points. I was having trouble figuring out what system to use for scoring and levels, so I based it loosely upon the original Nintendo scoring system. I will post code and details later, since now I need to go to bed.


1 Like

For scoring, I made up a simple system.

For each shape that appears on the board, one point is awarded. The rationale is that the player can make room for new shapes by moving the current shape on the board.

Then, whenever rows are removed after a shape can no longer move, the player receives a number of points equal to the square of the number of rows that get removed at that time.

Most of the scoring is done in the do_move and an increment_score method of the Tetris class.

    def do_move(self, direction):
        dx, dy = self.DIRECTION[direction]
        if self.current_shape.can_move(self.board, dx, dy):
            self.current_shape.move(dx, dy)
            return True
        else:
            if direction == "Down":
                self.board.add_shape(self.current_shape)
                removed = self.board.remove_complete_rows()
                self.increment_score(removed * removed)
                self.current_shape = self.create_new_shape()
                if not self.board.draw_shape(self.current_shape):
                    # self.board.game_over()
                    self.scoreBoard.set_final_score(self.score)
                else:
                    self.increment_score(1)
            return False
    def increment_score(self, inc):
        self.score += inc
        self.scoreBoard.set_score(self.score)
        return self.score
1 Like

Smart move. I’ve done it different: check in loop and one move as separate method - do_shift().
I don’t like if/else statements, so i use dictionary:

    do = {"Left": self.do_move, "Right": self.do_move,
          "Up": self.do_rotate, "Down": self.do_move,
          "space": self.do_shift}
    if key in do.keys():
        do[key](key)
2 Likes

Nice pattern, @Jacek, somewhat reminiscent of a switch-case structure, but most importantly, neatly mapping values to actions …
value1: action1
value2: action2
value3: action3

I’m curious how you increased levels. I’m trying with linear function of turn (time) , but it seems like exponential in game. I left minimum value of delay as 10 ms.

I think my scoring system makes sense, but I’m not sure.

I wanted the levels to come in constant time, to keep the player interested, but also to give increasing points as the play goes on for the increasing difficulty due to the piece moving faster.

I thought the Nintendo scoring system worked well, so I used it. It gives the level plus one times the points given for clearing a certain amount of rows. 40 points times the level plus one for one row. 100 points times the level plus one for two rows. 300 times level plus one for three rows, and 1200 times level plus one for four rows.

I wanted the player’s level to increase in constant time, (a fixed interval reinforcement schedule, not that that’s important). Their “income” increases linearly (n1). Their score then would be integral, or accumulated area up until that point. If I’m remembering my calculus correctly, the accumulated area of a linear function is a parabola (n2), according to the power rule. Therefore, I made a guess as to something that would work, which looks like this:

        if self.score > 50*((self.level + 1) ** 2):
            self.level += 1

So the pattern looks like:

+-------+---------------+
| Level | Score (up to) |
+-------+---------------+
| 0     | 50            |
+-------+---------------+
| 1     | 200           |
+-------+---------------+
| 2     | 450           |
+-------+---------------+
| 3     | 800           |
+-------+---------------+
| 4     | 1250          |
+-------+---------------+
| 5     | 1800          |
+-------+---------------+
| 6     | 2450          |
+-------+---------------+
| 7     | 3200          |
+-------+---------------+
| 8     | 4050          |
+-------+---------------+
| 9     | 5000          |
+-------+---------------+

Every level, the delay decreases by 10%, roughly.

If I wanted longer games, I would likely modify the constant 50 to something larger, that way levels wouldn’t increase so fast. Or vice versa for shorter games.

If you would like to try my version yourself, or read my code, you may find it here.

1 Like

I am so incredibly close to a PiecePreview Class, I can taste it. It’s still not working, but I have at least gotten somewhere:

I have really gone on circles with this one. I originally attempted to just create the class at the bottom, being ignorant of canvas placing. It didn’t really fit well on my monitor, so I attempted to place the piece preview window to the side. I tried to do it using the Tkinter pack manager. Then I realized that in order to do so, I would have to create a Frame (or Canvas?) to the right of the Board. No problem. Then I wanted to move it to the top right for visibility. Much more problem. I tried to nest the PiecePreview class within a separate placeholder CanvasFrame, however, they don’t appear to nest. Then I tried…blaaa.

Anyways, after a long frustrating series of events I ended up giving up on the top right location and was just happy with it being to the right of the board. It still doesn’t work correctly, but I have a place to put the piece now and it sort of shows up.

The tetris_template.py seems like it was really not intended to be expanded beyond the original design. The shapes’ drawing methods are dependent upon the coordinate system of the Board class. Because of that, I really can’t figure out how to get a shape to draw on a separate location on screen. Incredibly annoying. Anyways, we will get there somehow.

1 Like

Not sure if this will help, but to accommodate the display of the score, I made room for both the ScoreBoard and the ScoreBoard in the main window in the Tetris __init__ method. Would it help for you to do the same for the PiecePreview class?

That would work only if you wanted to place all of the game components in the same window.

    def __init__(self, win):
        self.score = 0
        self.board = Board(win, self.BOARD_WIDTH, self.BOARD_HEIGHT)
        self.scoreBoard = ScoreBoard(win, self.BOARD_WIDTH * Block.BLOCK_SIZE, 50, 0)
 
 
        self.win = win
        self.delay = 1000 #ms
 
        # sets up the keyboard events
        # when a key is called the method key_pressed will be called
        self.win.bind_all('<Key>', self.key_pressed)
 
        # set the current shape to a random new shape
        self.current_shape = self.create_new_shape()
 
        # Draw the current_shape on the board (take a look at the
        # draw_shape method in the Board class)
        ####  YOUR CODE HERE ####
        self.board.draw_shape(self.current_shape)
        self.increment_score(1)
        # For Step 9:  animate the shape!
        ####  YOUR CODE HERE ####
        self.paused = False
        self.animate_shape()
1 Like

Sheesh, finally made the Piece Preview. Took long enough. I think the Pareto Principle applies here. It’s not fancy, and the pieces shift at gameover, but it’s close enough for me. I can’t seem to get a screenshot to work right, but you can find the code here.

1 Like

The Piece Preview is a nice feature that helps with planning where to steer the current shape.

I did manage, accidentally, to get your code to throw an error by hitting an invalid key at some point.

Exception in Tkinter callback
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-tk/Tkinter.py", line 1470, in __call__
    return self.func(*args)
  File "/Python/MechanicalMOOC/_Tetris_2015/cedar_mora_tetris.py", line 602, in key_pressed
    self.do_move(key)
  File "/Python/MechanicalMOOC/_Tetris_2015/cedar_mora_tetris.py", line 556, in do_move
    dx = self.DIRECTION[direction][0]
KeyError: '??'

It could be addressed by adding some defensive code to the Tetris class’s key_pressed method.

Fixed. I used a try and except block. Now it just ignores invalid keys to the DIRECTION dictionary in the Tetris class, as you may see in the commit.

By KeyError, it didn’t mean a key on a keyboard, it meant a key to a dictionary, the DIRECTION dictionary. So when you press some invalid key, say ‘a’, it will still accept it as input, but when it attempts to use the do_move method of the Tetris class, it would look up ‘a’ as a key in the DIRECTION dictionary, which doesn’t exist. The DIRECTION dictionary only has ‘Left’, ‘Right’, and ‘Down’. ‘a’ is not among them. ‘space’ doesn’t throw an exception because it is handled in the key_pressed method of the Tetris class.

1 Like