Sinatra Data Persistence Using Params 2 — Sinatra Battleships Project Part 5

My main story this week was to present a grid of ‘X’s to a web user — the first step in creating a web version of my console Battleships game. The user should be able to select a point on the grid, which should change from an ‘X’ to an ‘O’, and this state should be maintained through the rest of the game. I was to find the solution using only Sinatra’s params hash and not its sessions feature.

In my previous post, I discussed using the Sinatra params hash and HTML input forms, with the hidden input type, to make data persist across sequential webpages.

I was able to make data persist in this scenario using the same techniques as before even though this time the request is to the same GET route every time rather than through a sequence of different webpages. I also found a second way of achieving this using Sinatra’s ability to save route path patterns to the params hash.

Initial Project Setup

Please find the code here.

Constructing a 3x3 grid was fairly simple. I made a GET route in my app file in which I instantiated 9 grid point variables (‘@point_01’, ‘@point_02’ and so on), assigning them all the initial value of ‘X’. After this, I instantiated a ‘@grid_points’ array instance variable, constituted of the 9 grid point variables. I put HTML break points (‘<br>’) between each set of three in the array, to create line breaks when ultimately rendered by the browser.

Then in a ‘version1.erb’ template in the Views directory, I placed the ‘@grid_points’ variable within erb tags and called join on it to transform it into a string which would be rendered as a 3x3 grid of ‘X’s by the browser. I made 9 buttons within an input form, all with the same input name ‘point’ but with different values (‘01’, ‘02’ and so on).

As I discussed in my previous post, HTTP is a stateless protocol — each request is ignorant of the last. The trick here then, is to re-send the updated state of the grid with every new request, every click of a button — this is the way of getting around HTTP’s goldfish memory using only params. For each solution, I explain the setup then walk through it.

First Solution: Using Hidden Input

First, we name the path of our GET route ‘/battleships/version1’ and create a variable within it called ‘@grid_state’ to keep track of which buttons have already been pressed, as follows:

@grid_state = params[:grid_state] || ""
@grid_state += "#{params[:point]}"

Remember in our ‘version1.erb’ file, each of the 9 buttons has the name ‘point’ and the value of ‘01’, ‘02’ and so on. So the params[:point] will capture whichever button is pressed, and the value will correspond to whichever button has been pressed.

Underneath these two lines, we can also use the ‘@grid_state’ variable to determine which of the ‘X’s of the grid should be ‘O’s, using simple if/else logic — if ‘@grid_state’ includes ‘01’, ‘@point_01’ should be ‘O’, else it should be ‘X’, and so on for all of the ‘@point’ variables — which means that all of them will be ‘X’s unless they have been activated by a button.

In the ‘version1.erb’ file, we have an input form of 9 buttons. We do not need the action or type attributes as we can rely on the defaults — so whenever a button is pressed, a GET request will be made to the same path as our current URL, which is what we want. We want the same grid-display page to be pulled up again and again on loop.

However, to ensure the grid state is retained with each new request, we must add the following within the input form above the buttons:

<input type="hidden" name="grid_state" value=<%= @grid_state %>>

Let’s walk through what we’ve done here.

If you start the server and open up the webpage, an initial GET request will be made at the route path. The first line instantiating the ‘@grid_state’ variable is shorthand for: ‘If params[:grid_state] exists, set ‘@grid_state’ to that — if not (i.e. if the expression resolves to nil, which is ‘falsey’ in Ruby), set ‘@grid_state’ to an empty string.

At this point, there is no params[:point] so nothing will be added by the second line. ‘@grid_point’ is an empty string so all the ‘@point’ variables will remain as ‘X’s.

‘version1.erb’ is resolved and returned by the erb method at the end of the route. The browser is sent a string which shows the 3x3 grid of ‘X’s plus the 9 buttons.

The user presses button 1. This creates the key/value pair ‘point’ and ‘01’ in theparams hash, and also the ‘@grid_state’ variable (still an empty string) as a hidden input, both of which are ‘sent’ within a new GET request at the same route.

Entering the route again, ‘@grid_state’ starts as an empty string but now adds the value at params[:point] to it, being ‘01’, making it the string ‘01’. As it now includes ‘01’, ‘@point_01’s ‘if’ logic is activated, making this variable an ‘O’ instead of an ‘X’.

The ‘version1.erb’ file is again rendered with the new version of the grid, showing the first grid point as an ‘O’ instead of an ‘X’, and you’ll notice the query parameters have been added after ‘version1’ in the URL.

This is the first way to make data persist using only params.

Second Solution: Using Path Pattern Matching

The second solution I arrived at, relies on much of the same logic but this time is based on Sinatra’s ability to capture a pattern placed at the end of a resource path in a route within its params hash.

Use the same setup as last time, except the top of your route should be replaced with the following (before all the ‘if/else’ ‘@point’ logic blocks).

get "/game/:grid" do
@grid_state = params[:grid]
@grid_state += "#{params[:point]}"

And in your ‘version1.erb’ file (which I would actually call ‘grid.erb’ here so it matches this route’s name), there is no need for the hidden input element but you should make the first line of the input form the following:

<form action="/game/<% @grid_state %>">

Again, let’s walk through this step by step to really get a grip on what’s going on.

We start by going to the URL ‘game/grid’. A GET request is made to our route. When you put a colon in a route’s path, Sinatra will save the name of this pattern as a params key which is matched to whatever has been entered in the URL, which will constitute the value against the key. In our case, both our key and value are ‘grid’.

Entering the route, ‘@grid_state’ is set to the params[:grid] value (currently ‘grid’). There is no params[:point] yet, no ‘O’s are activated, and the ‘grid.erb’ file is resolved and returned as an HTML document for the browser to render.

Button 1 is clicked. A GET request is made at ‘/game/grid’. ‘@grid_state’ is again set to params[:grid] (still ‘grid’), but this time the value ‘01’ is added to it, making it ‘grid01’, which triggers the first ‘@point’ variable to turn into a ‘O’. ‘grid.erb’ is then pulled up.

This time, when button 2 is pressed, the path the form takes us to is ‘/game/grid01’ — because ‘@grid_state’ now equals ‘grid01’. ‘02’ is then added to this, which triggers ‘@point_02’ to become a ‘O’ and ‘grid.erb’ is returned, and so on.

I found this story really challenging to complete, but in the end I managed to find two solutions. Although HTTP is a stateless protocol, it is possible to ‘cheat’ the system by storing values in the params hash and ensuring this is resent and added to with every new request.

Caution about route order

When I first attempted the above, I found the second solution first, then worked out how to do the same thing using the hidden input value. As I was working this out, I kept running my server and checking my website and it all seemed to be working perfectly — then I realised that I had placed the ‘game/version1’ route under the ‘game/:grid’ route… and routes are checked for a match in sequential order down the file. As soon as a match is found, it runs. Which means the whole time, the route using the ‘:grid’ pattern had been running (with ‘version1’ as the name of the key), as it picked up the GET request before ‘/game/version1’ had a chance to! So in fact, I was not checking the other solution at all, just rerunning the first solution. In general, it’s worth noting that any pattern-matching routes should probably go at the end of the app file as they can act as ‘buckets’ to catch anything that falls through the cracks. This is why I renamed the paths to ‘/battleships/version1’ and ‘/game/:grid’ so that there could be no mismatching… don’t make the same mistake I did!

--

--