Tic Tac Toe - Road to Game Development

The first game was easy, the scalable system was hard.

I played this as a kid. Not sure if the children growing up in the age of technology will be as familiar with this but it seemed like a great first-step into basic game programming. Let’s first look at an overview of the game itself before we get into the technicalities.

The game in essence is a two-player game in which four lines are drawn in a grid-system with nine spaces to draw shapes into (normally "x" and "o"). Each player takes turns drawing into those spaces their own shape and if they can line three in a row, that player wins.

Tic Tac Toe game
A simple game made complicated by engineering

Not a lot to debate here, but most of the time the first player has an advantage as they can put their shape in the middle. The middle space has more opportunity to be utilized in a row, leaving the opponent blocking most of the other player's moves. I remember in school you would have the occasional cool guy try and put one in the corner as their first move but normally ended with a stalemate.

This was relatively strait-forward:

  • HTML will provide interactive and non-interactive elements to the user.

  • JS will provide functionality and manipulate the elements depending on the state.

  • CSS will provide the visuals.

The game is simple enough that I do not need to add a canvas or worry about complicated animations as CSS animations could get me enough. Like a lot of work I have done in the past, I decide to take a basic Mode-View-Controller (MVP) approach.

The first thing I focus on is the functionality. I need the game to work in the end and the presentation layer is easily applied towards the end when its all working. Because of this I like to think of both forms of MVP:

  • Model-View-Controller which is a methodology to approach software.

  • Minimum-Viable-Product which is a way to focus development on things critical.

First let me explain minimum-viable product. If you have ever had a five-hundred word essay and you wrote exactly five-hundred words on said essay, than you should be familiar with what a minimum-viable product is.

A product manager may use this concept when deadlines close in, or if a project has issues with scope. It’s a great way for individuals to focus on critical features and tasks that support the core-project goals. In personal projects, I like to us it to make sure I first and foremost get my assignment functionally complete, then moving onto additional bells and whistles for added flare.

Feel free to research this more on your own, but simply put it is a methodology in software to segment the responsibility between three different areas:

  1. Model represents the data or the "brain" behind the application.

  2. View represents the presentation. Generally speaking I like to think of it as a projection of your data to the user.

  3. Controller represents the interactive elements. If you need to interact with the data, you need to provide a controller to your user to make those interactions.

Looking above, we need to come up with a way to store the data for the game. Thinking about the functionality I needed to support, I came up with the following for my model:

[
  [null, null, null],
  [null, null, null],
  [null, null, null]
]

There are many ways that we could have approached this but I decided this was the approach I was going to take. Looking at this model looks strikingly similar to a tic-tac-toe board doesn't it? I decided a two-dimensional array was ideal just to help me follow what I need to project to the screen.

I would use a null or "empty" value to represent a free space, and fill it with one value for the user and another value for the opponent.

The view was simple enough, I have to create elements in a grid and add click handlers to each of them. I ended up using CSS pseudo-classes ::before and ::after to make the "x" and "o" and do some basic animations. The :hover effect for the users-input also provided a good preview of your move before you commit to it.

const TicTacToe = () => (
  <div className='game'>
    {data.map(row => (
      <div className='row'>
        {row.map(col => (
          <Cell value={col} onClick={handleSelectCell(row, col)} />
        ))}
      </div>
    ))}
  </div>
)

With controllers, I like to keep in mind the logic flow of how the game might operate. Using the guide below, I began to write that logic out to keep the flow of the game logical.

Tic-Tac-Toe-Flow
A diagram to follow user-input and controller interaction for the game.

Pseudo-coding everything was pretty simple too. I already know I'm going to be using react-hooks to avoid life-cycle issues, and so I can use the guide above to guide state updates and opponent moves.

tic-tac-toe.js
const initialState = [
  [null, null, null],
  [null, null, null],
  [null, null, null]
]

const TicTacToe = () => {
  const [state, setState] = useState(initialState)
  
  // check if a winner exists (helps us know we are finished)
  const winner = determineWinner(state)

  // whenever ANYONE updates a cell
  const handleSelectCell = (row, col) => (e) => {
    e.preventDefault() // I just always need to keep that in mind...
    selectCell(row, col, label.player) // updates the state and we know only humans have handlers
  }

  // everytime we update, if the game isn't over and its the computers turn, go!
  useEffect() => {
    if (!winner && isComputersTurn()) {
      const { row, col } = getAvailableCell(state)
      selectCell(row, col, label.computer)
    }
  })

  const handleRestart = (e) => {
    e.preventDefault()
    updateState(initialState)
  }

  return (
    ...
  )
}

Now that all the functionality was in, I spent some time cleaning it up. Aside from the basic color and padding changes, I did experiment with a few things.

As noted above, I made the cells utilize the ::before and ::after pseudo classes. This allowed me to provide some basic animations in the cells. I needed only one for the circles, but I needed both for the "x" design.

I also experimented with the CSS clamp() function on my headings. The clamp function allows you to do responsive calculations between breakpoints so you can have fluid adjustments between units. I have seen it most on headings and so this is where I implemented it.

styles.module.css
    &::after, &::before {
      position: absolute;
      top: 10%;
      left: 10%;
      width: 80%;
      height: 80%;
      display: block;
      border: solid 0 red;
      content: ' ';

      transition: all 0.2s ease-out;
    }

    &.disabled {
      pointer-events: none;
    }

    &:hover::before, &:hover::after, &.x::before, &.x::after {
      background-color: blue;
      height: 0.25em;
      top: 50%;
      margin-top: -10%;
      transform: rotate(45deg);
    }

    &:hover::after, &.x::after {
      transform: rotate(-45deg);
    }

    &.y::before {
      border-radius: 100%;
      border-width: 0.25em;
    }
  }

I struggled with this. I could either call it a day and walk away, or I could build and build and build and never move on.

I'll be honest, my passion for this project was not in the game itself but in the system to support this and many other games. I quickly moved away from the game and moved on to areas I thought would support a game library. I made a quick list to help:

  • Games Page. We need to be able to list all the games that currently exist, either drafted or completed.

  • Saved States. There are things I want to persist, such as your high scores as you play.

  • Trophy System. This would allow players to keep track of accomplishments as they play the games I create.

  • Notification System. This was made to notify trophy unlocks, but figured it would be good to have for other things as well.

Out of this list, I created an HOC (Higher-Order Component) to wrap all my games in. By doing this, I could utilize the system across multiple pages, while also scaling to add and enhance the system later if I so choose.

I added a link to my navigation to surface the games page itself. Since this page is not connected to any database, I made a rudimentary database system that supports basic information about each game. With this, I can now keep track of static information about the games I create across multiple pages.

Saved States

I decided I wanted to utilize localStorage for my site. I delegate responsibility to the games themselves when they want to save the state, and what exactly to save, but I need to build a way to normalize this process.

Since we cannot access localStorage till the game is picked up from the client, and in case anyone is using extremely old browser tech, I needed to write a little utility to access localStorage without breaking the rest of the site.

localStorage.js
const getLocalStorage = () => {
  if (typeof window !== 'undefined') {
    return window.localStorage || {}
  } else {
    // writing down any and all functions I access externally...
    return {
      removeItem: () => {}
    }
  }
}

I wrote a Game class that would keep track of this for me. Each game would create a new instance of this class and then interact with it as needed.

Game.js
class Game {
  constructor (config) {
    this.config = config
  }

  getStateKey () {
    return `game:${this.config.id}:state`
  }

  saveState (data) {
    return getLocalStorage()[this.getStateKey()] = JSON.stringify(data)
  }

  loadState () {
    try {
      return JSON.parse(getLocalStorage()[this.getStateKey()])
    } catch (err) {
      return {}
    }
  }

  resetState () {
    return getLocalStorage().removeItem(this.getStateKey())
  }
}

With this system, I can now save content unique to each client that accesses the page and allow individuals to pick up where they left off.

Utilizing the Game.js class I wrote above, it was really easy to add in an additional system to access the user's unlocked trophies.

Game.js
class Game {
  
  // ...  

  getTrophyKey () {
    return `game:${this.config.id}:trophy`
  }

  getTrophyById (id) {
    return this.config.trophies.find(trophy => trophy.id === id) || {}
  }

  /**
   * Lists trophies, as well as unlocked status
   * @returns [Object[]] All the trophies, as well as the unlocked status
   */
  listTrophies () {
    const unlocked = this.getTrophyValue()

    return [
      ...this.config.trophies.map(trophy => ({
        ...trophy,
        unlocked: !!unlocked[trophy.id]
      }))
    ]
  }

  getTrophyValue () {
    try {
      return JSON.parse(getLocalStorage()[this.getTrophyKey()])
    } catch (err) {
      return {}
    }
  }

  unlockTrophy (id) {
    const trophies = this.getTrophyValue()

    if (!trophies[id]) {
      getLocalStorage()[this.getTrophyKey()] = JSON.stringify({
        ...trophies,
        [id]: {
          date: new Date().toISOString()
        }
      })

      return true
    }
  }
}

I wrote a basic system using the useContext api provided by React. I knew I would need some context of "global config" so I just created a global context and drop what I need into this. While this is a little unconventional I know that this is a system I am writing for myself and wont be going really anywhere else.

First I have to provide the notification handle at the global scope to let any components below know what to do when we see the notification:

Notifications.js
  const [notifications, updateNotifications] = useState([])

  // sometimes the notifications happen so fast, so we need to put them here
  // till react catches up...
  const pendingNotifiationUpdates = [
    ...notifications
  ]

  const removeNotification = (id) => {
    updateNotifications(notifications.filter(n => n.id !== id))
  }

  const addNotification = (notification) => {
    const id = Date.now()
    const newNotification = { ...notification, id }
    // if I get two notifications at once, the first one is snuffed out
    pendingNotifiationUpdates.push(newNotification)
    updateNotifications(pendingNotifiationUpdates)

    // remove after 5 seconds
    setTimeout(() => {
      removeNotification(id)
    }, 5000)
  }

  useEffect(() => {
    updateNotifications(notifications)
  }, [notifications])

Now, I need to intercept updates to the trophies with my Game HOC to send a notification to the top using the context API:

GameHOC.js
// override the default function, then call the prototype
game.unlockTrophy = function (id) {
    const trophy = Game.prototype.unlockTrophy.call(this, id)
    if (trophy) {
      const { title, description } = this.getTrophyById(id)

      context.addNotification({
        title,
        description
      })
    }
  }

This game was completely simple in nature, but I had a lot of fun developing the complexity around the game. I'm excited to see the system grow and to be one step closer to the final product. Click here to try the game out, and let me know if you learned something by sending me a message on Twitter and I look forward to next time when we visit something a little more complex.