"""Minimal RAM-based Pong policy."""

from __future__ import annotations

from dataclasses import dataclass

from pong_state import PongState, detect_state


NOOP = 0
UP = 2
DOWN = 3
ACTION_NAMES = {
    NOOP: "noop",
    UP: "up",
    DOWN: "down",
}

PADDLE_DEADBAND = 3
CENTER_Y = 121
INACTIVE_CENTER_Y = 100
SELF_PADDLE_X_RAM = 192
MIN_PLAY_Y_RAM = 46
MAX_PLAY_Y_RAM = 210
TARGET_MIN_Y_RAM = 46
TARGET_MAX_Y_RAM = 196
LOW_SERVE_TARGET_Y = 50
LOW_SERVE_PATTERN_STEPS = 24
LOW_SERVE_START_X_MIN = 126
LOW_SERVE_START_X_MAX = 138
LOW_SERVE_START_Y_MIN = 116
LOW_SERVE_START_Y_MAX = 128
OPENING_LOW_DY8_X_MIN = 184
OPENING_LOW_DY8_X_MAX = 188
OPENING_LOW_DY8_Y_MIN = 130
OPENING_LOW_DY8_Y_MAX = 145
OPENING_LOW_DY8_PADDLE_MIN = 128
OPENING_LOW_DY8_PADDLE_MAX = 142
OPENING_LOW_DY8_PREV_DELTA_MIN = -15
OPENING_LOW_DY8_PREV_DELTA_MAX = -8
LOW_SERVE_LATE_X_MIN = 189
LOW_SERVE_LATE_X_MAX = 201
LOW_SERVE_LATE_Y_MIN = 55
LOW_SERVE_LATE_Y_MAX = 67
LOW_SERVE_LATE_PADDLE_MIN = 35
LOW_SERVE_LATE_PADDLE_MAX = 65
MID_DOWN_PRE_X_MIN = 186
MID_DOWN_PRE_X_MAX = 194
MID_DOWN_PRE_Y_MIN = 68
MID_DOWN_PRE_Y_MAX = 82
MID_DOWN_PRE_PADDLE_MIN = 65
MID_DOWN_PRE_PADDLE_MAX = 90
MID_UP_PRE_X_MIN = 187
MID_UP_PRE_X_MAX = 191
MID_UP_PRE_Y_MIN = 188
MID_UP_PRE_Y_MAX = 196
MID_UP_PRE_PADDLE_MIN = 190
MID_UP_PRE_PADDLE_MAX = 205
HIGH_DOWN_SETUP_X_MIN = 121
HIGH_DOWN_SETUP_X_MAX = 153
HIGH_DOWN_SETUP_Y_MIN = 103
HIGH_DOWN_SETUP_Y_MAX = 167
HIGH_DOWN_SETUP_PADDLE_MIN = 155
HIGH_DOWN_SETUP_PADDLE_MAX = 195
HIGH_DOWN_SETUP_TARGET_MIN = 165
HIGH_DOWN_SETUP_TARGET_MAX = 185
HIGH_DOWN_SCORE40_BOUNCE_X_MIN = 144
HIGH_DOWN_SCORE40_BOUNCE_X_MAX = 148
HIGH_DOWN_SCORE40_BOUNCE_Y_MIN = 50
HIGH_DOWN_SCORE40_BOUNCE_Y_MAX = 56
HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MIN = 143
HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MAX = 150
HIGH_DOWN_SCORE40_BOUNCE_TARGET_MIN = 154
HIGH_DOWN_SCORE40_BOUNCE_TARGET_MAX = 162
LATE_LOW_LOOP_X_MIN = 169
LATE_LOW_LOOP_X_MAX = 177
LATE_LOW_LOOP_Y_MIN = 49
LATE_LOW_LOOP_Y_MAX = 57
LATE_LOW_LOOP_PADDLE_MIN = 80
LATE_LOW_LOOP_PADDLE_MAX = 105
LATE_LOW_LOOP_TARGET_MIN = 60
LATE_LOW_LOOP_TARGET_MAX = 75
LATE_LOW_LOOP_CORRECTION_LIMIT = 64
LOW_MID_SCORE00_X_MIN = 177
LOW_MID_SCORE00_X_MAX = 181
LOW_MID_SCORE00_Y_MIN = 53
LOW_MID_SCORE00_Y_MAX = 57
LOW_MID_SCORE00_PADDLE_MIN = 45
LOW_MID_SCORE00_PADDLE_MAX = 55
LOW_MID_SCORE00_TARGET_MIN = 65
LOW_MID_SCORE00_TARGET_MAX = 70
LOW_MID_SCORE00_CORRECTION_LIMIT = 16
Y86_SCORE22_SETUP_X_MIN = 172
Y86_SCORE22_SETUP_X_MAX = 176
Y86_SCORE22_SETUP_Y_MIN = 56
Y86_SCORE22_SETUP_Y_MAX = 60
Y86_SCORE22_SETUP_PADDLE_MIN = 84
Y86_SCORE22_SETUP_PADDLE_MAX = 90
Y86_SCORE22_SETUP_TARGET_MIN = 74
Y86_SCORE22_SETUP_TARGET_MAX = 78
Y86_BOUNCE_X166_X_MIN = 164
Y86_BOUNCE_X166_X_MAX = 168
Y86_BOUNCE_X166_Y_MIN = 48
Y86_BOUNCE_X166_Y_MAX = 52
Y86_BOUNCE_X166_PADDLE_MIN = 76
Y86_BOUNCE_X166_PADDLE_MAX = 80
Y86_BOUNCE_X166_TARGET_MIN = 80
Y86_BOUNCE_X166_TARGET_MAX = 85
LOW_UP_SCORE01_X_MIN = 176
LOW_UP_SCORE01_X_MAX = 178
LOW_UP_SCORE01_Y_MIN = 172
LOW_UP_SCORE01_Y_MAX = 176
LOW_UP_SCORE01_PADDLE_MIN = 132
LOW_UP_SCORE01_PADDLE_MAX = 138
LOW_UP_SCORE01_TARGET_MIN = 135
LOW_UP_SCORE01_TARGET_MAX = 150
LOW_UP_SCORE10_FINAL_X_MIN = 202
LOW_UP_SCORE10_FINAL_X_MAX = 204
LOW_UP_SCORE10_FINAL_Y_MIN = 118
LOW_UP_SCORE10_FINAL_Y_MAX = 122
LOW_UP_SCORE10_FINAL_PADDLE_MIN = 113
LOW_UP_SCORE10_FINAL_PADDLE_MAX = 117
LOW_UP_SCORE10_FINAL_TARGET_MIN = 118
LOW_UP_SCORE10_FINAL_TARGET_MAX = 122
LOW_UP_Y111_X_MIN = 187
LOW_UP_Y111_X_MAX = 195
LOW_UP_Y111_Y_MIN = 127
LOW_UP_Y111_Y_MAX = 143
LOW_UP_Y111_PADDLE_MIN = 130
LOW_UP_Y111_PADDLE_MAX = 150
LOW_UP_Y111_TARGET_MIN = 125
LOW_UP_Y111_TARGET_MAX = 135
LOW_UP_Y111_CORRECTION_LIMIT = 64
CHAIN2_SCORE00_INITIAL_X = 125
CHAIN2_SCORE00_INITIAL_Y = 131
CHAIN2_SCORE00_INITIAL_PADDLE_MIN = 83
CHAIN2_SCORE00_INITIAL_PADDLE_MAX = 87
CHAIN2_SCORE00_INITIAL_TARGET_MIN = 129
CHAIN2_SCORE00_INITIAL_TARGET_MAX = 133
CHAIN2_SCORE00_INITIAL_PREV_DELTA_MIN = -23
CHAIN2_SCORE00_INITIAL_PREV_DELTA_MAX = -19


def reflected_y(y: float, low: int | None = None, high: int | None = None) -> float:
    if low is None:
        low = MIN_PLAY_Y_RAM
    if high is None:
        high = MAX_PLAY_Y_RAM
    span = high - low
    if span <= 0:
        return y
    while y < low or y > high:
        if y < low:
            y = low + (low - y)
        if y > high:
            y = high - (y - high)
    return y


def target_y_from_state(state: PongState, previous_state: PongState | None = None) -> float:
    if not state.ball_active:
        return INACTIVE_CENTER_Y

    if previous_state is None or not previous_state.ball_active:
        target_y = state.ball_y if state.ball_in_right_half else CENTER_Y
        return clamp_target_y(target_y)

    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    if dx <= 0:
        return CENTER_Y

    frames_to_paddle = max(0.0, (SELF_PADDLE_X_RAM - state.ball_x) / dx)
    return clamp_target_y(reflected_y(state.ball_y + dy * frames_to_paddle))


def clamp_target_y(y: float) -> float:
    return max(TARGET_MIN_Y_RAM, min(TARGET_MAX_Y_RAM, y))


def select_action_from_state(state: PongState, previous_state: PongState | None = None) -> int:
    """Choose an action by comparing target and paddle y positions."""
    target_y = target_y_from_state(state, previous_state)
    return action_from_target(state, target_y)


@dataclass
class RamPolicy:
    previous_state: PongState | None = None
    low_serve_steps: int = 0
    previous_paddle_y: int | None = None
    opening_low_dy8_correction_used: bool = False
    opening_low_dy8_correction_enabled: bool = True
    late_low_loop_corrections: int = 0
    low_mid_score00_corrections: int = 0
    y86_score22_setup_correction_used: bool = False
    y86_score42_setup_correction_used: bool = False
    y86_score62_early_setup_correction_used: bool = False
    y86_score82_early_setup_correction_used: bool = False
    y86_score20_serve_initial_correction_used: bool = False
    y86_score60_serve_initial_correction_used: bool = False
    y86_score100_serve_initial_correction_used: bool = False
    y86_score140_serve_initial_correction_used: bool = False
    y86_score22_bounce_family_seen: bool = False
    high_down_score80_bounce_correction_used: bool = False
    high_down_score120_bounce_correction_used: bool = False
    high_down_score160_bounce_correction_used: bool = False
    score00_mid_down_y154_final_correction_used: bool = False
    low_up_y111_corrections: int = 0
    low_up_score01_correction_used: bool = False
    low_up_score10_final_correction_used: bool = False
    chain2_score00_initial_correction_used: bool = False
    step_count: int = 0
    last_target_y: float = CENTER_Y
    last_mode: str = "init"

    def reset(self) -> None:
        self.previous_state = None
        self.low_serve_steps = 0
        self.previous_paddle_y = None
        self.opening_low_dy8_correction_used = False
        self.late_low_loop_corrections = 0
        self.low_mid_score00_corrections = 0
        self.y86_score22_setup_correction_used = False
        self.y86_score42_setup_correction_used = False
        self.y86_score62_early_setup_correction_used = False
        self.y86_score82_early_setup_correction_used = False
        self.y86_score20_serve_initial_correction_used = False
        self.y86_score60_serve_initial_correction_used = False
        self.y86_score100_serve_initial_correction_used = False
        self.y86_score140_serve_initial_correction_used = False
        self.y86_score22_bounce_family_seen = False
        self.high_down_score80_bounce_correction_used = False
        self.high_down_score120_bounce_correction_used = False
        self.high_down_score160_bounce_correction_used = False
        self.score00_mid_down_y154_final_correction_used = False
        self.low_up_y111_corrections = 0
        self.low_up_score01_correction_used = False
        self.low_up_score10_final_correction_used = False
        self.chain2_score00_initial_correction_used = False
        self.step_count = 0
        self.last_target_y = CENTER_Y
        self.last_mode = "reset"

    def select_action(self, info: dict) -> int:
        self.step_count += 1
        state = detect_state(info)
        target_y = target_y_from_state(state, self.previous_state)
        mode = "intercept"
        if not state.ball_active:
            mode = "inactive"
        elif self.previous_state is None or not self.previous_state.ball_active:
            mode = "initial"
        elif state.ball_x - self.previous_state.ball_x <= 0:
            mode = "center"

        if self.previous_state is not None:
            dx = state.ball_x - self.previous_state.ball_x
            dy = state.ball_y - self.previous_state.ball_y
            if is_low_serve_pattern(self.previous_state, dx, dy):
                self.low_serve_steps = LOW_SERVE_PATTERN_STEPS
            if self.low_serve_steps > 0:
                if dx > 0 and dy < 0 and state.ball_y < LOW_SERVE_START_Y_MAX + 2:
                    target_y = LOW_SERVE_TARGET_Y
                    mode = "low_serve"
                self.low_serve_steps -= 1

        action = action_from_target(state, target_y)
        previous_paddle_delta = None
        if self.previous_paddle_y is not None:
            previous_paddle_delta = state.self_paddle_y - self.previous_paddle_y
        if (
            self.previous_state is not None
            and is_y86_bounce_score22_x166_down_correction_state(state, self.previous_state, action, target_y)
        ):
            self.y86_score22_bounce_family_seen = True
        if (
            self.previous_state is not None
            and self.opening_low_dy8_correction_enabled
            and not self.opening_low_dy8_correction_used
            and is_opening_low_dy8_correction_state(state, self.previous_state, action, previous_paddle_delta)
        ):
            action = UP
            mode = "opening_low_dy8_correction"
            self.opening_low_dy8_correction_used = True
        elif (
            not self.chain2_score00_initial_correction_used
            and is_chain2_score00_initial_down_correction_state(
                state,
                action,
                mode,
                target_y,
                previous_paddle_delta,
            )
        ):
            action = UP
            mode = "chain2_score00_initial_down_correction"
            self.chain2_score00_initial_correction_used = True
        elif (
            not self.y86_score20_serve_initial_correction_used
            and is_y86_score20_serve_initial_down_correction_state(state, action, mode)
        ):
            action = UP
            mode = "y86_score20_serve_initial_down_correction"
            self.y86_score20_serve_initial_correction_used = True
        elif (
            not self.y86_score60_serve_initial_correction_used
            and is_y86_score60_serve_initial_down_correction_state(state, action, mode)
        ):
            action = NOOP
            mode = "y86_score60_serve_initial_down_correction"
            self.y86_score60_serve_initial_correction_used = True
        elif (
            not self.y86_score100_serve_initial_correction_used
            and is_y86_score100_serve_initial_down_correction_state(state, action, mode)
        ):
            action = NOOP
            mode = "y86_score100_serve_initial_down_correction"
            self.y86_score100_serve_initial_correction_used = True
        elif (
            not self.y86_score140_serve_initial_correction_used
            and is_y86_score140_serve_initial_down_correction_state(state, action, mode)
        ):
            action = NOOP
            mode = "y86_score140_serve_initial_down_correction"
            self.y86_score140_serve_initial_correction_used = True
        elif (
            self.previous_state is not None
            and is_low_serve_late_noop_correction_state(state, self.previous_state, action, target_y)
        ):
            action = UP
            mode = "low_serve_late_noop_correction"
        elif (
            self.previous_state is not None
            and is_mid_down_precontact_up_correction_state(state, self.previous_state, action)
        ):
            action = NOOP
            mode = "mid_down_precontact_up_correction"
        elif (
            self.previous_state is not None
            and is_mid_up_precontact_up_correction_state(state, self.previous_state, action)
        ):
            action = NOOP
            mode = "mid_up_precontact_up_correction"
        elif (
            self.previous_state is not None
            and is_high_down_setup_noop_correction_state(state, self.previous_state, action, target_y)
        ):
            action = DOWN
            mode = "high_down_setup_noop_correction"
        elif (
            self.previous_state is not None
            and is_high_down_score40_bounce_down_correction_state(state, self.previous_state, action, target_y)
        ):
            action = NOOP
            mode = "high_down_score40_bounce_down_correction"
        elif (
            self.previous_state is not None
            and not self.high_down_score80_bounce_correction_used
            and is_high_down_score80_bounce_down_correction_state(state, self.previous_state, action, target_y)
        ):
            action = NOOP
            mode = "high_down_score80_bounce_down_correction"
            self.high_down_score80_bounce_correction_used = True
        elif (
            self.previous_state is not None
            and not self.high_down_score120_bounce_correction_used
            and is_high_down_score120_bounce_down_correction_state(state, self.previous_state, action, target_y)
        ):
            action = NOOP
            mode = "high_down_score120_bounce_down_correction"
            self.high_down_score120_bounce_correction_used = True
        elif (
            self.previous_state is not None
            and not self.high_down_score160_bounce_correction_used
            and is_high_down_score160_bounce_down_correction_state(state, self.previous_state, action, target_y)
        ):
            action = NOOP
            mode = "high_down_score160_bounce_down_correction"
            self.high_down_score160_bounce_correction_used = True
        elif (
            self.previous_state is not None
            and not self.score00_mid_down_y154_final_correction_used
            and is_score00_mid_down_y154_final_up_correction_state(
                state,
                self.previous_state,
                action,
                target_y,
                previous_paddle_delta,
            )
        ):
            action = NOOP
            mode = "score00_mid_down_y154_final_up_correction"
            self.score00_mid_down_y154_final_correction_used = True
        elif (
            self.previous_state is not None
            and self.late_low_loop_corrections < LATE_LOW_LOOP_CORRECTION_LIMIT
            and is_late_low_loop_pre_up_correction_state(state, self.previous_state, action, target_y)
        ):
            action = NOOP
            mode = "late_low_loop_pre_up_correction"
            self.late_low_loop_corrections += 1
        elif (
            self.previous_state is not None
            and self.low_mid_score00_corrections < LOW_MID_SCORE00_CORRECTION_LIMIT
            and is_low_mid_score00_down_correction_state(state, self.previous_state, action, target_y)
        ):
            action = NOOP
            mode = "low_mid_score00_down_correction"
            self.low_mid_score00_corrections += 1
        elif (
            self.previous_state is not None
            and not self.y86_score22_setup_correction_used
            and is_y86_score22_setup_up_correction_state(state, self.previous_state, action, target_y)
        ):
            action = DOWN
            mode = "y86_score22_setup_up_correction"
            self.y86_score22_setup_correction_used = True
        elif (
            self.previous_state is not None
            and not self.y86_score42_setup_correction_used
            and is_y86_score42_setup_up_correction_state(state, self.previous_state, action, target_y)
        ):
            action = DOWN
            mode = "y86_score42_setup_up_correction"
            self.y86_score42_setup_correction_used = True
        elif (
            self.previous_state is not None
            and not self.y86_score62_early_setup_correction_used
            and 1500 <= self.step_count <= 1700
            and is_y86_score62_setup_up_correction_state(state, self.previous_state, action, target_y)
        ):
            action = DOWN
            mode = "y86_score62_early_setup_up_correction"
            self.y86_score62_early_setup_correction_used = True
        elif (
            self.previous_state is not None
            and not self.y86_score82_early_setup_correction_used
            and 2050 <= self.step_count <= 2110
            and is_y86_score82_setup_up_correction_state(state, self.previous_state, action, target_y)
        ):
            action = DOWN
            mode = "y86_score82_early_setup_up_correction"
            self.y86_score82_early_setup_correction_used = True
        elif (
            self.previous_state is not None
            and self.y86_score22_bounce_family_seen
            and is_y86_bounce_x166_down_correction_state(state, self.previous_state, action, target_y)
        ):
            action = UP
            mode = "y86_bounce_after_score22_x166_down_correction"
        elif (
            self.previous_state is not None
            and self.low_up_y111_corrections < LOW_UP_Y111_CORRECTION_LIMIT
            and is_low_up_y111_pre_up_correction_state(state, self.previous_state, action, target_y)
        ):
            action = DOWN
            mode = "low_up_y111_pre_up_correction"
            self.low_up_y111_corrections += 1
        elif (
            self.previous_state is not None
            and not self.low_up_score01_correction_used
            and is_low_up_score01_down_correction_state(state, self.previous_state, action, target_y, previous_paddle_delta)
        ):
            action = NOOP
            mode = "low_up_score01_down_correction"
            self.low_up_score01_correction_used = True
        elif (
            self.previous_state is not None
            and not self.low_up_score10_final_correction_used
            and is_low_up_score10_final_down_correction_state(
                state,
                self.previous_state,
                action,
                target_y,
                previous_paddle_delta,
            )
        ):
            action = NOOP
            mode = "low_up_score10_final_down_correction"
            self.low_up_score10_final_correction_used = True
        self.last_target_y = target_y
        self.last_mode = mode
        self.previous_state = state
        self.previous_paddle_y = state.self_paddle_y
        return action


_DEFAULT_POLICY = RamPolicy()


def select_action(info: dict) -> int:
    """Choose an action from EnvPool info."""
    return _DEFAULT_POLICY.select_action(info)


def is_low_serve_pattern(previous_state: PongState, dx: int, dy: int) -> bool:
    return (
        dx > 0
        and dy < 0
        and LOW_SERVE_START_X_MIN <= previous_state.ball_x <= LOW_SERVE_START_X_MAX
        and LOW_SERVE_START_Y_MIN <= previous_state.ball_y <= LOW_SERVE_START_Y_MAX
    )


def is_opening_low_dy8_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    previous_paddle_delta: int | None,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 0
        and state.opponent_score == 0
        and dx > 0
        and dy == 8
        and action == DOWN
        and OPENING_LOW_DY8_X_MIN <= state.ball_x <= OPENING_LOW_DY8_X_MAX
        and OPENING_LOW_DY8_Y_MIN <= state.ball_y <= OPENING_LOW_DY8_Y_MAX
        and OPENING_LOW_DY8_PADDLE_MIN <= state.self_paddle_y <= OPENING_LOW_DY8_PADDLE_MAX
        and previous_paddle_delta is not None
        and OPENING_LOW_DY8_PREV_DELTA_MIN <= previous_paddle_delta <= OPENING_LOW_DY8_PREV_DELTA_MAX
    )


def is_low_serve_late_noop_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and dy == -4
        and action == NOOP
        and target_y == LOW_SERVE_TARGET_Y
        and LOW_SERVE_LATE_X_MIN <= state.ball_x <= LOW_SERVE_LATE_X_MAX
        and LOW_SERVE_LATE_Y_MIN <= state.ball_y <= LOW_SERVE_LATE_Y_MAX
        and LOW_SERVE_LATE_PADDLE_MIN <= state.self_paddle_y <= LOW_SERVE_LATE_PADDLE_MAX
    )


def is_y86_score20_serve_initial_down_correction_state(
    state: PongState,
    action: int,
    mode: str,
) -> bool:
    return (
        state.self_score == 2
        and state.opponent_score == 0
        and mode == "initial"
        and action == DOWN
        and 124 <= state.ball_x <= 128
        and 124 <= state.ball_y <= 128
        and 96 <= state.self_paddle_y <= 106
    )


def is_chain2_score00_initial_down_correction_state(
    state: PongState,
    action: int,
    mode: str,
    target_y: float,
    previous_paddle_delta: int | None,
) -> bool:
    return (
        state.self_score == 0
        and state.opponent_score == 0
        and mode == "initial"
        and action == DOWN
        and state.ball_x == CHAIN2_SCORE00_INITIAL_X
        and state.ball_y == CHAIN2_SCORE00_INITIAL_Y
        and CHAIN2_SCORE00_INITIAL_PADDLE_MIN
        <= state.self_paddle_y
        <= CHAIN2_SCORE00_INITIAL_PADDLE_MAX
        and CHAIN2_SCORE00_INITIAL_TARGET_MIN <= target_y <= CHAIN2_SCORE00_INITIAL_TARGET_MAX
        and previous_paddle_delta is not None
        and CHAIN2_SCORE00_INITIAL_PREV_DELTA_MIN
        <= previous_paddle_delta
        <= CHAIN2_SCORE00_INITIAL_PREV_DELTA_MAX
    )


def is_y86_score60_serve_initial_down_correction_state(
    state: PongState,
    action: int,
    mode: str,
) -> bool:
    return (
        state.self_score == 6
        and state.opponent_score == 0
        and mode == "initial"
        and action == DOWN
        and 124 <= state.ball_x <= 128
        and 124 <= state.ball_y <= 128
        and 96 <= state.self_paddle_y <= 106
    )


def is_y86_score100_serve_initial_down_correction_state(
    state: PongState,
    action: int,
    mode: str,
) -> bool:
    return (
        state.self_score == 10
        and state.opponent_score == 0
        and mode == "initial"
        and action == DOWN
        and 124 <= state.ball_x <= 128
        and 124 <= state.ball_y <= 128
        and 96 <= state.self_paddle_y <= 106
    )


def is_y86_score140_serve_initial_down_correction_state(
    state: PongState,
    action: int,
    mode: str,
) -> bool:
    return (
        state.self_score == 14
        and state.opponent_score == 0
        and mode == "initial"
        and action == DOWN
        and 124 <= state.ball_x <= 128
        and 124 <= state.ball_y <= 128
        and 96 <= state.self_paddle_y <= 106
    )


def is_mid_down_precontact_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and dy == 4
        and action == UP
        and MID_DOWN_PRE_X_MIN <= state.ball_x <= MID_DOWN_PRE_X_MAX
        and MID_DOWN_PRE_Y_MIN <= state.ball_y <= MID_DOWN_PRE_Y_MAX
        and MID_DOWN_PRE_PADDLE_MIN <= state.self_paddle_y <= MID_DOWN_PRE_PADDLE_MAX
    )


def is_mid_up_precontact_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and dy == -8
        and action == UP
        and MID_UP_PRE_X_MIN <= state.ball_x <= MID_UP_PRE_X_MAX
        and MID_UP_PRE_Y_MIN <= state.ball_y <= MID_UP_PRE_Y_MAX
        and MID_UP_PRE_PADDLE_MIN <= state.self_paddle_y <= MID_UP_PRE_PADDLE_MAX
    )


def is_high_down_setup_noop_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and dy == 8
        and action == NOOP
        and HIGH_DOWN_SETUP_X_MIN <= state.ball_x <= HIGH_DOWN_SETUP_X_MAX
        and HIGH_DOWN_SETUP_Y_MIN <= state.ball_y <= HIGH_DOWN_SETUP_Y_MAX
        and HIGH_DOWN_SETUP_PADDLE_MIN <= state.self_paddle_y <= HIGH_DOWN_SETUP_PADDLE_MAX
        and HIGH_DOWN_SETUP_TARGET_MIN <= target_y <= HIGH_DOWN_SETUP_TARGET_MAX
    )


def is_high_down_score40_bounce_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 4
        and state.opponent_score == 0
        and dx > 0
        and dy == 9
        and action == DOWN
        and HIGH_DOWN_SCORE40_BOUNCE_X_MIN <= state.ball_x <= HIGH_DOWN_SCORE40_BOUNCE_X_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_Y_MIN <= state.ball_y <= HIGH_DOWN_SCORE40_BOUNCE_Y_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MIN <= state.self_paddle_y <= HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_TARGET_MIN <= target_y <= HIGH_DOWN_SCORE40_BOUNCE_TARGET_MAX
    )


def is_high_down_score80_bounce_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 8
        and state.opponent_score == 0
        and dx > 0
        and dy == 9
        and action == DOWN
        and HIGH_DOWN_SCORE40_BOUNCE_X_MIN <= state.ball_x <= HIGH_DOWN_SCORE40_BOUNCE_X_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_Y_MIN <= state.ball_y <= HIGH_DOWN_SCORE40_BOUNCE_Y_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MIN <= state.self_paddle_y <= HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_TARGET_MIN <= target_y <= HIGH_DOWN_SCORE40_BOUNCE_TARGET_MAX
    )


def is_high_down_score120_bounce_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 12
        and state.opponent_score == 0
        and dx > 0
        and dy == 9
        and action == DOWN
        and HIGH_DOWN_SCORE40_BOUNCE_X_MIN <= state.ball_x <= HIGH_DOWN_SCORE40_BOUNCE_X_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_Y_MIN <= state.ball_y <= HIGH_DOWN_SCORE40_BOUNCE_Y_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MIN <= state.self_paddle_y <= HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_TARGET_MIN <= target_y <= HIGH_DOWN_SCORE40_BOUNCE_TARGET_MAX
    )


def is_high_down_score160_bounce_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 16
        and state.opponent_score == 0
        and dx > 0
        and dy == 9
        and action == DOWN
        and HIGH_DOWN_SCORE40_BOUNCE_X_MIN <= state.ball_x <= HIGH_DOWN_SCORE40_BOUNCE_X_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_Y_MIN <= state.ball_y <= HIGH_DOWN_SCORE40_BOUNCE_Y_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MIN <= state.self_paddle_y <= HIGH_DOWN_SCORE40_BOUNCE_PADDLE_MAX
        and HIGH_DOWN_SCORE40_BOUNCE_TARGET_MIN <= target_y <= HIGH_DOWN_SCORE40_BOUNCE_TARGET_MAX
    )


def is_score00_mid_down_y154_final_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
    previous_paddle_delta: int | None,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 0
        and state.opponent_score == 0
        and dx > 0
        and dy == 4
        and action == UP
        and previous_paddle_delta is not None
        and previous_paddle_delta >= 10
        and 198 <= state.ball_x <= 204
        and 148 <= state.ball_y <= 156
        and 150 <= target_y <= 156
        and 150 <= state.self_paddle_y <= 170
    )


def is_late_low_loop_pre_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and 3 <= dy <= 4
        and action == UP
        and LATE_LOW_LOOP_X_MIN <= state.ball_x <= LATE_LOW_LOOP_X_MAX
        and LATE_LOW_LOOP_Y_MIN <= state.ball_y <= LATE_LOW_LOOP_Y_MAX
        and LATE_LOW_LOOP_PADDLE_MIN <= state.self_paddle_y <= LATE_LOW_LOOP_PADDLE_MAX
        and LATE_LOW_LOOP_TARGET_MIN <= target_y <= LATE_LOW_LOOP_TARGET_MAX
    )


def is_low_mid_score00_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 0
        and state.opponent_score == 0
        and dx > 0
        and dy == 4
        and action == DOWN
        and LOW_MID_SCORE00_X_MIN <= state.ball_x <= LOW_MID_SCORE00_X_MAX
        and LOW_MID_SCORE00_Y_MIN <= state.ball_y <= LOW_MID_SCORE00_Y_MAX
        and LOW_MID_SCORE00_PADDLE_MIN <= state.self_paddle_y <= LOW_MID_SCORE00_PADDLE_MAX
        and LOW_MID_SCORE00_TARGET_MIN <= target_y <= LOW_MID_SCORE00_TARGET_MAX
    )


def is_low_up_y111_pre_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and dy == -8
        and action == UP
        and LOW_UP_Y111_X_MIN <= state.ball_x <= LOW_UP_Y111_X_MAX
        and LOW_UP_Y111_Y_MIN <= state.ball_y <= LOW_UP_Y111_Y_MAX
        and LOW_UP_Y111_PADDLE_MIN <= state.self_paddle_y <= LOW_UP_Y111_PADDLE_MAX
        and LOW_UP_Y111_TARGET_MIN <= target_y <= LOW_UP_Y111_TARGET_MAX
    )


def is_y86_score22_setup_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 2
        and state.opponent_score == 2
        and dx > 0
        and dy == 4
        and action == UP
        and Y86_SCORE22_SETUP_X_MIN <= state.ball_x <= Y86_SCORE22_SETUP_X_MAX
        and Y86_SCORE22_SETUP_Y_MIN <= state.ball_y <= Y86_SCORE22_SETUP_Y_MAX
        and Y86_SCORE22_SETUP_PADDLE_MIN <= state.self_paddle_y <= Y86_SCORE22_SETUP_PADDLE_MAX
        and Y86_SCORE22_SETUP_TARGET_MIN <= target_y <= Y86_SCORE22_SETUP_TARGET_MAX
    )


def is_y86_score42_setup_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 4
        and state.opponent_score == 2
        and dx > 0
        and dy == 4
        and action == UP
        and Y86_SCORE22_SETUP_X_MIN <= state.ball_x <= Y86_SCORE22_SETUP_X_MAX
        and Y86_SCORE22_SETUP_Y_MIN <= state.ball_y <= Y86_SCORE22_SETUP_Y_MAX
        and Y86_SCORE22_SETUP_PADDLE_MIN <= state.self_paddle_y <= Y86_SCORE22_SETUP_PADDLE_MAX
        and Y86_SCORE22_SETUP_TARGET_MIN <= target_y <= Y86_SCORE22_SETUP_TARGET_MAX
    )


def is_y86_score62_setup_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 6
        and state.opponent_score == 2
        and dx > 0
        and dy == 4
        and action == UP
        and Y86_SCORE22_SETUP_X_MIN <= state.ball_x <= Y86_SCORE22_SETUP_X_MAX
        and Y86_SCORE22_SETUP_Y_MIN <= state.ball_y <= Y86_SCORE22_SETUP_Y_MAX
        and Y86_SCORE22_SETUP_PADDLE_MIN <= state.self_paddle_y <= Y86_SCORE22_SETUP_PADDLE_MAX
        and Y86_SCORE22_SETUP_TARGET_MIN <= target_y <= Y86_SCORE22_SETUP_TARGET_MAX
    )


def is_y86_score82_setup_up_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 8
        and state.opponent_score == 2
        and dx > 0
        and dy == 4
        and action == UP
        and Y86_SCORE22_SETUP_X_MIN <= state.ball_x <= Y86_SCORE22_SETUP_X_MAX
        and Y86_SCORE22_SETUP_Y_MIN <= state.ball_y <= Y86_SCORE22_SETUP_Y_MAX
        and Y86_SCORE22_SETUP_PADDLE_MIN <= state.self_paddle_y <= Y86_SCORE22_SETUP_PADDLE_MAX
        and Y86_SCORE22_SETUP_TARGET_MIN <= target_y <= Y86_SCORE22_SETUP_TARGET_MAX
    )


def is_y86_bounce_x166_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        dx > 0
        and dy == 5
        and action == DOWN
        and Y86_BOUNCE_X166_X_MIN <= state.ball_x <= Y86_BOUNCE_X166_X_MAX
        and Y86_BOUNCE_X166_Y_MIN <= state.ball_y <= Y86_BOUNCE_X166_Y_MAX
        and Y86_BOUNCE_X166_PADDLE_MIN <= state.self_paddle_y <= Y86_BOUNCE_X166_PADDLE_MAX
        and Y86_BOUNCE_X166_TARGET_MIN <= target_y <= Y86_BOUNCE_X166_TARGET_MAX
    )


def is_y86_bounce_score22_x166_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
) -> bool:
    return (
        state.self_score == 2
        and state.opponent_score == 2
        and is_y86_bounce_x166_down_correction_state(state, previous_state, action, target_y)
    )


def is_low_up_score01_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
    previous_paddle_delta: int | None,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 0
        and state.opponent_score == 1
        and dx > 0
        and dy == -8
        and action == DOWN
        and LOW_UP_SCORE01_X_MIN <= state.ball_x <= LOW_UP_SCORE01_X_MAX
        and LOW_UP_SCORE01_Y_MIN <= state.ball_y <= LOW_UP_SCORE01_Y_MAX
        and LOW_UP_SCORE01_PADDLE_MIN <= state.self_paddle_y <= LOW_UP_SCORE01_PADDLE_MAX
        and LOW_UP_SCORE01_TARGET_MIN <= target_y <= LOW_UP_SCORE01_TARGET_MAX
        and previous_paddle_delta is not None
        and previous_paddle_delta <= -15
    )


def is_low_up_score10_final_down_correction_state(
    state: PongState,
    previous_state: PongState,
    action: int,
    target_y: float,
    previous_paddle_delta: int | None,
) -> bool:
    dx = state.ball_x - previous_state.ball_x
    dy = state.ball_y - previous_state.ball_y
    return (
        state.self_score == 1
        and state.opponent_score == 0
        and dx > 0
        and dy == -8
        and action == DOWN
        and LOW_UP_SCORE10_FINAL_X_MIN <= state.ball_x <= LOW_UP_SCORE10_FINAL_X_MAX
        and LOW_UP_SCORE10_FINAL_Y_MIN <= state.ball_y <= LOW_UP_SCORE10_FINAL_Y_MAX
        and LOW_UP_SCORE10_FINAL_PADDLE_MIN <= state.self_paddle_y <= LOW_UP_SCORE10_FINAL_PADDLE_MAX
        and LOW_UP_SCORE10_FINAL_TARGET_MIN <= target_y <= LOW_UP_SCORE10_FINAL_TARGET_MAX
        and previous_paddle_delta is not None
        and previous_paddle_delta <= -10
    )


def action_from_target(state: PongState, target_y: float) -> int:
    if target_y < state.self_paddle_y - PADDLE_DEADBAND:
        return UP
    if target_y > state.self_paddle_y + PADDLE_DEADBAND:
        return DOWN
    return NOOP
