# Problem Description
This is a grid-based watermelon growth simulation problem where:
- Watermelons start from seeds placed on a square grid
- Each seed grows in 4 directions (up, down, left, right) each day
- The simulation runs for T days
- Seeds can die under certain conditions
- We need to track which seeds survive until day T

# Visual Facts
1. Grid and Layout:
   - Square grid with field_size × field_size dimensions
   - Uses 0-based indexing for coordinates
   - Green cells represent living watermelon growth
   - Gray cells represent dead watermelon remains

2. Growth Pattern:
   - Day 1: Single cell (seed position)
   - Day 2: Cross shape (+) occupying 5 cells
   - Growth extends one cell in each cardinal direction daily

3. Death Conditions Illustrated:
   - Example 1: Shows boundary death
   - Example 2: Shows boundary death
   - Example 3: Shows boundary death
4. State Representation:
   - Living watermelons shown in green
   - Dead watermelons shown in gray
   - Original seed position marked with watermelon icon

# Visual Patterns
1. Growth Rules:
   - Each day: Expands 1 cell in all 4 directions (up, down, left, right)
   - Growth is simultaneous for all living watermelons
   - Growth pattern forms a '+' shape on day 2

2. Death Conditions:
   - Boundary Death: If any part touches/crosses grid boundary
   - Chain Reaction: Dead watermelon areas can cause new watermelons to die

3. State Transitions:
   - Once a watermelon dies, its occupied cells remain (shown in gray)
   - Death occurs on the day of boundary crossing

4. Survival Rules:
   - Must stay within boundaries
   - Must have space to grow in all four directions


# Solution
from typing import List, Tuple
from collections import defaultdict

def solution(seed_positions: List[Tuple[int, int]], field_size: int, T: int) -> int:
    """
    Determines which watermelon seeds remain alive at Day T, based on field size and growth rules.

    Args:
        seed_positions: List of (x, y) coordinate tuples representing initial seed positions (0-based indexing)
        field_size: Side length of the square field (e.g., 5 means a 5x5 field)
        T: The day number at which to check for surviving seeds

    Returns:
        The number of surviving seeds at Day T
    """
    # Define the four possible growth directions with their respective coordinate changes
    directions = {
        'up': (-1, 0),
        'down': (1, 0),
        'left': (0, -1),
        'right': (0, 1)
    }

    # Assign each seed a unique index for easy reference
    seed_indices = list(range(len(seed_positions)))

    # Initialize the set of alive seeds (all seeds start as alive)
    alive_seeds = set(seed_indices)

    # Initialize active directions for each seed
    # For each seed, maintain a dictionary mapping direction to the last grown position in that direction
    seed_active_fronts = {
        seed: {dir: pos for dir in directions.keys()}
        for seed, pos in enumerate(seed_positions)
    }

    # Initialize the grid with the initial seed positions
    grid = {}  # position -> seed index (or None if no seed is alive at that position)

    # Initialize a death schedule mapping T to sets of seeds to die on that day
    death_schedule = defaultdict(set)

    # Simulate the growth day by day
    for day in range(1, T + 1):
        # Temporary dictionary to track new growths: position -> set of (seed, direction)
        new_growths = defaultdict(set)

        # Flag to indicate if any growth hits the boundary on this day
        boundary_hit = False

        # Iterate over each alive seed to attempt growth in all active directions
        for seed in alive_seeds:
            active_directions = list(seed_active_fronts[seed].keys())  # List to allow modification during iteration
            for dir in active_directions:
                last_pos = seed_active_fronts[seed][dir]
                dx, dy = directions[dir]
                next_x = last_pos[0] + dx
                next_y = last_pos[1] + dy
                next_pos = (next_x, next_y)

                # Check if the next position is out of bounds (hits the boundary)
                if not (0 <= next_x < field_size and 0 <= next_y < field_size):
                    boundary_hit = True
                    # Remove this direction from active fronts as it cannot grow further
                    del seed_active_fronts[seed][dir]
                    continue  # Move to the next direction

                # Record the attempted growth
                new_growths[next_pos].add((seed, dir))

        # Process new growths to detect collisions and update the grid
        for pos, seed_dir_set in new_growths.items():
            if pos in grid:
                # If the position is already occupied by a dead seed, refresh it to alive
                if grid[pos] is None:
                    grid[pos] = next(iter(seed_dir_set))[0]  # Assign the first seed in the set
            else:
                # If the position is not occupied, assign it to the first seed in the set
                grid[pos] = next(iter(seed_dir_set))[0]

        # Schedule deaths due to boundary hit
        if boundary_hit:
            # All seeds die on the next day
            death_day = day + 1
            death_schedule[death_day].update(alive_seeds)

        # Update the active fronts for each seed with their new growth positions
        for pos, seed_dir_set in new_growths.items():
            for seed, dir in seed_dir_set:
                # Only update the last position if the seed is still alive today
                # Seeds scheduled to die on the next day are still active today
                seed_active_fronts[seed][dir] = pos

        # Apply deaths scheduled for today (end of the day)
        if day in death_schedule:
            seeds_to_die = death_schedule[day]
            alive_seeds -= seeds_to_die  # Remove dead seeds from the alive set
            # Mark the positions of dead seeds as None in the grid
            for seed in seeds_to_die:
                for pos in seed_active_fronts[seed].values():
                    if pos in grid and grid[pos] == seed:
                        grid[pos] = None

    return len(alive_seeds)


# Test Script
def test():
    test_cases = [
        {
            "input": ([(2, 2)], 5, 3),
            "expected": [(2, 2)]
        },
        {
            "input": ([(2, 2)], 5, 4),
            "expected": []
        },
        {
            "input": ([(2, 1)], 4, 2),
            "expected": [(2, 1)]
        },
        {
            "input": ([(2, 1)], 4, 3),
            "expected": []
        },
        {
            "input": ([(2, 1), (1, 2)], 4, 1),
            "expected": [(2, 1), (1, 2)]
        },
        {
            "input": ([(2, 1), (1, 2)], 4, 2),
            "expected": [(2, 1), (1, 2)]
        },
        {
            "input": ([(2, 1), (1, 2)], 4, 3),
            "expected": []
        },
        {
            "input": ([(1, 1), (3, 2)], 4, 1),
            "expected": [(1 ,1), (3, 2)]
        },

        {
            "input": ([(1, 1), (4, 1)], 6, 2),
            "expected": [(1, 1), (4, 1)]
        },
        {
            "input": ([(1, 1), (4, 1)], 6, 3),
            "expected": []
        }
    ]

    for i, test_case in enumerate(test_cases, start=1):
        seed_positions, field_size, T = test_case["input"]
        expected_output = len(test_case["expected"])
        
        try:
            output = solution(seed_positions, field_size, T)
        except Exception as e:
            error_message = (
                f"An exception occurred while running the test case:\n"
                f"`seed_positions`: {seed_positions}\n"
                f"`field_size`: {field_size}\n" 
                f"`T`: {T}\n\n"
                f"Exception: '{str(e)}'"
            )
            assert False, error_message

        if output != expected_output:
            error_message = (
                f"The following Test Case failed:\n"
                f"`seed_positions`: {seed_positions}\n"
                f"`field_size`: {field_size}\n"
                f"`T`: {T}\n\n"
                f"Actual Output: {output}\n"
                f"Expected Output: {expected_output}"
            )
            assert False, error_message

test()