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.
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.
Initial files are available in mp10.zip, including one file you’ll edit: tetris.py
.
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.
Return the contents of the provided index.html
, which is our example client.
This is implemented for you in the initial file.
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.
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.
left
and right
move the active tetromino one cell to the left (‒1 x) or right (+1 x), respectively.
cw
and ccw
rotate the active tetromino clockwise (+1 orientation) or counter-clockwise (‒1 orientation), respectively. If that would result in part of the tetromino being off the screen, they also move the tetromino enough to keep it on the screen.
When a rotate also moves, the two happen atomically: either it both rotates and moves, or if that would overlap a filled cell then it neither rotates nor moves and the entire action is instead ignored.
Descending keys are never ignored:
down
either moves the active tetromino one cell downward (+1 y), or if that would cause overlap it instead advances to the next tetromino.drop
moves the active tetromino as far downward as it can go, and then advances to the next tetromino.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
It will also stop the game, so no further updates will be sent from the server.
"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
A tetromino is a set of four adjacent squares. There are seven possible shapes; we give each its own color.
Code | Meaning | Total orientations |
---|---|---|
0 | empty cell | |
1 | O | 1 |
2 | I | 2; 0 is the tallorientation, 1 is the wideorientation |
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 tallorientation, 1 is the wideorientation |
7 | Z | 2; 0 is the tallorientation, 1 is the wideorientation |
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.
"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
"next"
"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.
"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.
When a Tetris game is first created, it should
Have an empty board.
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.
Send a single websocket message with keys "live"
, "board"
, and "next"
.
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) ...
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.
When the live tetromino would normally move down but cannot, do the following:
"next"
for where to place it) and pick a new next tetronimo using tilestub.new_tile()
."board"
, "live"
, and "next"
(and "event"
if the game is now over).We recommend making a class
to represent a Tetris game.
Do all Game Setup from the class’s constructor. Have the class store the WebSocket connection so it can send time-pass updates.
Have a method in the class for each kind of action you might do: moving the live tetronimo
Copy the set of cells covered by each tetronimo in each orientation from index.html
. Use these to detect overlaps, drop distances, etc.
Avoid using lists to represent tetronimo cells; modifications of them can mess up other functions. Tuples are preferred.
Suppose you have a list of (x, y) points and want to offset them in some direction. You can do this easily with a list comprehension like
= tuple( (x+dx, y+dy) for (x,y) in original_points ) offset_points
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
.