Skip to main content
  1. Posts/

New Day 2 - Beholder on Your Phone pt7

NewDays sms-beholder ruby GitLab

Global Beholder Jam
Global Beholder Jam

Pushing to a MVP of sorts, while many others do the same.

Beholder on Your Phone pt7
#

At the Jam
#

This year the Global Game Jam was scheduled at the beginning of February; so since I had recently been gifted some additional free time, I decided to take a road trip over to Cleveland, Ohio to help out at their site. While the attendees made a bunch of cool games, we made sure they were well-fed and offered help and advice when needed. In the in-between times, I progressed further on my SMS Beholder High project, getting close to a final form.

Answering the Phone
#

It had been a while since my last foray into SMS, so I was eager to make sure I could still leverage Twilio easily. After a bit of trial and error with their Ruby API, I realized I only needed to use it if I wanted to start a SMS conversation. Since I only wanted to respond, I could stick to using Sinatra and point Twilio to my simple web server:

Twilio messaging setup for the number
Twilio messaging setup for the number

Luckily their TwiML™️ schema includes a way to reference MMS media by URL, so I had pictures going to phones pretty quickly:

sms_beholder.rb
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
  post '/sms' do
    source = params['From']

    if player_data.key?(source)
      puts "Player #{source} has data #{player_data[source]}"
      player_data[source]['old_state'] = player_data[source]['state']
      player_data[source] = process_misc(params, player_data[source])
      player_data[source] = process_input(params, player_data[source])
    else
      puts "Player #{source} is a new player!"
      player_data[source] = {}
      player_data[source]['old_state'] = ''
      player_data[source]['state'] = params['SkipToState'] || 'SPLASH'
      player_data[source] = process_misc(params, player_data[source])
    end

    response_body = process_output(player_data[source])
    response_old_state = player_data[source]['old_state']
    response_state = player_data[source]['state']
    response = '<?xml version="1.0" encoding="UTF-8"?>' \
               '<Response>' \
               '    <Message>' \
               "       <Body>#{response_body}</Body>"
    unless GameStates.same_image?(response_old_state, response_state)
      puts "New image! #{response_state}"
      response += '       <Media>https://301days.com/assets/beholder_high/' \
                  "#{response_state}.png</Media>"
    end
    response += '    </Message>' \
                '</Response>'

    puts response
    response
  end

Expediency
#

Jumping over fences
Jumping over fences

Yes, I did put all of the images in a handy directory of this site, which seemed like the easiest thing to do. And yes, I uploaded them by hand instead of adding it to the CI. And yes, the XML is assembled by hand; it’s simple enough that I didn’t see the need to call Twilio’s methods to assemble it, so I get away with one fewer dependency.

Speaking of expediency, SMS is kind of slow and expensive, so how can we optimize the interactions?

Duplicate images
#

Multiple messages in a row with the same image
Multiple messages in a row with the same image

The first thing I did was to prevent the same image from being sent twice in a row. You may have noticed the call to GameStates.same_image? above. My initial implementation of that method was a little kludgy, since a given state didn’t necessarily include all of the image parameters, but it worked:

sms_beholder.rb
120
121
122
123
124
125
126
127
128
  def self.same_image?(state1, state2)
    puts "Comparing states #{state1} and #{state2}..."
    return false if @states[state1].nil? || @states[state2].nil?

    @states[state1].fetch('bg', nil) == @states[state2].fetch('bg', nil) &&
      @states[state1].fetch('beholder', nil) == @states[state2].fetch('beholder', nil) &&
      @states[state1].fetch('beholderPos', nil) == @states[state2].fetch('beholderPos', nil) &&
      @states[state1].fetch('beholderExp', nil) == @states[state2].fetch('beholderExp', nil)
  end

Too much SMS 相槌
#

Extra player messages necessary to proceed
Extra player messages necessary to proceed

While testing, sending a new message to trigger each state got tiring quickly. If there’s no response required from the player, why not just send the next piece of text?

sms_beholder.rb
130
131
132
133
  def self.needs_input?(state)
    @states[state].key?('textEntry') || @states[state].key?('selection') ||
      @states[state].key?('conditionalSelection')
  end
sms_beholder.rb
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
  until GameStates.needs_input?(player_data[source]['state'])
    sleep(1)
    player_data[source]['old_state'] = player_data[source]['state']
    player_data[source] = process_misc(params, player_data[source])
    player_data[source] = process_input(params, player_data[source])
    response_body = process_output(player_data[source])
    response_old_state = player_data[source]['old_state']
    response_state = player_data[source]['state']
    response += '    <Message>' \
                "       <Body>#{response_body}</Body>"
    unless GameStates.same_image?(response_old_state, response_state)
      puts "New image! #{response_state}"
      response += '       <Media>https://301days.com/assets/beholder_high/' \
                  "#{response_state}.png</Media>"
    end
    response += '    </Message>'
  end
  response += '</Response>'

Including multiple Messages in a single Response appears to work fine, until we run into SMS’s notorious unreliability:

Messages being delivered out-of-order
Messages being delivered out-of-order

Even including multiple messages in the same XML payload, they are delivered in apparently random order, subject to the vagaries of the path from Twilio to my phone. Multiple messages at once is a no-go.

Compromise
#

If I could really only send one message, at least I could include multiple game states where it made sense. Until we exceed 300 characters, or need input, or have a new image to show, we just keep adding to the message body:

sms_beholder.rb
258
259
260
261
262
263
264
265
266
  while response_body.length < 300 &&
        !GameStates.needs_input?(player_data[source]['state']) &&
        GameStates.same_image?(image_state,
                                GameStates.next(player_data[source]['state'], player_data[source]))
    player_data[source]['prev_state'] = player_data[source]['state']
    player_data[source] = process_misc(params, player_data[source])
    player_data[source] = process_input(params, player_data[source])
    response_body += "\n" + process_output(player_data[source])
  end

Not pretty, but it worked. I may do some restructuring around this way of handling the messages, and possibly convert to a more serverless approach to reduce infrastructure cost. But for now, I finally have a phone number to share with friends so they can play.


More to come
More to come

sms-beholder New Day 2 code