'''
        8""""8 8"""88 8     8""""
        8    " 8    8 8     8
        8e     8    8 8e    8eeee
        88  ee 8    8 88    88
        88   8 8    8 88    88
        88eee8 8eeee8 88eee 88

GOLF


Despite being a text based game, the code uses simple geometry to simulate a course.
Fairways are 40 yard wide rectangles, surrounded by 5 yards of rough around the perimeter.
The green is a circle of 10 yards radius around the cup.
The cup is always at point (0,0).

Using basic trigonometry we can plot the ball's location using the distance of the stroke and
and the angle of deviation (hook/slice).

The stroke distances are based on real world averages of different club types.
Lots of randomization, "business rules", and luck influence the game play.
Probabilities are commented in the code.

note: 'courseInfo', 'clubs', & 'scoreCard' arrays each include an empty object so indexing
can begin at 1. Like all good programmers we count from zero, but in this context,
it's more natural when hole number one is at index one


    |-----------------------------|
    |            rough            |
    |   ----------------------    |
    |   |                     |   |
    | r |        =  =         | r |
    | o |     =        =      | o |
    | u |    =    .     =     | u |
    | g |    =   green  =     | g |
    | h |     =        =      | h |
    |   |        =  =         |   |
    |   |                     |   |
    |   |                     |   |
    |   |      Fairway        |   |
    |   |                     |   |
    |   |               ------    |
    |   |            --        -- |
    |   |           --  hazard  --|
    |   |            --        -- |
    |   |               ------    |
    |   |                     |   |
    |   |                     |   |   out
    |   |                     |   |   of
    |   |                     |   |   bounds
    |   |                     |   |
    |   |                     |   |
    |            tee              |


Typical green size: 20-30 yards
Typical golf course fairways are 35 to 45 yards wide
Our fairway extends 5 yards past green
Our rough is a 5 yard perimeter around fairway

We calculate the new position of the ball given the ball's point, the distance
of the stroke, and degrees off line (hook or slice).

Degrees off (for a right handed golfer):
Slice: positive degrees = ball goes right
Hook: negative degrees = left goes left

The cup is always at point: 0,0.
We use atan2 to compute the angle between the cup and the ball.
Setting the cup's vector to 0,-1 on a 360 circle is equivalent to:
0 deg = 12 o'clock;  90 deg = 3 o'clock;  180 deg = 6 o'clock;  270 = 9 o'clock
The reverse angle between the cup and the ball is a difference of PI (using radians).

Given the angle and stroke distance (hypotenuse), we use cosine to compute
the opposite and adjacent sides of the triangle, which, is the ball's new position.

        0
        |
270 - cup - 90
        |
        180


        cup
        |
        |
        | opp
        |-----* new position
        |    /
        |   /
    adj  |  /
        | /  hyp
        |/
        tee

<- hook    slice ->


Given the large number of combinations needed to describe a particular stroke / ball location,
we use the technique of "bitwise masking" to describe stroke results.
With bit masking, multiple flags (bits) are combined into a single binary number that can be
tested by applying a mask. A mask is another binary number that isolates a particular bit that
you are interested in. You can then apply your language's bitwise opeartors to test or
set a flag.

Game design by Jason Bonthron, 2021
www.bonthron.com
for my father, Raymond Bonthron, an avid golfer

Inspired by the 1978 "Golf" from "Basic Computer Games"
by Steve North, who modified an existing golf game by an unknown author

Ported in 2022 to Python by Martin Thoma
'''


import enum
import math
import random
import time
from dataclasses import dataclass
from functools import partial
from typing import Any, Callable, List, NamedTuple, Tuple


def clear_console() -> None:
    print("\033[H\033[J", end="")


class Point(NamedTuple):
    x: int
    y: int


class GameObjType(enum.Enum):
    BALL = enum.auto()
    CUP = enum.auto()
    GREEN = enum.auto()
    FAIRWAY = enum.auto()
    ROUGH = enum.auto()
    TREES = enum.auto()
    WATER = enum.auto()
    SAND = enum.auto()


class CircleGameObj(NamedTuple):
    # center point
    X: int
    Y: int
    Radius: int
    Type: GameObjType


class RectGameObj(NamedTuple):
    # Upper left corner
    X: int
    Y: int
    Width: int
    Length: int
    Type: GameObjType


Ball = CircleGameObj
Hazard = CircleGameObj


class HoleInfo(NamedTuple):
    hole: int
    yards: int
    par: int
    hazards: List[Hazard]
    description: str


class HoleGeometry(NamedTuple):
    cup: CircleGameObj
    green: CircleGameObj
    fairway: RectGameObj
    rough: RectGameObj
    hazards: List[Hazard]


@dataclass
class Plot:
    x: int
    y: int
    offline: int


def get_distance(pt1: Point, pt2: Point) -> float:
    """distance between 2 points"""
    return math.sqrt(math.pow((pt2.x - pt1.x), 2) + math.pow((pt2.y - pt1.y), 2))


def is_in_rectangle(pt: CircleGameObj, rect: RectGameObj) -> bool:
    # only true if its completely inside
    return (
        (pt.X > rect.X)
        and (pt.X < rect.X + rect.Width)
        and (pt.Y > rect.Y)
        and (pt.Y < rect.Y + rect.Length)
    )


def to_radians(angle: float) -> float:
    return angle * (math.pi / 180.0)


def to_degrees_360(angle: float) -> float:
    """radians to 360 degrees"""
    deg = angle * (180.0 / math.pi)
    if deg < 0.0:
        deg += 360.0
    return deg


def odds(x: int) -> bool:
    # chance an integer is <= the given argument
    # between 1-100
    return random.randint(1, 101) <= x


# THE COURSE
CourseInfo = [
    HoleInfo(0, 0, 0, [], ""),  # include a blank so index 1 == hole 1
    # -------------------------------------------------------- front 9
    HoleInfo(
        1,
        361,
        4,
        [
            Hazard(20, 100, 10, GameObjType.TREES),
            Hazard(-20, 80, 10, GameObjType.TREES),
            Hazard(-20, 100, 10, GameObjType.TREES),
        ],
        "There are a couple of trees on the left and right.",
    ),
    HoleInfo(
        2,
        389,
        4,
        [Hazard(0, 160, 20, GameObjType.WATER)],
        "There is a large water hazard across the fairway about 150 yards.",
    ),
    HoleInfo(
        3,
        206,
        3,
        [
            Hazard(20, 20, 5, GameObjType.WATER),
            Hazard(-20, 160, 10, GameObjType.WATER),
            Hazard(10, 12, 5, GameObjType.SAND),
        ],
        "There is some sand and water near the green.",
    ),
    HoleInfo(
        4,
        500,
        5,
        [Hazard(-14, 12, 12, GameObjType.SAND)],
        "There's a bunker to the left of the green.",
    ),
    HoleInfo(
        5,
        408,
        4,
        [
            Hazard(20, 120, 20, GameObjType.TREES),
            Hazard(20, 160, 20, GameObjType.TREES),
            Hazard(10, 20, 5, GameObjType.SAND),
        ],
        "There are some trees to your right.",
    ),
    HoleInfo(
        6,
        359,
        4,
        [Hazard(14, 0, 4, GameObjType.SAND), Hazard(-14, 0, 4, GameObjType.SAND)],
        "",
    ),
    HoleInfo(
        7,
        424,
        5,
        [
            Hazard(20, 200, 10, GameObjType.SAND),
            Hazard(10, 180, 10, GameObjType.SAND),
            Hazard(20, 160, 10, GameObjType.SAND),
        ],
        "There are several sand traps along your right.",
    ),
    HoleInfo(8, 388, 4, [Hazard(-20, 340, 10, GameObjType.TREES)], ""),
    HoleInfo(
        9,
        196,
        3,
        [Hazard(-30, 180, 20, GameObjType.TREES), Hazard(14, -8, 5, GameObjType.SAND)],
        "",
    ),
    # -------------------------------------------------------- back 9
    HoleInfo(
        hole=10,
        yards=400,
        par=4,
        hazards=[
            Hazard(-14, -8, 5, GameObjType.SAND),
            Hazard(14, -8, 5, GameObjType.SAND),
        ],
        description="",
    ),
    HoleInfo(
        11,
        560,
        5,
        [
            Hazard(-20, 400, 10, GameObjType.TREES),
            Hazard(-10, 380, 10, GameObjType.TREES),
            Hazard(-20, 260, 10, GameObjType.TREES),
            Hazard(-20, 200, 10, GameObjType.TREES),
            Hazard(-10, 180, 10, GameObjType.TREES),
            Hazard(-20, 160, 10, GameObjType.TREES),
        ],
        "Lots of trees along the left of the fairway.",
    ),
    HoleInfo(
        12,
        132,
        3,
        [
            Hazard(-10, 120, 10, GameObjType.WATER),
            Hazard(-5, 100, 10, GameObjType.SAND),
        ],
        "There is water and sand directly in front of you. A good drive should clear both.",
    ),
    HoleInfo(
        13,
        357,
        4,
        [
            Hazard(-20, 200, 10, GameObjType.TREES),
            Hazard(-10, 180, 10, GameObjType.TREES),
            Hazard(-20, 160, 10, GameObjType.TREES),
            Hazard(14, 12, 8, GameObjType.SAND),
        ],
        "",
    ),
    HoleInfo(14, 294, 4, [Hazard(0, 20, 10, GameObjType.SAND)], ""),
    HoleInfo(
        15,
        475,
        5,
        [Hazard(-20, 20, 10, GameObjType.WATER), Hazard(10, 20, 10, GameObjType.SAND)],
        "Some sand and water near the green.",
    ),
    HoleInfo(16, 375, 4, [Hazard(-14, -8, 5, GameObjType.SAND)], ""),
    HoleInfo(
        17,
        180,
        3,
        [
            Hazard(20, 100, 10, GameObjType.TREES),
            Hazard(-20, 80, 10, GameObjType.TREES),
        ],
        "",
    ),
    HoleInfo(
        18,
        550,
        5,
        [Hazard(20, 30, 15, GameObjType.WATER)],
        "There is a water hazard near the green.",
    ),
]


# -------------------------------------------------------- bitwise Flags
dub = 0b00000000000001
hook = 0b00000000000010
slice_ = 0b00000000000100
passed_cup = 0b00000000001000
in_cup = 0b00000000010000
on_fairway = 0b00000000100000
on_green = 0b00000001000000
in_rough = 0b00000010000000
in_sand = 0b00000100000000
in_trees = 0b00001000000000
in_water = 0b00010000000000
out_of_bounds = 0b00100000000000
luck = 0b01000000000000
ace = 0b10000000000000


class Golf:
    ball: Ball
    hole_num: int = 0
    stroke_num: int = 0
    handicap: int = 0
    player_difficulty: int = 0
    hole_geometry: HoleGeometry

    # all fairways are 40 yards wide, extend 5 yards beyond the cup, and
    # have 5 yards of rough around the perimeter
    fairway_width: int = 40
    fairway_extension: int = 5
    rough_amt: int = 5

    # ScoreCard records the ball position after each stroke
    # a new list for each hole
    # include a blank list so index 1 == hole 1
    score_card: List[List[Ball]] = [[]]

    # YOUR BAG
    clubs: List[Tuple[str, int]] = [
        ("", 0),
        # name, average yardage
        ("Driver", 250),
        ("3 Wood", 225),
        ("5 Wood", 200),
        ("Hybrid", 190),
        ("4 Iron", 170),
        ("7 Iron", 150),
        ("9 Iron", 125),
        ("Pitching wedge", 110),
        ("Sand wedge", 75),
        ("Putter", 10),
    ]

    def __init__(self) -> None:
        print(" ")
        print('          8""""8 8"""88 8     8"""" ')
        print('          8    " 8    8 8     8     ')
        print("          8e     8    8 8e    8eeee ")
        print("          88  ee 8    8 88    88    ")
        print("          88   8 8    8 88    88    ")
        print("          88eee8 8eeee8 88eee 88    ")
        print(" ")
        print("Welcome to the Creative Computing Country Club,")
        print("an eighteen hole championship layout located a short")
        print("distance from scenic downtown Lambertville, New Jersey.")
        print("The game will be explained as you play.")
        print("Enjoy your game! See you at the 19th hole...")
        print(" ")
        print("Type QUIT at any time to leave the game.")
        print("Type BAG at any time to review the clubs in your bag.")
        print(" ")

        input("Press any key to continue.")
        clear_console()
        self.start_game()

    def start_game(self) -> None:
        print(" ")
        print("              YOUR BAG")
        self.review_bag()
        print("Type BAG at any time to review the clubs in your bag.")
        print(" ")

        input("Press any key to continue.")
        clear_console()
        self.ask_handicap()

    def ask_handicap(self) -> None:
        print(" ")

        self.ask(
            "PGA handicaps range from 0 to 30.\nWhat is your handicap?",
            0,
            30,
            self.set_handicap_ask_difficulty,
        )

    def set_handicap_ask_difficulty(self, i: int) -> None:
        self.handicap = i
        print(" ")

        self.ask(
            (
                "Common difficulties at golf include:\n"
                "1=Hook, 2=Slice, 3=Poor Distance, 4=Trap Shots, 5=Putting\n"
                "Which one is your worst?"
            ),
            1,
            5,
            self.set_difficulty_and_hole,
        )

    def set_difficulty_and_hole(self, j: int) -> None:
        self.player_difficulty = j
        clear_console()
        self.new_hole()

    def new_hole(self) -> None:
        self.hole_num += 1
        self.stroke_num = 0

        info: HoleInfo = CourseInfo[self.hole_num]

        yards: int = info.yards
        # from tee to cup
        cup = CircleGameObj(0, 0, 0, GameObjType.CUP)
        green = CircleGameObj(0, 0, 10, GameObjType.GREEN)

        fairway = RectGameObj(
            0 - int(self.fairway_width / 2),
            0 - (green.Radius + self.fairway_extension),
            self.fairway_width,
            yards + (green.Radius + self.fairway_extension) + 1,
            GameObjType.FAIRWAY,
        )

        rough = RectGameObj(
            fairway.X - self.rough_amt,
            fairway.Y - self.rough_amt,
            fairway.Width + (2 * self.rough_amt),
            fairway.Length + (2 * self.rough_amt),
            GameObjType.ROUGH,
        )

        self.ball = Ball(0, yards, 0, GameObjType.BALL)

        self.score_card_start_new_hole()

        self.hole_geometry = HoleGeometry(cup, green, fairway, rough, info.hazards)

        print(f"                |> {self.hole_num}")
        print("                |        ")
        print("                |        ")
        print("          ^^^^^^^^^^^^^^^")

        print(
            f"Hole #{self.hole_num}. You are at the tee. Distance {info.yards} yards, par {info.par}."
        )
        print(info.description)

        self.tee_up()

    def set_putter_and_stroke(self, strength: float) -> None:
        putter = self.clubs[self.putt]
        self.stroke((putter[1] * (strength / 10.0)), self.putt)

    def ask_gauge(self, c: int) -> None:
        self.club = self.clubs[c]

        print(" ")
        print(f"[{self.club[0].upper()}: average {self.club[1]} yards]")

        foo = partial(self.make_stroke, c=c)

        self.ask(
            "Now gauge your distance by a percentage of a full swing. (1-10)",
            1,
            10,
            foo,
        )

    def make_stroke(self, strength: float, c: int) -> None:
        self.stroke((self.club[1] * (strength / 10.0)), c)

    def tee_up(self) -> None:
        # on the green? automatically select putter
        # otherwise Ask club and swing strength
        if self.is_on_green(self.ball) and not self.is_in_hazard(
            self.ball, GameObjType.SAND
        ):
            self.putt = 10
            print("[PUTTER: average 10 yards]")
            if odds(20):
                msg = "Keep your head down.\n"
            else:
                msg = ""

            self.ask(
                msg + "Choose your putt potency. (1-10)",
                1,
                10,
                self.set_putter_and_stroke,
            )
        else:
            self.ask("What club do you choose? (1-10)", 1, 10, self.ask_gauge)

    def stroke(self, club_amt: float, club_index: int) -> None:
        self.stroke_num += 1

        flags = 0b000000000000

        # fore! only when driving
        if (self.stroke_num == 1) and (club_amt > 210) and odds(30):
            print('"...Fore !"')

        # dub
        if odds(5):
            # there's always a 5% chance of dubbing it
            flags |= dub

        # if you're in the rough, or sand, you really should be using a wedge
        if (
            (
                self.is_in_rough(self.ball)
                or self.is_in_hazard(self.ball, GameObjType.SAND)
            )
            and not (club_index == 8 or club_index == 9)
            and odds(40)
        ):
            flags |= dub

        # trap difficulty
        if (
            self.is_in_hazard(self.ball, GameObjType.SAND)
            and self.player_difficulty == 4
        ) and odds(20):
            flags |= dub

        # hook/slice
        # There's 10% chance of a hook or slice
        # if it's a known player_difficulty then increase chance to 30%
        # if it's a putt & putting is a player_difficulty increase to 30%

        rand_hook_slice: bool
        if (
            self.player_difficulty == 1
            or self.player_difficulty == 2
            or (self.player_difficulty == 5 and self.is_on_green(self.ball))
        ):
            rand_hook_slice = odds(30)
        else:
            rand_hook_slice = odds(10)

        if rand_hook_slice:
            if self.player_difficulty == 1:
                if odds(80):
                    flags |= hook
                else:
                    flags |= slice_
            elif self.player_difficulty == 2:
                if odds(80):
                    flags |= slice_
                else:
                    flags |= hook
            else:
                if odds(50):
                    flags |= hook
                else:
                    flags |= slice_

        # beginner's luck !
        # If handicap is greater than 15, there's a 10% chance of avoiding all errors
        if (self.handicap > 15) and (odds(10)):
            flags |= luck

        # ace
        # there's a 10% chance of an Ace on a par 3
        if CourseInfo[self.hole_num].par == 3 and odds(10) and self.stroke_num == 1:
            flags |= ace

        # distance:
        # If handicap is < 15, there a 50% chance of reaching club average,
        # a 25% of exceeding it, and a 25% of falling short
        # If handicap is > 15, there's a 25% chance of reaching club average,
        # and 75% chance of falling short
        # The greater the handicap, the more the ball falls short
        # If poor distance is a known player_difficulty, then reduce distance by 10%

        distance: float
        rnd = random.randint(1, 101)

        if self.handicap < 15:
            if rnd <= 25:
                distance = club_amt - (club_amt * (self.handicap / 100.0))
            elif rnd > 25 and rnd <= 75:
                distance = club_amt
            else:
                distance = club_amt + (club_amt * 0.10)
        else:
            if rnd <= 75:
                distance = club_amt - (club_amt * (self.handicap / 100.0))
            else:
                distance = club_amt

        if self.player_difficulty == 3 and odds(80):  # poor distance
            distance = distance * 0.80

        if (flags & luck) == luck:
            distance = club_amt

        # angle
        # For all strokes, there's a possible "drift" of 4 degrees
        # a hooks or slice increases the angle between 5-10 degrees,
        # hook uses negative degrees
        angle = random.randint(0, 5)
        if (flags & slice_) == slice_:
            angle = random.randint(5, 11)
        if (flags & hook) == hook:
            angle = 0 - random.randint(5, 11)
        if (flags & luck) == luck:
            angle = 0

        plot = self.plot_ball(self.ball, distance, angle)
        # calculate a new location
        if (flags & luck) == luck and plot.y > 0:
            plot.y = 2

        flags = self.find_ball(
            Ball(plot.x, plot.y, plot.offline, GameObjType.BALL), flags
        )

        self.interpret_results(plot, flags)

    def plot_ball(self, ball: Ball, stroke_distance: float, degrees_off: float) -> Plot:
        cup_vector = Point(0, -1)
        rad_from_cup = math.atan2(ball.Y, ball.X) - math.atan2(
            cup_vector.y, cup_vector.x
        )
        rad_from_ball = rad_from_cup - math.pi

        hypotenuse = stroke_distance
        adjacent = math.cos(rad_from_ball + to_radians(degrees_off)) * hypotenuse
        opposite = math.sqrt(math.pow(hypotenuse, 2) - math.pow(adjacent, 2))

        new_pos: Point
        if to_degrees_360(rad_from_ball + to_radians(degrees_off)) > 180:
            new_pos = Point(int(ball.X - opposite), int(ball.Y - adjacent))
        else:
            new_pos = Point(int(ball.X + opposite), int(ball.Y - adjacent))

        return Plot(new_pos.x, new_pos.y, int(opposite))

    def interpret_results(self, plot: Plot, flags: int) -> None:
        cup_distance: int = int(
            get_distance(
                Point(plot.x, plot.y),
                Point(self.hole_geometry.cup.X, self.hole_geometry.cup.Y),
            )
        )
        travel_distance: int = int(
            get_distance(Point(plot.x, plot.y), Point(self.ball.X, self.ball.Y))
        )

        print(" ")

        if (flags & ace) == ace:
            print("Hole in One! You aced it.")
            self.score_card_record_stroke(Ball(0, 0, 0, GameObjType.BALL))
            self.report_current_score()
            return

        if (flags & in_trees) == in_trees:
            print("Your ball is lost in the trees. Take a penalty stroke.")
            self.score_card_record_stroke(self.ball)
            self.tee_up()
            return

        if (flags & in_water) == in_water:
            if odds(50):
                msg = "Your ball has gone to a watery grave."
            else:
                msg = "Your ball is lost in the water."
            print(msg + " Take a penalty stroke.")
            self.score_card_record_stroke(self.ball)
            self.tee_up()
            return

        if (flags & out_of_bounds) == out_of_bounds:
            print("Out of bounds. Take a penalty stroke.")
            self.score_card_record_stroke(self.ball)
            self.tee_up()
            return

        if (flags & dub) == dub:
            print("You dubbed it.")
            self.score_card_record_stroke(self.ball)
            self.tee_up()
            return

        if (flags & in_cup) == in_cup:
            if odds(50):
                msg = "You holed it."
            else:
                msg = "It's in!"
            print(msg)
            self.score_card_record_stroke(Ball(plot.x, plot.y, 0, GameObjType.BALL))
            self.report_current_score()
            return

        if ((flags & slice_) == slice_) and not ((flags & on_green) == on_green):
            if (flags & out_of_bounds) == out_of_bounds:
                bad = "badly"
            else:
                bad = ""
            print(f"You sliced{bad}: {plot.offline} yards offline.")

        if ((flags & hook) == hook) and not ((flags & on_green) == on_green):
            if (flags & out_of_bounds) == out_of_bounds:
                bad = "badly"
            else:
                bad = ""
            print(f"You hooked{bad}: {plot.offline} yards offline.")

        if self.stroke_num > 1:
            prev_ball = self.score_card_get_previous_stroke()
            d1 = get_distance(
                Point(prev_ball.X, prev_ball.Y),
                Point(self.hole_geometry.cup.X, self.hole_geometry.cup.Y),
            )
            d2 = cup_distance
            if d2 > d1:
                print("Too much club.")

        if (flags & in_rough) == in_rough:
            print("You're in the rough.")

        if (flags & in_sand) == in_sand:
            print("You're in a sand trap.")

        if (flags & on_green) == on_green:
            if cup_distance < 4:
                pd = str(cup_distance * 3) + " feet"
            else:
                pd = f"{cup_distance} yards"
            print(f"You're on the green. It's {pd} from the pin.")

        if ((flags & on_fairway) == on_fairway) or ((flags & in_rough) == in_rough):
            print(
                f"Shot went {travel_distance} yards. "
                f"It's {cup_distance} yards from the cup."
            )

        self.score_card_record_stroke(Ball(plot.x, plot.y, 0, GameObjType.BALL))

        self.ball = Ball(plot.x, plot.y, 0, GameObjType.BALL)

        self.tee_up()

    def report_current_score(self) -> None:
        par = CourseInfo[self.hole_num].par
        if len(self.score_card[self.hole_num]) == par + 1:
            print("A bogey. One above par.")
        if len(self.score_card[self.hole_num]) == par:
            print("Par. Nice.")
        if len(self.score_card[self.hole_num]) == (par - 1):
            print("A birdie! One below par.")
        if len(self.score_card[self.hole_num]) == (par - 2):
            print("An Eagle! Two below par.")
        if len(self.score_card[self.hole_num]) == (par - 3):
            print("Double Eagle! Unbelievable.")

        total_par: int = 0
        for i in range(1, self.hole_num + 1):
            total_par += CourseInfo[i].par

        print(" ")
        print("-----------------------------------------------------")
        if self.hole_num > 1:
            hole_str = "holes"
        else:
            hole_str = "hole"
        print(
            f" Total par for {self.hole_num} {hole_str} is: {total_par}. "
            f"Your total is: {self.score_card_get_total()}."
        )
        print("-----------------------------------------------------")
        print(" ")

        if self.hole_num == 18:
            self.game_over()
        else:
            time.sleep(2)
            self.new_hole()

    def find_ball(self, ball: Ball, flags: int) -> int:
        if self.is_on_fairway(ball) and not self.is_on_green(ball):
            flags |= on_fairway
        if self.is_on_green(ball):
            flags |= on_green
        if self.is_in_rough(ball):
            flags |= in_rough
        if self.is_out_of_bounds(ball):
            flags |= out_of_bounds
        if self.is_in_hazard(ball, GameObjType.WATER):
            flags |= in_water
        if self.is_in_hazard(ball, GameObjType.TREES):
            flags |= in_trees
        if self.is_in_hazard(ball, GameObjType.SAND):
            flags |= in_sand

        if ball.Y < 0:
            flags |= passed_cup

        # less than 2, it's in the cup
        d = get_distance(
            Point(ball.X, ball.Y),
            Point(self.hole_geometry.cup.X, self.hole_geometry.cup.Y),
        )
        if d < 2:
            flags |= in_cup

        return flags

    def is_on_fairway(self, ball: Ball) -> bool:
        return is_in_rectangle(ball, self.hole_geometry.fairway)

    def is_on_green(self, ball: Ball) -> bool:
        d = get_distance(
            Point(ball.X, ball.Y),
            Point(self.hole_geometry.cup.X, self.hole_geometry.cup.Y),
        )
        return d < self.hole_geometry.green.Radius

    def hazard_hit(self, h: Hazard, ball: Ball, hazard: GameObjType) -> bool:
        d = get_distance(Point(ball.X, ball.Y), Point(h.X, h.Y))
        result = False
        if (d < h.Radius) and h.Type == hazard:
            result = True
        return result

    def is_in_hazard(self, ball: Ball, hazard: GameObjType) -> bool:
        result: bool = False
        for h in self.hole_geometry.hazards:
            result = result and self.hazard_hit(h, ball, hazard)
        return result

    def is_in_rough(self, ball: Ball) -> bool:
        return is_in_rectangle(ball, self.hole_geometry.rough) and (
            not is_in_rectangle(ball, self.hole_geometry.fairway)
        )

    def is_out_of_bounds(self, ball: Ball) -> bool:
        return (not self.is_on_fairway(ball)) and (not self.is_in_rough(ball))

    def score_card_start_new_hole(self) -> None:
        self.score_card.append([])

    def score_card_record_stroke(self, ball: Ball) -> None:
        clone = Ball(ball.X, ball.Y, 0, GameObjType.BALL)
        self.score_card[self.hole_num].append(clone)

    def score_card_get_previous_stroke(self) -> Ball:
        return self.score_card[self.hole_num][len(self.score_card[self.hole_num]) - 1]

    def score_card_get_total(self) -> int:
        total: int = 0
        for h in self.score_card:
            total += len(h)
        return total

    def ask(
        self, question: str, min_: int, max_: int, callback: Callable[[int], Any]
    ) -> None:
        # input from console is always an integer passed to a callback
        # or "quit" to end game
        print(question)
        i = input().strip().lower()
        if i == "quit":
            self.quit_game()
            return
        if i == "bag":
            self.review_bag()

        try:
            n = int(i)
            success = True
        except Exception:
            success = False
            n = 0

        if success:
            if n >= min_ and n <= max_:
                callback(n)
            else:
                self.ask(question, min_, max_, callback)
        else:
            self.ask(question, min_, max_, callback)

    def review_bag(self) -> None:
        print(" ")
        print("  #     Club      Average Yardage")
        print("-----------------------------------")
        print("  1    Driver           250")
        print("  2    3 Wood           225")
        print("  3    5 Wood           200")
        print("  4    Hybrid           190")
        print("  5    4 Iron           170")
        print("  6    7 Iron           150")
        print("  7    9 Iron           125")
        print("  8    Pitching wedge   110")
        print("  9    Sand wedge        75")
        print(" 10    Putter            10")
        print(" ")

    def quit_game(self) -> None:
        print("\nLooks like rain. Goodbye!\n")
        return

    def game_over(self) -> None:
        net = self.score_card_get_total() - self.handicap
        print("Good game!")
        print(f"Your net score is: {net}")
        print("Let's visit the pro shop...")
        print(" ")
        return


if __name__ == "__main__":
    Golf()
