Matrix Based Game

p
"""
Matrix-Based Game Script
=========================
This script implements a matrix-based game where players interact with a grid of
elements. The primary goals are to:
- Identify connected elements of the same type from a selected position.
- Remove those elements, adjust the matrix by simulating gravity, and reorganize empty
  columns.
- Calculate and display the score based on the number of elements removed in each move.

Functions:
-----------
1. `find_repeat`: Finds all connected elements of the same type.
2. `increment_score`: Calculates the score for a given move.
3. `move_x`: Simulates gravity in a column.
4. `move_y`: Reorganizes the matrix by shifting columns leftward when a column becomes
    empty.
5. `play`: Executes a single move, updating the matrix and returning the score.

Input Format:
--------------
1. Matrix size (`lines`): Integer specifying the size of the matrix (N x N).
2. Matrix content (`matrix`): Rows of the matrix, each consisting of characters.
3. Number of moves (`movs`): Integer indicating the number of moves.
4. List of moves (`movements`): A comma-separated string of coordinates for each move.

(0,0) position starts from first left column to last right, and below row to up row


Example Input:
---------------
4
RRBG
RBBG
YYGG
XYGG
2
0 1,1 1

Example (0,0) = X

Output:
--------
The script outputs the total score after processing all moves.

Usage:
-------
Run the script and provide the required inputs as prompted.

"""


def validate_matrix_size(size: int) -> None:
    """
    >>> validate_matrix_size(-1)
    Traceback (most recent call last):
        ...
    ValueError: Matrix size must be a positive integer.
    """
    if not isinstance(size, int) or size <= 0:
        raise ValueError("Matrix size must be a positive integer.")


def validate_matrix_content(matrix: list[str], size: int) -> None:
    """
    Validates that the number of elements in the matrix matches the given size.

    >>> validate_matrix_content(['aaaa', 'aaaa', 'aaaa', 'aaaa'], 3)
    Traceback (most recent call last):
        ...
    ValueError: The matrix dont match with size.
    >>> validate_matrix_content(['aa%', 'aaa', 'aaa'], 3)
    Traceback (most recent call last):
        ...
    ValueError: Matrix rows can only contain letters and numbers.
    >>> validate_matrix_content(['aaa', 'aaa', 'aaaa'], 3)
    Traceback (most recent call last):
        ...
    ValueError: Each row in the matrix must have exactly 3 characters.
    """
    print(matrix)
    if len(matrix) != size:
        raise ValueError("The matrix dont match with size.")
    for row in matrix:
        if len(row) != size:
            msg = f"Each row in the matrix must have exactly {size} characters."
            raise ValueError(msg)
        if not all(char.isalnum() for char in row):
            raise ValueError("Matrix rows can only contain letters and numbers.")


def validate_moves(moves: list[tuple[int, int]], size: int) -> None:
    """
    >>> validate_moves([(1, 2), (-1, 0)], 3)
    Traceback (most recent call last):
        ...
    ValueError: Move is out of bounds for a matrix.
    """
    for move in moves:
        x, y = move
        if not (0 <= x < size and 0 <= y < size):
            raise ValueError("Move is out of bounds for a matrix.")


def parse_moves(input_str: str) -> list[tuple[int, int]]:
    """
    >>> parse_moves("0 1, 1 1")
    [(0, 1), (1, 1)]
    >>> parse_moves("0 1, 1 1, 2")
    Traceback (most recent call last):
        ...
    ValueError: Each move must have exactly two numbers.
    >>> parse_moves("0 1, 1 1, 2 4 5 6")
    Traceback (most recent call last):
        ...
    ValueError: Each move must have exactly two numbers.
    """
    moves = []
    for pair in input_str.split(","):
        parts = pair.strip().split()
        if len(parts) != 2:
            raise ValueError("Each move must have exactly two numbers.")
        x, y = map(int, parts)
        moves.append((x, y))
    return moves


def find_repeat(
    matrix_g: list[list[str]], row: int, column: int, size: int
) -> set[tuple[int, int]]:
    """
    Finds all connected elements of the same type from a given position.

    >>> find_repeat([['A', 'B', 'A'], ['A', 'B', 'A'], ['A', 'A', 'A']], 0, 0, 3)
    {(1, 2), (2, 1), (0, 0), (2, 0), (0, 2), (2, 2), (1, 0)}
    >>> find_repeat([['-', '-', '-'], ['-', '-', '-'], ['-', '-', '-']], 1, 1, 3)
    set()
    """

    column = size - 1 - column
    visited = set()
    repeated = set()

    if (color := matrix_g[column][row]) != "-":

        def dfs(row_n: int, column_n: int) -> None:
            if row_n < 0 or row_n >= size or column_n < 0 or column_n >= size:
                return
            if (row_n, column_n) in visited:
                return
            visited.add((row_n, column_n))
            if matrix_g[row_n][column_n] == color:
                repeated.add((row_n, column_n))
                dfs(row_n - 1, column_n)
                dfs(row_n + 1, column_n)
                dfs(row_n, column_n - 1)
                dfs(row_n, column_n + 1)

        dfs(column, row)

    return repeated


def increment_score(count: int) -> int:
    """
    Calculates the score for a move based on the number of elements removed.

    >>> increment_score(3)
    6
    >>> increment_score(0)
    0
    """
    return int(count * (count + 1) / 2)


def move_x(matrix_g: list[list[str]], column: int, size: int) -> list[list[str]]:
    """
    Simulates gravity in a specific column.

    >>> move_x([['-', 'A'], ['-', '-'], ['-', 'C']], 1, 2)
    [['-', '-'], ['-', 'A'], ['-', 'C']]
    """

    new_list = []

    for row in range(size):
        if matrix_g[row][column] != "-":
            new_list.append(matrix_g[row][column])
        else:
            new_list.insert(0, matrix_g[row][column])
    for row in range(size):
        matrix_g[row][column] = new_list[row]
    return matrix_g


def move_y(matrix_g: list[list[str]], size: int) -> list[list[str]]:
    """
    Shifts all columns leftward when an entire column becomes empty.

    >>> move_y([['-', 'A'], ['-', '-'], ['-', 'C']], 2)
    [['A', '-'], ['-', '-'], ['-', 'C']]
    """

    empty_columns = []

    for column in range(size - 1, -1, -1):
        if all(matrix_g[row][column] == "-" for row in range(size)):
            empty_columns.append(column)

    for column in empty_columns:
        for col in range(column + 1, size):
            for row in range(size):
                matrix_g[row][col - 1] = matrix_g[row][col]
        for row in range(size):
            matrix_g[row][-1] = "-"

    return matrix_g


def play(
    matrix_g: list[list[str]], pos_x: int, pos_y: int, size: int
) -> tuple[list[list[str]], int]:
    """
    Processes a single move, updating the matrix and calculating the score.

    >>> play([['R', 'G'], ['R', 'G']], 0, 0, 2)
    ([['G', '-'], ['G', '-']], 3)
    """

    same_colors = find_repeat(matrix_g, pos_x, pos_y, size)

    if len(same_colors) != 0:
        for pos in same_colors:
            matrix_g[pos[0]][pos[1]] = "-"
        for column in range(size):
            matrix_g = move_x(matrix_g, column, size)

        matrix_g = move_y(matrix_g, size)

    return (matrix_g, increment_score(len(same_colors)))


def process_game(size: int, matrix: list[str], moves: list[tuple[int, int]]) -> int:
    """Processes the game logic for the given matrix and moves.

    Args:
        size (int): Size of the game board.
        matrix (List[str]): Initial game matrix.
        moves (List[Tuple[int, int]]): List of moves as (x, y) coordinates.

    Returns:
        int: The total score obtained.
    >>> process_game(3, ['aaa', 'bbb', 'ccc'], [(0, 0)])
    6
    """

    game_matrix = [list(row) for row in matrix]
    total_score = 0

    for move in moves:
        pos_x, pos_y = move
        game_matrix, score = play(game_matrix, pos_x, pos_y, size)
        total_score += score

    return total_score


if __name__ == "__main__":
    import doctest

    doctest.testmod(verbose=True)
    try:
        size = int(input("Enter the size of the matrix: "))
        validate_matrix_size(size)
        print(f"Enter the {size} rows of the matrix:")
        matrix = [input(f"Row {i+1}: ") for i in range(size)]
        validate_matrix_content(matrix, size)
        moves_input = input("Enter the moves (e.g., '0 0, 1 1'): ")
        moves = parse_moves(moves_input)
        validate_moves(moves, size)
        score = process_game(size, matrix, moves)
        print(f"Total score: {score}")
    except ValueError as e:
        print(f"{e}")