Tuesday, May 7, 2019

Romans Ruins Adventure, Part 3: Twitch Minigame

In this series of posts I'm sharing my experiences creating an old-style text Adventure game, adapted somewhat for the 21st century. In Part 1 I reviewed the history of Adventure games and the game design for my version, Roman Ruins Adventure. In Part 2, I covered an Alexa Skill implementation of Roman Ruins Adventure. Here in Part 3 I'm covering a Twitch Minigame Extension implementation.

For those unfamiliar with Twitch, it's the world's largest video live streaming platform for the gaming community. Tens of millions of people around the world watch gamers play and chat about it; this is called esports.


Twitch Extensions: The Basics

Anyway, back to Twitch Extensions. If you've joined Twitch, you're either a broadcaster or a potential broadcaster, and you have a channel page from which people can access your live stream or videos. You can customize your channel page with additional elements, including Twitch Extensions. One use for Twitch Extensions are mini-games, and that's what I'm building here.

I'm brand-new to writing Twitch extensions, so this was an exercise in getting familiar with their structure and methodology. There are several kinds of extensions. For our game, we'll be making a panel extension, which will appear below the video player on a broadcaster's channel page.

To help developers get started, Twitch has assets you can download including a Developer Rig (for locally testing your extension) as well as a Hello World sample. I'm doing this work in Visual Studio Code on Windows, but you can also develop Twitch extensions on MacOS or Linux.

The Hello World sample has a two-fold structure: there is a front-end portion and a back-end portion. The front-end is just good old HTML, CSS, and JavaScript/TypeScript. That's what's going to render the panel UI. The back-end is a service that can handle POST requests. The Hello World extension simply puts a colored circle on the screen that you can change by clicking. In getting familiar with how extensions work I added to this starter code to make a simple color matching game. Here's what it looks like running in the Twitch Developer Rig:

Testing an Extension in the Twitch Developer Rig

Once an extension is developed, there are other steps to follow before it can become available on Twitch. It proceeds from Local Test to Hosted Test, where the developer can privately try it out on Twitch. It then gets submitted for approval. More on that process later. Armed with confidence from this initial exercise, it was time to make an extension for Roman Ruins Adventure.

Roman Ruins Adventure as an Extension

With tbe basic gameplay JavaScript code already in place (as described in Parts 1 and 2), this involved three activities:

1. Injecting the JavaScript game code into the extension project structure.
2. Adapting the user interface for the Twitch environment.
3. Making changes to satisfy the requirments for Twitch Extensions.

Injecting the Game Code

Adding the JavaScript game code was fairly easy. The place for that in the extension project structure is the front-end code. The front-end code as provided in the Hello World project has a pretty simple structure, with these notable files:

  • panel.html is the page that renders the extension panel, with a max display are size of 318 x 496 pixels.
  • viewer.js is supporting code for panel.html.
  • config.html is a configuration page for configuring the extension, if applicable.
  • config.js is supporting code for config.html.
  • helper.js contains helper functions.



For my game, I would be adding game play code to viewer.js. I decided to put the game data code into a separate file, gamedata.js, due to its size. I also added jQuery and a tooltip library.

User Interface

My initial thoughts on a user interface were to keep things simple: after all, this project is about giving new life to old-style textual Adventure games in modern computing interfaces. For the Alexa voice skill described in Part 2, this was a slam dunk (although there was a visual element to consider for some Alexa devices).

My first implementation had a command bar at bottom for entering commands like LOOK or TAKE BOTTLE, and the rest of the panel was reserved for the game's text narrative. But it looked very plain.

In prototyping my mini-game as text-only display with text input, I couldn't get away from the feeling that this was going to be a non-starter with much of the Twitch audience. Would a young audience have the patience for entering text commands and reading through narratives, or would it be in everybody's best interest to provide some refinements?

After playing around with some different ideas, I decided to first off make use of the pictures of Pompeii I'd taken last year, by making the panel background an image of the current room. This certainly added atmosphere and complemented the textual descriptions (a picture is worth 1000 words after all).

With Image Added for Current Room

The second improvement I made is toolbar buttons as an alternative to text commands. The command bar remains, but you can now click toolbar buttons with icons for common commands like LOOK, INVENTORY-TAKE-DROP, and directional movements. As you click these buttons, the command being executed is shown in the command bar. Toolbar buttons result in much faster navigation, which might make all the difference to a player with a short attention span. In addition, the directional toolbar button are enabled/disabled to show in which directions movement is possible. This again can make the game much quicker to figure out and complete, but I think it's an acceptable compromise in usability for the target audience. It is a mini-game after all.

With Toolbar Added for Common Commands

Inventory-Take-Drop Toolbar Action

The third usability improvement is in the narrative text itself. I've added hyperlinks to prompt the user about common commands they might want to take. That means, for example, coming to the Gladiator Field a player can just click a bone in the narrative, instead of typing EXAMINE BONE in the command bar. Likewise, from the Examine Bone text they can click take bone instead of typing TAKE BONE in the command bar.


With Links in Narrative Text

All of these changes streamline game play, perhaps to the point where the game world needs to be made significantly larger in order to remain engaging. I'll be working on that. For those who feel this foreshortens the traditional Adventure experience, you can always play the Alexa version.

Getting Things Working Locally

With some UI decisions made and the basic gameplay code added, it wasn't too much work to get this extension working locally. In viewer.js, click events were added to connect UI elements such as the toolbar buttons to game code functions.
// Initialization - runs immediately after loading

$(function () {
  // when we click the cycle button
  $('#execute').click(function () {
    if(!token) { return twitch.rig.log('Not authorized'); }
      execute();
    });
  $('#command').focus(function() {
      $('#command').val('');
  });
  $('#command').keyup(function(event) {
      if (event.which == 13 || event.keyCode == 13) {
        execute();
        return false;
    }
    return true;
  });
  $('#btn-actions').click(function() {
    if (gameOver) return;
    actions();
  });
  $('#btn-inventory').click(function() {
    if (gameOver) return;
    $('#command').val('INVENTORY');
    execute();
  });
  $('#btn-west').click(function() {
    if (gameOver) return;
    $('#command').val('WEST');
    execute();
  });
  $('#btn-east').click(function() {
    if (gameOver) return;
    $('#command').val('EAST');
    execute();
  });
  $('#btn-north').click(function() {
    if (gameOver) return;
    $('#command').val('NORTH');
    execute();
  });
  $('#btn-south').click(function() {
    if (gameOver) return;
    $('#command').val('SOUTH');
    execute();
  });
  $('#btn-up').click(function() {
    if (gameOver) return;
    $('#command').val('UP');
    execute();
  });
  $('#btn-down').click(function() {
    if (gameOver) return;
    $('#command').val('DOWN');
    execute();
  });
  $('#btn-examine').click(function() {
    if (gameOver) return;
    $('#command').val('LOOK');
    execute();
  });
  $('#btn-help').click(function() {
    if (gameOver) return;
    $('#command').val('HELP');
    execute();
  });
  newGame();
});
A departure from the implementation for Alexa is that the dynamic output being generated is HTML, not text sentences. Sometimes that HTML is just text, but often it has embedded elements such as clickable links (SPAN elements) and line breaks (BR elements).

Here's a part of the game code that executes the EXAMINE object command. If no recognizable object was referenced, the response is Please identify the object to examine. If we have a known object, objects[object] is the object definition and we can output its name property as a title and its examine property as a description. A link is then added to TAKE the object (or DROP it, if we're already carrying it). The object's carrying flag tells us if we're carrying the object or not.
// Examine an object.
// Inputs: object : object ID (integer)
// Outputs: command result display to #output

function examine(object) {
  if (object===null) {
    $('#output').text('Please identify the object to examine.');
    return;
  }
  $('#command').val('EXAMINE ' + objects[object].name.toUpperCase());
  var html = '' + objects[object].name + '
' + objects[object].examine + '

';
  if (objects[object].carrying)
    html += 'Drop ' + objects[object].name + '';
  else
    html += 'Take ' + objects[object].name + '';
  $('#output').html(html);
}
examine function

Once I could fully play the game locally and completed general debugging, I was ready for the next step: hosted testing on Twitch.

Hosted Testing

My first time through the extension process, I struggled a bit uploading the project. You create a zip file and upload it via the Twitch developer console. Each time I tried this the upload failed, but there weren't any details about why it failed. After ruling out any issues with the zipping itself, I eventually realized I wasn't supposed to upload the entire project, only the front-end files. I presume that if you do have a back-end, you must find your own hosting for this, such as an AWS Lambda function.

Once your files are uploaded, you can tell the developer console to switch your extension from Local Test mode to Hosted Test mode. Now, from your own channel page the extension appears below the video player and you can test it there.

Since I'd tested fairly extensively with the local Developer Rig, I expected the hosted test to go pretty quickly, but I was in for some surprises. Browsing to my channel on twitch.tv, I activated my panal extension and started playing Roman Ruins Adventure. My game started out okay, but almost immediately it got to a non-working state and would not respond to interactions.

It turns out there are some requirements extensions need to meet that aren't caught by the Developer Rig. The F12 browser console confirmed there was unhappiness, and most of it was due to Content Security Policy.

If you aren't familiar with Content Security Policy, it's a standard intended to prevent a variety of attacks including cross-site scripting and clickjacking. It works like this: if the web server includes a Content-Security-Policy header in its responses, then a CSP-compliant client (like your browser) will enforce its regulations. Anything Twitch-hosted will send the Content-Security-Policy header, so any extensions you write for Twitch need to conform.

Well. I wish the Twitch Developer Rig had alerted me to all of that during local test, but it didn't. Now that I was aware, it was time to refactor the code. One change I had to make was making my JavaScript library references use local files. That was easy enough.

What else was CSP unhappy with? Primarily, it was the inline onclick="function(...)" attributes on the dynamically-generated HTML responses to commands. The biggest challenge here was the dynamic HTML the game functions generated, which often contained embedded links in the form of SPAN elements with onclick attributes. Somehow I needed to retain embedded links in my HTML without resorting to onclick attributes.

After thinking about this for a bit, what I settled on was a click handler for #output, the div that holds the entire output for a command. The click handler can be set in the initialization code in viewer.js:
// Initialization - runs immediately after loading

$(function () {
...
  $('#output').click(function(e) {
    outputClick(e);
  });
  newGame();
});
The click handler itself, outputClick, works as follows. When it responds, it determines the specific element that was clicked: that could be #output, but it could also be something within that element, such as a SPAN element. It's the SPAN elements we're interested in, because they represent clickable links. But we're not permitted to use onclick attributes, so how to know what command to associate with a SPAN? What I ended up doing is inventing a custom HTML attribute for my SPANs called command. When the click handler determines a SPAN element was clicked, it checks for a command attribute, which holds the command it needs to execute.
// Click handler for the output text, which may contain dynamically embedded links.
// If an item was clicked that contains a command="..." attribute, that command is executed.

function outputClick(e) {
  if (e) {
    var target = e.target;
    if (target) {
      var id = target.id;
      var command = $(target).attr('command');
      if (command) {
        if (command==="RESTART") {
          newGame();
          startGame();
        }
        else
          cmd(command);
      }
    }
  }
}
It took a little bit of time to come to this design, but once I did I felt good about it. The code remains simple and understandable, and the extension now works on Twitch in Hosted Test mode. Earlier we looked at some game code, the examine() function. Here's the revised version that is CSP-compliant:
// Examine an object.
// Inputs: object : object ID (integer)
// Outputs: command result display to #output

function examine(object) {
  if (object===null) {
    $('#output').text('Please identify the object to examine.');
    return;
  }
  $('#command').val('EXAMINE ' + objects[object].name.toUpperCase());
  var html = '' + objects[object].name + '
' + objects[object].examine + '

';
  if (objects[object].carrying)
    html += 'Drop ' + objects[object].name + '';
  else
    html += 'Take ' + objects[object].name + '';
  $('#output').html(html);
}
examine function (revised for CSP)

With these changes uploaded, Roman Ruins Adventure now works in Hosted Test mode. Yippee!

Extension on Twitch

Approval

All I had to do next was submit Roman Ruins Adventure for approval. There was a form to fill out, screenshots and icons to provide. After a few days' wait, I was approved. Now anyone who is retro-game minded (there are some of us, Retro Gaming is an entire category on Twitch) can partake.

Roman Ruins Adventure Extension Description on Twitch

If you're a Twitch user, feel free to add Roman Ruins Adventure to your channel as a mini-game panel and give it a whirl. I'd be happy to hear your feedback.

Previous: Part 2: Alexa Skill

No comments: