Ganbaru Games

Lights Out

Lights Out was a game made by Tiger Electronics (known for their crappy LCD handhelds) back in 1995. The concept was that a grid of backlit buttons could be pressed by the player. The grid was initialized with randomly lit buttons, and the goal was to “turn off” all the lights. Pressing a lit button turns it off, but also causes the four adjacent buttons to turn on.

This sort of game is perfect to re-implement in Waffle. First, download Waffle, which includes an index.html file that automatically loads the necessary JavaScript and CSS files. Next, open the game.js file in a text editor; you’ll see that there is some example code there already which initializes a 10x10 grid, and sets up a simple event handler to toggle grid cell background colors. The first thing we can do is change the grid size to a 5x5 board.

- const rows = 10;
- const columns = 10;
+ const rows = 5;
+ const columns = 5;

Waffle.init(rows, columns);

The first thing we need to do is randomly “light up” squares on the game board. After the call to Waffle.init(), add the following:

Waffle.init(rows, columns);
+
+ const newState = Waffle.state;
+
+ for (let x = 0; x < Waffle.rows; x += 1) {
+  for (let y = 0; y < Waffle.columns; y += 1) {
+    if (Math.random() > 0.5) {
+      newState[x][y] = 'light';
+    }
+  }
+ }
+
+ Waffle.state = newState;

This code loops through the game board, and will randomly give cells in the grid a value of light. As some pedants will tell you, Math.random() is not truly random, but for our purposes it’s good enough. It will return a value between 0 and 1. If you save the game.js file and load index.html in a browser (File → Open File → index.html), you should see the 5x5 grid in the center of the page. But you won’t see the randomized pattern of lit squares. That’s because Waffle uses CSS classes to apply styling to each square. The page looks for a class style of .light inside of main.css, but it’s not there. Let’s add it now. Make your own image to represent a light, or else download an example here, and put it in the same folder as the rest of the Waffle files. Then add the following at the bottom of main.css:

#grid {
  /* ... */

-  .highlight {
-    background-color: blueviolet;
-  }
+ .light {
+   background: url('light.png') center/100%;
+ }
}

This background shorthand CSS property (MDN reference) specifies the image file that should be used for the background, and that the image should be placed in the center of the element, and take up 100% of the available background space. Reload the page, and you should now see lights randomly appear on the grid.

randomly placed lights
randomly placed lights

Next we want to toggle lights when the user touches or clicks the grid. We can accomplish this by modifying the onPointDown callback function example that’s already in the game.js source file. The rules of the game stipulate that touching a cell will toggle it on/off, as well as the four neighbors (above/below/left/right) surrounding it. Fortunately, Waffle has a handy getNeighbors helper function that will get those four adjacent cells. Once we have a reference to each neighbor, we can turn each one “on” or “off.”

- Waffle.onKeyDown(({ key }) => {
-   console.log(`pressed ${key}`);
- });

Waffle.onPointDown(({ x, y }) => {
- console.debug(`clicked grid cell (${x}, ${y})`);

- /* replace this with your own code! */
  const newState = Waffle.state;

- if (Waffle.isEmpty(newState[x][y])) {
-   newState[x][y] = 'highlight';
- } else {
-   newState[x][y] = '';
- }

+ // helper function to turn a cell on/off
+ const toggle = ({x, y}) => newState[x][y] = Waffle.isEmpty(newState[x][y]) ? 'light' : '';
+
+ // update the clicked cell
+ toggle({x, y});
+
+ // update the neighboring cells
+ const neighbors = Waffle.getNeighbors({x, y});
+ for (const neighbor of neighbors) {
+   toggle(neighbor);
+ }

  Waffle.state = newState;
});

Save and reload, and click around. You should see the expected behavior, where on each click up to five cells are turned off or on.

toggling lights on and off
toggling lights on and off

The last feature we need to add is to check whether the game has been won. To do this we basically check to ensure that each cell in the grid is “off.” We can write a new function called hasWonGame to handle this:

Waffle.onPointDown(({ x, y }) => {
  // omitted for brevity...
});

+ const hasWonGame = () => {
+   const state = Waffle.state;
+
+   for (let x = 0; x < Waffle.rows; x += 1) {
+     for (let y = 0; y < Waffle.columns; y += 1) {
+       // if any cell is not empty (e.g. has a value of 'light'), instantly fail
+       if (!Waffle.isEmpty(state[x][y])) {
+         return false;
+       }
+     }
+   }
+
+   // we verified the whole board is empty!
+   return true;
+ };

Which can then be used like so:

Waffle.onPointDown(({ x, y }) => {
  const newState = Waffle.state;

  // helper function to turn a cell on/off
  const toggle = ({x, y}) => newState[x][y] = Waffle.isEmpty(newState[x][y]) ? 'light' : '';

  // update the clicked cell
  toggle({x, y});

  // update the neighboring cells
  const neighbors = Waffle.getNeighbors({x, y});
  for (const neighbor in neighbors) {
    toggle(neighbor);
  }

  Waffle.state = newState;

+ if (hasWonGame()) {
+   Waffle.alert('You win!!');
+ }
});
winning the game
winning the game

That’s it! You now have a functional version of “Lights Out.”

Extra Credit

Reset the game board after a win, so that the player doesn’t need to reload the page in order to re-initialize the game. One way to do this might be to move the code that randomly lights up the board (after the call to Waffle.init()) into its own function, called setup or reset or something. That new function could then be called inside the hasWonGame() conditional block.

Ganbaru Games publishes games written by Nathan Demick. Each game is playable mobile and desktop web browsers. In fact, you can load a game, then "save to homescreen" on your smartphone to have convenient access at any time!

Buy Me a Coffee at ko-fi.com