How to Build a Picture Scavenger Hunt Using Twilio MMS, Twilio and Sinatra

September 24, 2014
Written by

blog-feature

When I get together with my siblings (one younger sister, and two little brothers- 12 and 9) I tend to come up with crazy ideas. A couple years ago in Northern California I was visiting my little brothers and had the grand idea to create a scavenger hunt for them when they woke up in the morning. Of course, this blew their mind and we had a great time. So, when I ended up visiting for an extended period a couple months ago, I decided it was time to reprise the scavenger hunt– MMS Style.

Hunt Photo
Left: Simon showing one of the picture clues. Middle: Brenden trying to find a keyword. Right: Jacob pondering the meaning of the clue he just received.

 

How will the game work?

There are lots of ways to set up a scavenger hunt. You could take pictures of locations, and then ask people to send pictures back at those locations. But then you have to figure out how to verify the location, which would be tricky. In this case, the way I verified that the player found the location was to plant a keyword somewhere in the vicinity of the location.

Image of Keyword

Most of the keywords I wrote were 80’s bands since I wanted keywords that the boys were unlikely to know or guess :) If you are building this for a younger audience, definitely be sure to make the keywords easier.

Once the player finds the keyword, they text it back to the number, which checks it and then sends the next clue. Here is a diagram of the entire interaction:

diagram.png

If you’d like to interact with the demo app you can text 619.555.5555. Seems pretty simple right? Now let’s build the dang thing.

Setting Up

It might make sense to download all of the sample code and follow along in your IDE of choice. If you’d like to checkout the code you can get it here: Github

Before we can start building an interactive scavenger hunt, we have a little bit of work to do. We need to sign up for a Twilio account for starters, and then obtain an MMS-enabled phone number to use with our application. Let’s get that taken care of so we can move on to the code.

Sign up for a Twilio account

If you don’t already have one, sign up for a Twilio account now. During the signup process, you’ll be prompted to search for and buy a number. You can go ahead and just take the default number for now.

After you take the default number, you might want to play around with it and send yourself a few text messages, maybe receive a phone call. Once you’re ready to get to the MMS action, click the button to go to your account.

Buy an MMS-enabled phone number

On your account dashboard, you’ll see your account SID and auth token near the top of the screen. These are like your username and password for the Twilio API – you’ll need these values later on to make authenticated requests to the Twilio API.

But for now, we need to either buy or use an existing phone number that can send MMS messages. Click on “Numbers” in the top nav. In your list of phone numbers, there are icons indicating the capabilities of each number. If you already have a number with MMS enabled, then you’re all set!

If you still need a number with MMS, click the “Buy Number” button. In the resulting dialogue, search for a number that has MMS capabilities.

In the resulting list, choose the option to buy one of these MMS-enabled phone numbers. Great! Now that we have an MMS-capable number, we’re ready to write code that sends and receives MMS messages. We’ll start sending our first pictures by using a high level helper library that makes it easier to work with the Twilio API.

Set up our project

Before we begin, we assume you have Ruby and Ruby Gems installed. If you’re on a Mac, these should be installed already – if you’re on Windows, you might consider this installer. If you’re on Linux, you probably know what you’re doing ;) – but here’s a solid guide to getting started through apt-get on Ubuntu.

In a terminal window, create a new folder called “scavenger-demo”. Change into this directory – we’ll put all our application code here. Next, let’s install some Ruby gems (in the github directory I use bundler and a gemfile)

sudo] gem install twilio-ruby sinatra shotgun
[sudo] gem install haml sanitize

Next, let’s create the files and directory structures we will need:

touch app.rb
mkdir views
mkdir public
touch views/index.erb

For this application, we will be using the lightweight Ruby web framework Sinatra. We’ll use the Twilio Ruby gem to make interacting with the Twilio APIs a bit easier – we’ll see how as the tutorial goes on.

Now that our project is all set up, let’s write some logic to manage the scavenger hunt.

Server-side game mechanics

I chose to use Sinatra as the scavenger hunt Server. Sinatra allows us to write a simple one-page Ruby file that runs the whole shebang. In this case our server only needs to do two things:

  • Send and Receive texts and parse the Body
  • Maintain a database of Players to track clues completed

First let’s setup our Sinatra file with all of the requirements. Github ➭

require "bundler/setup"
require "sinatra"
require "data_mapper"
require "twilio-ruby"
require "sanitize"
require "haml"

# Load up our necessary requirements before each function
before do
  @game_commander = ENV['SCAVENGER_NUMBER']
  @client = Twilio::REST::Client.new ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN']
end

Here we include our requirements and initialize a Twilio client that will handle all of our Message sending and receiving.

Next we need to create a database to store some players in, and in this case we’re going to use Data Mapper. I like using Data Mapper (DM) for small projects like this since I don’t need to write any migrations and I can define the model once in ruby and datamapper will create the necessary layout in the datastore. It also works with a bunch of different datastores, in this case we’ll be using Postgres.

# Using DataMapper for our psql data manager
DataMapper::Logger.new(STDOUT, :debug)
DataMapper::setup(:default, ENV['DATABASE_URL'] || 'postgres://localhost/YOUR_DB_NAME')

class Player
  include DataMapper::Resource

  property :id, Serial
  property :phone_number, String, :length => 30, :required => true
  property :name, String
  property :current, String
  property :status, Enum[ :new, :naming, :hunting], :default => :new
  property :missed, Integer, :default => 0
  property :complete, Integer, :default => 0
  property :remaining, Object

end

DataMapper.finalize
DataMapper.auto_upgrade!

The only thing we actually need from the player to start is a phone number, so we define it as required. This will prevent us from creating a model without a phone number. Also notice that status is an enum, this allows us to quickly access this property by name even though it’s stored as an int in the DB. The Player.status needs to be 0 or ‘new’ when we begin.

Lastly we call two magical methods on datamapper, finalize and auto_upgrade!. Finalize checks the models for validity and initializes the necessary properties, while auto_upgrade! runs any migrations needed to update the datastore while not removing any existing entries. Great, so we have somewhere to store the players, now let’s build the interface for the players.

Sending picture clues and checking their validity

First we need to define a set of clues that the user will receive as the hunt progresses. Let’s store these inside a Ruby Hash for easy access throughout the code.

$CLUES = {
  "clue1" => {
    "keyword" => 'boygeorge',
    "riddle" => 'Humdinger of a clue',
    "url" => 'https://dl.dropboxusercontent.com/u/123971/scavenger-hunt/clue01.jpg'
  },
  "clue2" => {
    "keyword" => 'scumbucket',
    "riddle" => 'Let this clue float in your head for a bit.',
    "url" => 'https://dl.dropboxusercontent.com/u/123971/scavenger-hunt/clue02.jpg'
  },
}

The riddle will accompany the image so that you can give your hunters a little more context for what they might be looking for… in my case some of the clues were pretty abstract so it was important to include this. You could drop the riddle and just send pictures however.

Init the player and kick-off the hunt!

When I kicked off my scavenger hunt I got all the kids in a room and told them to type the number into their messaging app but not send anything. Then on my mark they all texted “let’s hunt!” to the number I gave them. When they texted what actually ended up happening is my Twilio number made a request to my master route which then kicked off the game.

The first thing it did was create our players in the database and then prepare to listen for incoming texts.

get '/scavenger/?' do
  # Decide what do based on status and body
  @phone_number = Sanitize.clean(params[:From])
  @body = params[:Body].downcase

  # Find the player associated with this number if there is one
  @player = Player.first(:phone_number => @phone_number)

  # if the player doesn't exist create a new player.
  if @player.nil?
    @player = createPlayer(@phone_number)
  end

  # more code here ...
end

At the beginning of our master route ‘scavenger/’ we’ll throw the phone_number into a global var so that other methods can access it. Next the router checks to see if this user exists by the phone_number, and if she doesn’t exist it creates a new one.

def createPlayer(phone_number)
  clues = ($CLUES.keys).join(',')
  player = Player.create(
    :phone_number => phone_number,
    :remaining => clues,
  )
  player.save
  return player
end

Now that we have a player we can send them their first clue.

Send a picture clue

First we need to select a clue. When we created the user we also stored an array of clue keys (‘clue1’, ‘clue2’, etc) under their profile that shows us which clues are remaining to be sent. Now all our program needs to do is choose one and send it.

 # more code here...
  sendNextClue(@player)
end

def sendNextClue(user)
  remaining = (user.remaining).split(',')
  next_clue = remaining[rand(remaining.length)]

  clue = $CLUES[next_clue]
  sendPicture(@phone_number, clue['riddle'], clue['url'])

  @player.update(:current => next_clue)
end

Aside from selecting a random clue, this snippet also stores the current clue that the player is hunting and calls sendPicture():

def sendPicture(to, riddle, mediaUrl)
  message = @client.account.messages.create(
    :from => ENV['SCAVENGER_NUMBER'],
    :to => @phone_number,
    :body => riddle,
    :media_url => mediaUrl,
  ) 
  puts message.to
end

Notice we’re using our handy global objects @client and @phone_number. By using the Twilio Ruby library we can send an MMS by using the same messages.create() method we always used to send SMS, we just include a media_url parameter and it’s a picture message  (as long as the phone number is MMS enabled).

Okay, to recap our user has been created in the database, along with some state about where in the game she is and fired off the first picture clue. As far as we know the hunt is on and we should probably setup something to listen for those incoming texts.

Finding clues!

If you remember, the way this particular scavenger hunt works is by sending the user a picture clue and then waiting for them to send a keyword back. This keyword is hidden somewhere around where the picture was taken. This could be on a conference table, written on the back of playing cards, or tattooed on the belly of a miniature pig. However it’s delivered, we need a way to verify that the keyword sent is correct.

Next, inside of our master router ‘/scavenger/?’ we need to add these lines:

# check what the current clue is
      current = @player.current
      clue = $CLUES[current]

      # Turn the remaining object into a proper array, to remove
      # the correct clue from it later.
      remaining = (@player.remaining).split(',')

      if @body == clue['keyword']

        # Score this point
        complete = @player.complete++

        # Remove the clue that was just completed
        remaining.delete(current)

        # UPDATE THE player
        @player.update(:complete => complete, :remaining => remaining.join(','))

        # If the player has no more clues, they are done. Send a nice wrap-up message.
        if remaining.length == 0
          output = "Congratulations #{@player.name}! You've finished the game and found #{@player.complete} clues! Your fastest time was #{@minutes}, which is pretty good! Now just wait for the others to finish and a special rewards ceremony."
        else
        # Other wise send the next clue
          output = "Well done #{@player.name}! You've just found a treasure! Now here's the next clue!"

          # Get next clue and send it.
          sendNextClue(@player)
        end

      else
        # Player missed one, increment
        missed = @player.missed++
        @player.update(:missed => missed)

        output = "That's not completely right (in fact it's wrong). Here's another clue, see if you can find it."

        # Get next clue and send it.
        sendNextClue(@player)

You can view the master route in it’s entirety here: Github ➭.

Okay we are mostly done with the heavy lifting code wise, now we just need to connect it all to Twilio and we’re ready to hunt.

Connecting to Twilio

Now that we have our master route, we need to tell Twilio to send all of our messaging traffic to that endpoint. Once you login to Twilio go to your numbers portal. Once you’re there you can click on the number you provisioned for the scavenger hunt and point it to your server. It should look like this:

Screen Shot 2014-09-22 at 7.20.07 PM.png

 Great you’re all ready! Now all you have to do is arguably the hardest part– build the actual scavenger hunt!

Building the Arena

If you’re me and you’re building this for some exceptionally clever young men, you should probably take the weirdest pictures possible and hide them in the hardest spots imaginable.

blog-clues
Left: Brenden looking for a clue hidden inside the head of a hose attachment. Right: one of the actual picture clues.

But it might be a better approach to take pictures of obvious landmarks, with clear instructions. That way your participants are guaranteed to enjoy themselves.

Happy Hunting Compadres

Now you are competently armed with the tools to write an interactive scavenger hunt using Twilio and Sinatra. Feel free to tweak the experience to your liking, in fact you can checkout a more “game-ified” version on a branch of the repo I shared earlier, including injuries, fastest hunter and a console dashboard. Some other improvements would be:

  • 2-factor auth
  • Picture submission in place of keywords
  • A live dashboard of all the teams/players progress
  • Virtual “pitfalls” that players can use on one another

I’ve always loved scavenger hunts, and I would love to see a whole new generation of hunts arise with all of the technology we developers have at our fingertips. If you have ideas about the next great MMS hunt, or you just want to wax poetic about the illustrious brilliance and inevitable demise of Steve Irwin, shoot me an email or find me on twitter.

Until next time, happy hunting!