301 Days

A year of gamedev experiments.

Day 86 - Beholder on Your Phone Pt4

| Comments

In which this is already taking longer than I thought it would.

ConditionalSelection

When we quit yesterday, I promised to tackle conditionalSelection, since that’s what my “play the whole game” test case is currently choking on.

So what does that look like in the script?

script.jsonlink
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
   "M00": {
        "beholder": "mox",
        "beholderPos": "center",
        "beholderExp": "neutral",
        "bg": "SoccerField",
        "namePlate": ":name:",
        "conditionalSelection": {
            "condition": {
                "mox-kick": "known"
            },
            "constants": {
                "How do you like soccer?": "M08",
                "Do you like dancing?": "M08"
            },
            "ifConditionTrue": {
                "So I was wondering if you could show me that killer bicycle kick sometime...": "M01"
            }
        }
    },

There are some selections which are always displayed (the constants), so we’ll test that first.

Constants

The constant selections are easy enough to test:

spec/sms_beholder_spec.rblink
86
87
88
89
90
  it 'shows constant selections from conditional selection set' do
    get '/', From: 'Dude', SkipToState: 'M00'

    expect(last_response.body).to include("1) How do you like soccer?\n2) Do you like dancing?")
  end

And pretty easy to implement:

sms_beholder.rblink
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
  if @states[state] && @states[state].key?('conditionalSelection')
    @states[state]['conditionalSelection']['constants'].keys.each_with_index do |selection, index|
      response += "\n#{index + 1}) #{selection}"
    end
  end
  response
end

Not Constants

We can add these tests:

spec/sms_beholder_spec.rblink
92
93
94
95
96
97
98
99
100
101
102
103
  it 'hides conditional selection without condition' do
    get '/', From: 'Dude', SkipToState: 'M00'

    expect(last_response.body).not_to include("that killer bicycle kick")
  end

  it 'hides conditional selection with failed condition' do
    get '/', From: 'Dude', SkipToState: 'M00', SetKey: 'mox-kick', SetKeyValue: 'unknown'

    expect(last_response.body).to include("1) How do you like soccer?\n2) Do you like dancing?")
    expect(last_response.body).not_to include("that killer bicycle kick")
  end

And get some nice false positives because the non-constant selections are being ignored. But that’ll catch us later if we start showing selections that we shouldn’t.

So now:

spec/sms_beholder_spec.rblink
105
106
107
108
109
110
  it 'shows conditional selection with passed condition' do
    get '/', From: 'Dude', SkipToState: 'M00', SetKey: 'mox-kick', SetKeyValue: 'known'

    expect(last_response.body).to include("1) How do you like soccer?\n2) Do you like dancing?")
    expect(last_response.body).to include("3) So I was wondering if you could show me that killer bicycle kick")
  end

can be satisfied with this in the selections method:

sms_beholder.rblink
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
    conditions_met = true
    @states[state]['conditionalSelection']['condition'].each_pair do | key, value |
      puts "Checking player #{player} for key #{key} value #{value}"
      if player[key] != value
        puts "nope"
        conditions_met = false
      else
        puts "yep"
      end
    end

    if conditions_met
      @states[state]['conditionalSelection']['ifConditionTrue'].keys.each do |selection|
        response += "\n#{curr_selection}) #{selection}"
        curr_selection += 1
      end
    end

Selections refactor

Having shown the conditional selection, can we choose it? Not yet. We could add a completely separate check to see if a selection is valid according to the state of the player, but that would be needlessly repetitive. We could save and parse through the selection string, but that seems silly. Let’s just modify the selections method to return an array of selections.

sms_beholder.rblink
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    def self.selections(state, player)
      selections = []
      if @states[state] && @states[state].key?('selection')
        @states[state]['selection'].keys.each do |selection|
          selections << selection
        end
      end
      if @states[state] && @states[state].key?('conditionalSelection')
        @states[state]['conditionalSelection']['constants'].keys.each do |selection|
          selections << selection
        end

        conditions_met = true
        @states[state]['conditionalSelection']['condition'].each_pair do | key, value |
          puts "Checking player #{player} for key #{key} value #{value}"
          if player[key] != value
            puts "nope"
            conditions_met = false
          else
            puts "yep"
          end
        end

        if conditions_met
          @states[state]['conditionalSelection']['ifConditionTrue'].keys.each do |selection|
            selections << selection
          end
        end

      end

      selections
    end
  end

Just a little mod to the way we consume the result, and we’re good.

sms_beholder.rblink
125
126
127
128
129
130
131
  def process_output(player)
    state = player['state']

    response = GameStates.story(state)
    GameStates.selections(state, player).each_with_index do |selection, index|
      response += "\n#{index + 1}) #{selection}"
    end

Being relatively well-covered by unit tests, we can be confident in these refactors as long as the tests are still passing.

Choosing Conditional Selections

Now we’re ready for some tests.

spec/sms_beholder_spec.rblink
112
113
114
115
116
117
  it 'allows conditional selection with passed condition' do
    get '/', From: 'Dude', SkipToState: 'M00', SetKey: 'mox-kick', SetKeyValue: 'known'
    get '/', From: 'Dude', Body: '3'

    expect(last_response.body).to include("Do you think you can keep up with me?")
  end

With a little more refactoring of the selections method to include the actual destination state (which I’ll leave out in the interest of brevity (too late)), we can satisy this one easily:

sms_beholder.rblink
98
99
100
101
102
103
104
105
106
    if params.key?('SkipToState')
      state = params['SkipToState']
    elsif GameStates.states[state].key?('selection') || GameStates.states[state].key?('conditionalSelection')
      # handle using the input to choose selection
      if (index = body.to_i) != 0
        selections = GameStates.selections(state, player)
        if selections.length >= index
          state = selections[index - 1].last
        else

and a few more tests to tighten things up:

spec/sms_beholder_spec.rblink
125
126
127
128
129
130
131
132
133
134
135
136
137
  it 'disallows conditional selection without condition' do | example |
    get '/', From: @dude, SkipToState: 'M00'
    get '/', From: @dude, Body: '3'

    expect(last_response.body).to include("1) How do you like soccer?")
  end

  it 'disallows conditional selection with failed condition' do
    get '/', From: @dude, SkipToState: 'M00', SetKey: 'mox-kick', SetKeyValue: 'unknown'
    get '/', From: @dude, Body: '3'

    expect(last_response.body).to include("1) How do you like soccer?")
  end

But what’s this @dude stuff? Oh yeah, I did a little more refactoring. I can explain. Tomorrow.


Useful Stuff


Day 86 code

Comments