Building a live leaderboard on Twitter

Jim Moffitt - Sep 1 '20 - - Dev Community

On the Twitter Insights blog, we recently shared the motivation for building a "live golf leaderboard" using the Twitter API. In that post, we describe the development of a Twitter chatbot designed to privately receive scores and Tweet out leaderboard updates. In this blog post, we will pop the hood on how we used the Twitter API, ngrok, Heroku, Python, and a Postgres database to build a Twitter chatbot experience that provided a live leaderboard of team scores at a shotgun-style golf tournament.

We developed this chatbot during a company Hack Week. To start our adventure, we created a @HackerScorer Twitter account to host the chatbot prototype. This account was used to provide 'day of show' commentary, shout-outs for scores under par, and of course live leaderboard updates. To get a sense of the look and feel, the pinned Tweet below displays the final event leaderboard.

If you want to try out the leaderboard chatbot, just send the word leaderboard via Direct Message to the @HackerScorer account. You'll receive the leaderboard graphic in response.

Like all Twitter chatbots, this prototype is driven by the Account Activity endpoint, a webhook-based API that delivers a variety of events associated with a Twitter account that has granted access. These events include when that account Tweets, gets Retweeted, receives a 'Like' to a tweet, and when it receives a Direct Message. In the case of this leaderboard "manager" app, the key thing the Account Activity endpoint offered was the ability to listen for Direct Messages with incoming golf scores.

The heart of all chatbots is the app behind the scenes that is receiving webhook events from Twitter and marshaling responses to users. When developing a chatbot, building out the web app and its underlying infrastructure will take the majority of your time. The server-side backend app is where you get to implement your ideas and, in some cases, build connections between Twitter and other platforms.

There is a lot of technical terrain to cover here. When building a chatbot there are many 'moving pieces', required 'wiring', and deployment challenges. As we set out to develop the proof-of-concept, these efforts and topics were involved:

  • Creating or identifying an existing Twitter account to host the chatbot.
  • Establishing access to the Twitter Account Activity, Direct Message, and Post Tweet endpoints.
  • Deploying a server to host the chatbot app.
  • Implementing a chatbot that listens for private Direct Messages and sends both Direct Messages and Tweets.
  • Developing code to manage and store incoming scores, and compiling leaderboard data.
  • Developing code to generate a real-time leaderboard.

To cover this span of material we split this content into four parts:

Part 1 - Getting started Go here to learn more about deploying a web app, registering it with Twitter, and establishing connectivity between Twitter and your web app.

Part 2 - Designing the chatbot experience Go here to learn more about the leaderboard chatbot's features and how they were implemented with Twitter endpoints.

Part 3 - Keeping score Go here to see code examples related to how the chatbot app stored and processed leaderboard data.

Part 4 - Generating a leaderboard graphic Go here to learn more about our adventures to build the cornerstone of our user experience, the leaderboard graphic.

Also, you can take a tour of the chatbot's code at:

GitHub logo xdevplatform / live-leaderboard

This Flask app listens for incoming scores via Twitter Direct Messages, ranks them, and Tweets the rankings.

Live Leaderboard

Twitter Premium API v1.1

Twitter Enterprise API

Using the Account Activity, Direct Message, and Post Tweet APIs, this Twitter chatbot listens for incoming scores, ranks them, and Tweets the rankings. This app supports public 'leaderboard' announcements and private data/vote submissions.

This example was written to keep golf scores, but the underlying patterns are general enough to serve many other use cases. Other events come to mind, such as soccer and bowling tournaments. Scavenger hunts are a good use case match, where updates are sent in from the field. Since this protoype is based on scores/votes being privately submitted, these patterns are the building blocks for supporting any election event.

What does the bot do?

  • Keeps track of all teams scores in real-time on Twitter
  • Tweets out updates (Leaderboards and Birdie Alerts!)
  • Foster engagement and get people talking about the event and interacting with the scorer account.

To learn more about the event this pilot was

Part 1 - Getting started

Introduction

So, you want to build a Twitter chatbot? Awesome! Now before you jump in, just know that there is a fair amount of effort needed to build the chatbot and you’ll need to be familiar with (or learn!) the following:
Webhooks. You’ll need to learn about webhooks (here’s a primer). There is some (fun) discovery to do there if you’re unfamiliar.

  • Deploying a web app. You must learn the basics of implementing and deploying a basic web app. Here are some helpful tutorials:
  • While developing your chatbot it is very handy to be able to receive Twitter webhook events with a local server (e.g. your own laptop!), as an alternative to coding and debugging on a remote, cloud-hosted server. Tunneling tools like ngrok are highly recommended.

  • The Twitter API. There are at least three Twitter API endpoints that your chatbot app should integrate.

    • Account Activity - This webhook-based (!) API sends real-time "events" for subscribed accounts. The chatbot events of interest are incoming Direct Messages with user requests and scores.
    • Direct Message - This API provides a private communication channel, along with essential user-interface support such as Welcome Messages. Be sure to set your chatbot's Twitter account to have open Direct Messages, meaning you can receive Direct Messages from Twitter users that do not follow you.
    • Post Tweet - Many chatbots support public interactions. In our case, we wanted to Tweet out updated leaderboards during the tournament and also post shoutouts to teams when they posted scores under par.

If you are unfamiliar with these endpoints, check out the documentation links above.

You’ll need an approved Twitter developer account and a Twitter App in order to integrate with the necessary Twitter endpoints. To learn more about how to establish access to the Twitter chatbot endpoints (Account Activity, Direct Message, and Post Tweet) and more Python-environment details, see the project README and code at https://github.com/twitterdev/live-leaderboard.

Starting development

So, what comes first? The server or the web app? Both come early in the prototyping process, and in our case, we built out the Flask web app relatively quickly (more details below).

Meanwhile, we figured out the tunneling necessary to enable events coming from the Twitter webhook publisher to arrive on our development laptops. We ended up using the ngrok tool and quickly had the development environment needed. You should definitely plan on setting up this ability to jumpstart development with your own local environment, before deploying on a 'real' server.

We started out with the free 'sandbox' version of the Premium Account Activity API, which supports a single webhook to be registered (details on that below). If you are building a chatbot on your own, this works well. If you are working on a team, and are developing with multiple local environments, it requires coordination to share a single webhook. Unless you upgrade to a paid tier that supports multiple webhooks, you will need to repeatedly delete and re-register your webhook.

Next, we'll take a code tour of the Flask-based web app for the live leaderboard prototype.

Building a simple Flask web app

Coming into this project, we had experience building a Ruby-based chatbot (@SnowBotDev) using the Sinatra web app framework. We wanted to use Python for this project, so we decided to build the web app using the Python equivalent, Flask. Luckily, the two frameworks are similar enough that our previous Sinatra experience helped us to get up and running quickly with Flask. Regardless of the web app framework you choose, there are three "routes" that you should plan on supporting:

  • GET /webhook - This path is used to first establish the connection between Twitter and your web app. Once established, Twitter will "challenge" your authenticated "ownership" of the web app on a regular basis. See below for more details.
    Note: While the 'webhook' path is a reasonable default, this path is up to you. If you are consuming other webhook events, this could be something like "twitter_webhook".

  • POST /webhook - The path where Twitter will send account events via the Account Activity endpoint.
    Note: the "webhook" path can be what you want. A key point is that this path needs to support both GET and POST HTTP methods.

  • GET / - The default 'home' path of your web app. Supporting this route is optional, but it gives you the chance to serve up 'home page' information about your chatbot.

When using Flask, here is a simplified look at the web app code:

app = Flask(__name__)

#generic index route
@app.route('/')
def default_route():
    return "Hello world"

@app.route("/webhook", methods=["GET"])
def twitter_crc_validation():
    #Handle CRC response here. See example code below.
    return response

@app.route("/webhook", methods=["POST"])
def event_manager():
    #Handle incoming account event here. See below for more details. 
    return "200"
Enter fullscreen mode Exit fullscreen mode

All of the chatbot's Python code is in a scorer_app.py file. When the scorer_app.py flask app starts up, execution starts with the following code that binds the web app to port 5000.

if __name__ == '__main__':
    # Bind to PORT if defined, otherwise default to 5000.
    port = int(os.getenv('PORT', 5000))
    # Logger code
    gunicorn_logger = logging.getLogger('gunicorn.error')
    app.logger.handlers = gunicorn_logger.handlers
    app.logger.setLevel(gunicorn_logger.level)
    app.run(host='0.0.0.0', port=port, debug=True)
Enter fullscreen mode Exit fullscreen mode

Deploying web app

Once you have your chatbot app up and running with your local server, it's time to deploy it on your 'production' server. In our case we were using Heroku, so a first step is connecting our Heroku app with the chatbot's GitHub code repository. Connecting a GitHub repository makes it possible to deploy code updates with the click of a button. To connect your repository and manage code deployments, go to the "Deploy" tab of your Heroku app dashboard.

Another deployment step is setting your Heroku app's 'Config Vars'. These Config Vars include your Twitter App's authentication tokens (consumer key and secret, along with the @HackerScorer access token and secret. The Config Vars also include settings needed to connect to the Heroku instance of the Postgres database (database username, password, host, URL, and name/key).

Once the chatbot code is deployed and the app's settings are configured, it is time to start testing the chatbot running on your production server by submitting scores and requesting the leaderboard.

Registering web app with Twitter

A fundamental step of deploying a Twitter chatbot is registering the chatbot with Twitter. This registration establishes the link between Twitter and your chatbot web app. When registering, you will provide the URL where webhooks should be sent, and the Twitter Account Activity API will immediately request your app's consumer secret token. This process is referred to as a Challenge-Response Check (CRC). After registration, Twitter will continue to issue CRCs frequently to confirm the security of your webhook server.

For initial development and testing, we used ngrok and registered temporary URLs associated with our laptops. Successful CRC responses need to be returned within three seconds. To help develop your CRC response code (see example code below), you can manually trigger a CRC by making a PUT request.

To help with registering our webhook URLs, we put together a Python script to help automate the process. This came in handy as we moved our webhook destination back and forth from our laptops to an instance hosted on Heroku.

Example CRC code

The following code implements CRC code for a Flash GET route (/webhook) registered with Twitter:

# The GET method for webhook should be used for the CRC check
@app.route("/webhook", methods=["GET"])
def twitter_crc_validation():

    crc = request.args['crc_token']

    validation = hmac.new(
        key=bytes(CONSUMER_SECRET, 'utf-8'),
        msg=bytes(crc, 'utf-8'),
        digestmod = hashlib.sha256
    )
    digested = base64.b64encode(validation.digest())
    response = {
        'response_token': 'sha256=' + format(str(digested)[2:-1])
    }
    print('responding to CRC call')
    return json.dumps(response)
Enter fullscreen mode Exit fullscreen mode

Putting it all together

After we had registered our webhook URL, our next step was subscribing the @HackerScorer account so we could start receiving webhook events associated with the account. We also wrote a Python script to help with this step.

Once your webhook URL is registered and you have your chatbot 'host' account subscribed, you can start implementing the design of your chatbot. Sending a Direct Message to the @HackerScorer account, and seeing that message arrive instantly in the web app was a fun 'ah-ha' moment.

At that point, you get to start handling the type of incoming account webhook events your chatbot needs to listen for, and writing code to generate responses and server-side processing. In the case of the live leaderboard chatbot, we were focused on listening for Direct Messages containing scores, having those incoming scores trigger the generation of a leaderboard graphic, and responding with a Direct Message containing the leaderboard graphic.

Next, we explore the @HackerScorer chatbot design in more detail

Part 2 - Designing the chatbot experience

Introduction

Designing the user experience is a fun part of developing your chatbot. After getting the fundamental 'plumbing' set up, this is when you get to implement the features you have dreamed up. In the case of the @HackerScorer live leaderboard proof-of-concept, we wanted these features:

  • Scorekeepers submit their golf scores with Direct Messages. There is one scorer per team, and that scorer is familiar with the required syntax to submit a team number, a hole number, and the team's score for that hole. Ideally, there will be a way to enable scorers to correct scoring mistakes.
  • When the bot receives a score, it stores it in a database, generates a real-time leaderboard graphic, and sends the graphic to the scorer via Direct Message.
  • Players are able to send a 'leaderboard' Direct Message to the chatbot and immediately receive a current leaderboard via Direct Message.
  • When a score below par is submitted, a #BirdieAlert Tweet is posted to congratulate the team.
  • Event "marshals" would know commands to make special requests, such as "Tweet leaderboard now".

Let's explore these features in more detail.

Submitting scores

Our goal was to make submitting scores easy and quick, in a format that was easy to parse. For each score, we needed to know what team was reporting, what golf hole they just completed, and the number of strokes needed to complete the hole. We came up with a simple shorthand where:

  • t# = Your team number. This doesn't change. Your team number is indicated by the hole you started on. Team 6 started on hole 6.
  • h# = Hole number you are reporting a score for
  • s# = Your score. The number of strokes.

So, say you are on team 6, you just completed hole 12 in four strokes. This information would be formatted like this: t6 h12 s4 (and the order of tokens does not matter).

Listening for scores and commands

For the @HackerScorer chatbot, the only account event we listened for were incoming Direct Messages. The following code in the scorer_app.py file is triggered every time the Account Activity API sends a webhook event related to the @HackerScorer account. This code inspects the incoming JSON and if the event is a Direct Message, it calls a handle_dm method. All other account events are ignored. If you are building a chatbot that needs to listen for other events, such as account mentions or account Tweets being liked, the if statement can be expanded to test for these other kinds of events.

# Event manager block
@app.route("/webhook", methods=["POST"])
def event_manager():

    #Match on event types that we care about. So far, just paying attention to DMs.
    if 'direct_message_events' in request.json:
        handle_dm(request.json)

    return "200"
Enter fullscreen mode Exit fullscreen mode

The handle_dm method has the simple job of checking whether:

  • The incoming Direct Message contains a score. The is_score method checks whether the Direct Message contains the scoring format described above.
  • The Direct Message is a request for having the current leaderboard sent back via Direct Message.
  • The Direct Message is an event marshal command for generating and Tweeting a real-time leaderboard.

If the incoming Direct Message is a new score, the message is parsed, and the score is written to the database (see Part 3).

Sending leaderboards

The @HackerScorer chatbot generates a leaderboard graphic and sends it via Direct Message when scores are submitted and when players request one with a Direct Message with the 'leaderboard' command. The chatbot also sends the current leaderboard when a 'Tweet leaderboard' command arrives from an event marshal.

The create_standings method does the work of retrieving all the scores from the database, loads the scores into a Panda data frame, sorts the data, calculates the over/under score, generates a new leaderboard image (see below for how that is done), and writes the image to a folder.

When the chatbot sends a leaderboard via Direct Message, or is commanded to Tweet it, the updated leaderboard image is first uploaded to Twitter. This process returns a media_id which is then referenced when the Direct Message or Tweet is posted.

Sending #BirdieAlert Tweets

The @HackerScorer chatbot was also designed to send a congratulatory Tweet whenever a team reported an under-par score. As incoming scores are processed in the handle_score method, a score with a "over under" of -1 triggers a #BirdieAlert Tweet.

When a #BirdieAlert Tweet is sent, one of four graphics is randomly uploaded to Twitter, and the Tweet is posted referencing the returned media_id. These #BirdieAlert graphics are located in a static folder within the project GitHub repository. After the repository is connected and deployed to the Heroku app, these images are available to the chatbot app by referencing the ./static path. (Fun fact: 80 #BirdieAlert Tweets were sent, so nearly 25% of scores were birdies. Teamwork works!)

A key part of implementing these chatbot features was writing the code to store scores and generating the leaderboard data. The next part of this post will discuss how the chatbot app used a database deployed on Heroku and used a Pandas data frame to process the score data.

Part 3 - Keeping score

Introduction

When we began designing the prototype we knew a primary back-end concern was keeping score. Given our tight timeline, we first considered whether we could build a stateless scoring system. We first pondered having participants submit their scores with Tweets and then making requests to a search endpoint (like the recently launched Twitter API v2 recent search endpoint) to compile and manage the scores. With this design, as score Tweets arrived, they would trigger a search request to retrieve all the event's scores and generate a new leaderboard based on that.

Submitting 18 scores for each of the 18 teams would require 324 Tweets, and that seemed like too much chatter. We also wanted to support private score submissions. So we decided to have scores submitted in private Direct Messages. So, it became obvious that we would need to have a server-side datastore of scores. We decided that a simple relational database would suffice. The proof-of-concept was being built on Heroku, which has native support for Postgres databases.

So our next steps were adding a Postgres database to our Heroku environment, designing a simple table schema, and establishing a database connection from our web app.

Let's take a tour of the code written to support keeping score with a database. All of the code below is from the scorer_app.py file, which encapsulates the entire chatbot code.

Setting up the database

First, we created two database tables with the following fields:

  • scores: team_id, hole, score, over_under, time_stamp
  • standings: team_id, total_strokes, par_score, current_hole, holes_complete

The following database details and credentials were set up as environment variables. On Heroku, these are referred to as "Config Vars" and from a code perspective are equivalent to working with a ".env" file.

DATABASE = os.getenv('DATABASE', None)
DATABASE_HOST = os.getenv('DATABASE_HOST', None)
DATABASE_USER = os.getenv('DATABASE_USER', None)
DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD', None)
Enter fullscreen mode Exit fullscreen mode

For working with our PostgreSQL database, we imported the psycopg2 Python package:

import psycopg2
Enter fullscreen mode Exit fullscreen mode

Inserting scores

Below is the code for inserting a score into the scores table. Here we make a connection to the database, execute our query (in this case an INSERT statement), and return whether the database operation was successful.

def insert_score(team_id, hole, score):
   ''' Database wrapper for storing scores. '''

   over_under = get_over_under(hole, score)

   try:
       #Create database connection.
       con = psycopg2.connect(database=DATABASE, user=DATABASE_USER, password=DATABASE_PASSWORD, host=DATABASE_HOST, port="5432")
       cur = con.cursor()
       cur.execute(f"INSERT INTO scores (time_stamp,team_id,hole,score, over_under) VALUES (NOW(),{team_id},{hole},{score}, {over_under});")
       con.commit()
       success = True
   except:
       print ("Error on INSERT, assuming duplicate!")
       success = False

   con.close()

   return success
Enter fullscreen mode Exit fullscreen mode

Supporting score corrections

Knowing that scoring mistakes would be made, we needed to enable scorers to correct scores. This was easily implemented by supporting an UPDATE query in the update_score method:

cur.execute(f"UPDATE scores SET score = {score}, over_under = {over_under} WHERE team_id = {team_id} AND hole = {hole};")
Enter fullscreen mode Exit fullscreen mode

Generating leaderboard data

OK, so now with the scores being stored in the database, it was time to write the methods to pull all that data in order to build the leaderboard. To do that we pull all the scores into a Pandas data frame using the following package:

import pandas.io.sql as psql
Enter fullscreen mode Exit fullscreen mode

With this package, loading the database scores into a data frame is as easy as calling the read_sql_query method, as the following code illustrates.

sql = "SELECT * FROM scores;"
con = psycopg2.connect(database=DATABASE, user=DATABASE_USER, password=DATABASE_PASSWORD, host=DATABASE_HOST, port="5432")
#Load recordset into dataframe.
scores_df = psql.read_sql_query(sql, con)
Enter fullscreen mode Exit fullscreen mode

Now that the scores are in a data frame, the next step was compiling each team's current score and ordering the results by ascending order. If some teams had the same score, then they were ranked in descending order of how many holes had been completed.

To do this, we create a two-dimensional team_scores array, then add an entry for each team, and calculate each team's total score, over/under score, and how many holes have been completed.

team_scores = [ [] for i in range(18)]

    for i in range(18):

        team = i + 1
        team_scores[i].append(team) #Add team number.

        score = scores_df.loc[scores_df['team_id'] == team, 'score'].sum()
        over_under = scores_df.loc[scores_df['team_id'] == team, 'over_under'].sum()
        holes_complete = scores_df.loc[scores_df['team_id'] == team, 'hole'].count()
        last_hole = get_last_hole(team, holes_complete)

        team_scores[i].append(over_under)
        team_scores[i].append(score)
        team_scores[i].append(holes_complete)
Enter fullscreen mode Exit fullscreen mode

Once that array is built, it is again put into a data frame so we can sort the data first by total score and then by the total of holes played. At this point, we also add a "+" character to positive scores as is done in the world of golf. The following code shows these steps:

#Make data frame.
df_standings = pd.DataFrame(team_scores, columns=['Team','Score','Total','Holes played'])

#Sort dataframe
df_sorted = df_standings.sort_values(by=['Score', 'Holes played'], ascending=[True, False])

#Add '+' character to positive scores.
df_sorted.loc[df_sorted.Score > 0, 'Score'] = '+' + df_sorted['Score'].astype(str)
Enter fullscreen mode Exit fullscreen mode

These steps didn't take long and soon we were storing incoming scores, sorting the results, and generating a data frame ready for publishing. Next, we will describe the coding adventure of building a nice looking leaderboard image in real-time.

Part 4 - Generating a leaderboard graphic

With our data frame now sorted by total score (lowest to highest) and a team’s cumulative over/under calculated alongside it, we had everything needed for the leaderboard. The next step was finding a way to share it on Twitter.

Exploring the options

Neither of us had any experience building a graphic in Python, so we started by exploring a few different approaches:

  • Plain text: couldn’t we just render the leaderboard as text? This sounded simple enough, plus text is at the core of Twitter. But going from a DataFrame to plain text felt unnatural and we quickly bumped up against the 280 character limit it. This approach also lacked visual appeal.
  • HTML table: with the built-in to_html() method for DataFrames, we explored rendering the leaderboard as an html table. This was more familiar and would make styling easier; however, we hit some roadblocks exporting it to an image.
    The command-line utility, webkit2png, came up as a possible solution, but it was a challenge to integrate it within our app.
    Example:

  • Matplotlib: the internet (okay, StackOverflow) kept pointing to the python visualization library, Matplotlib. We had no experience with this library, so the learning curve was steep but it immediately showed promise.

Example:

Despite our unfamiliarity with Matplotlib and short runway (this was a Hack Week project after all), we landed on using Matplotlib to display the leaderboard table. While we had some formatting issues to iron out, at least we had a reliable way to display and export the leaderboard.

Using Matplotlib (step by step)

Matplotlib is typically used for creating plots for data visualization, with tables as added context below the plot. So it requires some massaging in order to just display a table. We started by creating a square figure window with a size of (5,5) to contain our leaderboard table. Then hid the x and y-axis and the overall frame for the subplot.

fig, ax = plt.subplots(figsize=(5,5))
# Hide axes
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
# Hide frame
ax.set_frame_on(False)
Enter fullscreen mode Exit fullscreen mode

Next, we overrode the default index of the data frame and replaced it with a custom index, representing the “place” of each team (e.g., 1st, 2nd, 3rd).

# Set index to place list
df["my_index"] = ['1st','2nd','3rd','4th','5th','6th','7th','8th','9th','10th','11th','12th','13th','14th','15th','16th','17th','18th']
df.set_index("my_index",inplace = True)
Enter fullscreen mode Exit fullscreen mode

From there, it was time to plot the table. The most time consuming (and frustrating) part was adjusting the positioning and formatting of the table. Fortunately, the table module supports several optional arguments that control how it’s rendered:

  • loc - Position of the cell with respect to ax
  • cellLoc - Centers the text within a cell
  • colWidths - Set the width of table columns
  • colColours - Sets column header colors
  • cellColours - Sets background color of cells
  • bbox - Adjust where table sits within the figure ([left, bottom, width, height])

After a bit of trial and error, we landed on the following values to render our leaderboard:

# Plot the table
tab = table(ax, df, loc='center', cellLoc='center', colWidths=[0.17, 0.17, 0.17, 0.28], colColours=header_colors, cellColours=cell_colors, bbox=[0,0,1,1.05])
Enter fullscreen mode Exit fullscreen mode

Note: Twitter will auto-crop images attached to a Tweet based on saliency. So we had to run multiple tests to see how our generated leaderboard image would be cropped when attached to a Tweet.

Lastly, we just needed to save the leaderboard using the built-in savefig method:

# Save the result
if not os.path.exists('./img'):
    os.makedirs('./img')
plt.savefig('./img/scores.png')
print 'Saved new image'
Enter fullscreen mode Exit fullscreen mode

Final results

Finally, we had generated a clean leaderboard image that rendered well on Twitter! Here’s the full method for reference (from the scorer_app.py file) :

def create_standings_image(df):
    header_colors = ['#7ed4ff'] * 4
    # For table row striping
    cell_colors = [(['#ffffff'] * 4 if x % 2 == 0 else ['#D3D3D3'] * 4)
                   for x in range(0, df.shape[0])]

    # Set fig size
    (fig, ax) = plt.subplots(figsize=(5, 5))

    # Hide axes
    ax.xaxis.set_visible(False)
    ax.yaxis.set_visible(False)

    # Hide frame
    ax.set_frame_on(False)

    # Set index to place list
    df["my_index"] = ['1st','2nd','3rd','4th','5th','6th','7th','8th','9th',      '10th', '11th','12th','13th','14th','15th','16th','17th','18th']

    df.set_index('my_index', inplace=True)

    # Plot table
    tab = table(
        ax,
        df,
        loc='center',
        cellLoc='center',
        colWidths=[0.17, 0.17, 0.17, 0.28],
        colColours=header_colors,
        cellColours=cell_colors,
        bbox=[0,0,1,1.05]
        )

    # Set font manually
    tab.auto_set_font_size(False)
    tab.set_fontsize(11)

    # Save the result
    if not os.path.exists('./img'):
        os.makedirs('./img')
    plt.savefig('./img/scores.png')
Enter fullscreen mode Exit fullscreen mode

And the final result – a leaderboard that could be regenerated and saved off as a png file any time a user-submitted a new score, or requested to see the latest standings:

While there’s probably more ways and alternatives to generating a leaderboard, we were happy with the results given the confines of Hack Week and the need to have this ready in time for the tournament. Most importantly, this approach worked great for us throughout the tournament and the golfers really enjoyed the live leaderboard experience as a whole!

Conclusion

The chatbot app was developed in a Hack Week-like sprint, so several shortcuts were taken to get it ready in time for the tournament. In fact, the golf tournament was on the Thursday of Hack Week! We were still debugging and deploying code the night before the tournament :)

If we were building a version 2, we’d clean up the code to be more modular and make certain event details configurable – like loading the course's par in from a 'config' file. Here are some other improvements we’d make:

  • Add both eagle and hole-in-one alerts (still surprised there was an ace on the course!)
  • If the scorers are known in advance, we could infer the team that is sending a score, and not require the team number to be submitted (t8 h5 s4 => h5 s4)
  • Enable other users to access admin-level commands, such as the command to publicly Tweet the leaderboard
  • Support an admin-level command to clear the database (useful just before play begins)

We also learned some valuable lessons along the way. The spotty cell service out on course was a risk we didn’t even consider ahead of time! We also started out using the free tier of the Account Activity API which only supports one webhook. This was problematic because we were constantly deleting and re-registering webhooks to support local testing and our prod app.

All things considered, the app worked great and the golfers really enjoyed the interactive experience. Being able to see where teams ranked throughout the event, definitely fueled the competition and added to the event as a whole. While this app was tailored to golf, there are other applications such as scavenger hunts, bowling tournaments, and realtime polling with private votes and public results that it could be modified to support.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .