Introducing Neno - Part 2

Introducing Neno - Part 2

Project architecture and components

I've introduced the game idea and its rules in the previous part of this series. In this article., I'll be going over the architecture of the project.

This article is a work in progress and I'll frequently update it.

Game Requirements

The first step to building any real world application is requirements analysis and gathering.

Goal Building an online word guessing game with an early 2000's/grade school aesthetic.

Real time

The game is going to be online meaning we'd need to allow users to play in real time.

The game allows players to create or join public/private rooms and play with others in real-time. Such functionality is achieved via web sockets.

I'll be using GraphQL subscriptions to achieve this functionality since we're using Hasura.

I've enabled authentication via Facebook and Google to make it easier to invite friends for new games.

Screenshot 2022-04-05 at 23.27.33.png

Retro aesthetic

I needed to use a UI library to simplify my development process because I wanted the game to have the following:

  • Consistent theme
  • Layout components
  • Customisable components

I found Chakra UI to be a good library because it gave me tons of flexibility by allowing me to use my own custom styles in most situations. I've only needed to use Chakra components for accessibility and well designed APIs.

Development experience

I chose Next.js as my framework due to it's performance optimisation and easy to follow structure.

Next.js is an opinionated framework for building server rendered applications.

State management

I want the state of my game to be predictable and I also wanted to limit the number of states that I'd have to deal with.

For this I'm going to be using xState which is a state management library that uses state machines.

If you're interested in learning more about state machines checkout this awesome article from Kent C. Dodds.


Directory Structure

I've used Next.js which is an opinionated JavaScript framework but I had an option to create my directorates.

📦src
 ┣ 📂Game
 ┣ 📂Rooms
 ┣ 📂User
 ┣ 📂components
 ┣ 📂hooks
 ┣ 📂layout
 ┣ 📂lib
 ┃ ┣ 📂graphql
 ┣ 📂machines
 ┃ ┣ 📂game
 ┃ ┗ 📂player
 ┣ 📂providers
 ┣ 📂utils

Root

The root consists of configuration files and project directories.

Config files

At the very root of my project, I have some config files which I had use to initialize the project.

  • nvm.rc to dynamically switch to the desired Node version for this project.
  • tsconfig.json for my TypeScript rules
  • codegen to generate types for my GraphQL schema
  • eslint.rc to enforce proper code styles and rule`
  • .husky I use it for pre-commit hooks to ensure that my code adheres to quality standards before pushing it. It mainly lints and formats my code before every commit.

Directories

I have structured my directories based on the domain.

Components

These are common UI components that are shared between different parts of the application. They include Button, Inputs timers etc.

Game

Contains game specific components.

Rooms

Contains room specific components.

Hooks

I have created hooks to help aid abstract my GraphQL operations. This is general structure for many of my network related hooks.

Here's how the raw gql mutation looks like:

export const INSERT_ROOM_MEMBER = gql`
  mutation insertRoomMember($member: rooms_members_insert_input!) {
    insert_rooms_members_one(object: $member) {
      id
      role
      roomId
      member {
        id
        lastSeen
        createdAt
        email
      }
    }
  }
`;

This is how it looks like after wrapping the mutation in a hook.

export const useInsertRoomMember = () => {
  const [insertRoomMember, { data, loading, error }] = useMutation<
    InsertRoomMemberMutation,
    InsertRoomMemberMutationVariables
  >(INSERT_ROOM_MEMBER);
  return useMemo(
    () => ({
      memberLoading: loading,
      memberError: error,
      member: data?.insert_rooms_members_one,
      insertRoomMember: (member: OperationVariables) => {
        return insertRoomMember({ variables: { member } }).then(
          ({ data }) => data?.insert_rooms_members_one
        );
      },
    }),
    [loading, error, data?.insert_rooms_members_one, insertRoomMember]
  );
};
Machines

I use this directory to define state machines.

I have created two machines to represent/manage the state of my game.

  • player controls player specific actions
  • game controls the flow of the game
Providers

This is for global providers which I use to wrap my entire project. I mainly use two providers.

  • Theme to control theme of the entire game
  • Game to control the data of the game
Utils

A set of utility functions to help with the control of the game.

Lib

For any 3rd party libraries that I intend to use in my game.

Here is where I initialise my Apollo Client and point it to my running Hasura instance.

I've created a few tables and enabled read/write permissions for operations necessary to play the game.

Codegen

I have also setup codegen which is a very useful tool to generate types for your schema.

I have a codegen script in my package.json file which uses apollo-codegen to generates relevant types to be mapped to our GraphQL queries and mutations. These generated types are what we then use in our query and mutation hooks to state the types.

To specify to the codegen what we would like to generate, a file codegen.js is passed to it.

Game Logic

Screenshot 2022-03-13 at 20.15.46.png

  • Players are required to guess 4 words within 40 seconds.
  • Active players are shown along with their results for each round.
  • The last player to submit is eliminated while the others proceed to the next round.
  • Each round generates a new letter and thus a new set of words to be guessed.