301 Days

A year of gamedev experiments.

Day 87 - Beholder on Your Phone Pt5

| Comments

In which we step on the gas a bit.


Yesterday’s post was way too long; sorry about that. I’m not sure if today’s will be shorter.

Dude Refactor

Ran into a few bumps with duplicate Dude names in the tests, and didn’t want to have to manually keep them unique, so wrote a quick before hook to make sure I have two (likely) unique Dude names available to the test case.

spec/sms_beholder_spec.rblink
13
14
15
16
17
  before(:example) do | example |
    puts "\n*** #{example.location} ***\n"
    @dude = "Dude" + example.id.hash.to_s
    @dude2 = "Dude" + (example.id.hash + 1).to_s
  end

…and we also get a nice marker at the beginning of each test case’s execution; there’s all sorts of other stuff we could output or act on here: Documentation for the Example class in Rspec.

Misc Process

Another refactor was the move the SetKey cheat into a method called process_misc so we can call it for both new players (for whom we don’t process input yet) and existing players (for whom we will process input).

sms_beholder.rblink
76
77
78
79
80
81
82
  def process_misc(params, player)
    if params.key?('SetKey')
      puts "setting key #{params['SetKey']} to #{params['SetKeyValue']}"
      player[params['SetKey']] = params['SetKeyValue']
    end
    player
  end

Conditional Story?

Trying my “can be played all the way through” test again, I get stuck on state S09:

script.jsonlink
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
    "S09": {
        "beholder": "siri",
        "beholderPos": "left",
        "beholderExp": "neutral",
        "beholderAnimation": "fadeInLeft",
        "bg": "Lunchroom",
        "buttonText": "Next",
        "namePlate": ":name:",
        "conditionalStory": {
            "condition": {
                "siri-smoked": "true"
            },
            "ifConditionTrue": {
                "story": "Sorry. I'm always nervous when we hang out. You're like too cool.",
                "next": "S04",
                "buttonText": "Next"
            },
            "ifConditionFalse": {
                "story": "But it's a really cool car!",
                "next": "S10",
                "buttonText": "Next"
            }
        }
    },

So now there are conditional story text and state transitions. A bunch more tests:

spec/sms_beholder_spec.rblink
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
  it 'shows ifConditionFalse story without condition' do
    get '/', From: @dude, SkipToState: 'S09'

    expect(last_response.body).to include("But it's a really cool car!")
    expect(last_response.body).not_to include("I'm always nervous when we hang out.")
  end

  it 'shows ifConditionFalse story with false condition' do
    get '/', From: @dude, SkipToState: 'S09', SetKey: 'siri-smoked', SetKeyValue: 'false'

    expect(last_response.body).to include("But it's a really cool car!")
    expect(last_response.body).not_to include("I'm always nervous when we hang out.")
  end

  it 'shows ifConditionTrue story with true condition' do
    get '/', From: @dude, SkipToState: 'S09', SetKey: 'siri-smoked', SetKeyValue: 'true'

    expect(last_response.body).to include("I'm always nervous when we hang out.")
    expect(last_response.body).not_to include("But it's a really cool car!")
  end

  it 'proceeds to ifConditionFalse state without condition' do
    get '/', From: @dude, SkipToState: 'S09'
    get '/', From: @dude

    expect(last_response.body).to include("Can you like, buzz off?")
  end

  it 'proceeds to ifConditionFalse state with false condition' do
    get '/', From: @dude, SkipToState: 'S09', SetKey: 'siri-smoked', SetKeyValue: 'false'
    get '/', From: @dude

    expect(last_response.body).to include("Can you like, buzz off?")
  end

  it 'proceeds to ifConditionTrue state with true condition' do
    get '/', From: @dude, SkipToState: 'S09', SetKey: 'siri-smoked', SetKeyValue: 'true'
    get '/', From: @dude

    expect(last_response.body).to include("Hehe. You're such a nerd")
  end

and some additions to the GameStates module to pass them:

sms_beholder.rblink
30
31
32
33
34
35
36
37
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
72
73
74
75
76
77
78
    def self.story(state, player)
      story = ''
      if @states[state] && @states[state].key?('story')
        story += @states[state]['story']
      end
      if @states[state] && @states[state].key?('conditionalStory')
        conditions_met = true
        @states[state]['conditionalStory']['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
          story += @states[state]['conditionalStory']['ifConditionTrue']['story']
        else
          story += @states[state]['conditionalStory']['ifConditionFalse']['story']
        end
      end
      story
    end

    def self.next(state, player)
      next_state = nil
      if @states[state] && @states[state].key?('story')
        next_state = @states[state]['next']
      end
      if @states[state] && @states[state].key?('conditionalStory')
        conditions_met = true
        @states[state]['conditionalStory']['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
          next_state = @states[state]['conditionalStory']['ifConditionTrue']['next']
        else
          next_state = @states[state]['conditionalStory']['ifConditionFalse']['next']
        end
      end
      next_state
    end

Looks pretty repetitive, and we may DRY it up sooner than later. But it does work.

Storyless States

In order to get the “can be played all the way through” test case to work, we had to allow for states that don’t actually have a story key. The fix was trivial, but the important part is that we added a test for it:

spec/sms_beholder_spec.rblink
39
40
41
42
43
44
  it 'can handle states with no story' do
    get '/', From: @dude, SkipToState: 'DAY2'
    get '/', From: @dude

    expect(last_response.body).to include("You arrive at school early")
  end

Bad Endings

We can happily leave the “can be played all the way through” test enabled now, but something is troubling.

1
It looks like you'll be going to the Big Dance after all. And you won't be alone...

It always hits the “Bad” ending. Despite its random choices, it get this ending every single time.

Well, let’s find a “Good” ending and work backwords to what is required. Spoiler alert.

script.jsonlink
1268
1269
1270
1271
    "SIRIWIN": {
        "bg": "siri-splash",
        "story": "This car is a castle. The passenger seat is your throne. Royalty does not concern itself with common dances."
    },

How do we get to SIRIWIN?

script.jsonlink
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
    "S03": {
        "beholder": "siri",
        "beholderPos": "center",
        "beholderExp": "shy",
        "beholderAnimation": "fadeIn",
        "bg": "Lunchroom",
        "buttonText": "Success",
        "namePlate": "Siriak the Rapid",
        "story": "Yeah, well... I was planning on hanging out around this lame dance. There's nothing else to do tonight. You want to come?",
        "next": "SIRIWIN"
    },

How do we get to S03?

script.jsonlink
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
    "S02": {
        "beholder": "siri",
        "beholderPos": "left",
        "beholderExp": "neutral",
        "beholderAnimation": "fadeIn",
        "bg": "Lunchroom",
        "namePlate": ":name:",
        "conditionalStory": {
            "condition": {
                "siri-smoked": "true"
            },
            "ifConditionTrue": {
                "story": "Yeah. Or we could just hang out by the dumpster. Maybe just you and me this time?",
                "next": "S03",
                "buttonText": "Next"
            },
            "ifConditionFalse": {
                "story": "Sweet! I love beer!",
                "next": "S04",
                "buttonText": "Next"
            }
        }
    },

How do we get siri-smoked set to true?

script.jsonlink
373
374
375
376
377
378
379
380
381
382
383
384
385
    "D1S307": {
        "bg": "Dumpster",
        "story": "Someone offers you the cigarette, and you can feel Siriak's gaze drawn down upon you. You draw, hold it, and exhale, all without erupting into a fit of coughing. They think you're cool. Siriak thinks you're cool. And you've made a fool of the surgeon general.",
        "next": "DAY2",
        "buttonText": "Next",
        "beholder": "siri",
        "beholderPos": "center",
        "beholderExp": "neutral",
        "beholderAnimation": "fadeIn",
        "plotPoint": {
            "siri-smoked": "true"
        }
    },

Oh, it’s a plotPoint. I’m not handling plotPoints, yet.

Plot Points

spec/sms_beholder_spec.rblink
188
189
190
191
192
193
  it 'sets conditions at plot points' do
    get '/', From: @dude, SkipToState: 'D1S307'
    get '/', From: @dude, SkipToState: 'S02'

    expect(last_response.body).to include("hang out by the dumpster.")
  end
sms_beholder.rblink
114
115
116
117
118
119
120
    def self.plot_points(state)
      if @states[state] && @states[state].key?('plotPoint')
        @states[state]['plotPoint']
      else
        {}
      end
    end
sms_beholder.rblink
180
181
182
183
184
185
186
187
188
189
190
191
  def process_output(player)
    state = player['state']

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

    GameStates.plot_points(state).each_pair do |key, value|
      player[key] = value
      puts "Plot point set player key #{key} to #{value}, and now player is #{player}"
    end

and there we have it. It may seem weird to have put the plot points in process_output, but they are an output of the state (just not one that is directly exposed to the player).

Good Endings

Rather than just rerunning the tests a bunch of times, let’s actually seek out a good ending:

spec/sms_beholder_spec.rblink
208
209
210
211
212
213
214
215
216
217
218
219
  it 'can be played to a good ending' do
    get '/', From: @dude
    Timeout::timeout(10) do
      until 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
        @dude += '0' if last_response.body.include?('And you won\'t be alone...')
        get '/', From: @dude, Body: ['1','2','3'].sample
        puts last_response.body
      end
    end
  end

When we hit a bad ending, we just add a zero to the end of the player ID and we get restarted. We also test the bad ending the same way.

And it works.

Now that we can follow the narrative threads, we can tackle the visuals. But maybe we’ll clean the code a bit first. Tomorrow.


Useful Stuff


Day 87 code

Comments