Code me some golf

15

1

If you haven't played golf before, here's a list of golf-related terms I use in this question

  • Shot, also called a stroke: Every time the ball is hit, this is a shot.
  • Hole: A golf course is split into holes, in which the goal is to hit a ball from one designated location to another in as few shots as possible.
  • Tee: Where you start a hole.
  • Pin or Flag: Where you finish a hole
  • Fairway, Rough, Water, and Green: Features on a golf course that affect how one plays the ball in real life. (How they affect the program is specified below)

I'm going out to play golf tomorrow, and I find that sometimes, I have trouble figuring out what club to use to hit a certain yardage. So I decided to write down my clubs and their yardages per shot.

First Assumption: All holes are due north of their tee boxes.

All of these yardages measure the possibilities for how far north the ball travels. The ball will travel a random integer distance between the bounds specified for each club (inclusive).

As a master golfer, none of my shots have any horizontal shift. This means that all of my shots go in a straight line directly at the flag.

Club #     Club        Yardage
1          Driver      300-330
2          3-Wood      270-299
3          5-Wood      240-269
4          3-Iron      220-239
5          4-Iron      200-219
6          5-Iron      180-199
7          6-Iron      160-179
8          7-Iron      140-159
9          8-Iron      120-139
10         9-Iron      100-119
11         P-Wedge     80-99
12         S-Wedge     50-79
13         L-Wedge     0-49
14         Putter      (only on green)

As a person who enjoys programming, I decide that I want to model a round of golf and set a goal for how well I want to do tomorrow. However, like any amateur programmer, after ten minutes, I gave up and asked for help on Stack Overflow (just kidding). Here are some more data about the course.

Second Assumption: Hole Geography

  • All numbers that describe distances on the course are integers.

  • Each hole is a straight line. The straight line distance between each hole and the pin (the end of the hole) is Length.

  • Fairways are segments with length defined by flen. The value listed for flen is the range of yardages north from the tee where fairway is.

  • Water hazards are segments that have length defined by wlen, which has the same properties as flen.

  • The green has a length defined by glen.

  • All parts of the course that are not fairway, water, or green are rough.

Here is a chart describing each hole on the course.

Hole #     Length      flen               wlen        glen   
1          401         54-390                         391-425
2          171                            1-165       166-179
3          438         41-392             393-420     421-445
4          553         30-281,354-549     282-353     550-589
5          389         48-372                         373-404
6          133                                        125-138
7          496         37-413             414-484     484-502
8          415         50-391                         392-420
9          320         23-258             259-303     304-327

How to play Golf (for this program)

  • Always aim exactly at the flag.
  • Hit the ball as close to the pin as possible, trying to keep the ball on the fairway or (preferably) on the green.
  • When you land a shot in the water, your next shot must be played from the same place as the shot that went into the water.
  • Once the ball lands on the green, only the putter can be used. If the ball lands strictly more than 5 yards from the pin, then I putt twice. Otherwise, I putt once.
  • It is possible to hit a shot past the pin.

Scoring

My score on a hole is the number of shots I take, plus one stroke for every time I land in rough or the water.

The program

Okay, that was a lot of rules, now let's talk about the program.

The course should be defined as above in the program, because the course is constant. Different golfers, however, have different distances for each shot, so the input to STDIN should be a set of ranges of yardages, arranged in increasing order of club number and separated by commas (with no whitespace).

The output should be how I "play" the round of golf. The hold number should be specified at the beginning of each line as Hole #: where # is the current hole. Each shot that is not a putt is of the following form: {club,distance of shot,condition of ball,distance to pin}. The details of the shot should be separated by commas but no whitespace in the above order. The shots themselves should be written in order of how they are played and separated by a space. Once the ball lands on the green, the program should print how many putts I take, in the format {# putts}. At the end of each line, the number of shots I took on the hole should be separated from the other shots by a space and printed as (#). Each hole should be on its own line and written in order. Finally, on the last (tenth) line of the program, the total number of shots for the round should be printed as Total: # shots.

There is no set "strategy" that your program is required to take. You can write a program with any strategy you want. Example strategies include maximizing the percent chance of landing on the green and maximizing the distance of each shot until reaching the hole.

SAMPLE INPUT

300-330,270-299,240-269,220-239,200-219,180-199,160-179,140-159,120-139,100-119,80-99,50-79,0-49

SAMPLE OUTPUT

Hole 1: {Driver,324,Fairway,77} {S-Wedge,70,Green,7} {Two putts} (4)
Hole 2: {6-Iron,162,Water,171} {6-Iron,168,Green,3} {One putt} (4)
Hole 3: {Driver,301,Fairway,137} {8-Iron,131,Green,6} {Two putts} (4)
Hole 4: {3-Wood,288,Water,553} {3-Wood,276,Fairway,277} {3-Wood,291,Green,14} {Two putts} (6)
Hole 5: {Driver,322,Fairway,67} {S-Wedge,62} {One putt} (3)
Hole 6: {8-Iron,120,Rough,18} {L-Wedge,10,Green,8} {Two putts} (5)
Hole 7: {Driver,325,Fairway,171] {6-Iron,170,Green,1} {One putt} (3)
Hole 8: {Driver,306,Fairway,109} {9-Iron,100,Green,9} {Two putts} (4)
Hole 9: {Driver,308,Green,12} {Two putts} (3)
Total: 36 shots

I'll admit, this is a rather ambitious challenge for a first post on CG.SE, so I'd be glad to talk about how to improve this challenge in comments. Thank you for your help.

Arcturus

Posted 2015-09-13T18:04:12.950

Reputation: 6 537

2I'd really appreciate it if, for us non-golfers, you didn't use as many golfing terms (e.g. "tee boxes" and "horizontal shift"). :) – kirbyfan64sos – 2015-09-13T18:11:33.970

I'll add a list of golf-related terms. When playing golf, the ball doesn't always go straight, so I just said that the ball always goes directly towards the hole, and thus does not have any horizontal shift. – Arcturus – 2015-09-13T18:13:22.153

Say the pin is at 301 yards, and there is fairway from 0~299 yards, green from 300~315 yards, and water from 316~330 yards. What club will be chosen? What if the water is replaced by rough? – lirtosiast – 2015-09-13T19:09:16.043

Ideally, the program should be able to come up with its own strategy. – Arcturus – 2015-09-13T19:10:56.663

What do you mean by "optimal strategy"? Minimizing average number of strokes? As for winning criterion I'd go with code-golf. – lirtosiast – 2015-09-13T19:14:25.070

I meant that the program should come up with its own strategy for playing the round. For the example you gave, the program could choose the "safe" option of trying to get on the green in two shots, or using a driver to go for the green in one as the "risky" option. – Arcturus – 2015-09-13T19:18:01.617

So either strategy is admissible there? What if the pin is at 300 yards, and there is green from 0~300 yards and water from 300 yards on? – lirtosiast – 2015-09-13T19:21:16.347

@ThomasKwa I think OP means that some sort of standard risk factor applies. Say the shot has a 50% chance of success (ending up with one less shot), but a 75% chance of failing (landing in the water, +1 shot), the risk outweighs the gains and the shot shouldn't be attempted. Say that maybe the chance of a failed shot has to be x% less than the chance of "gaining" a shot for it to be attempted. You could then experiment with that value to create riskier or safer players. If this is what OP means though, it should be a requirement. – Jan – 2015-09-13T19:30:13.030

@Jan I added that specification. – Arcturus – 2015-09-13T19:56:15.417

Does "choose a random club" count as a strategy? What about "choose the driver every time"? – lirtosiast – 2015-09-13T20:07:37.623

Those are possible strategies, but not great ones in terms of number of shots. "Driver every time" would probably do better in a golfing challenge than in shot-optimization, so I think I'm going to change this into a programming challenge. – Arcturus – 2015-09-13T20:31:53.433

Answers

9

Python 2.7: 43 40.5 shots average

This is my first post here so bear with me.

Since the poster was thinking of treating this like a programming challenge, not code golf, I tackled it as more a programming challenge. I attempted to keep my solution and shot logic simple but it turned out more ugly as things got complicated quickly.

My Code

Some things to think of while reading: the program creates a list of clubs used called 'clubs', and a list called 'distances' that is distance the ball has traveled from the tee, hlen is the length of the hole, d1s is the distance each shot travels.

First I define the course. Each fairway, water, and green length had to be defined so later the program could check the condition of the ball, so I added non-integers values for the parts of the course that did not exist.

from random import randint
import numpy as np

#Hole      Length     flen               wlen           glen    Name 
hole1 = [    401,     54, 390,       390.5, 390.5,    391, 425, 'Hole 1']
hole2 = [    171,    0.5, 0.5,           1, 165,      166, 179, 'Hole 2']
hole3 = [    438,     41, 392,         393, 420,      421, 445, 'Hole 3']
hole4 = [    553,     30, 549,         282, 353,      550, 589, 'Hole 4']
hole5 = [    389,     48, 372,         1.5, 1.5,      373, 404, 'Hole 5']
hole6 = [    133,    0.5, 0.5,         1.5, 1.5,      125, 138, 'Hole 6']
hole7 = [    496,     37, 413,         414, 484,      484, 502, 'Hole 7']
hole8 = [    415,     50, 391,         1.5, 1.5,      392, 420, 'Hole 8']
hole9 = [    320,     23, 258,         259, 303,      304, 327, 'Hole 9']

holes = [hole1, hole2, hole3, hole4, hole5, hole6, hole7, hole8, hole9]

Here I defined the main logic for choosing a club. The program attempts to maximize distance by choosing the driver for all lengths greater than the max driver distance and chooses a club with range that contains the distance to the hole otherwise. This requires the range provides by the club input to be continuous, i.e. no gaps in shot distance. A realistic requirement since one can hit a club without a full backswing to limit the distance of their shot to the maximum distance of the next most powerful club.

def stroke(distance):
    Length = abs(hlen - distance)
    if Length >= Driver_a:
        club = 'Driver'
        d = randint(Driver_a,Driver_b)
    elif Length >= Wood3_a and Length <= Wood3_b:
        club = '3-Wood'
        d = randint(Wood3_a,Wood3_b)
    elif Length >= Wood5_a and Length <= Wood5_b:
        club = '5-Wood'
        d = randint(Wood5_a,Wood5_b)
    elif Length >= Iron3_a and Length <= Iron3_b:
        club = '3-Iron'
        d = randint(Iron3_a,Iron3_b)
    elif Length >= Iron4_a and Length <= Iron4_b:
        club = '4-Iron'
        d = randint(Iron4_a,Iron4_b)
    elif Length >= Iron5_a and Length <= Iron5_b:
        club = '5-Iron'
        d = randint(Iron5_a,Iron5_b)
    elif Length >= Iron6_a and Length <= Iron6_b:
        club = '6-Iron'
        d = randint(Iron6_a,Iron6_b)
    elif Length >= Iron7_a and Length <= Iron7_b:
        club = '7-Iron'
        d = randint(Iron7_a,Iron7_b)
    elif Length >= Iron8_a and Length <= Iron8_b:
        club = '8-Iron'
        d = randint(Iron8_a,Iron8_b)
    elif Length >= Iron9_a and Length <= Iron9_b:
        club = '9-Iron'
        d = randint(Iron9_a,Iron9_b)
    elif Length >= Pwedge_a and Length <= Pwedge_b:
        club = 'P wedge'
        d = randint(Pwedge_a,Pwedge_b)
    elif Length >= Swedge_a and Length <= Swedge_b:
        club = 'S wedge'
        d = randint(Swedge_a,Swedge_b)
    elif Length >= Lwedge_a and Length <= Lwedge_b:
        club = 'L wedge'
        d = randint(Lwedge_a,Lwedge_b)        
    else : print 'stroke error'
    return club, d

Next, I define a put function that two putts for all lengths greater than 5 yards to the hole and one putt for 5 and less. I also include an option for hitting the ball directly into the hole called 'chip in'.

def putt(distance):
    Length = abs(hlen - distance)
    if Length > 5:
        club = '2 putts'
    elif Length == 0:
        club = 'chip in'
    else:
        club = '1 putt'
    return club

Here is where the strategy gets a little funky. In order to keep it simple and also prevents getting stuck in a loop of driving into water only to drop the ball at the location of the previous shot and drive into the water again, I actually backtrack, hitting the ball backward with the sand wedge and then have the code evaluate the shot again this time hopefully shooting just in front of the water so the next shot can clear it. This strategy is penalized by the rough penalty but is effective for clearing water.

def water():
    club = 'S wedge'
    d = randint(50,79)
    return club, d

This program counts the number of strokes per hole after that hole has been played. It adds the penalties for the shots in the rough and adds the penalties for hitting into the water by summing a array called water that is appended after each water shot. This takes advantage of the fact that fairway always leads to water or to the green for every hole in the course. It would have to be changed for courses that contained rough in the middle of the fairway.

def countstrokes(clubs, distances, waters):
    distances = np.array(distances)
    mask1 = distances < flen1
    mask2 = distances > grn2
    extra = sum(mask1*1)+sum(mask2*1) + sum(waters)
    if clubs[-1] == 'chip in' : strokes = len(clubs)-1+extra
    elif clubs[-1] == '2 putts' : strokes = len(clubs) +1+extra
    elif clubs[-1] == '1 putt' : strokes = len(clubs)+extra
    else : print 'strokes error'
    return strokes

After the main code runs, condition looks at the distances the ball was at during the hole and reports the condition of the ball. I ran into one issue with condition because of the way I treated hitting the ball into the water in the main program. In the program, if the ball was hit into the water, it was immediately moved back to the location of where the shot was hit. The distance was recorded after the ball is moved back so the condition of the ball cannot be ‘water’. If you hit the ball off the tee on hole 4 into the water, the program prints the distance you hit the ball and the club but the length to the hole will remain unchanged and the condition will be 'rough' since the ball is dropped at 0 distance which is in the rough. You can uncomment a print 'water' flag if you want to be told how many times the ball is hit into the water before each hole.

def condition(distances):
    conditions=[]
    for distance in distances:
        if distance >= grn1 and distance <= grn2:
            conditions.append('green')
        elif distance >= flen1 and distance <= flen2:
            conditions.append('fair')
        else:
            conditions.append('rough')
    return conditions

Here is the main part of the code which loads the holes and plays the game. After initializing some conditions the code runs 'stroke' hitting the ball towards the hole, including reverse if the hole was overshot, until either water or green is encountered. If water is encountered, it adds to a penalty counter and runs the program water and after moving the ball back to the location it was hit from. If the green is encountered, put is called and the hole is terminated. After the distances and clubs are analyzed to determine the condition of each shot and the shots are tallied.

def golf(driver_a, driver_b, wood3_a, wood3_b, wood5_a, wood5_b, iron3_a, iron3_b, iron4_a, iron4_b, iron5_a, iron5_b, iron6_a, iron6_b, iron7_a, iron7_b, iron8_a, iron8_b, iron9_a, iron9_b, pwedge_a, pwedge_b, swedge_a, swedge_b, lwedge_a, lwedge_b):
    global Driver_a, Driver_b, Wood3_a, Wood3_b, Wood5_a, Wood5_b, Iron3_a, Iron3_b, Iron4_a, Iron4_b, Iron5_a, Iron5_b, Iron6_a, Iron6_b, Iron7_a, Iron7_b, Iron8_a, Iron8_b, Iron9_a, Iron9_b, Pwedge_a, Pwedge_b, Swedge_a, Swedge_b, Lwedge_a, Lwedge_b
    Driver_a, Driver_b, Wood3_a, Wood3_b, Wood5_a, Wood5_b, Iron3_a, Iron3_b, Iron4_a, Iron4_b, Iron5_a, Iron5_b, Iron6_a, Iron6_b, Iron7_a, Iron7_b, Iron8_a, Iron8_b, Iron9_a, Iron9_b, Pwedge_a, Pwedge_b, Swedge_a, Swedge_b, Lwedge_a, Lwedge_b = driver_a, driver_b, wood3_a, wood3_b, wood5_a, wood5_b, iron3_a, iron3_b, iron4_a, iron4_b, iron5_a, iron5_b, iron6_a, iron6_b, iron7_a, iron7_b, iron8_a, iron8_b, iron9_a, iron9_b, pwedge_a, pwedge_b, swedge_a, swedge_b, lwedge_a, lwedge_b
    totals =[]
    for hole in holes:
        distance = 0
        strokes = 0
        clubs = []
        distances = []
        d1s = []
        waters=[]
        global hlen, flen1, flen2, wtr1, wtr2, grn1, grn2
        hlen, flen1, flen2, wtr1, wtr2, grn1, grn2, name = hole
        while True:
            club1, d1 = stroke(distance)
            clubs.append(club1)
            if distance > hlen:
                d1 = -d1
            distance = distance + d1
            d1s.append(d1)
            if distance >= wtr1 and distance <= wtr2:
                #print 'water'
                waters.append(1)
                distance = distance - d1
                distances.append(distance)
                club1, d1 = water()
                if distance < wtr1:
                    d1 = - d1
                distance = distance + d1
                d1s.append(d1)
                clubs.append(club1)
            distances.append(distance)
            if distance >= grn1 and distance <= grn2:
                club1 = putt(distance)
                clubs.append(club1)
                break
        strokes =  countstrokes(clubs, distances, waters)
        totals.append(strokes)
        conditions = condition(distances)
        shots = len(d1s)
        print name, ':',
        for x in xrange(0,shots):
            print '{', clubs[x], ',', d1s[x],',', conditions[x],',', hlen-distances[x], '}',
        print '{',clubs[-1], '}', '{',strokes ,'}'
    print 'Total:', sum(totals), 'shots'
    return sum(totals)

The code is run like

golf(300,330,270,299,240,269,220,239,200,219,180,199,160,179,140,159,120,139,100,119,80,99,50,79,0,49)

and the out looks like this:

Hole 1 : { Driver , 308 , fair , 93 } { P wedge , 96 , green , -3 } { 1 putt } { 3 }
Hole 2 : { 6-Iron , 166 , green , 5 } { 1 putt } { 2 }
Hole 3 : { Driver , 321 , fair , 117 } { 9-Iron , 105 , green , 12 } { 2 putts } { 4 }
Hole 4 : { Driver , 305 , rough , 553 } { S wedge , -62 , rough , 615 } { Driver , 326 , fair , 289 } { 3-Wood , 293 , green , -4 } { 1 putt } { 8 }
Hole 5 : { Driver , 323 , fair , 66 } { S wedge , 73 , green , -7 } { 2 putts } { 4 }
Hole 6 : { 8-Iron , 125 , green , 8 } { 2 putts } { 3 }
Hole 7 : { Driver , 314 , fair , 182 } { 5-Iron , 181 , green , 1 } { 1 putt } { 3 }
Hole 8 : { Driver , 324 , fair , 91 } { P wedge , 91 , green , 0 } { chip in } { 2 }
Hole 9 : { Driver , 317 , green , 3 } { 1 putt } { 2 }
Total: 31 shots

This was one the lowest score of many trials, with an absolute lowest of 26 in 100,000 runs. But still under a typical par of 34-36 even with 8 strokes on hole 4.

I'll include the code I used to find the distribution of games with the above specified clubs.

import matplotlib.pyplot as plt
class histcheck(object):

    def __init__(self):
        self = self

    def rungolf(self, n=10000):
        results=[]
        for x in xrange(0,n):
            shots = golf(300,330,270,299,240,269,220,239,200,219,180,199,160,179,140,159,120,139,100,119,80,99,50,79,0,49)
            results.append(shots)
        self.results = results

    def histo(self, n=20):
        plt.figure(figsize=(12,12))
        plt.hist(self.results, bins=(n))
        plt.title("Histogram")
        plt.xlabel("Shots")
        plt.ylabel("Frequency")
        plt.show()

Running

play = histcheck()
play.rungolf()
play.hist()

gives the following histogram Golf Histogram

and the mean and median can be found using

np.mean(play.results)
np.meadian(play.results)

a mean of about 43 and a median of 41. Not too bad for 9 holes with simple shot optimization.

It is all yours now

Go ahead and copy and tweak my program and evaluate it using my tools in order to lower the average number of shots. Let me know if there is anything scenario I didn't account for or go ahead and make a golfed version. I think that the best program would be one that returned the lowest average shots for a number of club inputs. My code isn't the best option for that but I thought I'd get the ball rolling.

Update

def water():
    if clubs[-1] =='S wedge':
        club = 'S wedge'
        d = randint(50,79)
    elif clubs[-1] !='S wedge':
        club = 'S wedge'
        d = -randint(50,79)
    else: print 'water error'
    return club, d

By changing the water logic so that it attempts to hit the ball forward by a small amount after encountering water instead of backwards if the previous club used was not the sand wedge, it improved the mean to 40.5 and the median to 39 after testing with one million runs. Minimum of 23, maximum of 135. Sometimes you get lucky, sometimes you don't. Check out the new histogram.

Histogram2

Adam

Posted 2015-09-13T18:04:12.950

Reputation: 91