MP10
Tetris
Due dates may be found on the schedule.

1 Concept

In this MP, you will implement a Tetris game in a way that can later be expanded upon to be a multiplayer game with multiple servers.

If you’ve not played Tetris before, we recommend doing so first. There are many implementations online; djblue.github.io is a reasonable one.

2 Technical Overview

We provide an HTML5 client. It displays a 10-by-20 grid of colored cells, where each cell has 15 possible colors. It also sends user keypresses to the server.

You write the server. It tracks all game state and sends the resulting board state (or board changes) to the client. In general, this means tracking both the fixed cells and a moving tetromino, and updating game state both periodically and when a keypress is sent.from the client.

3 Initial Files

Initial files are available in mp10.zip, including one file you’ll edit: tetris.py.

4 Specification

You will build a server that runs a Tetris game. It will communicate with clients using WebSockets. We will provide an example client written using HTML and Javascript.

4.1 HTTP requests to handle

GET /

Return the contents of the provided index.html, which is our example client.

This is implemented for you in the initial file.

GET /ws

Upgrade the request to a WebSocketResponse.

Create a Tetris game instance; have it interact with this WebSocket.

The initialization and closing of the websocket is implemented for you in the initial file. The Tetris logic is not.

4.2 Incoming WebSocket messages

The client will send messages over the WebSocket, which will be strings representing player actions.

Positioning keys are ignored if they would result in the tetromino overlapping a filled cell or going off the screen. If the message is ignored, do not send any message back over the WebSocket: act the same as if the request had not even arrived.

Descending keys are never ignored:

4.3 Outgoing WebSocket messages

When a client first connects, and any time the state of the game changes, send a WebSocket message. This message should be a JSON object that contains any subset of the following entries.

The message

{
    "live": [1,0, 4,1, 18],
    "next": 2,
    "board": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
    "event": "gameover"
}

will clear the board, set a live O and queue an I next, and render as

An empty Tetris game with a live O tetromino at the top of the board and an I tetromino up next.

It will also stop the game, so no further updates will be sent from the server.

4.3.1 "live"

The "live" key tells the client the position of the live tetromino (the one the player can control). Its value is a list of five integers, being in order

  • The shape (a number between 1 and 7 inclusive).
  • The orientation (a number between 0 and 4, though some shapes have fewer orientations).
  • The x coordinate of the reference point of the shape.
  • The y coordinate of the reference point of the shape.
  • The maximum number1 This number most easily found by brute force: try moving down 1 and see if it overlaps anything; then 2, then 3, and so on until and overlap is found. of moves down this tetromino could make from its current position.

A tetromino is a set of four adjacent squares. There are seven possible shapes; we give each its own color.

List of tetrominos and their orientations
Code Meaning Total orientations
0 empty cell
1 O 1
2 I 2; 0 is the tall orientation, 1 is the wide orientation
3 T 4; 0 is flat on the top, +1 rotates 90° clockwise
4 L 4; 0 is flat on the left, +1 rotates 90° clockwise
5 J 4; 0 is flat on the right, +1 rotates 90° clockwise
6 S 2; 0 is the tall orientation, 1 is the wide orientation
7 Z 2; 0 is the tall orientation, 1 is the wide orientation

The position of a tetromino is communicated as a single coordinate, which we call the tetromino’s reference point. This has the maximum y of any point in the tetromino, and the median x (rounded down).2 The reference point might not be one of those covered by the tetromino; for example, an S tetromino in orientation 0 that covers cells [(3,8), (3,9), (4,9), (4,10)] has reference point (3,10).

You can find the coordinates of each cell of each tetromino in each orientation (relative to reference point (0,0)) in the index.html provided in mp10.zip.

4.3.2 "next"

The "next" key tells the client what shape the next live tetromino will have. It should be an integer between 1 and 7. See "live" for a list of tetromino shapes.

When a new tetromino becomes live, it should always have

  • the shape most recently indicated by "next"
  • orientation 0
  • x = 4
  • the smallest y that keeps it entirely in-screen

4.3.3 "event"

The "event" key is used to tell the client that the game is over. Its value is ignored for MP10, though it might gain meaning in the project.

For the game over to be valid, the "live" tetromino must both (a) touch the top of the screen and (b) overlap a filled-in board cell.

4.3.4 "board"

The "board" key is used to update the set of filled-in cells on the board. These should not include the live tetromino, only fixed cells.

The value of this key is a list of 20 integers. Each 3 bits of the integer represent what to draw in one cell of a row (0 for an empty cell, or a shape number from "live" for the color of that shape). The first integer is the top row and its low-order bits are the right column.

The integer 0b1110000000000000000000001011013 Note that JSON only accepts integers in base-10, but you can still write them in base-2 in Python if you wish; json_response will convert them to base-10 for you. has one cell colored like a Z on the left, two cells colored like a J on the right, and empty cells in between.

4.4 Other Game Actions

4.4.1 Game Setup

When a Tetris game is first created, it should

  1. Have an empty board.

  2. Call tilestub.new_tile() twice, once to get the live tetromino and once to get the next one.

    The version of this function you’ll play with just returns a random number, but making it a separate function allows us to change its behavior during testing.

  3. Send a single websocket message with keys "live", "board", and "next".

  4. Create an async task to create automatic motions in the future.

The async task is created using asyncio.create_task(function_to_call()). That returns a handle to the task that you’ll need when the game ends to cancel the task. The function that is called should look like this:

while True:
    await asyncio.sleep(0.5)
    ... # do one time step (move the live tetromino down)

4.4.2 Game Cleanup

When the game is over or the client’s WebSocket disconnects, you should call .cancel() on the return value of asyncio.create_task you ran during game setup.

4.4.3 Changing Tetrominos

When the live tetromino would normally move down but cannot, do the following:

  1. Convert the cells it covers to be filled board cells, using the color of the tetromino.
  2. If one or more row of the board is entirely filled, remove those rows and move any rows above it down by one.
  3. Place make the previously-picked next tetronimo live (see "next" for where to place it) and pick a new next tetronimo using tilestub.new_tile().
  4. Send a WebSocket message with keys "board", "live", and "next" (and "event" if the game is now over).

5 Suggestions

6 Testing your code

The best way to test you Tetris game is to play it. Many errors that will appear confusing when running automated tests will make much more sense when you see them in a running game.

You probably want to run your code in development mode during debugging to see more WebSocket error messages. The provided Makefile has a target make debug that runs in development mode.

We do provide automated tests, which you can run with make test or python3 -m pytest. These operate by trying to play pre-set games, verifying your WebSocket messages.

If you fail to provide a message when one is expected, the automated tests will freeze. We use timeouts to detect such freezing, closing each WebSocket connection after a few seconds. Such timeouts will create error messages like TypeError: Received message 256:None is not str.