I learned about Knight’s Tour from Steve Wozniak’s autobiograhy. He made a hardware version of the game when he was still a student, which I found pretty amazing. The basic gist is that a player tries to move a single chess knight to each square on an 8x8 chess board, but you can only land on each square once. The challenge comes from the wonky movement rules of the knight, which means you can get stuck pretty easily. Well, you had me at “8x8 chess board” — that’s a grid! We can easily make a web version of this game by using a 2D array to keep track of the game’s data.
Download Waffle, then edit game.js
to create that 8x8 grid. The one bit of extra logic we’ll run here is to fill the grid with alternating black and white tiles, in order to make it look like a chess board. Add the lines with a +
in front, and remove lines prefixed by a -
:
- const rows = 10;
- const columns = 10;
+ const rows = 8;
+ const columns = 8;
Waffle.init(rows, columns);
- Waffle.onKeyDown(({ key }) => {
- console.log(`pressed ${key}`);
- });
+ let newState = Waffle.state;
+
+ // simple helper function to determine if a number is even or odd
+ const isEven = number => number % 2 === 0;
+
+ // make the grid look like a chessboard, with alternating white/black tiles
+ for (let y = 0; y < rows; y += 1) {
+ for (let x = 0; x < columns; x += 1) {
+ newState[x][y] = isEven(x + y) ? 'white' : 'black';
+ }
+ }
+
+ Waffle.state = newState;
Waffle.onPointDown(({ x, y }) => {
//...
Add the following CSS rules to the end of main.css
:
.highlight {
background-color: blueviolet;
}
}
+
+ body {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ #grid {
+ width: 50%;
+ }
+
+ .white { background: white; }
+ .black { background: black; }
+ .visited { background: limegreen; }
+ .knight { background: url('knight.png') center/100%; }
Download a copy of knight.png
and put it in same folder as the rest of the Waffle files. Save the game.js
and main.css
files, and load index.html
in your browser (File → Open File → index.html). You should see a page that displays a grid with a chessboard pattern.

We now need to handle user interactions. Waffle has a helper for this called onPointDown
— this function will run some code whenever the player clicks or touches the touchscreen. The game.js
file already has an example of how this helper can be used; we’re going to delete some of that code and write our own. For this game, we’ll let the player choose their own starting point, which means the first click will place the knight piece. We can keep track of the knight’s position by using a local variable named, appropriately enough, knight
.
+ let knight;
+
Waffle.onPointDown(({ x, y }) => {
console.debug(`clicked grid cell (${x}, ${y})`);
- /* replace this with your own code! */
const newState = Waffle.state;
+ // save the knight's position
+ knight = {x, y};
+
+ // draw the knight on the grid
+ newState[knight.x][knight.y] = 'knight';
+
- if (Waffle.isEmpty(newState[x][y])) {
- newState[x][y] = 'highlight';
- } else {
- newState[x][y] = '';
- }
-
Waffle.state = newState;
});
Save game.js
and reload your browser. Now, click around on the grid — you should be able to create knight pieces in each cell.

Kinda cool, but this isn’t exactly the functionality that we want, which is to have a single knight move around the board. Let’s update the code so that when a cell is clicked, the knight moves there, and the previous cell is marked as “visited” (the idea being that you can’t land on the same spot twice).
Waffle.onPointDown(({ x, y }) => {
console.debug(`clicked grid cell (${x}, ${y})`);
const newState = Waffle.state;
+ // if the knight has already been placed on the grid...
+ if (knight) {
+ // draw the current position as "visited"
+ newState[knight.x][knight.y] = 'visited';
+ }
// save the knight's new position
knight = {x, y};
// re-draw the knight on the grid
newState[knight.x][knight.y] = 'knight';
Waffle.state = newState;
});
More progress! Now there is only a single knight that can move around the board.

But we still need to program in more game logic: the knight can’t land on any “visited” spaces, and has to move like it does in the game of chess (two forward, one to the side). The way we’ll add these rules is by checking the point where the user clicks: if it’s in a list of allowed moves, then the knight can proceed. If not, then nothing happens. So, since we have the current (x, y) position of the knight, stored in the knight
variable, we can generate a list of allowable spaces by adding or subtracting from the knight’s location. For example, we can create an array of {x, y}
objects that represent valid moves:
const allowedMoves = [
// above
{ x: knight.x - 1, y: knight.y - 2},
{ x: knight.x + 1, y: knight.y - 2},
// left
{ x: knight.x - 2, y: knight.y - 1},
{ x: knight.x - 2, y: knight.y + 1},
// right
{ x: knight.x + 2, y: knight.y - 1},
{ x: knight.x + 2, y: knight.y + 1},
// below
{ x: knight.x - 1, y: knight.y + 2},
{ x: knight.x + 1, y: knight.y + 2},
];
If you’re a visual person, like me, you could imagine the allowedMoves
array looks something like this, with K
being the knight, and x
being a valid move:
[ ][x][ ][x][ ] [x][ ][ ][ ][x] [ ][ ][K][ ][ ] [x][ ][ ][ ][x] [ ][x][ ][x][ ]
So what we need to do is check if the allowedMoves
array contains (x, y) values that are equal to where the player clicked. Since the knight’s position changes after each turn, it makes sense to take the example allowedMoves
array and put it inside a function. The function can take the {x, y}
position of the knight as an argument, and return the new list of allowed moves after each turn. Add this new code outside of the onPointDown
function.
Waffle.onPointDown(({ x, y }) => {
// ...omitted for brevity
});
+ const getAllowedMoves = (knight) => {
+ return [
+ // above
+ { x: knight.x - 1, y: knight.y - 2},
+ { x: knight.x + 1, y: knight.y - 2},
+
+ // left
+ { x: knight.x - 2, y: knight.y - 1},
+ { x: knight.x - 2, y: knight.y + 1},
+
+ // right
+ { x: knight.x + 2, y: knight.y - 1},
+ { x: knight.x + 2, y: knight.y + 1},
+
+ // below
+ { x: knight.x - 1, y: knight.y + 2},
+ { x: knight.x + 1, y: knight.y + 2},
+ ].filter(Waffle.withinBounds);
+ };
Note the use of the Waffle.withinBounds
helper function. What this does is ensure the (x, y) coordinates returned from this function are all within the boundaries of the grid. For example, if the knight is at (0, 0), some of the computed moves will be outside the grid, such as (-1, -2). Passing Waffle.withinBounds
to filter
will discard those. We can then use this new function in onPointDown
to see if the knight is allowed to move:
Waffle.onPointDown(({ x, y }) => {
console.debug(`clicked grid cell (${x}, ${y})`);
const newState = Waffle.state;
- // if the knight has already been placed on the grid...
- if (knight) {
- // draw the current position as "visited"
- newState[knight.x][knight.y] = 'visited';
- }
-
- // save the knight's new position
- knight = {x, y};
-
- // re-draw the knight on the grid
- newState[knight.x][knight.y] = 'knight';
+ const allowedMoves = getAllowedMoves(knight);
+
+ // if the clicked {x, y} coords are in the list of allowed moves...
+ if (allowedMoves.find(move => x === move.x && y === move.y)) {
+ // draw the current position as "visited"
+ newState[knight.x][knight.y] = 'visited';
+
+ // set the new knight position
+ knight = {x, y};
+
+ // draw on the grid
+ newState[knight.x][knight.y] = 'knight';
+ }
Waffle.state = newState;
});
Save and reload the page, then click around on the game board. Nothing happens. What? If you open the browser console (Tools → Browser Tools → Browser Console), you’ll see an error message pop up whenever you click: Uncaught TypeError: knight is undefined
. The problem is that our previous code would always set the knight’s position, and now we’re checking to see if a move is allowed based on the current position. Since knight
is initially declared without a value (undefined
), this means we need to add some special handling for the very first move of the game.
Waffle.onPointDown(({ x, y }) => {
console.debug(`clicked grid cell (${x}, ${y})`);
const newState = Waffle.state;
+ // handle condition of first move
+ if (!knight) {
+ knight = {x, y};
+
+ newState[knight.x][knight.y] = 'knight';
+ }
const allowedMoves = getAllowedMoves(knight);
// if the clicked {x, y} coords are in the list of allowed moves...
if (allowedMoves.find(move => x === move.x && y === move.y)) {
// draw the current position as "visited"
newState[knight.x][knight.y] = 'visited';
// set the new knight position
knight = {x, y};
// draw on the grid
newState[knight.x][knight.y] = 'knight';
}
Waffle.state = newState;
});
At this point you should be able to place the knight anywhere on the board on the first turn, then on subsequent turns the knight has to follow the traditional movement rules of the chess piece.

There are a few more features it would be good to add. If you’ve been observant, you’ll notice that the knight can still move to previously visited spaces. This is actually pretty simple to implement, if we modify the getAllowedMoves
function.
const getAllowedMoves = (knight) => {
+ const state = Waffle.state;
return [
// above
{ x: knight.x - 1, y: knight.y - 2},
{ x: knight.x + 1, y: knight.y - 2},
// left
{ x: knight.x - 2, y: knight.y - 1},
{ x: knight.x - 2, y: knight.y + 1},
// right
{ x: knight.x + 2, y: knight.y - 1},
{ x: knight.x + 2, y: knight.y + 1},
// below
{ x: knight.x - 1, y: knight.y + 2},
{ x: knight.x + 1, y: knight.y + 2},
+ ].filter(Waffle.withinBounds).filter(point => state[point.x][point.y] !== 'visited'); // can't move to a visited space
};
What we’re doing here is getting the current state of the game board, then using the filter function again to remove any spaces that have already been visited.
The core logic of the game is now complete. The last feature we’ll add is a function to check if the player has “won,” by visiting all 64 squares on the board. At the end of the game, there will be no more moves left, because each square in our grid will have a value of visited
(except for the last square which will have the knight). So to check for a win condition, we loop over the game board and check that only those two values are present.
const getAllowedMoves = (knight) => {
// ...omitted for brevity
};
+
+ const hasWonGame = () => {
+ const state = Waffle.state;
+
+ for (let y = 0; y < Waffle.rows; y += 1) {
+ for (let x = 0; x < Waffle.columns; x += 1) {
+ if (state[x][y] !== 'visited' && state[x][y] !== 'knight') {
+ return false;
+ }
+ }
+ }
+
+ // if we've made it here, then the entire grid has been visited
+ return true;
+ };
This method can then be used at the very end of the onPointDown
method, to display a message to the player if they manage to traverse the entire board.
Waffle.onPointDown(({ x, y }) => {
// ...omitted for brevity
Waffle.state = newState;
+ if (hasWonGame()) {
+ Waffle.alert('Congratulations!');
+ }
};

And that’s more or less it! If your version isn’t working as expected, download a copy of the completed project and compare it with what you’ve typed in.
Exercises left for the reader
- Keep score: increment a point counter whenever the player makes a successful move
- You lose: display a message when the player has no moves available
- Reset: add a button (or listen for a key press) that resets the game, rather than making the player reload the page
- Beginner mode: update the game board to highlight allowed next moves