Trello as an Interface via Webhooks

So I’m a bit particular about how I keep my finances in order.  To the point that I wrote my own web-based ledger software to help myself keep it all straight.  Yeah, there are third-party solutions out there, but I wanted something that worked exactly the way I wanted.

Every couple days I pull receipts out of my pocket and go through my email inbox and drop new entries in my ledger.  Every couple weeks I manually reconcile the ledger with my banking statements.  Manual processes – ew – but it’s important enough to me that I do it.

The issue I ran into was transactions that I didn’t have a receipt for.  Tim Horton’s drive-thru or gas station pumps with a broken receipt printer or the Flint Firebirds’ souvenir shop using SquareCash.  No receipt means no ledger entry means confusion when I go to reconcile.

I could have made a mobile version of the ledger entry form but I really didn’t want to. As such I decided I could fix the issue by keeping faux-receipts electronically in Trello.  A single list.  Card title is the place I spent the money, description is the amount, a label to represent the account.  Once there’s a ledger entry, archive the card.

And that would have been enough.  I decided to take it a step further and automate things.

I use a webhook subscribed to that single list to look for new cards, get the data from them, automatically add the record to my ledger, then archive the card. I’m essentially using Trello’s app as an interface for my own so that I don’t have to make a mobile interface.

It’s a bit hacky but I figured I’d throw some of the code out here since I feel like there’s not a lot of Trello webhook documentation out there.  Unlike my usual, I’m going to redact some of the code as it deals with my financial system and I’d prefer not to put that out there.

<?php
  require_once 'trello_api.php';
  $trello = new trello_api($trello_config['key'], $trello_config['secret'], $trello_config['token']);

  $data = json_decode(file_get_contents('php://input'));

  if (($data->action->type == 'createCard') AND ($data->model->id == $data->action->data->list->id)) {
    // CREATE WEBHOOK ON CARD
    $webhook = $trello->request('POST', '/1/webhook', array('description' => ('Temporary callback for Card ' . $data->action->data->card->id), 'callbackURL' => 'http://www.example.com/webhook_callback.php', 'idModel' => $data->action->data->card->id));
    $trello->request('POST', ('/1/card/' . $data->action->data->card->id . '/actions/comments'), array('text' => ('Webhook ID: ' . $webhook->id)));
  }

  if (($data->model->id == $data->action->data->card->id) AND (($data->action->type == 'createCard') OR ($data->action->type == 'updateCard') OR ($data->action->type == 'addLabelToCard'))) {
    // ASSIGN VARIABLES
    $date = date('Y-m-d', strtotime($data->action->date));
    $label = trim($data->model->name);
    $amt = trim(str_replace('$', '', $data->model->desc));

    $category = get_category_id($label);
    $account_id = get_account_name($data->model->labels[0]->name);

    $details = array(array('category_id' => $category, 'amt' => $amt));

    if (($account_id) AND ($category) AND (is_numeric($amt))) {
      // DATA LOOKS GOOD, REMOVE WEBHOOK, ARCHIVE CARD, AND CREATE ENTRY
      $comments = $trello->request('GET', ('/1/cards/' . $data->model->id . '/actions'), array('filter' => 'commentCard', 'fields' => 'data'));

      $webhook_id = 0;
      foreach ($comments AS $comment) {
        if (strstr($comment->data->text, 'Webhook ID: ')) {
          $webhook_id = str_replace('Webhook ID: ', '', $comment->data->text);
          break;
        }
      }

      if ($webhook_id) {
        // FOUND A WEBHOOK, EVERYTHING WORKING AS IT SHOULD
        $trello->request('DELETE', ('/1/webhook/' . $webhook_id));
        $trello->request('PUT', ('/1/card/' . $data->model->id . '/closed'), array('value' => true));
        create_ledger_entry($account_id, $date, $label, $amt, $details);
      } else {
        // DIDN'T FIND A WEBHOOK, STUFF IS BROKEN
        $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => 'No webhook found.'));
      }
    } else {
      // DATA LOOKS NOT GOOD, NOTE IN CARD
      $msg = '';
      if (!$account_id) $msg .= 'Invalid account selected' . "\n\n";
      if (!is_numeric($amt)) $msg .= 'Invalid amount selected: ' . $amt . "\n\n";
      if (!$category) $msg .= 'No category found' . "\n\n";

      if ($msg) $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => trim($msg)));
    }
  }

  unset($trello);
?>

As I said, I’ve replaced some of the actual code with obscured-away psudo-functions. I’ll point those out those spots as necessary while going through this piece-by-piece.

require_once 'trello_api.php';
$trello = new trello_api($trello_config['key'], $trello_config['secret'], $trello_config['token']);

$data = json_decode(file_get_contents('php://input'));

Right off the bat we pull in my Trello API wrapper class (which really could use some love, maybe I’ll get to that sooner or later) and instantiate an object that we’ll use later. Then we pull in the data Trello posted to us via the input stream.

if (($data->action->type == 'createCard') AND ($data->model->id == $data->action->data->list->id)) {
  // CREATE WEBHOOK ON CARD
  $webhook = $trello->request('POST', '/1/webhook', array('description' => ('Temporary callback for Card ' . $data->action->data->card->id), 'callbackURL' => 'http://www.example.com/webhook_callback.php', 'idModel' => $data->action->data->card->id));
  $trello->request('POST', ('/1/card/' . $data->action->data->card->id . '/actions/comments'), array('text' => ('Webhook ID: ' . $webhook->id)));
}

Webhooks subscribed to a list don’t give us all the details of cards on the list, so if we detect a card creation and we’re certain we’re getting data from the webhook attached to the list, we add a webhook to the new card. This is done with a POST to /1/webhook passing in a description (which is optional and doesn’t really matter), callbackURL (same as the URL of this script, though obscured here), and idModel (the ID of the card). For future reference, we then post the ID of the newly-created webhook to a comment on the card via POST to /1/card/<card_id>/actions/comments with text set as needed.

if (($data->model->id == $data->action->data->card->id) AND (($data->action->type == 'createCard') OR ($data->action->type == 'updateCard') OR ($data->action->type == 'addLabelToCard'))) {
  // ASSIGN VARIABLES
  $date = date('Y-m-d', strtotime($data->action->date));
  $label = trim($data->model->name);
  $amt = trim(str_replace('$', '', $data->model->desc));

  $category = get_category_id($label);
  $account_id = get_account_name($data->model->labels[0]->name);

  $details = array(array('category_id' => $category, 'amt' => $amt));

The rest of the code only fires if we’re receiving from the new webhook attached to the card, and if it’s triggered by either a card creation (which should never happen since the webhook won’t have been created yet), card update (which happens when I set the card’s description), or label addition (which is how I define what account the transaction is for). We only care about those actions because they match the ones I take on the front-end.

From there, we use attributes of the card to build the transaction. The transaction date is whatever the action is taking place. The label is the name of the card. The amount comes from the card description. The category is determined based on the label. The account_id is determined based on the first label selected. A single-item, multidimensional array (the actual interface handles more complex data) is assembled from this data.

if (($account_id) AND ($category) AND (is_numeric($amt))) {
  // DATA LOOKS GOOD, REMOVE WEBHOOK, ARCHIVE CARD, AND CREATE ENTRY
  $comments = $trello->request('GET', ('/1/cards/' . $data->model->id . '/actions'), array('filter' => 'commentCard', 'fields' => 'data'));

  $webhook_id = 0;
  foreach ($comments AS $comment) {
    if (strstr($comment->data->text, 'Webhook ID: ')) {
      $webhook_id = str_replace('Webhook ID: ', '', $comment->data->text);
      break;
    }
  }

If we got an account ID and a category and the amount is numeric, we know we got good data and we’re ready to try to add the record. First, though, we make a GET call to /1/cards/<card_id>/actions with filter set to commentCard and fields set to data, so that we can get all of the comments posted to the card. We loop through them until we find the one where we stored the card webhook ID and then we save that ID off. There’s probably a better way to do this.

if ($webhook_id) {
  // FOUND A WEBHOOK, EVERYTHING WORKING AS IT SHOULD
  $trello->request('DELETE', ('/1/webhook/' . $webhook_id));
  $trello->request('PUT', ('/1/card/' . $data->model->id . '/closed'), array('value' => true));
  create_ledger_entry($account_id, $date, $label, $amt, $details);

If we found a webhook ID (and we always should), we delete the webhook, archive the card, and create the ledger entry. The first is accomplished with a DELETE request to /1/webhook/<webhook_id>. The second is a PUT to /1/card/<card_id>/closed with value set to true.

} else {
  // DIDN'T FIND A WEBHOOK, STUFF IS BROKEN
  $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => 'No webhook found.'));
}

If we didn’t get a webhook ID, something is seriously broken so we log that finding to the card as a comment via a POST to /1/card/<card_id>/actions/comments with text set to “No webhook found.”

    } else {
      // DATA LOOKS NOT GOOD, NOTE IN CARD
      $msg = '';
      if (!$account_id) $msg .= 'Invalid account selected' . "\n\n";
      if (!is_numeric($amt)) $msg .= 'Invalid amount selected: ' . $amt . "\n\n";
      if (!$category) $msg .= 'No category found' . "\n\n";

      if ($msg) $trello->request('POST', ('/1/card/' . $data->model->id . '/actions/comments'), array('text' => trim($msg)));
    }
  }

  unset($trello);
?>

Lastly, if we don’t have all of the things we need to make a ledger entry we log that to the card as a comment. Again, that’s done with a POST to /1/card/<card_id>/actions/comments.

As I said, it’s hacky. It gave me a chance to play with Trello webhooks a bit, though, and was a lot of fun. As I use it more, we’ll see what I did wrong.

Exporting from Snagit to Amazon S3: Revisited

One of the first things I wrote about when I started this blog was my workaround solution for exporting from TechSmith Snagit to Amazon S3.  That worked okay for Windows but I’ve started working on Mac significantly more of late and I missed that functionality.  As such, I took another look at options for this since TechSmith itself still hasn’t developed a Snagit to S3 output for either Windows or Mac.

I feel like exporting on Mac shouldn’t be a problem.  There’s no S3 Browser available but you could replace it with s3cmd and do the same thing.  The catch: There’s no Program Output option in Snagit Mac.  That’s right, on Windows you can essentially make your own outputs but on Mac you’re out of luck.

I came up with a workaround, though.  It’s not pretty but it works.  It also works on Windows, but with better options available I’m not sure there’s a reason to use it.

I use ExpanDrive to map my S3 buckets as a local drive.  Then I can save from Snagit straight to the location I want in S3.  That part’s great.  It’s pretty much seamless.  ExpanDrive is a really awesome tool.  Probably too expensive if all you’re using it for is Snagit exporting, but worth taking a look at if you’re working with S3 in other ways.

The problem is you don’t get the uploaded URL out of this.  That’s where it gets hacky.

I wrote a Chrome extension that gets me a list of the last five files uploaded to this particular S3 bucket.  So after saving my file, I have to go to my browser to get its URL.  Extra steps.  The bonus is that I can get the URL any time later.

A view of my Chrome extension, showing the last five files uploaded to my filebox S3 bucket.
A view of my Chrome extension, showing the last five files uploaded to my filebox S3 bucket.

Since the ExpanDrive part of it works out of the box, here’s the breakdown of my Chrome extension.

<?php
  // GET THE DATA
  $s3 = Aws\S3\S3Client::factory(array('key' => $access_key, 'secret' => $secret_key, 'region' => $region));

  $objects = array();

  do {
    $response = $s3->listObjects(array('Bucket' => $bucket_name, 'Marker' => $response->NextMarker));

    foreach ($response['Contents'] AS $item) {
      $objects[] = array('key' => $item['Key'], 'url' => ($bucket_url . $item['Key']), 'timestamp' => strtotime($item['LastModified']));
    }
  } while ($response->NextMarker);

  unset($s3);

  // SORT THE DATA
  usort($objects, function($a, $b) {
    return $a['timestamp'] - $b['timestamp'];
  });

  $display = array();
  while (count($display) < 5) {
    $display[] = array_pop($objects);
  }

  // OUTPUT
  header ('Content-type: application/json');
  echo json_encode($display);
?>

I start with a script on the server side that uses the AWSSDKforPHP2 to read in the files from my filebox, sort by date, and grab the five most recent.  Those five are then spit out as JSON.

To access that file from the Chrome extension, it’s important to include that domain in the permissions section of the manifest.json file.  Also necessary is the clipboardWrite permission.  In addition to the required manifest file, the extension uses a single HTML page, a stylesheet (which I’ll skip here since how it looks doesn’t really matter), and a Javascript file.  There are also some images but I’ll skip those, too.

<!doctype html>
<html>
  <head>
    <title>ClarkRasmussen.com Online Filebox</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="container">
      <div id="branding_icon">
        <img src="img/icon-48.png" alt="icon" />
      </div>
      <div id="main">
        <h1>ClarkRasmussen.com Online Filebox</h1>

        <ul id="content"></ul>
      </div>
    </div>

    <script src="main.js"></script>
  </body>
</html>

The important things here are the UL element with the ID of content and the inclusion of main.js.  The UL will be targeted by our JS for dynamically adding elements.

var site_url = 'http://www.mydomain.com/';

function request_data () {
  var xhr = new XMLHttpRequest();
  xhr.open('get', (site_url + 'path/to/script.php'), true);
  xhr.onload = populate_list;
  xhr.send();
}

function populate_list () {
  var obj = JSON.parse(this.responseText);

  // CLEAR OUT EXISTING ELEMENTS
  var container = document.getElementById('content');
  while (container.firstChild) {
    container.removeChild(container.firstChild);
  }

  // ADD NEW ELEMENTS
  for (var n = 0; n < obj.length; n++) {
    var li = document.createElement('li');
    var a = document.createElement('a');

    a.innerHTML = obj[n].key;
    a.setAttribute('href', obj[n].url);
    a.setAttribute('target', '_blank');
    a.onclick = function () {
      copy_to_clipboard(this.getAttribute('href'));
    };

    li.appendChild(a);
    container.appendChild(li);
  }
}

function copy_to_clipboard (text) {
  const input = document.createElement('input');
  input.style.position = 'fixed';
  input.style.opacity = 0;
  input.value = text;
  document.body.appendChild(input);
  input.select();
  document.execCommand('Copy');
  document.body.removeChild(input);
};

window.addEventListener('load', request_data);

The request_data function wraps a call to the PHP script noted above.  Onload of that data, we call populate_list.

The first thing we do in populate_list is parse the text we got from the PHP script into an actual JSON object.  Then we remove any list items we may have in our previously-mentioned UL.  We loop through each of the items in our JSON object and create new elements for them.  Each item gets an LI with an A inside it.  The A has an HREF of the item’s URL and the TARGET is set to _blank so it opens in a new window.  Additionally, we use the copy_to_clipboard method that I grabbed from someone’s GitHub to save that URL to the clipboard, setting it as an onclick event for the A tag.

I’m certain that this could be cleaned up and made more configurable and turned into a publicly-available extension but I’m not going to bother with it.  I figured I’d put this out and hope that it helps someone.

I will say that one idea I’m intrigued by is replacing the PHP script with an AWS Lambda function that triggers any time the S3 bucket is updated.  I’m not entirely certain how that would work but it seems possible.

Retro Code Sample: TSC Interact! Hangout Video Controller

I Tweeted on Friday about futzing with Google Hangouts, something I hadn’t had to deal with in years.  The link I sent out went to a blog post about lessons learned from that experience but I realized that I never bothered to actually write up the code I wrote in that project and figured it might help some people.

The issue I was trying to solve was that I was working on a team that was trying to incorporate a remote employee and a handful of people that sometimes worked from home.  In the office, the team was split across two spaces that were next to each other.  Each space was given a TV, webcam, and microphone.  A problem immediately became apparent when the two local stations started causing feedback between each other.  Someone solved this by turning off the speakers and microphone on one of the stations, which left that room unable to easily communicate with people in the hangout.

To solve this, I took advantage of the fact that the two stations were logged in as the same user and wrote a Hangouts app to run on those machines.  The app looks for another person logged in as the same user and mutes them, eliminating feedback.  It also blocks that station from appearing on video if another user is available to be seen.

As per my usual, I’ll start with the big block of code, then break it down in small chunks.

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
  <ModulePrefs title="TSC Interact! Hangout Video Controller">
    <Require feature="rpc"/>
  </ModulePrefs>
  <Content type="html"><![CDATA[
<script src="//plus.google.com/hangouts/_/api/v1/hangout.js"></script>
<script>
  var my_id = 0;
  var my_hangout_id = 0;

  function init() {
    // When API is ready...
    gapi.hangout.onApiReady.add(function (eventObj) {
      if (eventObj.isApiReady) {
        my_id = gapi.hangout.getLocalParticipant().person.id;
        my_hangout_id = gapi.hangout.getLocalParticipant().id;

        update_video_options();
        gapi.hangout.onParticipantsChanged.add(function () {
          update_video_options();
        });
      }
    });
  }

  function update_video_options () {
    var participants = gapi.hangout.getParticipants();

    for (var n = 0; n < participants.length; n++) {
      if (participants[n].person.id == my_id) {
        gapi.hangout.av.setParticipantAudible(participants[n].id, false);
        if ((participants.length != 2) && (participants[n].id != my_hangout_id)) {
          gapi.hangout.av.setParticipantVisible(participants[n].id, false);
          gapi.hangout.av.setAvatar(participants[n].id, 'http://fakeurl.com/logo.png');
        }
      }

      if (participants.length == 2) {
        gapi.hangout.av.setParticipantVisible(participants[n].id, true);
      }
    }

    var feed = gapi.hangout.layout.getDefaultVideoFeed();
    if ((gapi.hangout.getParticipantById(feed.getDisplayedParticipant()).person.id == my_id) && (participants.length == 3)) {
      // just in case we get stuck on a blocked user's feed, force switch to the other person
      for (var n = 0; n < participants.length; n++) {
        if (participants[n].person.id != my_id) {
          feed.setDisplayedParticipant(participants[n].id);
          n = participants.length;
        }
      }
    } else {
      feed.clearDisplayedParticipant();
    }
  }

  gadgets.util.registerOnLoadHandler(init);
</script>
]]>
  </Content>
</Module>

The whole thing is JavaScript wrapped in an XML container.  I’ll ignore the XML because it’s pretty standard, aside from the title attribute of the ModulePrefs element being “TSC Interact! Hangout Video Controller” (TSC being the company abbreviation, Interact! being the internal team name).

<script src="//plus.google.com/hangouts/_/api/v1/hangout.js"></script>
<script>
  var my_id = 0;
  var my_hangout_id = 0;

We start by pulling in the Hangouts API JS from Google, then we open our own script block. The first thing we do is define my_id as 0 and my_hangout_id as 0. We’ll store the user’s ID and the Hangout ID (which is the user/machine’s unique connection to a Hangout) in these spots later.

function init() {
  // When API is ready...
  gapi.hangout.onApiReady.add(function (eventObj) {
    if (eventObj.isApiReady) {
      my_id = gapi.hangout.getLocalParticipant().person.id;
      my_hangout_id = gapi.hangout.getLocalParticipant().id;

      update_video_options();
      gapi.hangout.onParticipantsChanged.add(function () {
        update_video_options();
      });
    }
  });
}

We define an init() function that we’ll fire off later. In it, we use gapi.hangout.onApiReady to attach this code to the event of the API being loaded and ready for us to use. If eventObj.isApiReady is true (and it should be, because this should only be fired if the API is ready, but I went off some sample code that included this), we can do some stuff.

“Some stuff” is setting my_id to gapi.hangout.getLocalParticipant().person.id and my_hangout_id to gapi.hangout.getLocalParticipant().id. Then we fire off the update_video_options() function and set a listener on gapi.hangout.onParticipantsChanged to run that function again each time someone enters or leaves the Hangout.

function update_video_options () {
  var participants = gapi.hangout.getParticipants();

  for (var n = 0; n < participants.length; n++) {
    if (participants[n].person.id == my_id) {
      gapi.hangout.av.setParticipantAudible(participants[n].id, false);
      if ((participants.length != 2) && (participants[n].id != my_hangout_id)) {
        gapi.hangout.av.setParticipantVisible(participants[n].id, false);
        gapi.hangout.av.setAvatar(participants[n].id, 'http://fakeurl.com/logo.png');
      }
    }

    if (participants.length == 2) {
      gapi.hangout.av.setParticipantVisible(participants[n].id, true);
    }
  }

Here’s the bread and butter, the update_video_options() function. We start by using gapi.hangout.getParticipants() to get the list of Hangout participants, then we loop through it to find users with the same ID as the machine this is running on.

If the user is the same, we use gapi.hangout.av.setParticipantAudible() to mute them by passing in the user ID and false. If there are not only two users in the Hangout (which means someone other than the two local machines is logged in) and the user we’re looping through is not the machine this is running on, we also hide the user and reset their avatar. Hiding the user, much like muting them, is done by calling gapi.hangout.av.setParticipantVisible() and passing in the user ID and false. To change the avatar we call gapi.hangout.av.setAvatar() and pass in the user ID and a URL to the new avatar.

If there are only two participants in the Hangout (meaning the two local stations are the only thing logged in), we may as well show the other participant. We do the opposite of what we did to hide them, calling gapi.hangout.av.setParticipantVisible() and passing in the user ID and true.

  var feed = gapi.hangout.layout.getDefaultVideoFeed();
  if ((gapi.hangout.getParticipantById(feed.getDisplayedParticipant()).person.id == my_id) && (participants.length == 3)) {
    // just in case we get stuck on a blocked user's feed, force switch to the other person
    for (var n = 0; n < participants.length; n++) {
      if (participants[n].person.id != my_id) {
        feed.setDisplayedParticipant(participants[n].id);
        n = participants.length;
      }
    }
  } else {
    feed.clearDisplayedParticipant();
  }
}

We wrap up the update_video_options() function by making sure we don’t somehow get stuck on the video feed for the other local station if someone else is available to see. We use gapi.hangout.layout.getDefaultVideoFeed() to get some data about the displayed video feed. If the displayed participant (getDisplayedParticipant()) is the same user as the machine this is running on and there are three participants to choose from (the two local stations plus someone remote), we want to make a switch. We loop through the participants to find one that isn’t the same user, then we use setDisplayedParticipant() to set the video feed to that user. We re-check the number of participants before looping again because of some API weirdness I can’t explain.

Lastly, if we’re not displaying the other local station or there are multiple remote users to choose from, we clear the displayed participant with clearDisplayedParticipant() and let the Hangout decide who should be shown. Because the other local machine won’t be shown at this point and is muted, it allows whichever remote user is talking to appear.

gadgets.util.registerOnLoadHandler(init);

The last thing we do is use gadgets.util.registerOnLoadHandler() to set the init() function to run when the utility is fully loaded.

One thing I think I’d do differently if this were still in use is update it to handle a variable number of local stations. There’s no reason not to account for three or four or whatever, aside from the fact that there were only two when I wrote it.

Batch Requests with the Trello API

I write about the Trello API a lot.  So much that I’ve already noted that I write about it a lot. I keep coming across new stuff, though, so I’m going to keep writing about it.

My latest experiment was a look into using their batch call, which I don’t see a lot of documentation about so I figured was worth writing up.

Batch functionality lets you fire off a series of GET requests (and they have to be GET requests) as one request.  Depending on your code and what requests you’re making (and what data you’re getting back), this should speed things up a bit.  My test script went from running in 37 seconds to 20, for example.

In an extremely simple (and pretty much useless) case, you could replace GET calls to /1/members/me and /1/boards/511e8c0101d3982d05000d5b with a single batch call, /1/batch?urls=/members/me,/boards/511e8c0101d3982d05000d5b.

As shown, /1/batch takes the URLs parameter, a comma-separated list of the calls you want to make, minus their version number prefix.

Of course, this means you get only a single response back, and it looks a little different from a normal response.  The response is an array of objects – but not of the normal response objects you might expect.  Instead, it’s an object with a single property, with a name set to the HTTP response code of the request.

So if your first request was to /1/boards/511e8c0101d3982d05000d5b, a normal response would start as follows:

stdClass Object
(
    [id] => 511e8c0101d3982d05000d5b
    [name] => Welcome Board

The batch version of that response would look like this:

Array
(
    [0] => stdClass Object
        (
            [200] => stdClass Object
                (
                    [id] => 511e8c0101d3982d05000d5b
                    [name] => Welcome Board

Obviously that’s simplified, I just don’t think it’s necessary to show the whole response.

One nice little gotcha with that response and working in PHP is handling a numeric property name, which is done by putting curly braces around the number, as seen in the code to follow.

Lets say you want to get the names and IDs of all the boards you’re assigned to and the names and IDs of all of the lists on each of those boards.  Without batching, you could do the following:

  $trello = new trello_api($GLOBALS['config']['key'], $GLOBALS['config']['secret'], $GLOBALS['config']['token']);

  $data = $trello->request('GET', '/1/members/me');
  foreach ($data->idBoards AS $board_id) {
    $board_data = $trello->request('GET', ('/1/boards/' . $board_id));
    echo '<p>' . $board_data->name . ': ' . $board_id . '</p>';
    echo '<ul>';

    $list_data = $trello->request('GET', ('/1/boards/' . $board_id . '/lists'));
    foreach ($list_data AS $list) {
      echo '<li>' . $list->name . ': ' . $list->id . '</li>';
    }

    echo '</ul>';
  }

  unset($trello);

With batching, that becomes this:

  $trello = new trello_api($GLOBALS['config']['key'], $GLOBALS['config']['secret'], $GLOBALS['config']['token']);

  $batch = array();

  $data = $trello->request('GET', '/1/members/me');
  foreach ($data->idBoards AS $board_id) {
    $batch[] = ('/boards/' . $board_id);
    $batch[] = ('/boards/' . $board_id . '/lists');
  }

  $data = $trello->request('GET', ('/1/batch?urls=' . implode(',', $batch)));
  for ($n = 0; $n < count($data); $n += 2) {
    echo '<p>' . $data[$n]->{200}->name . ': ' . $data[$n]->{200}->id . '</p>';

    echo '<ul>';

    foreach ($data[($n + 1)]->{200} AS $list) {
      echo '<li>' . $list->name . ': ' . $list->id . '</li>';
    }

    echo '</ul>';
  }

  unset($trello);

This assumes that the API will respond with a 200, of course.

As I said, I didn’t see a ton of documentation about batch calls in the Trello API.  This is a stupid simple example but I thought it was worth putting out there.

Retro Code Sample: Press Your Luck

I was going through my portfolio recently and realized that I have an entry for my Press Your Luck game but I’ve only described how it works, never taken a deep dive into the code.

The current version (if you can call something no longer in use “current”) runs entirely on the client side.  There is one HTML file (with inline jQuery), one CSS file, an XML file with configuration values, and a handful of images and sounds.

Some parts of these files have been modified for display purposes.  None of the changes impact functionality.

We’ll start with the config file…

<?xml version="1.0" encoding="utf-8" ?>
<images>
  <image>
    <thumb>http://pressyourluck.info/images/prize_choices_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_choices_full.jpg</large>
    <type>prize</type>
  </image>
  <image>
    <thumb>http://pressyourluck.info/images/prize_giftcard_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_giftcard_full.jpg</large>
    <type>prize</type>
  </image>
  <image>
    <thumb>http://pressyourluck.info/images/prize_lunch_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_lunch_full.jpg</large>
    <type>prize</type>
  </image>
  <image>
    <thumb>http://pressyourluck.info/images/prize_mug_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_mug_full.jpg</large>
    <type>prize</type>
  </image>
  <image>
    <thumb>http://pressyourluck.info/images/prize_parking_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_parking_full.jpg</large>
    <type>prize</type>
  </image>
  <image>
    <thumb>http://pressyourluck.info/images/prize_swagbag_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_swagbag_full.jpg</large>
    <type>prize</type>
  </image>
  <image>
    <thumb>http://pressyourluck.info/images/prize_whammy1_thumb.jpg</thumb>
    <large>http://pressyourluck.info/images/prize_whammy1_full.jpg</large>
    <type>whammy</type>
  </image>
</images>

We’re defining a set of images, the tiles that make up the game board.  Each has a thumbail (the image displayed on the standard game board) and a full-size image (the one displayed in the center when that tile is selected by the player) and we define their URLs here.  We also define whether this is a prize image or a whammy, which determines what sound plays when that tile is selected.

Fairly simple.  Now we move on to the CSS…

body {
	margin: 0;
	padding: 0;
	background-image: url(./images/bg_page.jpg);
	background-repeat: no-repeat;
	background-position: center;
	background-color: #1a1a1a;
}

div#board {
	position: relative;
	margin: 30px auto 30px auto;
	padding: 0;
	width: 1002px;
	height: 800px;
	border: 5px solid #000;
	border-radius: 12px;
	background-color: #000;
}

div#board div {
	width: 167px;
	height: 160px;
	
}

div#board div img {
	position: relative;
	width: 150px;
	height: 140px;
	margin: 8px 6px 8px 7px;
	padding: 0;
	border: 2px solid #000;
}

div#board div.inactive {
	background-image: url(./images/bg_item_inactive.jpg);
	opacity: 0.35;
}

div#board div.active {
	background-color: yellow;
	background-image: url(./images/bg_item_active.gif);
	opacity: 1;
}

div#board div#item0 {
	position: absolute;
	left: 0;
	top: 0;
}

div#board div#item1 {
	position: absolute;
	left: 167px;
	top: 0;
}

div#board div#item2 {
	position: absolute;
	left: 334px;
	top: 0;
}

div#board div#item3 {
	position: absolute;
	left: 501px;
	top: 0;
}

div#board div#item4 {
	position: absolute;
	left: 668px;
	top: 0;
}

div#board div#item5 {
	position: absolute;
	left: 835px;
	top: 0;
}

div#board div#item6 {
	position: absolute;
	left: 0;
	top: 160px;
}

div#board div#item7 {
	position: absolute;
	left: 835px;
	top: 160px;
}

div#board div#item8 {
	position: absolute;
	left: 0;
	top: 320px;
}

div#board div#item9 {
	position: absolute;
	left: 835px;
	top: 320px;
}

div#board div#item10 {
	position: absolute;
	left: 0;
	top: 480px;
}

div#board div#item11 {
	position: absolute;
	left: 835px;
	top: 480px;
}

div#board div#item12 {
	position: absolute;
	left: 0;
	top: 640px;
}

div#board div#item13 {
	position: absolute;
	left: 167px;
	top: 640px;
}

div#board div#item14 {
	position: absolute;
	left: 334px;
	top: 640px;
}

div#board div#item15 {
	position: absolute;
	left: 501px;
	top: 640px;
}

div#board div#item16 {
	position: absolute;
	left: 668px;
	top: 640px;
}

div#board div#item17 {
	position: absolute;
	left: 835px;
	top: 640px;
}

div#board div#item_middle {
	position: absolute;
	left: 167px;
	top: 160px;
	width: 668px;
	height: 520px;
}

div#board div#item_middle img {
	position: relative;
	width: 648px;
	height: 460px;
	margin: 10px;
	padding: 0;
	border: 0;
}

div#board div#item_middle img.prize {
	width: 514px;
	height: 480px;
	margin: 0 77px 0 77px;
}

div#sound {
	width: 0;
	height: 0;
	overflow: hidden;
}

More pretty simple stuff. The page has a background. There’s a div that contains all the game elements. Those are positioned as needed. The tiles have a background image for their active and inactive states. The sound controls are hidden.

Now we get to the fun, the HTML and jQuery. Here’s the full page, we’ll break down the important parts afterwards…

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Bravo Press Your Luck</title>
    <style type="text/css" media="screen">
      @import "./styles.css";
    </style>
    <link href="./jquery-ui/css/custom-theme/jquery-ui-1.8.16.custom.css" rel="stylesheet" type="text/css" />
    <script src="./jquery-ui/js/jquery-1.6.2.min.js" type="text/javascript"></script>
    <script src="./jquery-ui/js/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script>
  </head>
  <body>
    <div id="board">
      <div id="item0" class="inactive">
        <img src="./images/space.gif" id="image0" />
      </div>
      <div id="item1" class="inactive">
        <img src="./images/space.gif" id="image1" />
      </div>
      <div id="item2" class="inactive">
        <img src="./images/space.gif" id="image2" />
      </div>
      <div id="item3" class="inactive">
        <img src="./images/space.gif" id="image3" />
      </div>
      <div id="item4" class="inactive">
        <img src="./images/space.gif" id="image4" />
      </div>
      <div id="item5" class="inactive">
        <img src="./images/space.gif" id="image5" />
      </div>
      <div id="item6" class="inactive">
        <img src="./images/space.gif" id="image6" />
      </div>
      <div id="item7" class="inactive">
        <img src="./images/space.gif" id="image7" />
      </div>
      <div id="item8" class="inactive">
        <img src="./images/space.gif" id="image8" />
      </div>
      <div id="item9" class="inactive">
        <img src="./images/space.gif" id="image9" />
      </div>
      <div id="item10" class="inactive">
        <img src="./images/space.gif" id="image10" />
      </div>
      <div id="item11" class="inactive">
        <img src="./images/space.gif" id="image11" />
      </div>
      <div id="item12" class="inactive">
        <img src="./images/space.gif" id="image12" />
      </div>
      <div id="item13" class="inactive">
        <img src="./images/space.gif" id="image13" />
      </div>
      <div id="item14" class="inactive">
        <img src="./images/space.gif" id="image14" />
      </div>
      <div id="item15" class="inactive">
        <img src="./images/space.gif" id="image15" />
      </div>
      <div id="item16" class="inactive">
        <img src="./images/space.gif" id="image16" />
      </div>
      <div id="item17" class="inactive">
        <img src="./images/space.gif" id="image17" />
      </div>
      <div id="item_middle">
        <img src="./images/space.gif" id="image_middle" />
      </div>
    </div>

    <div id="sound">
      <audio id="player_board" name="player_board" preload="auto" loop="loop">
        <source src="./sounds/board.ogg" />
        <source src="./sounds/board.mp3" />
      </audio>
      <audio id="player_buzz" name="player_buzz" preload="auto">
        <source src="./sounds/buzz.ogg" />
        <source src="./sounds/buzz.mp3" />
      </audio>
      <audio id="player_whammy" name="player_whammy" preload="auto">
        <source src="./sounds/whammy.ogg" />
        <source src="./sounds/whammy.mp3" />
      </audio>
    </div>

    <script type="text/javascript">
    //<![CDATA[
      $(document).ready(function () {
        var images = define_board_images();
        window.game_data = build_game_boards(images);
        load_game_board();
      });

      function define_board_images () {
        var images = new Array();

        $.ajax({
          url: 'config.xml',
          dataType: 'xml',
          async: false,
          success: function (data) {
            $($(data)).find('image').each(function () {
              images.push({'thumb': $(this).find('thumb').text(), 'large': $(this).find('large').text(), 'type': $(this).find('type').text()});
            });
          }
        });

        return images;
      }

      function build_game_boards (images) {
        var boards = new Array();

        do {
          var set = new Array();

          do {
            images = array_shuffle(images);
            var n = 0;

            do {
              set.push(images[n]);
              n++;
            } while ((set.length < 18) && (n < images.length));
          } while (set.length < 18);

          boards.push(set);
        } while (boards.length < 50);

        return boards;
      }

      function load_game_board () {
        $('div#board div.active').removeClass('active');
        $('img#image_middle').removeClass('prize');
        $('img#image_middle').attr('src', './images/logo.jpg');

        window.active_set = -1;
        window.active_space = -1;

        print_set(get_random_set(false));

        $(document).keydown(function (e) {
          if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) {
            e.preventDefault();
            start_game();
          }
        });

        $(document).bind('touchend', function (e) {
          e.preventDefault();
          start_game();
        });
      }

      function get_random_set (no_repeat) {
        var r = -1;

        if (no_repeat) {
          do {
            r = Math.floor(Math.random() * window.game_data.length);
          } while (r == window.active_set);
          window.active_set = r;
        } else {
          r = Math.floor(Math.random() * window.game_data.length);
        }
        return window.game_data[r];
      }

      function print_set (data) {
        $('div#board div img').each(function (index) {
          if ($(this).attr('id') != 'image_middle') {
            $(this).attr('src', data[index].thumb)
            $(this).attr('data-type', data[index].type)
            $(this).attr('data-large', data[index].large)
          }
        });
      }

      function move_active_space () {
        var r = -1;

        do {
          r = Math.floor(Math.random() * 18);
        } while (r == window.active_space);

        window.active_space = r;
        var new_selector = 'div#item' + r;

        $('div.active').removeClass('active');
        $(new_selector).addClass('active');
      }

      function start_game () {
        window.active_set = -1;
        window.active_space = -1;

        $(document).unbind();

        $(document).keydown(function (e) {
          if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) {
            e.preventDefault();
            stop_game();
          }
        });

        $(document).bind('touchend', function (e) {
          e.preventDefault();
          stop_game();
        });

        $('#player_board')[0].play();

        window.interval_set = setInterval(function () { print_set(get_random_set(true)); }, 850);
        window.interval_space = setInterval(function () { move_active_space(); }, 500);
      }

      function stop_game () {
        clearInterval(interval_set);
        clearInterval(interval_space);

        $(document).unbind();

        $(document).keydown(function (e) {
          if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) {
            e.preventDefault();
            load_game_board();
          }
        });

        $(document).bind('touchend', function (e) {
          e.preventDefault();
          load_game_board();
        });

        var winning_cell = 'div#item' + window.active_space;

        $('#player_board')[0].pause();
        $('#player_board')[0].currentTime = 0;

        if ($(winning_cell + ' img').attr('data-type') == 'whammy') {
          $('#player_whammy')[0].play();
        } else {
          $('#player_buzz')[0].play();
        }

        $(winning_cell).removeClass('active').delay(100).queue(function (next) {
          $(this).addClass('active').delay(100).queue(function (next) {
            $(winning_cell).removeClass('active').delay(100).queue(function (next) {
              $(this).addClass('active').delay(100).queue(function (next) {
                $(winning_cell).removeClass('active').delay(100).queue(function (next) {
                  $('img#image_middle').attr('src', './images/space.gif');
                  $('img#image_middle').addClass('prize');

                  $(this).addClass('active').delay(100).queue(function (next) {
                    $(winning_cell).removeClass('active').delay(100).queue(function (next) {
                      $('img#image_middle').attr('src', $(winning_cell + ' img').attr('data-large'));

                      $(this).addClass('active').delay(100).queue(function (next) {
                        $(winning_cell).removeClass('active').delay(100).queue(function (next) {
                          $(this).addClass('active').delay(100).queue(function (next) {
                            next();
                          });
                          next();
                        });
                        next();
                      });
                      next();
                    });
                    next();
                  });
                  next();
                });
                next();
              });
              next();
            });
            next();
          });
          next();
        });
      }

      function array_shuffle (orig_array) {
        var shuffled_array = orig_array.slice();
        var len = shuffled_array.length;
        var i = len;
        while (i--) {
          var p = parseInt(Math.random()*len);
          var t = shuffled_array[i];
          shuffled_array[i] = shuffled_array[p];
          shuffled_array[p] = t;
        }
        return shuffled_array; 
      };
    //]]>
    </script>
  </body>
</html>

Get the basic stuff out of the way… We import our CSS. We import jQuery UI. We lay out the game board and we set up some audio elements for the game sounds (which I pulled from some site that had all sorts of game show sounds archived, I can’t remember where it was).

$(document).ready(function () {
  var images = define_board_images();
  window.game_data = build_game_boards(images);
  load_game_board();
});

The first thing we do is initialize some stuff. Define our board images, build our possible game boards, throw a board onto the screen. Now let’s see how we do that.

function define_board_images () {
  var images = new Array();

  $.ajax({
    url: 'config.xml',
    dataType: 'xml',
    async: false,
    success: function (data) {
      $($(data)).find('image').each(function () {
        images.push({'thumb': $(this).find('thumb').text(), 'large': $(this).find('large').text(), 'type': $(this).find('type').text()});
      });
    }
  });

  return images;
}

We’re loading that config file, then looping through each “image” element to find the “thumb”, “large”, and “type” definitions we discussed earlier. Then we’re dropping those into an array.

When I wrote this I was shocked that there wasn’t an easier way to do this using XML. If it were similarly-structured JSON, it’d just parse automatically. Instead I have to do it manually. Considering what the X in AJAX stands for, I expected more out-of-the-box support for XML. Maybe I’m just missing something.

function build_game_boards (images) {
  var boards = new Array();

  do {
    var set = new Array();

    do {
      images = array_shuffle(images);
      var n = 0;

      do {
        set.push(images[n]);
        n++;
      } while ((set.length < 18) && (n < images.length));
    } while (set.length < 18);

    boards.push(set);
  } while (boards.length < 50);

  return boards;
}

With our available images defined, we cache a set of fifty possible game boards. We do this by shuffling the array of images (using a function I just grabbed from somewhere else) and adding them in order to a new set until there are 18 in that set. If we run out before we get to 18, we shuffle again and keep going. This means we can have as many or as few (as long as there’s at least one) images configured.

function load_game_board () {
  $('div#board div.active').removeClass('active');
  $('img#image_middle').removeClass('prize');
  $('img#image_middle').attr('src', './images/logo.jpg');

  window.active_set = -1;
  window.active_space = -1;

  print_set(get_random_set(false));

  $(document).keydown(function (e) {
    if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) {
      e.preventDefault();
      start_game();
    }
  });

  $(document).bind('touchend', function (e) {
    e.preventDefault();
    start_game();
  });
}

Finally we load the game board. We make sure no tiles are active, we set the middle image back to our placeholder, we get a randomly-selected one of our cached tile sets and display it on the board. Then we define some key events that allow the game to be controlled from the keyboard or from a presentation mouse, so that any event will trigger the start of the game. We bind the same action on touchend so that the person who commissioned this can play on her phone.

function get_random_set (no_repeat) {
  var r = -1;

  if (no_repeat) {
    do {
      r = Math.floor(Math.random() * window.game_data.length);
    } while (r == window.active_set);
    window.active_set = r;
  } else {
    r = Math.floor(Math.random() * window.game_data.length);
  }
  return window.game_data[r];
}

Our function for getting a random set is simple enough. Get a random number from 0 to the size of the set (should always be 50). If we don’t want to allow the same set to be picked twice in a row, compare that number to the current one and do it again until we get something different. Return the set of images with that number as the key.

function print_set (data) {
  $('div#board div img').each(function (index) {
    if ($(this).attr('id') != 'image_middle') {
      $(this).attr('src', data[index].thumb)
      $(this).attr('data-type', data[index].type)
      $(this).attr('data-large', data[index].large)
    }
  });
}

To print out the board, we loop through each image on the board that isn’t the one in the middle. We use the index of the image and pull from the array we set in get_random_set() to reset said image’s attributes.

function start_game () {
  window.active_set = -1;
  window.active_space = -1;

  $(document).unbind();

  $(document).keydown(function (e) {
    if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) {
      e.preventDefault();
      stop_game();
    }
  });

  $(document).bind('touchend', function (e) {
    e.preventDefault();
    stop_game();
  });

  $('#player_board')[0].play();

  window.interval_set = setInterval(function () { print_set(get_random_set(true)); }, 850);
  window.interval_space = setInterval(function () { move_active_space(); }, 500);
}

Ahh, yes, now we start the actual gameplay. We wipe out all of the events we set earlier and set new ones on the same triggers, this time for stopping the game. We start playing our in-game music. Then we set an interval to reload the game board every 850 milliseconds (allowing for the same board to be played twice in a row this time) and for the active tile to shift every half-second. I got those numbers from watching way too much Press Your Luck.

function move_active_space () {
  var r = -1;

  do {
    r = Math.floor(Math.random() * 18);
  } while (r == window.active_space);

  window.active_space = r;
  var new_selector = 'div#item' + r;

  $('div.active').removeClass('active');
  $(new_selector).addClass('active');
}

How do we switch the active tile? Well we know there are 18 tiles so we randomly select a number 0 to 17 until that number is not the same as the one we’ve already got. Then we remove the active class from whatever tile is active and add it to the one that corresponds to our randomly-selected number.

Our last step is to stop the game and it’s made up of a bunch of little things.

clearInterval(interval_set);
clearInterval(interval_space);

$(document).unbind();

$(document).keydown(function (e) {
  if ((e.keyCode == 32) || (e.keyCode == 33) || (e.keyCode == 34) || (e.keyCode == 66)) {
    e.preventDefault();
    load_game_board();
  }
});

$(document).bind('touchend', function (e) {
  e.preventDefault();
  load_game_board();
});

First we clear our intervals so the game won’t continue, then we wipe out our event bindings and set up new ones for the same triggers. These new ones will reset the game board and get us in a position to start a new game.

var winning_cell = 'div#item' + window.active_space;

$('#player_board')[0].pause();
$('#player_board')[0].currentTime = 0;

if ($(winning_cell + ' img').attr('data-type') == 'whammy') {
  $('#player_whammy')[0].play();
} else {
  $('#player_buzz')[0].play();
}

We get the winning tile and stop the in-game music. Based on what type of image that winning tile is, we play either the “buzz-in” sound or the “whammy” sound.

$(winning_cell).removeClass('active').delay(100).queue(function (next) {
  $(this).addClass('active').delay(100).queue(function (next) {
    $(winning_cell).removeClass('active').delay(100).queue(function (next) {
      $(this).addClass('active').delay(100).queue(function (next) {
        $(winning_cell).removeClass('active').delay(100).queue(function (next) {
          $('img#image_middle').attr('src', './images/space.gif');
          $('img#image_middle').addClass('prize');

          $(this).addClass('active').delay(100).queue(function (next) {
            $(winning_cell).removeClass('active').delay(100).queue(function (next) {
              $('img#image_middle').attr('src', $(winning_cell + ' img').attr('data-large'));

              $(this).addClass('active').delay(100).queue(function (next) {
                $(winning_cell).removeClass('active').delay(100).queue(function (next) {
                  $(this).addClass('active').delay(100).queue(function (next) {
                    next();
                  });
                  next();
                });
                next();
              });
              next();
            });
            next();
          });
          next();
        });
        next();
      });
      next();
    });
    next();
  });
  next();
});

This is how we make the lights around the winning tile flash and it’s ugly. We add and remove the “active” class from that tile in 100 millisecond intervals. Partway through that, we change the center image on the game board to match that of the winning tile. Again, those times were selected from watching way too much Press Your Luck.

And that’s really all there is to it.  There may be a better way by now (I hope there is for that flashing bit) but this is what I knew at the time.  It was a lot of fun to write and it was a lot of fun to see people play.

Linking Child and Parent Cards on Trello – Part 2

Last week I published a bit of code that uses the Trello API to keep parent and child cards synced across a set of boards.  It was a little piece of research that has absolutely taken off around the office so I’ve been expanding on it and demoing it and talking about it and generally losing my mind.

The thing I expanded on most is a flaw that appeared in my original script whereby a user could create a child card outside of the normal workflow and it would never be linked to the parent card.  Obviously “outside of the normal workflow” means it’s already an edge case but that doesn’t mean it’s as uncommon as we’d like, so I came up with a way to handle it.  It does rely on the child card being tagged with the same card tag as the parent but it’s better than nothing.

#!/usr/bin/php -q
<?php
  $trello = new trello_api($GLOBALS['config']['key'], $GLOBALS['config']['secret'], $GLOBALS['config']['token']);

  // LOOP THROUGH EACH CARD ON THE WIP BOARD TO GET THE PARENT CARD TAGS
  $parent = array();

  $card_data = $trello->request('GET', ('/1/boards/' . $GLOBALS['config']['board']['wip_prototype']['id'] . '/cards'));
  foreach ($card_data AS $card) {
    $card_tag = '';
    if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
      $card_tag = $matches[1];
    }

    if ($card_tag) {
      $parent[$card_tag] = array('id' => $card->id, 'url' => $card->shortUrl, 'label' => $card->labels[0]->color);
    }
  }

  // LOOP THROUGH EACH CHILD BOARD
  foreach ($GLOBALS['config']['child_boards'] AS $board_name) {
    // LOOP THROUGH ALL THE CARDS ON THE BOARD
    $card_data = $trello->request('GET', ('/1/boards/' . $GLOBALS['config']['board'][$board_name]['id'] . '/cards'));
    foreach ($card_data AS $card) {
      if (!preg_match('|Parent Card: https://trello.com/c/(.{8})|is', $card->desc)) {
        // NO PARENT CARD LINKED
        $card_tag = '';
        if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
          $card_tag = $matches[1];
        }

        if ($card_tag) {
          // WE'VE GOT A TAG, LET'S USE IT
          if (is_array($parent[$card_tag])) {
            // WE KNOW THE PARENT THIS BELONGS TO

            // UPDATE THE CHILD CARD
            $trello->request('PUT', ('/1/cards/' . $card->id), array('desc' => (trim('Parent Card: ' . $parent[$card_tag]['url'] . ' ' . $card->desc))));

            // UPDATE THE PARENT CARD LABEL IF NECESSARY
            if (!$parent[$card_tag]['label']) {
              $trello->request('PUT', ('/1/cards/' . $parent[$card_tag]['id'] . '/labels'), array('value' => $GLOBALS['config']['label'][$board_name]));
            }

            // GET THE SLICES CHECKLIST
            $checklist_id = 0;
            $checklist_data = $trello->request('GET', ('/1/cards/' . $parent[$card_tag]['id'] . '/checklists'));
            foreach ($checklist_data AS $checklist) {
              if ($checklist->name == 'Slices') {
                $checklist_id = $checklist->id;
              }
            }

            if (!$checklist_id) {
              // NO SLICES CHECKLIST FOUND, MAKE ONE
              $new_checklist = $trello->request('POST', ('/1/cards/' . $parent[$card_tag]['id'] . '/checklists'), array('name' => 'Slices'));
              $checklist_id = $new_checklist->id;
            }

            // ADD THE CHECKLIST ITEM TO THE SLICES CHECKLIST
            $trello->request('POST', ('/1/cards/' . $parent[$card_tag]['id'] . '/checklist/' . $checklist_id . '/checkItem'), array('name' => $card->shortUrl));
          }
        }
      }
    }
  }

  unset($trello);
?>

As with my previous post, this uses my Trello API wrapper class and pulls in the $GLOBALS[‘config’] array of configuration values from another file.  Also as with my previous post I think it’s commented pretty well but we’re going through the code piece-by-piece anyway.

// LOOP THROUGH EACH CARD ON THE WIP BOARD TO GET THE PARENT CARD TAGS
$parent = array();

$card_data = $trello->request('GET', ('/1/boards/' . $GLOBALS['config']['board']['wip_prototype']['id'] . '/cards'));
foreach ($card_data AS $card) {
  $card_tag = '';
  if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
    $card_tag = $matches[1];
  }

  if ($card_tag) {
    $parent[$card_tag] = array('id' => $card->id, 'url' => $card->shortUrl, 'label' => $card->labels[0]->color);
  }
}

We loop through all of the cards on our Work in Progress (“WIP”) board and use a regular expression to see if they have a card tag as a prefix (appearing in the pattern of “[TEST4] Test Project 4”). If the card does, we save an array of data about the parent to an array of parent cards for reference by card tag later.

// LOOP THROUGH EACH CHILD BOARD
foreach ($GLOBALS['config']['child_boards'] AS $board_name) {
  // LOOP THROUGH ALL THE CARDS ON THE BOARD
  $card_data = $trello->request('GET', ('/1/boards/' . $GLOBALS['config']['board'][$board_name]['id'] . '/cards'));
  foreach ($card_data AS $card) {
    if (!preg_match('|Parent Card: https://trello.com/c/(.{8})|is', $card->desc)) {

Then we loop through our list of child board names and get every card on that board using a GET request to /1/board/xxxxxx/cards (where xxxxxx is the board ID).  If the card’s description doesn’t match our convention for linking back to a parent, we know we’ve found a rogue card.

$card_tag = '';
if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
  $card_tag = $matches[1];
}

if ($card_tag) {
  // WE'VE GOT A TAG, LET'S USE IT
  if (is_array($parent[$card_tag])) {
    // WE KNOW THE PARENT THIS BELONGS TO

We check to see if the card has a tag in its name, using the same regular expression as we did earlier.  If it does, we can use it to move forward. If we know the parent that tag belongs to, we can do even more.

// UPDATE THE CHILD CARD
$trello->request('PUT', ('/1/cards/' . $card->id), array('desc' => (trim('Parent Card: ' . $parent[$card_tag]['url'] . ' ' . $card->desc))));

The first thing we do is fire off a PUT request to /1/cards/yyyyyy (where yyyyyy is the ID of the rogue card) with desc set to the current description with our parent link prepended to it.  This gives our child card the necessary link to the parent.

// UPDATE THE PARENT CARD LABEL IF NECESSARY
if (!$parent[$card_tag]['label']) {
  $trello->request('PUT', ('/1/cards/' . $parent[$card_tag]['id'] . '/labels'), array('value' => $GLOBALS['config']['label'][$board_name]));
}

On the off chance that the parent card doesn’t have a label, we use the fact that we already know what board the child card is on to set one.  That involves a PUT request to /1/cards/zzzzzz/labels (where zzzzzz is the ID of the parent card) with value set to the color name of the label that corresponds to the board.

// GET THE SLICES CHECKLIST
$checklist_id = 0;
$checklist_data = $trello->request('GET', ('/1/cards/' . $parent[$card_tag]['id'] . '/checklists'));
foreach ($checklist_data AS $checklist) {
  if ($checklist->name == 'Slices') {
    $checklist_id = $checklist->id;
  }
}

Then we get ID of the parent card’s “Slices” checklist, as that’s where the parent card links to each of it’s children.  We make a GET request to /1/cards/zzzzzz/checklists and loop through each one until we find the one with the right name, then save that ID off for later.

if (!$checklist_id) {
  // NO SLICES CHECKLIST FOUND, MAKE ONE
  $new_checklist = $trello->request('POST', ('/1/cards/' . $parent[$card_tag]['id'] . '/checklists'), array('name' => 'Slices'));
  $checklist_id = $new_checklist->id;
}

What if we didn’t get a checklist ID?  Then we make one.  We fire off a POST request to /1/cards/zzzzzz/checklists with name set to “Slices” and that gives us back a bunch of data about a newly-created checklist.  We save off the new checklist ID so we can move forward.

// ADD THE CHECKLIST ITEM TO THE SLICES CHECKLIST
$trello->request('POST', ('/1/cards/' . $parent[$card_tag]['id'] . '/checklist/' . $checklist_id . '/checkItem'), array('name' => $card->shortUrl));

And our last step of the loop is to link the parent card to the rogue child.  We fire off a POST request to /1/cards/zzzzzz/checklist/cccccc/checkItem (where cccccc is the “Slices” checklist ID) with name set to the URL of the rogue child card.  Trello’s interface will convert that to the name of the child card when the parent card is viewed.

As I mentioned in my previous post, this is my first pass and I’m sure there’s a better way to do this.  This fixes a gap in that earlier implementation, though, so obviously iterating on it is working.

Linking Child and Parent Cards on Trello via the Trello API

One of the things my team at work always seems to have a problem with is tracking our tasks and how they fit into the bigger picture of our team’s goals.  We had these problems when we were using a bulletin board, we have these problems now that we’re using a single Trello board.

We have a subset of the team currently researching new tools due to the perceived limitations of Trello. Being a developer and having never met a problem that couldn’t be solved by throwing code at it, I decided to see what could be done with the Trello API.

I’ve written about using the Trello API a couple times before.  It’s a powerful tool and it’s something I’ve had a lot of fun with.  This builds off of that work.

One thing that was the lynch-pin for figuring all of this out is the difference between a card’s ID, shortID, and shortLink.  Because shortLink isn’t returned by default when you make a request for a card’s information, I thought that it was just another term for shortID, much as how archived and closed are synonyms within the Trello environment.  It turns out that that’s not true, and since shortLink is exposed in every Trello URL, it opens up a lot of options.

Before getting into the code too much, though, a look at the problem we’re trying to solve…

Right now my team has one “Work in Progress” (or “WIP”) board with ten functional columns.  Requests Not Started, Requests In Progress, Slices Not Started, Slices In Progress, Ready for QA on DEV, QA on Dev, Need to Publish to Stage, QA on Stage, Ready for Publish, and Published.

Requests are large-scale projects.  Slices are parts of a request, which are supposed to be individually-shippable.

When you pick up a request that hasn’t started yet, your first task is to break it into slices.  Those slices then march across the board and when they’re all published the request card jumps over to join them (currently a manual process).  The idea is that the slices show the team a little bit more of what’s in progress while the requests are geared towards external stakeholders.

The problem with that is that it makes for a messy board.  If only we could have a WIP board just for requests, and have the slices flowing along some other path, but still tied back to the request card.  So that’s what I did.

Here’s my informal demo video, with code to follow.

Okay, now on to the code.

#!/usr/bin/php -q
<?php
  $trello = new trello_api($GLOBALS['config']['key'], $GLOBALS['config']['secret'], $GLOBALS['config']['token']);

  // LOOP THROUGH EACH CARD ON THE REQUESTS IN PROGRESS LIST
  $card_data = $trello->request('GET', ('/1/lists/' . $GLOBALS['config']['board']['wip_prototype']['rip_list'] . '/cards'));
  foreach ($card_data AS $card) {
    // GET THE CHILD BOARD
    $child_board_name = '';
    foreach ($GLOBALS['config']['label'] AS $board => $color) {
      if ($card->labels[0]->color == $color) {
        $child_board_name = $board;
      }
    }

    // GET THE CARD TAG
    $card_tag = '';
    if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
      $card_tag = '[' . $matches[1] . '] ';
    }    

    // GET THE CHILD TASKS / CARDS
    $children = 0;
    $children_completed = 0;

    foreach ($card->idChecklists AS $checklist_id) {
      $checklist_data = $trello->request('GET', ('/1/checklists/' . $checklist_id));

      if ($checklist_data->name == 'Slices') {
        foreach ($checklist_data->checkItems AS $item) {
          if (preg_match('|https://trello.com/c/(.{8})|is', $item->name, $matches)) {
            // WE HAVE THE CHILD CARD, CHECK IT
            $child_card = $trello->request('GET', ('/1/cards/' . $matches[1]));

            if ($child_card->id) {
              // CARD STILL EXISTS
              $child_card_state = 'incomplete';
              if (($child_card->closed) OR ($child_card->idList == $GLOBALS['config']['board']['qa_prototype']['completed_list'])) {
                $child_card_state = 'complete';
                $children_completed++;
              }

              // UPDATE THE STATUS IF NECESSARY
              if ($child_card_state != $item->state) {
                $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => $child_card_state));
              }

              $children++;
            } else {
              // CARD HAS BEEN DELETED, REMOVE THE CHECKITEM
              $trello->request('DELETE', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id));
            }
          } else {
            // CARD HASN'T BEEN CREATED YET, DO IT IF WE HAVE A LIST TO POST TO
            if ($GLOBALS['config']['board'][$child_board_name]['ns_list']) {
              $new_card = $trello->request('POST', '/1/cards', array('name' => ($card_tag . $item->name), 'idList' => $GLOBALS['config']['board'][$child_board_name]['ns_list'], 'desc' => ('Parent Card: ' . $card->shortUrl)));

              if ($new_card->id) {
                // UPDATE THE CHECKLIST ITEM WITH THE LINK TO THE NEW CARD
                $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/name'), array('value' => $new_card->shortUrl));
                $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => 'incomplete'));
              }

              $children++;
            }
          }
        }
      }
    }

    if (($children > 0) AND ($children == $children_completed)) {
      $trello->request('PUT', ('/1/cards/' . $card->id . '/'), array('pos' => 'top', 'idList' => $GLOBALS['config']['board']['wip_prototype']['completed_list']));
    }
  }

  unset($trello);
?>

This makes use of my Trello API wrapper class and has several values (all part of the $GLOBALS[‘config’] array) dumped off to an included configuration file.  I think the comments are pretty good, but let’s go through this a little bit of code at a time.

The whole thing is wrapped in a loop through the cards on the Requests in Progress column of the main WIP board.  We get that by doing a GET request to /1/lists/xxxxxx/cards (where xxxxxx us the list’s ID) and I won’t bother detailing it any further.

// GET THE CHILD BOARD
$child_board_name = '';
foreach ($GLOBALS['config']['label'] AS $board => $color) {
  if ($card->labels[0]->color == $color) {
    $child_board_name = $board;
  }
}

// GET THE CARD TAG
$card_tag = '';
if (preg_match('|[(.*)](.*)|is', $card->name, $matches)) {
  $card_tag = '[' . $matches[1] . '] ';
}

We figure out which board the child cards should go to by looking at the labels and our configured array that matches label colors to child boards.  In this example, the green label belongs to the ecommerce board and the red one to the marketing board.  If the first label on the card matches the color of one of those boards, we’ve got our board.

Then we take a look at the parent card name and look for anything inside square brackets using a regular expression.  That’s the tag that should be applied to child cards, something we use for filtering.

We set the number of children on this card and the number of completed children to zero, values we’ll update as we go.  Then we loop through the checklist IDs we got when we looked up the card data.

$checklist_data = $trello->request('GET', ('/1/checklists/' . $checklist_id));

if ($checklist_data->name == 'Slices') {
  foreach ($checklist_data->checkItems AS $item) {

We use a GET request to /1/checklists/yyyyyy (where yyyyyy is the ID of our checklist) to get more info about that checklist.  If the name of the checklist is “Slices” then we iterate through each item because we know those represent our child cards.

if (preg_match('|https://trello.com/c/(.{8})|is', $item->name, $matches)) {
  // WE HAVE THE CHILD CARD, CHECK IT
  $child_card = $trello->request('GET', ('/1/cards/' . $matches[1]));

  if ($child_card->id) {
    // CARD STILL EXISTS
    $child_card_state = 'incomplete';
    if (($child_card->closed) OR ($child_card->idList == $GLOBALS['config']['board']['qa_prototype']['completed_list'])) {
      $child_card_state = 'complete';
      $children_completed++;
    }

    // UPDATE THE STATUS IF NECESSARY
    if ($child_card_state != $item->state) {
      $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => $child_card_state));
    }

    $children++;

If the text of the checklist item matches the pattern of a Trello card URL, we know it’s an already-linked child.  We get the shortLink from that URL using our regular expression and then make a GET request to /1/cards/zzzzzz, where zzzzzz is that shortLink.  If the card has an ID it means it still exists, so we can do some work with it.

By default, we set the card’s status to incomplete, which is one of the states of a checkItem in Trello. If the card is closed or is in the Published list on our QA board, we know work has been completed and the state can be set to complete. We can also increment our counter of completed child cards by one.  If the state we’ve determined doesn’t match that of the checkItem, we fire off a PUT request to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa/state (wherre cccccc is the card ID, bbbbbb is the checklist ID and aaaaaa is the ID of the specific checklist item), passing in a value set to whatever we determined above.  This updates our checklist item to match the state of the card itself.  Then we increment the counter of children by one.

} else {
  // CARD HAS BEEN DELETED, REMOVE THE CHECKITEM
  $trello->request('DELETE', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id));
}

If the card’s ID wasn’t found, we know the card has been deleted.  In that case we fire a DELETE request to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa to completely remove the related checklist item.

} else {
  // CARD HASN'T BEEN CREATED YET, DO IT IF WE HAVE A LIST TO POST TO
  if ($GLOBALS['config']['board'][$child_board_name]['ns_list']) {
    $name = $card_tag . $item->name;
    $desc = 'Parent Card: ' . $card->shortUrl;
    $new_card = $trello->request('POST', '/1/cards', array('name' => $name, 'idList' => $GLOBALS['config']['board'][$child_board_name]['ns_list'], 'desc' => $desc));

    if ($new_card->id) {
      // UPDATE THE CHECKLIST ITEM WITH THE LINK TO THE NEW CARD
      $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/name'), array('value' => $new_card->shortUrl));
      $trello->request('PUT', ('/1/cards/' . $card->id . '/checklist/' . $checklist_data->id . '/checkItem/' . $item->id . '/state'), array('value' => 'incomplete'));
    }

    $children++;
  }
}

Now we’re back to if we didn’t find a URL in the checklist item’s name.  In that case, we know the child card hasn’t been created yet so we go on to do that, assuming we actually have a list ID to work with (just in case there’s no label on the parent card or something).

We build the child card’s name by combining the text of the list item with the card tag we defined earlier, then we build the description by just linking to the parent card.  Then we fire that data (along with the list ID) off via a POST request to /1/cards.

That request returns data about the newly-created card.  If the card was successfully created (determined by whether or not we got an ID back), we go back to the checklist item.  We update the name to be a link to the new card, which displays in Trello as that card’s name, and we make sure to set the status to incomplete just in case someone did something stupid.  That’s done with a pair of PUT requests, one to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa/name with value set to the new card’s shortUrl, the other to /1/cards/cccccc/checklist/bbbbbb/checkItem/aaaaaa/state with value set to incomplete.

At this point we’ve looped through every item on the “Slices” checklist for a single card.

if (($children > 0) AND ($children == $children_completed)) {
  $trello->request('PUT', ('/1/cards/' . $card->id . '/'), array('pos' => 'top', 'idList' => $GLOBALS['config']['board']['wip_prototype']['completed_list']));
}

The last thing we do before moving on to the next card is compare our counts of children and completed children. To account for the fact that a card may have been moved to Requests in Progress before the “Slices” checklist was completed, we require at least one child to exist.  If there are children and they’ve all been completed, we move the parent card over to the Published column of our WIP board with a PUT request to /1/cards/dddddd, where dddddd is the parent card ID, with the pos defined as top and the idList defined as the ID of the published column.

As I mentioned in the video, I’ve got this running once a minute as a cronjob.  I’m sure there’s a better way to do it but this is my first pass so I’m not worried about it.

CivClicker Type Availability User Script

Awhile ago I wrote about how I’d gotten addicted to the Cookie Clicker game to the point that I made a userscript that would display a countdown showing how long it would take for me to get to a certain amount of cookies.

The same people that pullled me into that game tried to get me into a new one, CivClicker.  I think I’ve managed to avoid getting pulled in too far but not before noticing an issue and writing a user script to handle it, which I figured I’d share.

In the game there are several classes of job you can assign your people to.  Six of them require there be enough space in a class of building to hold them.  Ten soldiers for each barracks built, for example.  The problem is that nothing shows how much space you have available, so you might intend to assign a bunch of soldiers only to be unable to and not realize why.  My script provides that information.

As with the Cookie Clicker one, this user script is available for download.  I’ll break it down here, though.

One caveat: There are classes that require not only space in buildings but other resources be available to assign people to them.  I didn’t take that into consideration, this is just about space.

// ==UserScript==
// @name          CivClicker Type Availability Display
// @version       1.0.0
// @namespace     http://www.clarkrasmussen.com/
// @description   See how many of a specific type of character you have the resources to make.
// @include       http://dhmholley.co.uk/*
//
// ==/UserScript==

const VERSION = "1.0.0";

window.addEventListener('load', function() {
	exec(function () {

Just like the Cookie Clicker script, we start with some basic stuff. On page load we run exec() which accepts a function and is defined below.

// write out our elements
var use_rows = ['tannergroup', 'blacksmithgroup', 'apothecarygroup', 'clericgroup', 'soldiergroup', 'cavalrygroup'];
for (var n = 0; row = n < document.getElementById('jobs').getElementsByTagName('tr').length; n++) {
	var row_id = document.getElementById('jobs').getElementsByTagName('tr')[n].getAttribute('id');
	if (use_rows.indexOf(row_id) > -1) {
		document.getElementById('jobs').getElementsByTagName('tr')[n].getElementsByTagName('td')[6].innerHTML += '&nbsp;(<span id="' + row_id.replace('group', 'Available') + '">0</span>)';
	}
}

In CivClicker there is a table of all of the job types. Each row has an ID matching the naming convention of <type>group. We create an array of the six types we care about, then we loop through the table rows. On the ones that match a name we’re looking for, we add a span to the sixth field (the one that shows how many you already have of that particular type). Through some string manipulation, we give that span the ID of <type>Available so we can reference it later.

// do the check every second
setInterval(function () {
	update_availability();
}, 1000);

We set the update_availability() function to run every second.  This means there can be some lag but I didn’t think this needed to run more often.

function update_availability () {
	document.getElementById('tannerAvailable').innerHTML = prettify(tannery.total - (population.tanners + population.tannersIll));
	document.getElementById('blacksmithAvailable').innerHTML = prettify(smithy.total - (population.blacksmiths + population.blacksmithsIll));
	document.getElementById('apothecaryAvailable').innerHTML = prettify(apothecary.total - (population.apothecaries + population.apothecariesIll));
	document.getElementById('clericAvailable').innerHTML = prettify(temple.total - (population.clerics + population.clericsIll));
	document.getElementById('soldierAvailable').innerHTML = prettify((barracks.total * 10) - (population.soldiers + population.soldiersIll + population.soldiersParty));
	document.getElementById('cavalryAvailable').innerHTML = prettify((stable.total * 10) - (population.cavalry + population.cavalryIll + population.cavalryParty));
}

We see that update_availability() is what actually finds the number of each class that you can have, then subtracts what you already have and displays that in the spans we established earlier.  I’d like to be able to get this down a little bit, we should be able to loop through each class, but since the game itself doesn’t have a list of all the classes available I didn’t bother to expand on that either.  The prettify() function is from the game, however, as I figured I should display numbers the same way they do.

	});
});

function exec (fn) {
	var script = document.createElement('script');
	script.setAttribute('type', 'application/javascript');
	script.textContent = '(' + fn + ')();';
	document.body.appendChild(script);
	document.body.removeChild(script);
}

Last we wrap things up and define the exec() function, which allows us to do this all via script injection, giving access to the game-defined variables.

It’s hardly revolutionary but is another thing I wanted to throw out there.  For the record, your screen ends up looking like this (when you have the font size turned down):

civclicker

Christmas Name Drawing Generator

Every year my in-laws do the whole “drawing names” thing at Christmas, where each person draws one other person and buys a gift for them.  Nearly as often, there’s drama about it.  Someone complains about “always” getting someone else.  Someone gets themself (that was me this year).

I decided to remove the human element from it and make a little tool that would do the draw for us.  As my wife said, “Just because you write an app doesn’t mean things are gonna change.”  It was a good little exercise, though.

To make it more of an exercise, I did the entire thing on the client-side.  I do enough server-side work that it would have been really easy to fall back on that and not learn anything.  It’s a little sloppy but it works.

On to the code…

<div id="name_list">
  <h3>Names</h3>

  <ul>
  </ul>
</div>

<h3>Add a New Name</h3>

<form action="#" id="name_form" method="get" class="general">
<div>
  <label for="new_name">New Name:</label>
  <input type="text" name="new_name" id="new_name" />
</div>
<div class="checkbox">
  <input type="checkbox" id="allow_self_selection" />
  <label for="allow_self_selection">Allow Self Selection?</label>
</div>
<div id="buttons">
  <input type="submit" id="add_name" value="Add" />
  <input type="button" id="generate" value="Generate" disabled="disabled" />
  <input type="button" id="clear_list" value="Clear List" disabled="disabled" />
</div>
</form>

I start with a div that contains a header and an empty list. This div is hidden on page load and will contain the list of names as they’re added. Then I’ve got a form for adding names to the list, clearing the list, generating the drawn list, and for determining whether or not we’re going to allow people to draw themselves. That’s it for the page content, everything else is JavaScript/jQuery.

var names = new Array();

$(document).ready(function() {
  $('div#name_list').hide();
});

$('form#name_form').submit(function(e) {
  e.preventDefault();
  add_name();
});

I start out by giving myself a global array called names to hold the drawn names. As previously mentioned, on page load I hide the div containing the names list. On submit of the form I run a function called add_name().

$('input#clear_list').click(function() {
  $('div#name_list ul').html('');
  $('div#name_list').hide();
  $('input#generate').attr('disabled', 'disabled');
  $('input#clear_list').attr('disabled', 'disabled');
  $('input#add_name').removeAttr('disabled');
});

To clear the name list I wipe out the contents of that particular UL and re-hide the div containing it. Then I make sure to disable the buttons for generating a list and clearing the list (since there’s no list to work with) and enable the ability to add names.

$('input#generate').click(function() {
  $('input#add_name').attr('disabled', 'disabled');
  $('input#generate').attr('disabled', 'disabled');
  $('input#clear_list').attr('disabled', 'disabled');

  populate_list();

  $('input#generate').removeAttr('disabled');
  $('input#clear_list').removeAttr('disabled');
});

To generate the list I have a populate_list() function wrapped in button enabling/disabling. All of the buttons are disabled, then populate_list() runs, then we re-enable the Generate and Clear List buttons.

function add_name () {
  var new_name = $('input#new_name').val().trim();
  if (new_name) {
    $('div#name_list').show();
    $('div#name_list ul').append('<li><span class="giver">' + new_name + '</span><span class="arrow"></span><span class="recipient"></span></li>');
  }

  if ($('div#name_list ul li span.giver').length > 0) {
    $('input#clear_list').removeAttr('disabled');
  }

  if ($('div#name_list ul li span.giver').length > 1) {
    $('input#generate').removeAttr('disabled');
  }

  $('input#new_name').val('');
}

Okay, some actual functionality. The add_name() function is what actually puts an entered name into the list. First we get the value of the new_name input field and trim it. If there’s actually something there, we show the name list container (in case it wasn’t already visible) and add this new name to the list as a list item, with some extra spans for structure. The name itself is in a span with the giver class while there are empty spans with classes of arrow and recipient.

Then I check the length of the name list. If there are any names (as there should be since we just added one), the ability to clear the list becomes available. If there’s more than one, the ability to generate a list from them is open. Then I clear out the contents of the new_name input field.

function populate_list () {
  names = new Array();
  $('div#name_list ul li span.giver').each(function() {
    names.push($(this).html().replace('<span class="giver">', '').replace('</span>', ''));
  });

  names = shuffle(names);

  $('div#name_list ul li span.giver').each(function() {
    var name = $(this).html().replace('<span class="giver">', '').replace('</span>', '');

    if ((name == names[0]) && (!$('input#allow_self_selection').is(':checked')) && (names.length == 1)) {
      populate_list();
    } else {
      do {
        names = shuffle(names);
      } while ((name == names[0]) && (!$('input#allow_self_selection').is(':checked')));

      $(this).parent().children('span.arrow').html('&raquo;');
      $(this).parent().children('span.recipient').html(names[0]);
      names.shift();
    }
  });
}

Now we’ve got the actual meat of the list generation, the populate_list() function. We start by blanking out the array of names (just in case), then we loop through the contents of the name list UL to re-populate it. Stripping out the span surrounding the name itself should work by doing $($(this).html()).text() but it didn’t for me so I brute forced it and just replaced the span tags with nothing. Then we shuffle that list with a function I grabbed from StackOverflow.

I loop through the UL a second time to assign a recipient to each giver. I strip out the offending HTML again (I could have pulled that into a function but didn’t out of laziness).

If the only name in the names array is the same as the one we’re working on and we’re not allowing names to select themselves, we’ve hit a problem. In that case, we throw the whole thing out and brute-force it, re-running the populate_list() function.

If that’s not the case, I shuffle the names array until either the first name in the array is not the one we’re currently drawing for or people are allowed to draw themselves. For aesthetics I pop an arrow in the arrow span for the LI we’re working on, then I put the first name from the names array in the recipient span. I wrap it up by shifting the array to get that name out of the way.

There’s more brute-force than I’d like, defaulting to shuffling whenever we find a conflict, but it seems like a real-world name list would be around ten people and I don’t see an issue with performance at that level. No, it’s not the best code but it does what it needs to do.

Cookie Clicker Countdown User Script

Over the past week or so I’ve been a little too obsessed with Cookie Clicker. It’s an awesome little JavaScript-based game and I can’t just leave it alone.

I’m at a point in the game where it takes hours before I can buy the next item I’m looking for.  Just out of curiousity, I was doing the math to figure out exactly how long it would take at my current rate to hit certain milestones.  Since I was doing that so much, I decided to build a little user script I could install that would do the math for me and keep a running countdown going.

The script is available for download but I’m going to break it down a bit (in the spirit of those code samples I’ve talked so much about over the last couple days) here.

// ==UserScript==
// @name          CookieClicker Target Countdown
// @version       1.0.0
// @namespace     http://www.clarkrasmussen.com/
// @description   Enter a target amount of banked cookies and see the amount of time it will take to reach that amount at your current CPS
// @include       http://orteil.dashnet.org/cookieclicker/*
//
// ==/UserScript==

const VERSION = &amp;quot;1.0.0&amp;quot;;

window.addEventListener('load', function() {
	exec(function () {

We start with some pretty standard header stuff. On page load we run the exec() function, passing into it a function that wraps all of the stuff to continue. The exec() function is last in my code so I’ll get to it near the bottom of this.

		// write out our elements
		var output_div = document.createElement('div');
		output_div.setAttribute('id', 'cookieclicker_countdown_container');
		output_div.setAttribute('style', 'position: fixed; right: 30px; bottom: 20px; margin: 0; padding: 7px 15px; border: 1px solid #999; background-color: #ccc; color: #000;');

		var countdown_target_label = document.createElement('label');
		countdown_target_label.setAttribute('style', 'display: block; padding-bottom: .25em; font-size: 80%; font-weight: bold;');
		countdown_target_label.setAttribute('for', 'countdown_target_input');
		countdown_target_label.innerHTML = 'Target Cookie Value:';

		var countdown_target_input = document.createElement('input');
		countdown_target_input.setAttribute('style', 'display: block; font-size: 90%;');
		countdown_target_input.setAttribute('id', 'countdown_target_input');
		countdown_target_input.setAttribute('value', '0');

		var output_content = document.createElement('p');
		output_content.setAttribute('style', 'padding-top: .5em; text-align: center;');
		output_content.innerHTML = '00:00';

		output_div.appendChild(countdown_target_label);
		output_div.appendChild(countdown_target_input);
		output_div.appendChild(output_content);
		document.body.appendChild(output_div);

Next we define all of the HTML elements we need to actually display our output and we append them to the page. This includes a form element which we’ll use to define our target value.

		// do the check every second
		setInterval(function () {
			countdown_check();
		}, 1000);

Simple enough, run the countdown_check() function once a second.

		function calculate_countdown (countdown_target) {
			var cookies = Game.cookies;
			var cookiesps = Game.cookiesPs;
			var seconds = 0;
			var time = '';

			if (countdown_target &amp;gt; cookies) {
				if (cookiesps &amp;gt; 0) seconds = (countdown_target - cookies) / cookiesps;

				var days = Math.floor(seconds / 86400);
				var hours = Math.floor((seconds % 86400) / 3600);
				var minutes = Math.floor(((seconds % 86400) % 3600) / 60);
				seconds = Math.round(((seconds % 86400) % 3600) % 60);

				if (days &amp;gt; 0) {
					time = days + ':';

					if (hours &amp;gt; 0) {
						time += zero_pad(hours, 2);
					} else {
					 	time += '00';
					}

					time += ':';
				} else if (hours &amp;gt; 0) {
					time += hours + ':';
				}

				time += zero_pad(minutes, 2) + ':' + zero_pad(seconds, 2);
			} else {
				time = '00:00';
			}

			return time;
		}

In the calculate_countdown() function we get the actual time remaining to the target. We pass in the target value, then grab the amount of cookies the user has and their cookies per second from the Game object so we don’t have to get it out of any HTML elements. It’s already readily available, why not take advantage? Then we do the math to determine how many cookies we need to hit the target and how many seconds it will take to get there.

If the number of seconds is greater than zero, we do some math to break the time into days, hours, minutes and seconds. Then we reassemble that into a string. If the time has already passed, we return 00:00.

The zero_pad() function seen implemented here is defined below.

		function countdown_check () {
			var countdown_target = parseInt(document.getElementById('countdown_target_input').value);

			if (countdown_target &amp;gt; 0) {
				var time = calculate_countdown(countdown_target);
				output_content.innerHTML = time;
			}
		}

Here’s that countdown_check() function. It grabs the countdown target from the form element and, if it’s greater than zero, throws it off to calculate_countdown() to get the time. The time returned is then put into the output_content container.

		function zero_pad (num, length) {
			var s = num + '';
			while (s.length &amp;lt; length) s = '0' + s;
			return s;
		}

	});
});

function exec (fn) {
	var script = document.createElement('script');
	script.setAttribute('type', 'application/javascript');
	script.textContent = '(' + fn + ')();';
	document.body.appendChild(script);
	document.body.removeChild(script);
}

Last we define zero_pad() and close out the containers, as this has all been wrapped in AddEventListener() and exec(). The exec() function is then defined as what allows us to do this all via script injection, which is what gives us access to that helpful Game object.

Hardly anything groundbreaking, in fact I’d bet someone else has done this before.  Just another one of those things I thought I’d share.

For the record, this is what the countdown looks like on the page.  Small enough to not interfere with a golden (or red) cookie appearing in the same spot.

cookieclicker

Update – 10:20 PM, 10/1/2013: After some quick feedback I made two changes.  One extracted the time formatting logic into its own function, the aptly-named format_time(), while the other fixed a bug by moving the rounding in calculate_countdown().  Those two functions now look as follows:

function calculate_countdown (countdown_target) {
	var cookies = Game.cookies;
	var cookiesps = Game.cookiesPs;
	var seconds = 0;
	var time = '';

	if (countdown_target &amp;gt; cookies) {
		if (cookiesps &amp;gt; 0) seconds = Math.round((countdown_target - cookies) / cookiesps);

		var days = Math.floor(seconds / 86400);
		var hours = Math.floor((seconds % 86400) / 3600);
		var minutes = Math.floor(((seconds % 86400) % 3600) / 60);
		seconds = ((seconds % 86400) % 3600) % 60;

		time = format_time(days, hours, minutes, seconds);
	} else {
		time = '00:00';
	}

	return time;
}

function format_time (days, hours, minutes, seconds) {
	var time = '';

	if (days &amp;gt; 0) {
		time = days + ':';

		if (hours &amp;gt; 0) {
			time += zero_pad(hours, 2);
		} else {
			time += '00';
		}

		time += ':';
	} else if (hours &amp;gt; 0) {
		time += hours + ':';
	}

	return (time + zero_pad(minutes, 2) + ':' + zero_pad(seconds, 2));
}