Canvas Tutorial: Halma Board Game
Here is a sample demo of a game using HTML Canvas.
This is a one-person version of the game halma.
The goal is to move all the pieces to the opposite corner, with minimum steps.
Click a circle to select, then click a empty square to move to.
There are 2 ways to move:
- Move a piece to adjacent square. (diagonal is ok)
- jump a piece over another to land on a empty square. (diagonal is ok) You can continue to jump.
Code:
// HTML5 Canvas Game Demo: Halma. http://xahlee.info/js/js_canvas_halma.html // The code is written by Mark Pilgrim. Licensed under http://creativecommons.org/licenses/by/3.0/ // edited and converted to ES2015 by xah lee // HTML Canvas Game Demo: Halma // http://xahlee.info/js/js_canvas_halma.html const kBoardWidth = 9; const kBoardHeight= 9; const kPieceWidth = 50; const kPieceHeight= 50; const kPixelWidth = 1 + (kBoardWidth * kPieceWidth); const kPixelHeight= 1 + (kBoardHeight * kPieceHeight); let gCanvasElement; let gDrawingContext; let gPattern; let gPieces; let gNumPieces; let gSelectedPieceIndex; let gSelectedPieceHasMoved; let gMoveCount; let gMoveCountElem; let gGameInProgress; const saveGameState = function() { return false; } const resumeGame = function() { return false; } function Cell (row, column) { this.row = row; this.column = column; } const getCursorPosition = ((e) => { /* returns Cell with .row and .column properties */ let x; let y; if (e.pageX != undefined && e.pageY != undefined) { 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; } x -= gCanvasElement.offsetLeft; y -= gCanvasElement.offsetTop; x = Math.min(x, kBoardWidth * kPieceWidth); y = Math.min(y, kBoardHeight * kPieceHeight); let cell = new Cell(Math.floor(y/kPieceHeight), Math.floor(x/kPieceWidth)); return cell; }); const halmaOnClick = ((e) => { let cell = getCursorPosition(e); for (let i = 0; i < gNumPieces; i++) { if ((gPieces[i].row == cell.row) && (gPieces[i].column == cell.column)) { clickOnPiece(i); return; } } clickOnEmptyCell(cell); }); const clickOnEmptyCell = ((cell) => { if (gSelectedPieceIndex == -1) { return; } let rowDiff = Math.abs(cell.row - gPieces[gSelectedPieceIndex].row); let columnDiff = Math.abs(cell.column - gPieces[gSelectedPieceIndex].column); if ((rowDiff <= 1) && (columnDiff <= 1)) { /* we already know that this click was on an empty square, so that must mean this was a valid single-square move */ gPieces[gSelectedPieceIndex].row = cell.row; gPieces[gSelectedPieceIndex].column = cell.column; gMoveCount += 1; gSelectedPieceIndex = -1; gSelectedPieceHasMoved = false; drawBoard(); return; } if ((((rowDiff == 2) && (columnDiff == 0)) || ((rowDiff == 0) && (columnDiff == 2)) || ((rowDiff == 2) && (columnDiff == 2))) && isThereAPieceBetween(gPieces[gSelectedPieceIndex], cell)) { /* this was a valid jump */ if (!gSelectedPieceHasMoved) { gMoveCount += 1; } gSelectedPieceHasMoved = true; gPieces[gSelectedPieceIndex].row = cell.row; gPieces[gSelectedPieceIndex].column = cell.column; drawBoard(); return; } gSelectedPieceIndex = -1; gSelectedPieceHasMoved = false; drawBoard(); }); const clickOnPiece = ((pieceIndex) => { if (gSelectedPieceIndex == pieceIndex) { return; } gSelectedPieceIndex = pieceIndex; gSelectedPieceHasMoved = false; drawBoard(); }); const isThereAPieceBetween = ((cell1, cell2) => { /* note: assumes cell1 and cell2 are 2 squares away either vertically, horizontally, or diagonally */ let rowBetween = (cell1.row + cell2.row) / 2; let columnBetween = (cell1.column + cell2.column) / 2; for (let i = 0; i < gNumPieces; i++) { if ((gPieces[i].row == rowBetween) && (gPieces[i].column == columnBetween)) { return true; } } return false; }); const isTheGameOver = (() => { for (let i = 0; i < gNumPieces; i++) { if (gPieces[i].row > 2) { return false; } if (gPieces[i].column < (kBoardWidth - 3)) { return false; } } return true; }); const drawBoard = (() => { if (gGameInProgress && isTheGameOver()) { endGame(); } gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight); gDrawingContext.beginPath(); /* vertical lines */ for (let x = 0; x <= kPixelWidth; x += kPieceWidth) { gDrawingContext.moveTo(0.5 + x, 0); gDrawingContext.lineTo(0.5 + x, kPixelHeight); } /* horizontal lines */ for (let 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(); for (let i = 0; i < 9; i++) { drawPiece(gPieces[i], i == gSelectedPieceIndex); } gMoveCountElem.innerHTML = gMoveCount; saveGameState(); }); const drawPiece = ((p, selected) => { let column = p.column; let row = p.row; let x = (column * kPieceWidth) + (kPieceWidth/2); let y = (row * kPieceHeight) + (kPieceHeight/2); let radius = (kPieceWidth/2) - (kPieceWidth/10); gDrawingContext.beginPath(); gDrawingContext.arc(x, y, radius, 0, Math.PI*2, false); gDrawingContext.closePath(); gDrawingContext.strokeStyle = "#000"; gDrawingContext.stroke(); if (selected) { gDrawingContext.fillStyle = "#000"; gDrawingContext.fill(); } }); const newGame = (() => { gPieces = [new Cell(kBoardHeight - 3, 0), new Cell(kBoardHeight - 2, 0), new Cell(kBoardHeight - 1, 0), new Cell(kBoardHeight - 3, 1), new Cell(kBoardHeight - 2, 1), new Cell(kBoardHeight - 1, 1), new Cell(kBoardHeight - 3, 2), new Cell(kBoardHeight - 2, 2), new Cell(kBoardHeight - 1, 2)]; gNumPieces = gPieces.length; gSelectedPieceIndex = -1; gSelectedPieceHasMoved = false; gMoveCount = 0; gGameInProgress = true; drawBoard(); }); const endGame = (() => { gSelectedPieceIndex = -1; gGameInProgress = false; }); const initGame = ((canvasElement, moveCountElement) => { if (!canvasElement) { canvasElement = document.createElement("canvas"); canvasElement.id = "halma_canvas"; document.getElementById("canvas_here_48508").appendChild(canvasElement); } if (!moveCountElement) { moveCountElement = document.createElement("p"); document.body.appendChild(moveCountElement); } gCanvasElement = canvasElement; gCanvasElement.width = kPixelWidth; gCanvasElement.height = kPixelHeight; gCanvasElement.addEventListener ("click", halmaOnClick, false); gMoveCountElem = moveCountElement; gDrawingContext = gCanvasElement.getContext("2d"); if (!resumeGame()) { newGame(); } }); initGame(undefined, undefined);