Halma is a centuries-old board game. Many variations exist. In this example, I’ve created a solitaire version of Halma with nine pieces on a 9 × 9 board. In the beginning of the game, the pieces form a 3 × 3 square in the bottom-left corner of the board. The object of the game is to move all the pieces so they form a 3 × 3 square in the upper-right corner of the board, in the least possible number of moves.
There are two types of legal moves in Halma:
Take a piece and move it to any adjacent empty square. An “empty” square is one that does not currently have a piece in it. An “adjacent” square is immediately north, south, east, west, northwest, northeast, southwest, or southeast of the piece’s current position. (The board does not wrap around from one side to the other. If a piece is in the leftmost column, it cannot move west, northwest, or southwest. If a piece is in the bottommost row, it cannot move south, southeast, or southwest.)
Take a piece and hop over an adjacent piece, and possibly repeat. That is, if you hop over an adjacent piece, then hop over another piece adjacent to your new position, that counts as a single move. In fact, any number of hops still counts as a single move. (Since the goal is to minimize the total number of moves, doing well in Halma involves constructing, and then using, long chains of staggered pieces so that other pieces can hop over them in long sequences.)
Figure 4-16 is a screenshot of the game itself; you can also play it online if you want to poke at it with your browser’s developer tools.
So how does it work? I’m so glad you asked. I won’t show
all the code here (you can see it at http://diveintohtml5.org/examples/halma.js).
I’ll skip over most of the gameplay code itself, but I want to highlight a
few parts of the code that deal with actually drawing on the canvas and
responding to mouse clicks on the <canvas>
element.
During page load, we initialize the game by setting the dimensions
of the <canvas>
itself and
storing a reference to its drawing context:
gCanvasElement.width = kPixelWidth; gCanvasElement.height = kPixelHeight; gDrawingContext = gCanvasElement.getContext("2d");
Then we do something you haven’t seen yet—we add an event listener
to the <canvas>
element to listen for click
events:
gCanvasElement.addEventListener("click", halmaOnClick, false);
The halmaOnClick()
function gets
called when the user clicks anywhere within the canvas. Its argument is a
MouseEvent
object that contains
information about where the user clicked:
function halmaOnClick(e) {
var cell = getCursorPosition(e);
// the rest of this is just gameplay logic
for (var i = 0; i < gNumPieces; i++) {
if ((gPieces[i].row == cell.row) &&
(gPieces[i].column == cell.column)) {
clickOnPiece(i);
return;
}
}
clickOnEmptyCell(cell);
}
The next step is to take the MouseEvent
object and calculate which square on
the Halma board just got clicked. The Halma board takes up the entire
canvas, so every click is somewhere on the board. We
just need to figure out where. This is tricky, because mouse events are
implemented differently in just about every browser:
function getCursorPosition(e) { var x; var y; if (e.pageX || e.pageY) { x = e.pageX; y = e.pageY; } else { x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; }
At this point, we have x
and
y
coordinates that are relative to the
document (that is, the entire HTML page). But we want
coordinates relative to the canvas. We can get them as follows:
x -= gCanvasElement.offsetLeft; y -= gCanvasElement.offsetTop;
Now we have x
and y
coordinates that are relative to the canvas
(see Canvas Coordinates). That is, if x
is 0
and
y
is 0
at this point, we know that the user just
clicked the top-left pixel of the canvas.
From here, we can calculate which Halma square the user clicked, and then act accordingly:
var cell = new Cell(Math.floor(y/kPieceWidth), Math.floor(x/kPieceHeight)); return cell; }
Whew! Mouse events are tough. But you can use the same logic (in fact, this exact code) in all of your own canvas-based applications. Remember: mouse click→document-relative coordinates→canvas-relative coordinates→application-specific code.
OK, let’s look at the main drawing routine. Because the graphics are so simple, I’ve chosen to clear and redraw the board in its entirety every time anything changes within the game. This is not strictly necessary. The canvas drawing context will retain whatever you have previously drawn on it, even if the user scrolls the canvas out of view or changes to another tab and then comes back later. If you’re developing a canvas-based application with more complicated graphics (such as an arcade game), you can optimize performance by tracking which regions of the canvas are “dirty” and redrawing just the dirty regions. But that is outside the scope of this book. Here’s the board-clearing code:
gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight);
The board-drawing routine should look familiar. It’s very similar to how we drew the canvas coordinates diagram (see Canvas Coordinates):
gDrawingContext.beginPath(); /* vertical lines */ for (var x = 0; x <= kPixelWidth; x += kPieceWidth) { gDrawingContext.moveTo(0.5 + x, 0); gDrawingContext.lineTo(0.5 + x, kPixelHeight); } /* horizontal lines */ for (var y = 0; y <= kPixelHeight; y += kPieceHeight) { gDrawingContext.moveTo(0, 0.5 + y); gDrawingContext.lineTo(kPixelWidth, 0.5 + y); } /* draw it! */ gDrawingContext.strokeStyle = "#ccc"; gDrawingContext.stroke();
The real fun begins when we go to draw each of the individual
pieces. A piece is a circle, something we haven’t drawn before.
Furthermore, if the user selects a piece in anticipation of moving it, we
want to draw that piece as a filled-in circle. Here, the argument p
represents a piece, which has row
and column
properties that denote the piece’s
current location on the board. We use some in-game constants to translate
(column, row)
into canvas-relative
(x, y)
coordinates, then draw a circle,
then (if the piece is selected) fill in the circle with a solid
color:
function drawPiece(p, selected) { var column = p.column; var row = p.row; var x = (column * kPieceWidth) + (kPieceWidth/2); var y = (row * kPieceHeight) + (kPieceHeight/2); var radius = (kPieceWidth/2) - (kPieceWidth/10);
That’s the end of the game-specific logic. Now we have (x, y)
coordinates, relative to the canvas, for
the center of the circle we want to draw. There is no circle()
method in the canvas
API, but there is an arc()
method. And really, what is a circle but
an arc that goes all the way around? Do you remember your basic geometry?
The arc()
method takes a center point
(x, y)
, a radius, a start and end angle
(in radians), and a direction flag (false
for clockwise, true
for counterclockwise). We can use the
Math
module that’s built into
JavaScript to calculate radians:
gDrawingContext.beginPath(); gDrawingContext.arc(x, y, radius, 0, Math.PI * 2, false); gDrawingContext.closePath();
But wait! Nothing has been drawn yet. Like moveTo()
and lineTo()
, the arc()
method is a “pencil” method (see Paths). To actually draw the circle, we need to set the
strokeStyle
and call stroke()
to trace it in “ink”:
gDrawingContext.strokeStyle = "#000"; gDrawingContext.stroke();
What if the piece is selected? We can reuse the same path we created to draw the outline of the piece to fill in the circle with a solid color:
if (selected) { gDrawingContext.fillStyle = "#000"; gDrawingContext.fill(); }
And that’s...well, that’s pretty much it. The rest of the program is
game-specific logic—distinguishing
between valid and invalid moves, keeping track of the number of moves,
detecting whether the game is over. With nine circles, a few straight
lines, and an onclick
handler, we’ve
created an entire game in <canvas>
. Huzzah!
Get HTML5: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.