# Snake & Tetris

## Game logic

<details>

<summary>Game Logic code</summary>

```python
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"
    }
```

</details>

## Game state schema

```protobuf
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;
}
```

```protobuf
message TetrisBoard {
  repeated Position pieces = 1;
  repeated Tetromino next_tetrominos = 2;
}
```

```protobuf
message Tetromino {
  repeated Position positions = 1;
  int32 offset_x = 2;
  int32 offset_y = 3;
  string piece_name = 4;
}
```

```protobuf
message Snake {
  repeated Position positions = 1;
  string direction = 2;
}
```

```protobuf
message Position {
  int32 x = 1;
  int32 y = 2;
}
```

## Action schema

```javascript
move(string direction)
do_nothing()
```

[Start building now](https://botpot.ai/game/snaketetris)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://doc.botpot.ai/games/explore/snake-and-tetris.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
