If you have read Entropy Arbitrage—you know, this blog?—for a while, or follow me in other technical spaces, you might remember my search for something like a board game library. I released Open Oubliette more than two years ago, for example, and wanted to implement it, leading me, if I remember the sequencing correctly, to create an empty generic board game project, where I planned to experiment with different libraries.

The problem—the gap in knowledge—that I’ve had has centered on moving the game pieces.

A clown dragging a box through a city crosswalk

I can figure out how to manage players for a local game easily enough, but I had no idea how to empower the player to move a piece, nor did I like the idea of trying to animate it and hoping that the endpoints matched up correctly. Yet, whenever I asked for advice online, people universally pointed me to boardgame.io, which…does all the straightforward parts, leaving the developer to figure out the interface pieces that I did not know how to do. (I don’t want to suggest that anybody shouldn’t use that library; it seems to make online gaming easier. I just happen to not really care about that, right now.)

While later than I would have liked, I finally set aside some time to fill that knowledge gap.

Salavi

GitHub - jcolag/salaviAn ancient board game...with a twist. Contribute to jcolag/salavi development by creating an account on GitHub.

If you follow my developer diary posts, then you might remember my mentioning my new project Salavi. While I have twists in mind for the game, it currently creates implementations of a simple Snakes and Ladders game.

Why choose Snakes and Ladders instead of Open Oubliette, my “generic board game” project, or anything else? I had three reasons.

  • Because we play the game on a grid of squares, I didn’t need to worry about layout. I can throw everything into an ugly table, and it’ll look fine.
  • A single-player game only has one piece to move around the board. For the purposes of the experiment, I won’t need to worry about which token can or should move.
  • My early plans for the blog included a series of posts to develop this game, as a way of teaching readers to program. I realized that the project didn’t have enough complexity to maintain reader interest for months of posts, but I still have plenty of notes on how I want the game to work, so I know where to go from this minimum viable product kind of phase.

In other words, I didn’t want to overcomplicate the code and distract from the specific thing that I wanted to learn, but I also wanted the result to have some value beyond the experiment. And this post documents the requirements.

Setup

I found that I needed two elements in the code, to make things work as expected, so we might as well get them out of the way first.

The more obvious prevents the user from accidentally dragging the wrong object, an unfortunately tedious process, since it requires setting the draggable attribute on every object on the page to false.

<div draggable="false">...</div>

I found it frustrating, when developing this game, to occasionally find myself dragging an entire space, instead of the specific game piece.

We especially have accidental dragging, unfortunately, when the user—also accidentally—selects something. The browser then recognizes the mouse gesture as dragging the selected text, which works against the goal. That means that we probably want to disable text selection on the webpage.

body {
  user-select: none;
}

If we have places on the page where we want to allow selection, we can always change the user-select property more locally.

The rest, we handle in mostly straightforward JavaScript. In fact, we should also set up some variables.

let dragged = null;
const dropTargets = [];

When the user starts dragging an object, we update the dragged variable to point at that object, so that we can keep track of it. And we’ll use dropTargets to list the various places that the user can drop their object.

Listen…to Nothing?

We’ll quickly set up the event handlers we need normally, only one of which has any substance.

window.addEventListener('load', (e) => {
  document.addEventListener('drag', () => {}, false);
  document.addEventListener('dragover', (e) => e.preventDefault(), false);
  document.addEventListener('drop', dropPiece);
}

Don’t do anything when dragging—if it becomes possible at all—or when dragging something over the page. Just have a plan to accept the drop, if it happens.

Where to, Mac?

That lacks a certain interest, of course, so far. In a game situation, we would need to allow the user to drag an object and identify where the user can drop the dragged object.

function chooseDestination(tokenId, targetId) {
  const target = document.getElementById(targetId);
  const token = document.getElementById(tokenId);

  dropTargets.length = 0;
  token.draggable = true;
  token.addEventListener('dragstart', dragPiece);

  if (target) {
    dropTargets.push(targetId);
  }
}

The function takes the name of the object to drag and the location to drop it. This keeps things simple, of course. In some cases, tokenId, or targetId, or both might represent arrays. A game in the mancala or pachisi families would allow the user to move a number of tokens. I vaguely remember that games like Clue/Cluedo allow the player to sometimes affect where the dice move them, by selecting direction. And games like backgammon include both features on every turn. If we needed to do that, then we’d just add some loops to this code.

Regardless, we make the token draggable, both by changing the DOM and setting a handler for the dragstart event. Then, assuming that the target(s) exist, we update the list of object IDs in dropTargets.

The dragstart event handler only needs to do the obvious thing.

function dragPiece(event) {
  dragged = event.target;
}

As mentioned, the code now “knows” which object the user has in motion.

The Complicated Part—Dropping

Now that the user has dragged the game piece around, we can focus on what to do on drop, which you might remember connecting back at the beginning.

document.addEventListener('drop', dropPiece);

First up, we bail, if our dropTargets variable doesn’t include the object raising the drop event, notifying us that the user has dropped something onto that object.

var target = event.target;
if (dropTargets.indexOf(target.id) < 0) {
  return;
}

Next, we want to make sure that we have the actual target object, rather than anything contained in the object, like another game piece.

if (target.className.indexOf('game-piece') >= 0) {
  target = target.parentElement;
}

These two conditions could stand some cleaning, honestly. It might make more sense to loop until we hit a valid ID or the root document object. They might even contradict each other. But I’d rather put in the work right now writing about how this works than I would debugging it and delaying the post.

Regardless, we should start cleaning up.

event.preventDefault();

This oversimplifies the situation, but preventDefault() basically tells the browser that our code manages everything that it needs to.

dragged.draggable = false;

We turn off the ability to turn off the ability to move our game piece.

dragged.parentNode.removeChild(dragged);
target.appendChild(dragged);

We move the game piece from its current container—wherever it came from—to where the user dropped it.

dragged = null;

Finally, clear out the dragged variable, so that nothing accidentally moves the game piece past this point.

You probably want to see the entire thing in context, though.

function dropPiece(event) {
  var target = event.target;

  if (dropTargets.indexOf(target.id) < 0) {
    return;
  }

  if (target.className.indexOf('game-piece') >= 0) {
    target = target.parentElement;
  }

  event.preventDefault();
  dragged.draggable = false;
  dragged.parentNode.removeChild(dragged);
  target.appendChild(dragged);
  dragged = null;
}

And Onward

Obviously, I have more work to do with the game itself. In the actual game, as I write this, dropPiece() goes on to check if the new space has the start of a snake or ladder, moving the piece over before emptying out dragged. Then, I need to figure out how to actually show the snakes and ladders.

Plus, it needs the secret part of the game.

However, it seems more important to notice how portably this can work. I don’t see any reason that someone—like myself—couldn’t refactor this in a way to extract this code for other games or even non-games.

Most importantly in my eyes, the dropTargets variable should make it easy to use anything as the drop-destination. Rather than every cell in a table, the path could weave randomly through a subset of cells or any arbitrarily placed objects on the page, to a point where players can no longer recognize the grid.

It took me long enough to sit down and figure that out, but it seems to work fairly well…


Credits: The header image is 1.12.10 by aprilzosia, made available under the terms of the Creative Commons Attribution Share-Alike 2.0 Generic license.