There’s not been much to update on since my last blog post - I’ve been gradually chipping away at the rest of the writing, filling in the gaps and finalising the routes. As it stands now there will be 9 different endings in total. But one of the more interesting things I’ve done recently is made a way of tracking player choices and displaying them. I didn’t find many guides on how to do this in RenPy so I’ll be giving a tutorial of sorts here. I hope some people find it useful.

How do we record what choices players make?

In order to have a centralised record of the choices that all players have made we need to create a web service (API) with a database. The service will respond to POST and GET requests from the game client. When a player makes a choice the game client POSTs data to the API to record the choice and to view the results the game client GETs the data from the API. Player choices are recorded in a database as a Vote that has a name for the choice and a value - true or false, since all the choices in the game roughly translate to yes or no answers.

The technical stuff

I set up an API in Ruby on Rails with a MongoDB database. Since we only have one model (Vote) we don’t need a relational database.

Setting up

To set up, go to terminal and in a new folder run:

$ rails new . --skip-active-record --api

The --skip-active-record and --api flags are important here. --skip-active-record will set the project up without options for using a relational database. Since we’re using MongoDB we don’t need this. --api will create the app without a frontend i.e. with no HTML pages. We just need this API to spit out JSON that we can use in our RenPy game.

Once that’s done, go into the project’s Gemfile. If there’s a line like this:

# Use sqlite3 as the database for Active Record
gem 'sqlite3'

Delete it as we’re not using a SQL database. Add this instead:

gem 'mongoid', '~> 6.0'

This gem allows us to talk to MongoDB. Now run bundle to install your gems. Then when that’s done we can run:

$ rails generate mongoid:config

To generate our config/mongoid.yml with settings for our database.

Database model

We just want one type of record in our database and this is a Vote. In app/models create vote.rb:

class Vote
  include Mongoid::Document
  field :name, type: String
  field :value, type: Boolean
end

So when we create a vote we provide a name and a value that is true or false.

In app/controllers create votes_controller.rb:

class VotesController < ApplicationController
  def index
    @votes = Vote.all
    render json: @votes
  end

  def new
    vote = Vote.new(vote_params)

    if vote.save
      render json: vote, status: :created
    else
      render json: vote.errors, status: :unprocessable_entity
    end
  end

private
  def vote_params
    params.require(:vote).permit(:name, :value)
  end
end

And in config/routes.rb:

Rails.application.routes.draw do
  get 'api/votes', to: 'votes#index'
  post 'api/votes/new', to: 'votes#new'
end

This maps the URL to the function in our VotesController. GET /api/votes will go to VotesController index and return all votes as JSON. If we POST to api/votes/new we need to provide a JSON object with name and value and the API will create a vote record for us.

To run the API, in another terminal window, run mongod to start the local MongoDB server and in another run rails s to start the rails server.

HTTP requests in RenPy

We can use the builtin python library urllib2 to make HTTP requests to our API from RenPy. To POST data to the server:

init -1 python:

    import urllib2
    import json

    # API
    # change this to actual url before releasing
    api_new = 'http://localhost:3000/api/votes/new'

    def post_to_api(name, val):
        data = json.dumps({ "name": name, "value": val })
        try:
            request = urllib2.Request(api_new, data, {
                'Content-Type': 'application/json'
            })
            urllib2.urlopen(request)
        except:  # if we can't connect to the API for whatever reason
            pass  # this avoids the function erroring and crashing the game

Then in our script we can do:

N "What am I supposed to say? He's definitely not the kind of guy who will take no for an answer."

menu:
    "Yeah, sure.":
        # only send result the first time this choice was seen
        if not persistent.lunch_choice_seen:
            python:
                post_to_api('lunch_choice', 'True')
        $ persistent.lunch_choice_seen == True
        $ lunch_choice = True

    "No thanks.":
        if not persistent.lunch_choice_seen:
            python:
                post_to_api('lunch_choice', 'False')
        $ persistent.lunch_choice_seen == True
        $ lunch_choice = False

For debugging purposes you can get rid of if not persistent.lunch_choice_seen: to test POSTing to the API multiple times without having to delete persistent data. When we release the game we only want people to be able to send their result once.

Once we’ve created a bunch of data in our API we need to be able to GET all the results:

init -2 python:

    # change this to actual url before releasing
    api_results = 'http://localhost:3000/api/votes'

    def fetch_results():
        try:
            response = urllib2.urlopen(api_results)
            return json.load(response)
        except:
            return None

    def update_results():
        persistent.results = fetch_results()

Now e.g. we can set up a stats screen with a button to fetch the latest results:

textbutton "Fetch results" action Function(update_results)

Formatting the data nicely

Getting a bunch of JSON objects from the API isn’t particularly easy to work with in RenPy so let’s make that data way simpler. What we actually want is a list of the choices players made and the percentage of players that chose them.

In our API in app/controllers/votes_controller.rb edit index and add a new function get_percentage:

class VotesController < ApplicationController
  def index
    @json_output = {}

    # list of all possible choices we want to track
    choices = [
      'prologue_choice',
      'lunch_choice',
      'kiss_choice',
      'after_school_choice',
      'jack_choice',
      'doctor_choice',
    ]

    # iterate over each choice and run get_percentage
    choices.each do |choice|
      get_percentage(choice)
    end

    render json: @json_output
  end

private
  def get_percentage(name)

    # lookup the votes in the database with the name of the choice
    choices = Vote.where(name: name)

    # if we found more than one record
    if choices.length > 0
      # get the number of 'true' choices and convert to float
      choice_true = choices.where(value: 'true').length.to_f

      # get the 'true' choices as a percentage of total votes for that choice
      choice_true_percentage = choice_true / choices.length.to_f * 100

      # the remaining number is the 'false' percentage
      choice_false_percentage = 100 - choice_true_percentage

      # put these in the json_output formatted to 1 decimal place
      @json_output["#{name}_true"] = '%.1f' % choice_true_percentage
      @json_output["#{name}_false"] = '%.1f' % choice_false_percentage

    # if there are no records yet just output 0
    else
      @json_output["#{name}_true"] = 0
      @json_output["#{name}_false"] = 0
    end
  end

end

This will spit out JSON like this:

{
  "prologue_choice_true": "55.6",
  "prologue_choice_false": "44.4",
  "lunch_choice_true": "50.0",
  "lunch_choice_false": "50.0",
  "kiss_choice_true": "72.7",
  "kiss_choice_false": "27.3",
  "after_school_choice_true": "42.9",
  "after_school_choice_false": "57.1",
  "jack_choice_true": "87.5",
  "jack_choice_false": "12.5",
  "doctor_choice_true": "82.4",
  "doctor_choice_false": "17.6"
}

Much nicer to work with. Now in RenPy we can access those percentage values with persistent.results['prologue_choice_true'] persistent.results['prologue_choice_false'] etc. We can do some cool stuff with these numbers like rendering bar charts. Since the numbers provided by the API are strings we need to convert them to floats, then to integers so we can use them to control the width of RenPy displayables in a stats screen like so:

$ prologue_choice_true = int(float(persistent.results['prologue_choice_true']))
$ prologue_choice_false = int(float(persistent.results['prologue_choice_false']))
$ red = "#a00"
$ blue = "#5ad"
add Solid(red, xsize=prologue_choice_true)
add Solid(blue, xsize=prologue_choice_false)

So far I have something like this:

The stats screen

I need to refine the layout but functionally, it’s exactly what I wanted.