Skip to main content
  1. Posts/

Day 85 - Beholder on Your Phone pt3

OldDays sms-beholder ruby

Test-drive porting a game, they said.
Test-drive porting a game, they said.

In which we continue to port a beholder dating sim to text messages.

Decisions, Decisions
#

This isn’t much of a game without interesting choices, so I tackled that bit next:

spec/sms_beholder_spec.rb
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
  it 'presents selections to player' do
    get '/', From: 'Dude8', SkipToState: 'D1S108'

    expect(last_response.body).to include("what is the solution?\n1) Six\n2) Sixteen\n3) I wasn't paying attention")
  end

  it 'respects selections from player' do
    get '/', From: 'Dude8', SkipToState: 'D1S108'
    get '/', From: 'Dude8', Body: '1'

    expect(last_response.body).to include('Wrong. You haven\'t been paying attention, have you?')
  end

  it 'rejects non-numeric selections from player' do
    get '/', From: 'Dude9', SkipToState: 'D1S108'
    get '/', From: 'Dude9', Body: 'Dan'

    expect(last_response.body).to include("what is the solution?\n1) Six\n2) Sixteen\n3) I wasn't paying attention")
  end

  it 'rejects invalid selections from player' do
    get '/', From: 'Dude', SkipToState: 'D1S108'
    get '/', From: 'Dude', Body: '4'

    expect(last_response.body).to include("what is the solution?\n1) Six\n2) Sixteen\n3) I wasn't paying attention")
  end

It may change in the future, but numeric choices seemed the best initial interface. Implementing the selections changed the engine code significantly.

First we have to change states based on the player’s input:

sms_beholder.rb
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    ## State transition

    if params.key?('SkipToState')
      state = params['SkipToState']
    elsif GameStates.states[state].key?('selection')
      # handle using the input to choose selection
      if (index = body.to_i) != 0
        selections = GameStates.states[state]['selection'].to_a
        if selections.length >= index
          state = selections[index - 1].last
        else
          puts "selections.length #{selections.length} index #{index}"
          puts "Player selected #{body} but selections are #{selections}"
        end
      else
        puts "Player gave non-numeric selection: #{body}"
      end
    elsif GameStates.states[state].key?('next')
      state = GameStates.states[state]['next']
    else
      puts "Not sure where to go from state: #{GameStates.states[state]}"
    end

This was initially a bit of a struggle, as can be seen with the debugging that I left in. A big result was the GameStates module replacing the awkward global I had before:

sms_beholder.rb
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  # The set of game states. After being loaded from a JSON file, should never change.
  module GameStates
    @states = {}

    def self.load
      @states = JSON.parse(File.open('script.json', 'r').read)
    end

    def self.states
      @states
    end

    def self.story(state)
      if @states[state] && @states[state].key?('story')
        String.new(@states[state]['story'])
      else
        ''
      end
    end

    def self.selections(state)
      response = ''
      if @states[state] && @states[state].key?('selection')
        @states[state]['selection'].keys.each_with_index do |selection, index|
          response += "\n#{index + 1}) #{selection}"
        end
      end
      response
    end
  end

I’ve been migrating the appropriate logic into it, so that (for example) creating the output with story text and selections is as simple as:

sms_beholder.rb
96
97
98
99
  def process_output(player)
    state = player['state']

    response = GameStates.story(state) + GameStates.selections(state)

Go For Broke
#

What to do next? I know one way to find out. Spoiler alert.

spec/sms_beholder_spec.rb
86
87
88
89
90
91
92
93
94
95
96
97
  it 'can be played all the way through' do
    get '/', From: 'Dude'
    Timeout::timeout(10) do
      until last_response.body.include?('And you won\'t be alone...') ||
        last_response.body.include?('Royalty does not concern itself with common dances.') ||
        last_response.body.include?('True power is slow dancing with someone who could beat you senseless.') ||
        last_response.body.include?('Thirst for knowledge. Hunger for power. No feast is fine enough.') do
        get '/', From: 'Dude', Body: ['1','2','3'].sample
        puts last_response.body
      end
    end
  end

(The .sample is to avoid spinning forever in the narrative loops that exist. Not elegant, but simple.)

We wind up spinning in the same state anyway, as I (kind of) expected.

Player Dude has data {"state"=>"M00"}
Processing input for player {"state"=>"M00"} params {"From"=>"Dude", "Body"=>"2"}
Not sure where to go from state: {"beholder"=>"mox", "beholderPos"=>"center", "beholderExp"=>"neutral", "bg"=>"SoccerField", "namePlate"=>":name:", "conditionalSelection"=>{"condition"=>{"mox-kick"=>"known"}, "constants"=
Job's log exceeded limit of 4194304 bytes.

So I guess we need to start handling conditionalSelection. Tomorrow.


Useful Stuff
#


More to come
More to come

Day 85 code