Wednesday, July 15, 2026

Build a Memory Match Game on ServiceNow: A Fun Hackathon Project for Developers

Memory Match: a ServiceNow hackathon project banner

Most ServiceNow hackathon demos look the same: a catalog item with an approval flow, a Flow Designer automation that closes tickets a little faster, a dashboard nobody asked for. A few of us wanted to try something different for our next internal hackathon — could we build something that's actually fun to demo, while still exercising real Service Portal development skills? That question turned into a small browser game called Memory Match, built entirely as a native ServiceNow widget.

The idea itself is simple: a flip-card memory game with a live moves/time tracker and a saved leaderboard. What makes it a genuinely useful hackathon project isn't the game concept — it's that building one small, self-contained feature forces you through nearly every layer of Now Platform widget development: client-side state management, GlideRecord operations, ACL decisions, and the HTML/CSS/client/server split that every production widget shares.

From there, we sketched out a project plan the same way we would for any other hackathon build.

Project Plan for the Memory Match Widget

1. Requirements Gathering

  • Understand what a Service Portal widget can and can't do out of the box.
  • Define what the game needs to actually work:
    • A card grid that flips and checks for matches, entirely client-side for responsiveness.
    • A moves counter and a running timer.
    • A win state that lets the player save their score.
    • A persisted leaderboard, stored server-side so it survives page reloads and is visible to everyone.
    • A custom table to hold scores, since nothing in the base schema fits.
    • Sensible ACLs — this is worth flagging early: the moment you add write access for anonymous or lightly-authenticated users, you need to think through who can insert records and whether the client can be trusted to self-report a score without any server-side sanity checking.

2. Technology Stack

  • Frontend: Service Portal's built-in AngularJS 1.x widget framework — HTML template plus a client controller, no external UI library needed.
  • Backend: the widget's native Server Script, running standard server-side JavaScript.
  • Data: a custom table (u_memory_game_score) accessed through GlideRecord.
  • Hosting: none needed — it runs entirely inside the existing ServiceNow instance, on Service Portal.

3. Design and Architecture

  • HTML Template: the card grid, stats bar, win screen, and leaderboard table — all driven by Angular bindings against the client controller's scope.
  • Client Controller: owns all real-time gameplay — card flips, match checking, the timer — so nothing needs a server round-trip while the player is actually playing.
  • Server Script: only gets involved twice — once to save a finished score, once to fetch the current leaderboard.
  • CSS: a simple 3D flip transform for the cards, kept in the widget's own CSS field so it doesn't leak into the rest of the portal theme.

4. Implementation Steps

  1. Create the table: u_memory_game_score with player name, moves, and seconds fields.
  2. Build the widget shell: new Service Portal widget, wire up the four fields — HTML Template, CSS, Client Controller, Server Script.
  3. Implement gameplay: shuffle a deck, handle flip/match logic, run the timer with $interval.
  4. Wire up persistence: on win, call c.server.get() to save the score and pull back a fresh leaderboard.
  5. Add the page: drop the widget onto a portal page through Service Portal Designer.
  6. Test: confirm the full loop — play, win, save name, see the leaderboard update — works end to end, on both desktop and mobile.

5. Deployment and Maintenance

  • Build and test it in a personal developer instance first (free at developer.servicenow.com) before touching anything production-facing.
  • Move it between instances with a standard Update Set, like any other customization.
  • Review the table's ACLs periodically — especially if it ever moves from an internal demo to something with wider portal exposure.

With the plan settled, here's what the actual widget code looks like — the same pattern you'd use for any Service Portal widget, just applied to a game instead of a form.

The Widget, Piece by Piece

The HTML Template lays out the card grid, stats, and leaderboard using standard Angular directives:

<div class="mg-board" ng-if="!c.won">
  <div class="mg-card"
       ng-repeat="card in c.cards track by $index"
       ng-class="{flipped: card.flipped || card.matched}"
       ng-click="c.flipCard($index)">
    <div class="mg-card-inner">
      <div class="mg-card-front">?</div>
      <div class="mg-card-back">{{card.icon}}</div>
    </div>
  </div>
</div>

The Client Controller handles every flip, entirely in the browser — no server call while the player is mid-game:

c.flipCard = function(index) {
  if (lockBoard) return;
  var card = c.cards[index];
  if (card.flipped || card.matched) return;

  card.flipped = true;
  flippedIndexes.push(index);

  if (flippedIndexes.length === 2) {
    c.moves++;
    lockBoard = true;
    var first = c.cards[flippedIndexes[0]];
    var second = c.cards[flippedIndexes[1]];

    if (first.icon === second.icon) {
      first.matched = true;
      second.matched = true;
      flippedIndexes = [];
      lockBoard = false;
      checkWin();
    } else {
      setTimeout(function() {
        $scope.$apply(function() {
          first.flipped = false;
          second.flipped = false;
          flippedIndexes = [];
          lockBoard = false;
        });
      }, 800);
    }
  }
};

And the Server Script is where GlideRecord does the only two things it needs to: save a score, and return the current top ten.

function getLeaderboard() {
  var results = [];
  var gr = new GlideRecord('u_memory_game_score');
  gr.orderBy('u_moves');
  gr.orderBy('u_seconds');
  gr.setLimit(10);
  gr.query();
  while (gr.next()) {
    results.push({
      name: gr.getValue('u_player_name'),
      moves: gr.getValue('u_moves'),
      seconds: gr.getValue('u_seconds')
    });
  }
  return results;
}

if (input.action === 'save_score') {
  var gr = new GlideRecord('u_memory_game_score');
  gr.initialize();
  gr.setValue('u_player_name', input.name);
  gr.setValue('u_moves', input.moves);
  gr.setValue('u_seconds', input.seconds);
  gr.insert();

  data.leaderboard = getLeaderboard();
} else if (input.action === 'get_leaderboard') {
  data.leaderboard = getLeaderboard();
}

That's the whole architecture: client owns the interaction, server owns the record of truth. It's a pattern you'll reuse constantly in real widget development — usually dressed up as "request status" or "approval state" instead of a game score.

Why This Works Well as a Hackathon Project

  • It demos itself. Judges don't need a walkthrough — they can just play it.
  • A working v1 is fast. A minimal playable version, no leaderboard, can be done in an hour or two.
  • It's memorable. Weeks later, "the team that built a game" outlasts "the team that streamlined another form" in people's memory.
  • It scales with skill level. Beginners ship a working game. More experienced developers can add difficulty scaling, real-time multiplayer, or tie the leaderboard into actual Performance Analytics data.

Additional Tips If You Try This

  • Lock down the leaderboard table: restrict write access appropriately once this leaves a sandbox — don't leave it open to any authenticated user by default.
  • Reuse the pattern for real training tools: the same widget skeleton works for an onboarding quiz, an incident-response speed drill, or a leaderboard tied to real ticket-resolution metrics.
  • Keep gameplay client-side: anything that needs to feel instant — flips, timers, animations — should never wait on a server round-trip.

If your next hackathon team is stuck between "another approval flow" and "something people will actually remember," build the game. It touches the same core skills as any production widget — just with a much better demo.

No comments:

Post a Comment