Lights Puzzle Outline

This is my first outline of a project I created a while ago. It's not going to delve deep into every piece of code, but an overview of what I was thinking when creating the project and some of the logistics I had to implement.


Inspiration

Inspired from Khanacademy's puzzles, I wanted to create my own project using the logic it would take to make this puzzle work.

Puzzles is something I'm really into and I try to incorporate more in my projects. This was my first complete Vue.js project.

Khanacademy Lights Puzzle Khanacademy Lights Puzzle
My Lights Puzzle My Lights Puzzle

Store

Vuex is used as a global store to hold information for:
  • board status: The board is initialized based on what level is selected.
  • moves count: Each time a grid is selected, the count increases until the game is reset or ends (then it's set to 0).
  • level selected: Assigned before the game starts to set the correct board size.
  • game progress: Set to true if the board is complete or false if the user chooses to reset or end the game prematurely.

The 3 board levels are stored in a board.js file to be used upon starting a new game.

export const boardLevel1 = [
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0]
];

export const boardLevel2 = [
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0],
];

export const boardLevel3 = [
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0],
];

Design & Colors

I used Vuetify to display a "Google-like" design to the buttons and use a layout offered in the framework to structure the project.

Depending on the level, the board had a specific dimension to show the grids. I also had to implement slightly different sizes for the level 2 and 3 boards so they fit in mobile.

.row {
  display: grid;
  margin-bottom: 10px;
  grid-gap: 5px;

  &.rowLevel1 {
    grid-template-rows: 100px;
    grid-template-columns: repeat(3, 100px);
  }
  &.rowLevel2 {
    grid-template-rows: 65px;
    grid-template-columns: repeat(5, 65px);
  }
  &.rowLevel3 {
    grid-template-rows: 45px;
    grid-template-columns: repeat(7, 45px);
  }
}

I set the color scheme in the base stylesheet base.scss.

Color Scheme Color Scheme
Color Scheme Variables Color Scheme Variables

Components

Home is where a level is selected to start the game.

Home Home

The Board passes each array of the board into the Row. In each Row, the elements in the arrays are assigned as Column.

The Row has minimal functionality whereas the Column has a lot going on.

<!-- Column.vue -->
<template>
  <div
    class="column"
    :ref="colRef"
    :class="{ on: active, off: !active }"
    @keydown.enter="changeStatus"
    @click="changeStatus"
    :tabindex="index_x === 0 && index_y === 0 ? 0 : -1"
    @keydown.up="setFocus(index_x - 1, index_y)"
    @keydown.down="setFocus(index_x + 1, index_y)"
    @keydown.left="setFocus(index_x, index_y - 1)"
    @keydown.right="setFocus(index_x, index_y + 1)"
    @focus="setFocus(index_x, index_y)"
  ></div>
</template>

The class active is set through a computed property determining if the grid's coordinates match the ones in focus in the store. This is called each time the moves count is increased (during every move).

active() {
  if (this.moves) {
    return this.$store.getters.isOn({
      row: this.index_x,
      col: this.index_y
    });
  }
}

To make this usable through the keyboard, I had to consider how the grid will be selected (enter key). That was the easy part, just call the changeStatus function to a keydown.enter event listener. The board's status is checked every time changeStatus is called to end the game once it's solved.

Only the first top left grid is able to be tabbed to (so tabindex=0 and tabindex=-1 otherwise) so only arrow keys can roam the board. Using the keydown listener for all arrow key directions, they call the setFocus method to the next grid the user is trying to select.

setFocus sends the coordinates to the store so it's reached by every grid on the board to know which one should have the pink outline.

Select outline in puzzle Select outline in puzzle
// Column.vue
setFocus (focusX, focusY) {
  this.$store.dispatch('setFocus', {
    x: focusX,
    y: focusY
  });
  eventBus.$emit('changeFocus');
}

There is a unique ref set to each grid (the first grid is col_0_0). This is how to reach the grid that should be set to focus by checking if it matches the coordinates that are in the store.

// Column.vue
computed: {
  focus() {
    return this.$store.getters.getFocus;
  },
  colRef () {
    return `col_${this.index_x}_${this.index_y}`;
  }
},
mounted () {
  // change focus depending on state's focus coordinates
  eventBus.$on("changeFocus", event => {
    const focusElem = `col_${this.focus.x}_${this.focus.y}`;
    if (!!this.$refs[focusElem] && this.focus.x === this.index_x && this.focus.y === this.index_y) {
      this.$refs[focusElem].focus();
    }
  });
}

Stats sits next to the board and displays the current number of moves taken as well as the option to reset or end the game to select a new level.

Rules uses a dropdown from Vuetify to show the description on how to solve the puzzle.

EndGame shows if the game is won displaying how many moves it took to solve it along with the options to start the same game or select a new level.

EndGame EndGame

Conclusion

This was my first complete puzzle I created when learning the Vue.js framework. I'm glad how it turned out and I hope this gave some insight to my process.

You can find the project on my GitHub .