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='https://gitlab.com/m-chrzan/hex-curler'>on my GitLab</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>