Sudoku - Road to Game Development

A big leap in the right direction

When I started this project, I had no idea how big it was going to be. As a personal journey, moving from Tic-Tac-Toe to Sudoku was substantial. Nevertheless I was commited to get this done and to do it well.

I'm no Sudoku expert, but I do enjoy a game here and there. I wrote this summary on the game page itself:

Sudoku is a puzzle game designed for a single player. Each puzzle is made of a grid of 81 boxes. The goal of the game is to complete the puzzle, under the assumption that numbers one through nine can only be used once per row, once per cell and once per matrix.

Outside of that, there are many other websites that explain different techniques to try, so I'll leave it to the professionals.

First things first is that I wanted a full-screen experience. I added a flag to my page API so that I could toggle full-screen. This flag would remove the global header and footer so that the page could be free of any clutter that comes from the rest of my site. I still have the notification system so that works great for my use-case.

Next, I worked hard to make the grid with basic inputs. Each of these were essentially using a lot of the tailwind utilities to make the cells evenly spaced.

I would say there were two primary areas where this game had challenges. First it was all of the state management, and second it was generating the board (as part of the state).

I ran into a few problems with the state. First I noticed that setInterval loses scope of the hooks. I was able to solve this by using Dan Abromov's recommended approach to the problem. The other thing I ran into was issues with event-hell. There was a lot of events I was trying to monitor with this game and thus my event-flow was really messed up.

In the end I ended up segmenting my hooks from my events so that I could focus on responsibility scope when I was monitoring responsibility in a modular fashion.

My root Game.js file essentially had all the events it cared about there, and the hooks were segmented to separate files. After that, I added either a reference to a hook I made from a separate file, or a hook that is needed in the immediate file. Essentially my folder looks like the following:

Screenshot 2020-10-26 155432
Sudoku game folder structure

This has cleaned up my main Game file to be much smaller and my game itself to be more organized.

  • Cell.js - The individual cell within the sudoku game cell.

  • Container.js - The shared container used between all states of the game so its all locked with a mobile view.

  • Game.js - The root game that has all of the details about the current board game the user is viewing.

  • hooks.js - Custom game-specific hooks that are required to run the game.

  • index.js - The main game wrapper. Lets you decide what difficulty you choose and also shows the result screen.

  • info.json - Used for each game. Houses the meta-information about the game.

  • Number.js - The number button at the bottom of the game screen.

  • utils.js - Any shared functionality to cross-communicate between all the components and hooks.

I thought this one would be a lot simpler (oh silly silly Matt). I was going to go through the process of just walking through the board with valid moves.

My first approach was problematic from the start. I created a loop that would walk cell-by-cell and make sure the number was valid. I thought it would be a sure-fire win:

const generateGame = () => {
  const data = []
  for (let i = 0; i < 9; i++) {
    const row = []
    for (let j = 0; j < 9; j++) {
      let num
      do {
        num = randomNumber()
      } while (row.indexOf(num) !== -1 || data.map(r => r[j]).indexOf(num) !== -1)
      // (done) not in the row
      // (done) not in the col
      // not in the matrix
      row.push(num)
    }
    data.push(row)
  }

  return data
}

Unfortunately this caused a problem when we would use the only usable numbers in previous cells on the current row:

Screen Shot 2020-10-04 at 11.50.04 PM
stuck repeating over and over and over

With that being a problem, I tried a third approach. I would reduce the available moves going cell-by-cell like so:

const randomIndex = (items) => items[Math.floor(Math.random() * items.length)]
const removeFromIndexes = (arr) => {
  return randomIndex([1, 2, 3, 4, 5, 6, 7, 8, 9].reduce((all, x) => {
    if (arr.indexOf(x) === -1) {
      all.push(x)
    }

    return all
  }, [])) || ''
}

const generateGame = () => {
  const data = []
  for (let i = 0; i < 9; i++) {
    const row = []
    for (let j = 0; j < 9; j++) {
      const num = removeFromIndexes([
        ...row,
        ...data.map(r => r[j]),
        // eventually also the matrix...
      ])

      row.push(num)
    }
    data.push(row)
  }

  return data
}

Using this approach posed some problems in which some cells had no available moves at the time of generation:

Screen Shot 2020-10-05 at 9.51.44 PM
Some places, there is nothing available...

To get around all of this, I settled on a third solution that "mostly" works. I shuffle the whole row and check the entire row. This seems to work most of the time. Every now and again I run into problems from what I presume is because I am not also considering the matrix (what I call the group of nine cells).

I ran into a lot of problems trying to listen to keydown events in the browser. Whenever I would press keys I wouldn't have access to the current state. A real scoping issue. The following code always threw unexpected results. Even though the key was valid in the cell, the game would not update the cell, and sometimes the whole game would completely update.

useEffect(() => {
  const listenerForKeypress = (e) => {
    e.preventDefault()
    // KEY_MAPPINGS is a lookup-table with keys and keycodes
    const value = KEY_MAPPINGS[e.keyCode]
    if (value) {
      tryValue(value)
    }
  }

  document.addEventListener('keydown', listenerForKeypress)
  return () => {
    document.removeEventListener('keydown', listenerForKeypress)
  }
}, [])

This was resolved by listening to keys individually. I found out the hard way that useRef was also a good idea when you are working with document events.

Now I want to discuss a lot of the fun things I came up with behind the scenes that helped the development process along the way.

I added the ability to use sound effects in the game. We use the React context API so that we can have each sound effect controlled from the top level, and add the ability to turn off sound effects and background music.

These are always fun. I didn't do the Portal idea even though I'm sure that is the right approach. I'll probably refactor this later for optimization purposes, but for now it works great.

I really tried to push this further than I have in the past. Some turned out great, others turned out "good enough". I still wouldn't say I'm a master but it was a great learning experience for me to experiment.

With Tic-Tac-Toe, I had saved your wins and fastest times, now I had to expand it a bit to save some of your options.

I took the achievements a little further and now you can unlock things. I'm goal oriented and so I always appreciate a good unlockable, even if it isn't the most creative thing.

Overall, this was a huge leap forward for me personally. I have made small interactives in the past, or experimented with something, only later to ditch it entirely. Regardless, check it out for yourself and let me know what you think!

My next task was scheduled to be Minesweeper. I'm on the fence on what it could really do for me. Will it actually expand my personal growth? Will it introduce anything new? I'll report back later with results, but until then: enjoy the game!