//
//          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
//
// C#
// .NET Core
// TargetFramework: netcoreapp 3.1
//
// Run source:
// dotnet run
//
// Linux compile:
// dotnet publish --self-contained -c Release -r linux-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true
//
// Windows compile:
// dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true
//
//
// INDEX
// ----------------- methods
// constructor
// NewHole
// TeeUp
// Stroke
// PlotBall
// InterpretResults
// ReportCurrentScore
// FindBall
// IsOnFairway
// IsOnGreen
// IsInHazard
// IsInRough
// IsOutOfBounds
// ScoreCardNewHole
// ScoreCardRecordStroke
// ScoreCardGetPreviousStroke
// ScoreCardGetTotal
// Ask
// Wait
// ReviewBag
// Quit
// GameOver
// ----------------- DATA
// Clubs
// CourseInfo
// ----------------- classes
// HoleInfo
// CircleGameObj
// RectGameObj
// HoleGeometry
// Plot
// ----------------- helper methods
// GetDistance
// IsInRectangle
// ToRadians
// ToDegrees360
// Odds
//
//  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
//
//

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;


namespace Golf
{
    using Ball = Golf.CircleGameObj;
    using Hazard = Golf.CircleGameObj;

    // --------------------------------------------------------------------------- Program
    class Program
    {
        static void Main(string[] args)
        {
            Golf g = new Golf();
        }
    }


    // --------------------------------------------------------------------------- Golf
    public class Golf
    {
        Ball BALL;
        int HOLE_NUM = 0;
        int STROKE_NUM = 0;
        int Handicap = 0;
        int PlayerDifficulty = 0;
        HoleGeometry holeGeometry;

        // all fairways are 40 yards wide, extend 5 yards beyond the cup, and
        // have 5 yards of rough around the perimeter
        const int FairwayWidth = 40;
        const int FairwayExtension = 5;
        const int RoughAmt = 5;

        // ScoreCard records the ball position after each stroke
        // a new list for each hole
        // include a blank list so index 1 == hole 1

        List<List<Ball>> ScoreCard = new List<List<Ball>> { new List<Ball>() };

        static void w(string s) { Console.WriteLine(s); } // WRITE
        Random RANDOM = new Random();


        // --------------------------------------------------------------- constructor
        public Golf()
        {
            Console.Clear();
            w(" ");
            w("          8\"\"\"\"8 8\"\"\"88 8     8\"\"\"\" ");
            w("          8    \" 8    8 8     8     ");
            w("          8e     8    8 8e    8eeee ");
            w("          88  ee 8    8 88    88    ");
            w("          88   8 8    8 88    88    ");
            w("          88eee8 8eeee8 88eee 88    ");
            w(" ");
            w("Welcome to the Creative Computing Country Club,");
            w("an eighteen hole championship layout located a short");
            w("distance from scenic downtown Lambertville, New Jersey.");
            w("The game will be explained as you play.");
            w("Enjoy your game! See you at the 19th hole...");
            w(" ");
            w("Type QUIT at any time to leave the game.");
            w("Type BAG at any time to review the clubs in your bag.");
            w(" ");

            Wait((z) =>
            {
                w(" ");
                w("              YOUR BAG");
                ReviewBag();
                w("Type BAG at any time to review the clubs in your bag.");
                w(" ");

                Wait((zz) =>
                {
                    w(" ");

                    Ask("PGA handicaps range from 0 to 30.\nWhat is your handicap?", 0, 30, (i) =>
                    {
                        Handicap = i;
                        w(" ");

                        Ask("Common difficulties at golf include:\n1=Hook, 2=Slice, 3=Poor Distance, 4=Trap Shots, 5=Putting\nWhich one is your worst?", 1, 5, (j) =>
                        {
                            PlayerDifficulty = j;
                            Console.Clear();
                            NewHole();
                        });
                    });
                });
            });
        }


        // --------------------------------------------------------------- NewHole
        void NewHole()
        {
            HOLE_NUM++;
            STROKE_NUM = 0;

            HoleInfo info = CourseInfo[HOLE_NUM];

            int yards = info.Yards;  // from tee to cup
            int par = info.Par;
            var cup = new CircleGameObj(0, 0, 0, GameObjType.CUP);
            var green = new CircleGameObj(0, 0, 10, GameObjType.GREEN);

            var fairway = new RectGameObj(0 - (FairwayWidth / 2),
                                          0 - (green.Radius + FairwayExtension),
                                          FairwayWidth,
                                          yards + (green.Radius + FairwayExtension) + 1,
                                          GameObjType.FAIRWAY);

            var rough = new RectGameObj(fairway.X - RoughAmt,
                                        fairway.Y - RoughAmt,
                                        fairway.Width + (2 * RoughAmt),
                                        fairway.Length + (2 * RoughAmt),
                                        GameObjType.ROUGH);

            BALL = new Ball(0, yards, 0, GameObjType.BALL);

            ScoreCardStartNewHole();

            holeGeometry = new HoleGeometry(cup, green, fairway, rough, info.Hazard);

            w("                |> " + HOLE_NUM);
            w("                |        ");
            w("                |        ");
            w("          ^^^^^^^^^^^^^^^");

            Console.WriteLine("Hole #{0}. You are at the tee. Distance {1} yards, par {2}.", HOLE_NUM, info.Yards, info.Par);
            w(info.Description);

            TeeUp();
        }


        // --------------------------------------------------------------- TeeUp
        // on the green? automatically select putter
        // otherwise Ask club and swing strength

        void TeeUp()
        {
            if (IsOnGreen(BALL) && !IsInHazard(BALL, GameObjType.SAND))
            {
                var putt = 10;
                w("[PUTTER: average 10 yards]");
                var msg = Odds(20) ? "Keep your head down.\n" : "";

                Ask(msg + "Choose your putt potency. (1-10)", 1, 10, (strength) =>
                {
                    var putter = Clubs[putt];
                    Stroke(Convert.ToDouble((double)putter.Item2 * ((double)strength / 10.0)), putt);
                });
            }
            else
            {
                Ask("What club do you choose? (1-10)", 1, 10, (c) =>
                {
                    var club = Clubs[c];

                    w(" ");
                    Console.WriteLine("[{0}: average {1} yards]", club.Item1.ToUpper(), club.Item2);

                    Ask("Now gauge your distance by a percentage of a full swing. (1-10)", 1, 10, (strength) =>
                    {
                        Stroke(Convert.ToDouble((double)club.Item2 * ((double)strength / 10.0)), c);
                    });
                });
            };
        }


        // -------------------------------------------------------- bitwise Flags
        int dub         = 0b00000000000001;
        int hook        = 0b00000000000010;
        int slice       = 0b00000000000100;
        int passedCup   = 0b00000000001000;
        int inCup       = 0b00000000010000;
        int onFairway   = 0b00000000100000;
        int onGreen     = 0b00000001000000;
        int inRough     = 0b00000010000000;
        int inSand      = 0b00000100000000;
        int inTrees     = 0b00001000000000;
        int inWater     = 0b00010000000000;
        int outOfBounds = 0b00100000000000;
        int luck        = 0b01000000000000;
        int ace         = 0b10000000000000;


        // --------------------------------------------------------------- Stroke
        void Stroke(double clubAmt, int clubIndex)
        {
            STROKE_NUM++;

            var flags = 0b000000000000;

            // fore! only when driving
            if ((STROKE_NUM == 1) && (clubAmt > 210) && Odds(30)) { w("\"...Fore !\""); };

            // dub
            if (Odds(5)) { flags |= dub; }; // there's always a 5% chance of dubbing it

            // if you're in the rough, or sand, you really should be using a wedge
            if ((IsInRough(BALL) || IsInHazard(BALL, GameObjType.SAND)) &&
                !(clubIndex == 8 || clubIndex == 9))
            {
                if (Odds(40)) { flags |= dub; };
            };

            // trap difficulty
            if (IsInHazard(BALL, GameObjType.SAND) && PlayerDifficulty == 4)
            {
                if (Odds(20)) { flags |= dub; };
            }

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

            bool randHookSlice = (PlayerDifficulty == 1 ||
                                  PlayerDifficulty == 2 ||
                                  (PlayerDifficulty == 5 && IsOnGreen(BALL))) ? Odds(30) : Odds(10);

            if (randHookSlice)
            {
                if (PlayerDifficulty == 1)
                {
                    if (Odds(80)) { flags |= hook; } else { flags |= slice; };
                }
                else if (PlayerDifficulty == 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 ((Handicap > 15) && (Odds(10))) { flags |= luck; };

            // ace
            // there's a 10% chance of an Ace on a par 3
            if (CourseInfo[HOLE_NUM].Par == 3 && Odds(10) && 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 playerDifficulty, then reduce distance by 10%

            double distance;
            int rnd = RANDOM.Next(1, 101);

            if (Handicap < 15)
            {
                if (rnd <= 25)
                {
                    distance = clubAmt - (clubAmt * ((double)Handicap / 100.0));
                }
                else if (rnd > 25 && rnd <= 75)
                {
                    distance = clubAmt;
                }
                else
                {
                    distance = clubAmt + (clubAmt * 0.10);
                };
            }
            else
            {
                if (rnd <= 75)
                {
                    distance = clubAmt - (clubAmt * ((double)Handicap / 100.0));
                }
                else
                {
                    distance = clubAmt;
                };
            };

            if (PlayerDifficulty == 3)  // poor distance
            {
                if (Odds(80)) { distance = (distance * 0.80); };
            };

            if ((flags & luck) == luck) { distance = clubAmt; }

            // 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
            int angle = RANDOM.Next(0, 5);
            if ((flags & slice) == slice) { angle = RANDOM.Next(5, 11); };
            if ((flags & hook) == hook) { angle = 0 - RANDOM.Next(5, 11); };
            if ((flags & luck) == luck) { angle = 0; };

            var plot = PlotBall(BALL, distance, Convert.ToDouble(angle));  // calculate a new location
            if ((flags & luck) == luck) { if(plot.Y > 0){ plot.Y = 2; }; };

            flags = FindBall(new Ball(plot.X, plot.Y, plot.Offline, GameObjType.BALL), flags);

            InterpretResults(plot, flags);
        }


        // --------------------------------------------------------------- plotBall
        Plot PlotBall(Ball ball, double strokeDistance, double degreesOff)
        {
            var cupVector = new Point(0, -1);
            double radFromCup = Math.Atan2((double)ball.Y, (double)ball.X) - Math.Atan2((double)cupVector.Y, (double)cupVector.X);
            double radFromBall = radFromCup - Math.PI;

            var hypotenuse = strokeDistance;
            var adjacent = Math.Cos(radFromBall + ToRadians(degreesOff)) * hypotenuse;
            var opposite = Math.Sqrt(Math.Pow(hypotenuse, 2) - Math.Pow(adjacent, 2));

            Point newPos;
            if (ToDegrees360(radFromBall + ToRadians(degreesOff)) > 180)
            {
                newPos = new Point(Convert.ToInt32(ball.X - opposite),
                                   Convert.ToInt32(ball.Y - adjacent));
            }
            else
            {
                newPos = new Point(Convert.ToInt32(ball.X + opposite),
                                   Convert.ToInt32(ball.Y - adjacent));
            }

            return new Plot(newPos.X, newPos.Y, Convert.ToInt32(opposite));
        }


        // --------------------------------------------------------------- InterpretResults
        void InterpretResults(Plot plot, int flags)
        {
            int cupDistance = Convert.ToInt32(GetDistance(new Point(plot.X, plot.Y),
                                                          new Point(holeGeometry.Cup.X, holeGeometry.Cup.Y)));
            int travelDistance = Convert.ToInt32(GetDistance(new Point(plot.X, plot.Y),
                                                             new Point(BALL.X, BALL.Y)));

            w(" ");

            if ((flags & ace) == ace)
            {
                w("Hole in One! You aced it.");
                ScoreCardRecordStroke(new Ball(0, 0, 0, GameObjType.BALL));
                ReportCurrentScore();
                return;
            };

            if ((flags & inTrees) == inTrees)
            {
                w("Your ball is lost in the trees. Take a penalty stroke.");
                ScoreCardRecordStroke(BALL);
                TeeUp();
                return;
            };

            if ((flags & inWater) == inWater)
            {
                var msg = Odds(50) ? "Your ball has gone to a watery grave." : "Your ball is lost in the water.";
                w(msg + " Take a penalty stroke.");
                ScoreCardRecordStroke(BALL);
                TeeUp();
                return;
            };

            if ((flags & outOfBounds) == outOfBounds)
            {
                w("Out of bounds. Take a penalty stroke.");
                ScoreCardRecordStroke(BALL);
                TeeUp();
                return;
            };

            if ((flags & dub) == dub)
            {
                w("You dubbed it.");
                ScoreCardRecordStroke(BALL);
                TeeUp();
                return;
            };

            if ((flags & inCup) == inCup)
            {
                var msg = Odds(50) ? "You holed it." : "It's in!";
                w(msg);
                ScoreCardRecordStroke(new Ball(plot.X, plot.Y, 0, GameObjType.BALL));
                ReportCurrentScore();
                return;
            };

            if (((flags & slice) == slice) &&
                !((flags & onGreen) == onGreen))
            {
                var bad = ((flags & outOfBounds) == outOfBounds) ? " badly" : "";
                Console.WriteLine("You sliced{0}: {1} yards offline.", bad, plot.Offline);
            };

            if (((flags & hook) == hook) &&
                !((flags & onGreen) == onGreen))
            {
                var bad = ((flags & outOfBounds) == outOfBounds) ? " badly" : "";
                Console.WriteLine("You hooked{0}: {1} yards offline.", bad, plot.Offline);
            };

            if (STROKE_NUM > 1)
            {
                var prevBall = ScoreCardGetPreviousStroke();
                var d1 = GetDistance(new Point(prevBall.X, prevBall.Y),
                                     new Point(holeGeometry.Cup.X, holeGeometry.Cup.Y));
                var d2 = cupDistance;
                if (d2 > d1) { w("Too much club."); };
            };

            if ((flags & inRough) == inRough) { w("You're in the rough."); };

            if ((flags & inSand) == inSand) { w("You're in a sand trap."); };

            if ((flags & onGreen) == onGreen)
            {
                var pd = (cupDistance < 4) ? ((cupDistance * 3) + " feet") : (cupDistance + " yards");
                Console.WriteLine("You're on the green. It's {0} from the pin.", pd);
            };

            if (((flags & onFairway) == onFairway) ||
                ((flags & inRough) == inRough))
            {
                Console.WriteLine("Shot went {0} yards. It's {1} yards from the cup.", travelDistance, cupDistance);
            };

            ScoreCardRecordStroke(new Ball(plot.X, plot.Y, 0, GameObjType.BALL));

            BALL = new Ball(plot.X, plot.Y, 0, GameObjType.BALL);

            TeeUp();
        }


        // --------------------------------------------------------------- ReportCurrentScore
        void ReportCurrentScore()
        {
            var par = CourseInfo[HOLE_NUM].Par;
            if (ScoreCard[HOLE_NUM].Count == par + 1) { w("A bogey. One above par."); };
            if (ScoreCard[HOLE_NUM].Count == par) { w("Par. Nice."); };
            if (ScoreCard[HOLE_NUM].Count == (par - 1)) { w("A birdie! One below par."); };
            if (ScoreCard[HOLE_NUM].Count == (par - 2)) { w("An Eagle! Two below par."); };
            if (ScoreCard[HOLE_NUM].Count == (par - 3)) { w("Double Eagle! Unbelievable."); };

            int totalPar = 0;
            for (var i = 1; i <= HOLE_NUM; i++) { totalPar += CourseInfo[i].Par; };

            w(" ");
            w("-----------------------------------------------------");
            Console.WriteLine(" Total par for {0} hole{1} is: {2}. Your total is: {3}.",
                              HOLE_NUM,
                              ((HOLE_NUM > 1) ? "s" : ""), //plural
                              totalPar,
                              ScoreCardGetTotal());
            w("-----------------------------------------------------");
            w(" ");

            if (HOLE_NUM == 18)
            {
                GameOver();
            }
            else
            {
                Thread.Sleep(2000);
                NewHole();
            };
        }


        // --------------------------------------------------------------- FindBall
        int FindBall(Ball ball, int flags)
        {
            if (IsOnFairway(ball) && !IsOnGreen(ball)) { flags |= onFairway; }
            if (IsOnGreen(ball)) { flags |= onGreen; }
            if (IsInRough(ball)) { flags |= inRough; }
            if (IsOutOfBounds(ball)) { flags |= outOfBounds; }
            if (IsInHazard(ball, GameObjType.WATER)) { flags |= inWater; }
            if (IsInHazard(ball, GameObjType.TREES)) { flags |= inTrees; }
            if (IsInHazard(ball, GameObjType.SAND))  { flags |= inSand;  }

            if (ball.Y < 0) { flags |= passedCup; }

            // less than 2, it's in the cup
            var d = GetDistance(new Point(ball.X, ball.Y),
                                new Point(holeGeometry.Cup.X, holeGeometry.Cup.Y));
            if (d < 2) { flags |= inCup; };

            return flags;
        }


        // --------------------------------------------------------------- IsOnFairway
        bool IsOnFairway(Ball ball)
        {
            return IsInRectangle(ball, holeGeometry.Fairway);
        }


        // --------------------------------------------------------------- IsOngreen
        bool IsOnGreen(Ball ball)
        {
            var d = GetDistance(new Point(ball.X, ball.Y),
                                new Point(holeGeometry.Cup.X, holeGeometry.Cup.Y));
            return d < holeGeometry.Green.Radius;
        }


        // --------------------------------------------------------------- IsInHazard
        bool IsInHazard(Ball ball, GameObjType hazard)
        {
            bool result = false;
            Array.ForEach(holeGeometry.Hazards, (Hazard h) =>
            {
                var d = GetDistance(new Point(ball.X, ball.Y), new Point(h.X, h.Y));
                if ((d < h.Radius) && h.Type == hazard) { result = true; };
            });
            return result;
        }


        // --------------------------------------------------------------- IsInRough
        bool IsInRough(Ball ball)
        {
            return IsInRectangle(ball, holeGeometry.Rough) &&
                (IsInRectangle(ball, holeGeometry.Fairway) == false);
        }


        // --------------------------------------------------------------- IsOutOfBounds
        bool IsOutOfBounds(Ball ball)
        {
            return (IsOnFairway(ball) == false) && (IsInRough(ball) == false);
        }


        // --------------------------------------------------------------- ScoreCardNewHole
        void ScoreCardStartNewHole()
        {
            ScoreCard.Add(new List<Ball>());
        }


        // --------------------------------------------------------------- ScoreCardRecordStroke
        void ScoreCardRecordStroke(Ball ball)
        {
            var clone = new Ball(ball.X, ball.Y, 0, GameObjType.BALL);
            ScoreCard[HOLE_NUM].Add(clone);
        }


        // ------------------------------------------------------------ ScoreCardGetPreviousStroke
        Ball ScoreCardGetPreviousStroke()
        {
            return ScoreCard[HOLE_NUM][ScoreCard[HOLE_NUM].Count - 1];
        }


        // --------------------------------------------------------------- ScoreCardGetTotal
        int ScoreCardGetTotal()
        {
            int total = 0;
            ScoreCard.ForEach((h) => { total += h.Count; });
            return total;
        }


        // --------------------------------------------------------------- Ask
        // input from console is always an integer passed to a callback
        // or "quit" to end game

        void Ask(string question, int min, int max, Action<int> callback)
        {
            w(question);
            string i = Console.ReadLine().Trim().ToLower();
            if (i == "quit") { Quit(); return; };
            if (i == "bag") { ReviewBag(); };

            int n;
            bool success = Int32.TryParse(i, out n);

            if (success)
            {
                if (n >= min && n <= max)
                {
                    callback(n);
                }
                else
                {
                    Ask(question, min, max, callback);
                }
            }
            else
            {
                Ask(question, min, max, callback);
            };
        }


        // --------------------------------------------------------------- Wait
        void Wait(Action<int> callback)
        {
            w("Press any key to continue.");

            ConsoleKeyInfo keyinfo;
            do { keyinfo = Console.ReadKey(true); }
            while (keyinfo.KeyChar < 0);
            Console.Clear();
            callback(0);
        }


        // --------------------------------------------------------------- ReviewBag
        void ReviewBag()
        {
            w(" ");
            w("  #     Club      Average Yardage");
            w("-----------------------------------");
            w("  1    Driver           250");
            w("  2    3 Wood           225");
            w("  3    5 Wood           200");
            w("  4    Hybrid           190");
            w("  5    4 Iron           170");
            w("  6    7 Iron           150");
            w("  7    9 Iron           125");
            w("  8    Pitching wedge   110");
            w("  9    Sand wedge        75");
            w(" 10    Putter            10");
            w(" ");
        }


        // --------------------------------------------------------------- Quit
        void Quit()
        {
            w("");
            w("Looks like rain. Goodbye!");
            w("");
            Wait((z) => { });
            return;
        }


        // --------------------------------------------------------------- GameOver
        void GameOver()
        {
            var net = ScoreCardGetTotal() - Handicap;
            w("Good game!");
            w("Your net score is: " + net);
            w("Let's visit the pro shop...");
            w(" ");
            Wait((z) => { });
            return;
        }


        // YOUR BAG
        // ======================================================== Clubs
        (string, int)[] Clubs = new (string, 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)
                };


        // THE COURSE
        // ======================================================== CourseInfo

        HoleInfo[] CourseInfo = new HoleInfo[]{
            new HoleInfo(0, 0, 0, new Hazard[]{}, ""), // include a blank so index 1 == hole 1


            // -------------------------------------------------------- front 9
            // hole, yards, par, hazards, (description)

            new HoleInfo(1, 361, 4,
                         new Hazard[]{
                             new Hazard( 20, 100, 10, GameObjType.TREES),
                             new Hazard(-20,  80, 10, GameObjType.TREES),
                             new Hazard(-20, 100, 10, GameObjType.TREES)
                         },
                         "There are a couple of trees on the left and right."),

            new HoleInfo(2, 389, 4,
                         new Hazard[]{
                             new Hazard(0, 160, 20, GameObjType.WATER)
                         },
                         "There is a large water hazard across the fairway about 150 yards."),

            new HoleInfo(3, 206, 3,
                         new Hazard[]{
                             new Hazard( 20,  20,  5, GameObjType.WATER),
                             new Hazard(-20, 160, 10, GameObjType.WATER),
                             new Hazard( 10,  12,  5, GameObjType.SAND)
                         },
                         "There is some sand and water near the green."),

            new HoleInfo(4, 500, 5,
                         new Hazard[]{
                             new Hazard(-14, 12, 12, GameObjType.SAND)
                         },
                         "There's a bunker to the left of the green."),

            new HoleInfo(5, 408, 4,
                         new Hazard[]{
                             new Hazard(20, 120, 20, GameObjType.TREES),
                             new Hazard(20, 160, 20, GameObjType.TREES),
                             new Hazard(10,  20,  5, GameObjType.SAND)
                         },
                         "There are some trees to your right."),

            new HoleInfo(6, 359, 4,
                         new Hazard[]{
                             new Hazard( 14, 0, 4, GameObjType.SAND),
                             new Hazard(-14, 0, 4, GameObjType.SAND)
                         },
                         ""),

            new HoleInfo(7, 424, 5,
                         new Hazard[]{
                             new Hazard(20, 200, 10, GameObjType.SAND),
                             new Hazard(10, 180, 10, GameObjType.SAND),
                             new Hazard(20, 160, 10, GameObjType.SAND)
                         },
                         "There are several sand traps along your right."),

            new HoleInfo(8, 388, 4,
                         new Hazard[]{
                             new Hazard(-20, 340, 10, GameObjType.TREES)
                         },
                         ""),

            new HoleInfo(9, 196, 3,
                         new Hazard[]{
                             new Hazard(-30, 180, 20, GameObjType.TREES),
                             new Hazard( 14,  -8,  5, GameObjType.SAND)
                         },
                         ""),

            // -------------------------------------------------------- back 9
            // hole, yards, par, hazards, (description)

            new HoleInfo(10, 400, 4,
                         new Hazard[]{
                             new Hazard(-14, -8, 5, GameObjType.SAND),
                             new Hazard( 14, -8, 5, GameObjType.SAND)
                         },
                         ""),

            new HoleInfo(11, 560, 5,
                         new Hazard[]{
                             new Hazard(-20, 400, 10, GameObjType.TREES),
                             new Hazard(-10, 380, 10, GameObjType.TREES),
                             new Hazard(-20, 260, 10, GameObjType.TREES),
                             new Hazard(-20, 200, 10, GameObjType.TREES),
                             new Hazard(-10, 180, 10, GameObjType.TREES),
                             new Hazard(-20, 160, 10, GameObjType.TREES)
                         },
                         "Lots of trees along the left of the fairway."),

            new HoleInfo(12, 132, 3,
                         new Hazard[]{
                             new Hazard(-10, 120, 10, GameObjType.WATER),
                             new Hazard( -5, 100, 10, GameObjType.SAND)
                         },
                         "There is water and sand directly in front of you. A good drive should clear both."),

            new HoleInfo(13, 357, 4,
                         new Hazard[]{
                             new Hazard(-20, 200, 10, GameObjType.TREES),
                             new Hazard(-10, 180, 10, GameObjType.TREES),
                             new Hazard(-20, 160, 10, GameObjType.TREES),
                             new Hazard( 14,  12,  8, GameObjType.SAND)
                         },
                         ""),

            new HoleInfo(14, 294, 4,
                         new Hazard[]{
                             new Hazard(0, 20, 10, GameObjType.SAND)
                         },
                         ""),

            new HoleInfo(15, 475, 5,
                         new Hazard[]{
                             new Hazard(-20, 20, 10, GameObjType.WATER),
                             new Hazard( 10, 20, 10, GameObjType.SAND)
                         },
                         "Some sand and water near the green."),

            new HoleInfo(16, 375, 4,
                         new Hazard[]{
                             new Hazard(-14, -8, 5, GameObjType.SAND)
                         },
                         ""),

            new HoleInfo(17, 180, 3,
                         new Hazard[]{
                             new Hazard( 20, 100, 10, GameObjType.TREES),
                             new Hazard(-20,  80, 10, GameObjType.TREES)
                         },
                         ""),

            new HoleInfo(18, 550, 5,
                         new Hazard[]{
                             new Hazard(20, 30, 15, GameObjType.WATER)
                         },
                         "There is a water hazard near the green.")
        };


        // -------------------------------------------------------- HoleInfo
        class HoleInfo
        {
            public int Hole { get; }
            public int Yards { get; }
            public int Par { get; }
            public Hazard[] Hazard { get; }
            public string Description { get; }

            public HoleInfo(int hole, int yards, int par, Hazard[] hazard, string description)
            {
                Hole = hole;
                Yards = yards;
                Par = par;
                Hazard = hazard;
                Description = description;
            }
        }


        public enum GameObjType { BALL, CUP, GREEN, FAIRWAY, ROUGH, TREES, WATER, SAND }


        // -------------------------------------------------------- CircleGameObj
        public class CircleGameObj
        {
            public GameObjType Type { get; }
            public int X { get; }
            public int Y { get; }
            public int Radius { get; }

            public CircleGameObj(int x, int y, int r, GameObjType type)
            {
                Type = type;
                X = x;
                Y = y;
                Radius = r;
            }
        }


        // -------------------------------------------------------- RectGameObj
        public class RectGameObj
        {
            public GameObjType Type { get; }
            public int X { get; }
            public int Y { get; }
            public int Width { get; }
            public int Length { get; }

            public RectGameObj(int x, int y, int w, int l, GameObjType type)
            {
                Type = type;
                X = x;
                Y = y;
                Width = w;
                Length = l;
            }
        }


        // -------------------------------------------------------- HoleGeometry
        public class HoleGeometry
        {
            public CircleGameObj Cup { get; }
            public CircleGameObj Green { get; }
            public RectGameObj Fairway { get; }
            public RectGameObj Rough { get; }
            public Hazard[] Hazards { get; }

            public HoleGeometry(CircleGameObj cup, CircleGameObj green, RectGameObj fairway, RectGameObj rough, Hazard[] haz)
            {
                Cup = cup;
                Green = green;
                Fairway = fairway;
                Rough = rough;
                Hazards = haz;
            }
        }


        // -------------------------------------------------------- Plot
        public class Plot
        {
            public int X { get; }
            public int Y { get; set; }
            public int Offline { get; }

            public Plot(int x, int y, int offline)
            {
                X = x;
                Y = y;
                Offline = offline;
            }
        }


        // -------------------------------------------------------- GetDistance
        // distance between 2 points
        double GetDistance(Point pt1, Point pt2)
        {
            return Math.Sqrt(Math.Pow((pt2.X - pt1.X), 2) + Math.Pow((pt2.Y - pt1.Y), 2));
        }


        // -------------------------------------------------------- IsInRectangle
        bool IsInRectangle(CircleGameObj pt, RectGameObj rect)
        {
            return ((pt.X > rect.X) &&
                    (pt.X < rect.X + rect.Width) &&
                    (pt.Y > rect.Y) &&
                    (pt.Y < rect.Y + rect.Length));
        }


        // -------------------------------------------------------- ToRadians
        double ToRadians(double angle) { return angle * (Math.PI / 180.0); }


        // -------------------------------------------------------- ToDegrees360
        // radians to 360 degrees
        double ToDegrees360(double angle)
        {
            double deg = angle * (180.0 / Math.PI);
            if (deg < 0.0) { deg += 360.0; }
            return deg;
        }


        // -------------------------------------------------------- Odds
        // chance an integer is <= the given argument
        // between 1-100
        Random RND = new Random();

        bool Odds(int x)
        {
            return RND.Next(1, 101) <= x;
        }
    }
}
