301 Days

A year of gamedev experiments.

Day 83 - Beholder on Your Phone Pt1

| Comments

In which we take a cool thing and make it weird.

Blast from the Past

One of my favourite games to come out of the 2014 Global Game Jam was Beholder High, a web-based dating sim set in a high school for beholders.

As the description says:

High school is a confusing time. Especially for beholders. Now the Big Dance is right around the corner, and you don’t have a date! Choose a fellow beholder student and woo them before it’s too late, or the consequences could be dire!

Unfortunately time has passed and you can’t play the game online anymore:

Fortunately, I was talking to a couple of the folks who made the game, and pitched the idea of resurrecting it as an SMS game. They were nice enough to say it was ok, so that’s what I’ll do.

First Things First

To have something to compare against, I needed to get the original up and running first. It was in PHP, so I set up a nice LAMP server and threw the source on it.

Ah, but it’s looking for http://<hostname>/InTheHighOfTheBeholder/css/style.css, so I just move everything under an InTheHighOfTheBeholder directory, and…

Yay! Part of the problem trying to deploy anyone else’s code is figuring out the setup. Now we have a working instance of the original game to compare against and experiment with.

How does it work?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/var/www/html/InTheHighOfTheBeholder$ find . -name "*.php" -print | column
./Beth_Fin/02.php ./D1S02/11.php      ./Siri_Fin/08.php   ./D2S01/day2.php    ./D2S03/00.php
./Beth_Fin/01.php ./D1S02/12.php      ./Siri_Fin/06.php   ./D2S01/06.php      ./D2S03/template.php
./Beth_Fin/08.php ./D1S02/06.php      ./Siri_Fin/05.php   ./D2S01/05.php      ./D2S03/04.php
./Beth_Fin/06.php ./D1S02/05.php      ./Siri_Fin/03.php   ./D2S01/03.php      ./D2S03/07.php
./Beth_Fin/05.php ./D1S02/16.php      ./Siri_Fin/09.php   ./D2S01/00.php      ./template.php
./Beth_Fin/Beth_end.php   ./D1S02/03.php      ./Siri_Fin/00.php   ./D2S01/template.php    ./Mox_Fin/02.php
./Beth_Fin/03.php ./D1S02/09.php      ./Siri_Fin/10.php   ./D2S01/04.php      ./Mox_Fin/01.php
./Beth_Fin/09.php ./D1S02/00.php      ./Siri_Fin/siri_end.php ./D2S01/07.php      ./Mox_Fin/08.php
./Beth_Fin/00.php ./D1S02/10.php      ./Siri_Fin/template.php ./_index2.php       ./Mox_Fin/06.php
./Beth_Fin/10.php ./D1S02/template.php    ./Siri_Fin/04.php   ./D2S04/02.php      ./Mox_Fin/05.php
./Beth_Fin/template.php   ./D1S02/13.php      ./Siri_Fin/07.php   ./D2S04/01.php      ./Mox_Fin/03.php
./Beth_Fin/04.php ./D1S02/18.php      ./D1S01/02.php      ./D2S04/08.php      ./Mox_Fin/09.php
./Beth_Fin/07.php ./D1S02/04.php      ./D1S01/01.php      ./D2S04/06.php      ./Mox_Fin/00.php
./index.php       ./D1S02/07.php      ./D1S01/08.php      ./D2S04/05.php      ./Mox_Fin/10.php
./D2S02/02.php        ./D1S02/14.php      ./D1S01/06.php      ./D2S04/03.php      ./Mox_Fin/template.php
./D2S02/01.php        ./bad_end.php       ./D1S01/day1.php    ./D2S04/00.php      ./Mox_Fin/mox_end.php
./D2S02/06.php        ./D1S03/02.php      ./D1S01/065.php     ./D2S04/template.php    ./Mox_Fin/04.php
./D2S02/05.php        ./D1S03/01.php      ./D1S01/05.php      ./D2S04/04.php      ./Mox_Fin/07.php
./D2S02/03.php        ./D1S03/06.php      ./D1S01/03.php      ./D2S04/07.php      ./D3S01/02.php
./D2S02/00.php        ./D1S03/test.php    ./D1S01/09.php      ./template/header.php   ./D3S01/01.php
./D2S02/template.php  ./D1S03/05.php      ./D1S01/00.php      ./template/footer.php   ./D3S01/03.php
./D2S02/04.php        ./D1S03/03.php      ./D1S01/10.php      ./D2S03/02.php      ./D3S01/day3.php
./D1S02/02.php        ./D1S03/00.php      ./D1S01/template.php    ./D2S03/01.php      ./D3S01/00.php
./D1S02/01.php        ./D1S03/template.php    ./D1S01/04.php      ./D2S03/08.php      ./D3S01/template.php
./D1S02/17.php        ./D1S03/04.php      ./D1S01/07.php      ./D2S03/06.php      ./D3S01/04.php
./D1S02/08.php        ./D1S03/07.php      ./D2S01/02.php      ./D2S03/05.php
./D1S02/15.php        ./Siri_Fin/02.php   ./D2S01/01.php      ./D2S03/03.php
./D1S02/19.php        ./Siri_Fin/01.php   ./D2S01/08.php      ./D2S03/09.php

There are a lot of PHP files; one for each possible state in the game. Awkward, but that’s not unusual for Jam games. Now before I write some code to parse through all of these, let’s see if the developer ever went back to refactor the code…

Yes, there’s another repository, where we have:

www/json/script.jsonlink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "SPLASH": {
        "bg": "Splash",
        "textEntry": {
            "key": "playerName",
            "next": "DAY1",
            "buttonText": "Start"
        },
        "audio": "Looping"
    },
    "DAY1": {
        "bg": "Day1",
        "next": "D1S100",
        "buttonText": "Next"
    },
    "D1S100": {
        "bg": "Classroom",
        "story": "We don't have long.",
        "next": "D1S101",
        "buttonText": "Next"
    },
    "D1S101": {

A nice big JSON file with all of the narrative logic. Consuming that will be a lot easier than parsing a bunch of PHP files.

Let’s get started

Since we’re not doing a Jam, let’s put together a somewhat proper TDD environment. To get started with RSpec on Sinatra, I found GetLaura: How to Test a Sinatra app with RSpec to be a good refresher.

A little CI goodness and we’re ready to get started!

.gitlab-ci.ymllink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
before_script:
  - apt-get update -qq
  - ruby -v
  - which ruby
  - gem install bundler --no-ri --no-rdoc
  - bundle install --jobs $(nproc)  "${FLAGS[@]}"

rubocop:
  script:
    - bundle exec rubocop --display-cop-names --fail-fast
  allow_failure: true

rspec:
  script:
    - bundle exec rspec

We want to run Rubocop and the Rspec tests, and only fail on an Rspec failure.

First Tests

spec/sms_beholder_spec.rblink
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  it 'responds with initial state' do
    get '/', From: 'Dude1'

    expect(last_response.body).to include('Please tell me your name.')
  end

  it 'responds with initial state for each player' do
    get '/', From: 'Dude1'
    get '/', From: 'Dude2'

    expect(last_response.body).to include('Please tell me your name.')
  end

  it 'moves to second state' do
    get '/', From: 'Dude3'
    get '/', From: 'Dude3'

    expect(last_response.body).to include('Day One.')
  end

First Code

https://gitlab.com/LiBiPlJoe/sms-beholder/blob/7216697bac48a4563993fd330339ecb6432a5818/sms_beholder.rb#L5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SMSBeholder < Sinatra::Base
  player_states = {}
  game_states = JSON.parse(File.open('script.json', 'r').read)

  get '/' do
    source = params['From']
    player_states[source] ||= 'SPLASH'
    puts game_states[player_states[source]]
    next_state = game_states[player_states[source]]['next']
    response = game_states[player_states[source]]['story']
    player_states[source] = next_state
    puts player_states
    response
  end

I did have to tweak the JSON file a bit to actually work with this code. Darn initial states and their often being special cases.

First Pass

After a while:

Now we can get on to implementing the rest. Tomorrow.


Useful Stuff


Comments