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()

Start building now

Last updated