301 Days (redux)

Another metric leap year of gamedev experiments and such

New Day 2 - Beholder on Your Phone pt7

Mar 29, 2023 - 5 minute read - NewDayssms-beholder

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

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 GitLab
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

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 GitLab
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 GitLab
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 GitLab
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 GitLab
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

sms-beholder New Day 2 code