301 Days (redux)

Another metric leap year of gamedev experiments and such

Jul 29, 2017 - OldDays sms-beholder

Day 85 - Beholder on Your Phone pt3

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

Day 85 code