m-chrzan.xyz
aboutsummaryrefslogtreecommitdiff
path: root/src/blog/hex-curler.html.erb
blob: f3da399b3da968486fccffd374c571e2abe5d6f2 (plain)
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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
title: "Hex Curler: A Minimalist Webgame"
date: August 29, 2019
---
<p>
I just published Hex Curler, a tiny dungeon crawler, based on Jeff Moore's
<a href='http://www.1km1kt.net/rpg/hex'>Hex</a>.
</p>

<p>
You can play it by running the following in a bash shell:

<pre>
c=x; while [ $c ]; do clear; curl -c k -b k hex.m-chrzan.xyz/$c; read c; done
</pre>
</p>

<p>
This was an exercise in minimalism. The game server is implemented in less than
a thousand lines of Ruby code. It is completely stateless, requiring no
database. The front end "client" is a single line of bash, less than 80
characters long. The only dependency is <code>curl</code>, a CLI tool already
available on most Unix-like systems.
</p>

<p>
The source code is available <a href='<%= git 'hex-curler' %>'>here</a>.
</p>

<h3>Let's get curling</h3>
<p>
The whole concept arose from two ideas I had:

<ul>
    <li>
        <code>curl</code> and a simple web server could be used to create a
        simple remote CLI program.
    </li>
    <li>
        For a webapp that implements a simple state-transition system (like a
        simple game), one could forget about session management and a database,
        and just store the state client-side in a cookie.
    </li>
</ul>

I abstracted these ideas away into a Ruby class called <code>Engine</code> and a
skeleton Sinatra app.
</p>

<p>
To create a new "curling" system, you extend <code>Engine</code> and implement
four methods:

<ul>
    <li>
        <code>step</code>: performs a single step of the state-transition
        function.
    </li>
    <li>
        <code>message</code>: outputs a message related to the most recent
        <code>step</code>.
    </li>
    <li>
        <code>hash_to_state</code> and <code>state_to_hash</code>: these are
        just overhead glue methods. They should deserialize and serialize
        between your engine's internal state and a Ruby <code>Hash</code>.
    </li>
</ul>

You also need to define a <code>secret</code> which is a string that is used to
validate that a submitted cookie represents a valid state. More on this later.
</p>

<p>
The Sinatra skeleton instantiates an engine with the received cookie,
runs a step, sends back the new state in the returned cookie, and responds with
the engine's message. The code for it fits in half of a browser window:

<pre>
require 'sinatra'
require 'sinatra/cookies'

# exposes `Hex`, which extends `Engine`, implementing a simple dungeon crawler
require './hex_engine'

def secret
  # get secret from environment
  ENV['HEX_SECRET']
end

get '/:command' do |command|
  # `new` uses `hash_to_state` to initialize the engine's state
  engine = Hex.new cookies.to_h
  engine.step command
  # `state_h` uses `state_to_hash` to serialize the engine's new state
  engine.state_h.each_pair do |key, value|
    cookies[key] = value
  end
  engine.message
end
</pre>
</p>

<h3><em>O</em>(1) space webapp</h3>
<p>
Hex Curler is hosted online but has no session management, no database. It's an
<em>O</em>(1) space webapp.
</p>

<p>
As mentioned before, the game's state is stored in a cookie. The server need
only know the contents of that cookie to return a new state back to the user.
</p>

<p>
To prevent a user from tampering with the cookie, for example by increasing
their health to a ridiculous number, becoming invulnerable to enemies in Hex,
the cookie also contains a <code>checksum</code> field. This checksum is the
hash of the state together with an appended secret only known by the game
server. The server will refuse to respond to requests whose cookie does not have
a valid checksum.
</p>

<p>
This introduces some interesting possibilities. For example, let's say Alice
wants to boast to her friends about how she just beat Hex, ended with 100 HP
remaining, and had upgraded her magic armor to level 5. Her friend Bob doesn't
just have to take her word for it, or trust a screenshot that could have easily
been photoshopped.
</p>

<p>
Alice can send Bob her state cookie. If a request to the game server with it
succeeds, Bob can be assured that he has cryptographic proof of Alice's
claims.
</p>