Snake & Tetris
Game logic
Game Logic code
import random
import copy
SNAKE_BOARD_WIDTH = 11
SNAKE_BOARD_HEIGHT = 20
TETRIS_BOARD_WIDTH = 10
TETRIS_BOARD_HEIGHT = 20
TETRIS_ROTATE_CENTERS = {
"I": (1.5, -0.5),
"J": (1, 0),
"L": (1, 0),
"O": (0.5, 0.5),
"S": (1, 0),
"T": (1, 0),
"Z": (1, 0)
}
def init_game(game_settings):
snake_positions = [_make_position(5, 1), _make_position(5, 0)]
state = {
"snake": {
"positions": snake_positions,
"direction": "up"
},
"tetris_board": {
"next_tetrominos": [_generate_random_tetromino()],
"pieces": []
},
"food": _generate_food_position(snake_positions),
"is_dead_snake": False,
"is_dead_tetris": False,
"score": 0,
"player_turn_idx": 0
}
return state
def _make_position(x, y):
return {
"x": x,
"y": y
}
def _generate_random_tetromino():
tetromino_funcs = [_I_tetromino, _J_tetromino, _L_tetromino, _O_tetromino, _S_tetromino, _T_tetromino, _Z_tetromino]
return random.choice(tetromino_funcs)()
def _I_tetromino():
return {
"positions": [
_make_position(x = 0, y = 0),
_make_position(x = 1, y = 0),
_make_position(x = 2, y = 0),
_make_position(x = 3, y = 0)
],
"offset_x": 3,
"offset_y": 20,
"piece_name": "I"
}
def _J_tetromino():
return {
"positions": [
_make_position(x = 0, y = 1),
_make_position(x = 0, y = 0),
_make_position(x = 1, y = 0),
_make_position(x = 2, y = 0)
],
"offset_x": 3,
"offset_y": 20,
"piece_name": "J"
}
def _L_tetromino():
return {
"positions": [
_make_position(x = 0, y = 0),
_make_position(x = 1, y = 0),
_make_position(x = 2, y = 0),
_make_position(x = 2, y = 1)
],
"offset_x": 3,
"offset_y": 20,
"piece_name": "L"
}
def _O_tetromino():
return {
"positions": [
_make_position(x = 0, y = 0),
_make_position(x = 1, y = 0),
_make_position(x = 1, y = 1),
_make_position(x = 0, y = 1)
],
"offset_x": 4,
"offset_y": 20,
"piece_name": "O"
}
def _S_tetromino():
return {
"positions": [
_make_position(x = 0, y = 0),
_make_position(x = 1, y = 0),
_make_position(x = 1, y = 1),
_make_position(x = 2, y = 1)
],
"offset_x": 3,
"offset_y": 20,
"piece_name": "S"
}
def _T_tetromino():
return {
"positions": [
_make_position(x = 0, y = 0),
_make_position(x = 1, y = 0),
_make_position(x = 1, y = 1),
_make_position(x = 2, y = 0)
],
"offset_x": 3,
"offset_y": 20,
"piece_name": "T"
}
def _Z_tetromino():
return {
"positions": [
_make_position(x = 0, y = 1),
_make_position(x = 1, y = 1),
_make_position(x = 1, y = 0),
_make_position(x = 2, y = 0)
],
"offset_x": 3,
"offset_y": 20,
"piece_name": "Z"
}
def _generate_food_position(snake_positions):
snake = set([(p["x"], p["y"]) for p in snake_positions])
valid = []
for i in range(SNAKE_BOARD_WIDTH):
for j in range(SNAKE_BOARD_HEIGHT):
if (i, j) not in snake:
valid.append((i, j))
x, y = random.choice(valid)
return _make_position(x = x, y = y)
def move(state, direction):
# Change the direction of the snake but does not move it
state = _change_snake_direction(state, direction)
# move the tetris left, right, soft drop, or spin immediately if possible
state = _move_tetris(state, direction)
return state
def _change_snake_direction(state, direction):
vector_new = __direction_to_vector(direction)
vector_original = __direction_to_vector(state["snake"]["direction"])
if __is_perpendicular(vector_original, vector_new):
state["snake"]["direction"] = direction
return state
def _move_tetris(state, direction):
"""
Attempts to move the piece, and is a No-op if it moves against the wall or existing piece on the board.
"""
existing_pieces = set([(p["x"], p["y"]) for p in state["tetris_board"]["pieces"]])
tetromino = copy.deepcopy(state["tetris_board"]["next_tetrominos"][0])
if direction == "up":
tetromino = _rotate_cw(tetromino)
else:
move_vector = __direction_to_vector(direction)
tetromino["offset_x"] += move_vector[0]
tetromino["offset_y"] += move_vector[1]
if _tetromino_can_be_there(existing_pieces, tetromino):
if _tetromino_is_bottom(existing_pieces, tetromino):
_handle_bottom(state, tetromino)
else:
state["tetris_board"]["next_tetrominos"][0] = tetromino
return state
def do_nothing(state):
return state
def auto_update(state):
if state["is_dead_snake"] or state["is_dead_tetris"]:
return state
# Every time this method is called, advance the state
# by moving the snake forward and tetris piece down
state = _auto_update_snake(state)
state = _auto_update_tetris(state)
return state
def _auto_update_snake(state):
"""
Snake moves forward by one.
If the snake hits itself or the wall, the game is over and is_dead_snake is set to True
"""
vector_move = __direction_to_vector(state["snake"]["direction"])
previous_head = state["snake"]["positions"][0]
new_x = previous_head["x"] + vector_move[0]
new_y = previous_head["y"] + vector_move[1]
# Check for collision with wall
if (new_x < 0 or new_x >= SNAKE_BOARD_WIDTH or
new_y < 0 or new_y >= SNAKE_BOARD_HEIGHT):
state["is_dead_snake"] = True
return state
for i in range(len(state["snake"]["positions"]) - 1): # skip last one because the snake moves forward
p = state["snake"]["positions"][i]
if p["x"] == new_x and p["y"] == new_y:
state["is_dead_snake"] = True
return state
new_snake = [_make_position(x = new_x, y = new_y)]
if new_x == state["food"]["x"] and new_y == state["food"]["y"]:
new_snake.extend(state["snake"]["positions"][:])
new_food = _generate_food_position(new_snake)
state["food"]["x"] = new_food["x"]
state["food"]["y"] = new_food["y"]
state["score"] += 1
else:
new_snake.extend(state["snake"]["positions"][0:-1])
state["snake"]["positions"] = new_snake
return state
def _auto_update_tetris(state):
"""
Attempts to drop the piece by one. If the current tetromino is above the board and cannot drop by one, the game is over and is_dead_tetris is set to True.
"""
existing_pieces = set([(p["x"], p["y"]) for p in state["tetris_board"]["pieces"]])
tetromino = copy.deepcopy(state["tetris_board"]["next_tetrominos"][0])
tetromino["offset_y"] -= 1
if _tetromino_can_be_there(existing_pieces, tetromino):
if _tetromino_is_bottom(existing_pieces, tetromino):
_handle_bottom(state, tetromino)
else:
state["tetris_board"]["next_tetrominos"][0] = tetromino
else:
state["is_dead_tetris"] = True
return state
def _rotate_cw(tetromino):
cx, cy = TETRIS_ROTATE_CENTERS[tetromino["piece_name"]]
rotated = []
for p in tetromino["positions"]:
rotated.append(_make_position(
x = round((p["y"] - cy) + cx),
y = round((cx - p["x"]) + cy)))
return {
"positions": rotated,
"offset_x": tetromino["offset_x"],
"offset_y": tetromino["offset_y"],
"piece_name": tetromino["piece_name"]
}
def _handle_bottom(state, tetromino):
for p in tetromino["positions"]:
new_x = p["x"] + tetromino["offset_x"]
new_y = p["y"] + tetromino["offset_y"]
state["tetris_board"]["pieces"].append(
{
"x": new_x,
"y": new_y,
"piece_name": tetromino["piece_name"]
})
state["tetris_board"]["next_tetrominos"][0] = _generate_random_tetromino()
_clear_lines(state)
return
def _clear_lines(state):
row_to_columns = {}
for p in state["tetris_board"]["pieces"]:
if p["y"] in row_to_columns:
row_to_columns[p["y"]].add(p["x"])
else:
row_to_columns[p["y"]] = set([p["x"]])
rows_to_clear = []
for row, columns in row_to_columns.items():
if len(columns) == TETRIS_BOARD_WIDTH:
rows_to_clear.append(row)
if len(rows_to_clear) == 0:
return
how_to_handle_piece = []
for p in state["tetris_board"]["pieces"]:
number_to_move_down = 0
should_clear = False
for row in rows_to_clear:
if p["y"] == row:
should_clear = True
elif p["y"] > row:
number_to_move_down += 1
if should_clear:
how_to_handle_piece.append(None)
else:
how_to_handle_piece.append(number_to_move_down)
new_pieces = []
for i in range(len(state["tetris_board"]["pieces"])):
if how_to_handle_piece[i] == None:
continue
current_piece = state["tetris_board"]["pieces"][i]
new_pieces.append(
{
"x": current_piece["x"],
"y": current_piece["y"] - how_to_handle_piece[i],
"piece_name": current_piece["piece_name"]
})
state["tetris_board"]["pieces"] = new_pieces
def _tetromino_can_be_there(existing_pieces, tetromino):
for p in tetromino["positions"]:
x = p["x"] + tetromino["offset_x"]
y = p["y"] + tetromino["offset_y"]
if (x, y) in existing_pieces:
return False
if x < 0 or x >= TETRIS_BOARD_WIDTH:
return False
if y < 0:
return False
return True
def _tetromino_is_bottom(existing_pieces, tetromino):
for p in tetromino["positions"]:
x = p["x"] + tetromino["offset_x"]
y = p["y"] + tetromino["offset_y"]
if (x, y - 1) in existing_pieces:
return True
if y == 0:
return True
return False
def __direction_to_vector(direction):
if direction == "left":
return (-1, 0)
elif direction == "right":
return (1, 0)
elif direction == "up":
return (0, 1)
elif direction == "down":
return (0, -1)
raise InvalidActionError("Not a valid direction")
def __is_perpendicular(v1, v2):
return (v1[0] * v2[0] + v1[1] * v2[1] == 0)
def get_game_result(state):
if state["is_dead_snake"] or state["is_dead_tetris"]:
return {
"game_result": "SinglePlayerCompleted",
"winner_idx": 0,
"score": state["score"]
}
return {
"game_result": "NoWinnerYet"
}
Game state schema
message State {
string user1 = 1;
Snake snake = 2;
Position food = 3;
TetrisBoard tetris_board = 4;
int32 score = 5;
bool is_dead_snake = 6;
bool is_dead_tetris = 7;
string user_turn = 8;
}
message TetrisBoard {
repeated Position pieces = 1;
repeated Tetromino next_tetrominos = 2;
}
message Tetromino {
repeated Position positions = 1;
int32 offset_x = 2;
int32 offset_y = 3;
string piece_name = 4;
}
message Snake {
repeated Position positions = 1;
string direction = 2;
}
message Position {
int32 x = 1;
int32 y = 2;
}
Action schema
move(string direction)
do_nothing()
Last updated