This is a case study in application architecture for the classic memory game Simon. The game requires the user to play back a computer generated sequence of notes that increases in length each turn.
Play now! https://simon.codenickycode.com
This application uses React to separate data, logic, and visual elements into separate components. State is maintained via React hooks, and UI is rendered as html via jsx. Musical sequencing and synthesis is made possible by Tone.js, an abstraction over the Web Audio API. It is deployed via Cloudflare Pages.
The parent function that orchestrates all other components to fetch data, maintain state, and render UI. This is the root of our React application.
A service to retrieve and update the global high score from our back end API. This component stores the pending, success, and error states of all requests.
A finite state machine that maintains the status of the game: New Game, Computer Turn, User Turn, Game Over. It uses the state reducer pattern, accepting a single action to transition state and update the user's current score.
A stateful hook that controls the "active" status of each pad and provides methods for accepting user and computer pad input.
Preset melodies to play when entering the game over state.
Builds a sequence of notes as the game progresses. These are the notes the user must repeat in order to continue playing. The sequencer plays tones using a synth and is observed by the pad controller to set the current playing pad as active.
These are Tone.Synth
instances that receive input from the pad controller, melody player, and sequencer to emit tones to the audio device. See Tone.js | Class Synth
The back end is a simple Hono REST API on Cloudflare Workers and a Cloudflare KV database. It exposes retrieving and updating a global high score.
-
Ensure you're using the correct version of Node:
nvm use 22.9.0
-
If necessary, install the correct version of pnpm:
npm i -g pnpm@9
-
Install dependencies
pnpm i
-
Start local dev server
pnpm run dev
-
Follow instructions in README.cloudflare
-
Follow instructions in README.sentry
- Click the "Actions" tab
- Select the "stage" workflow
- Open the dropdown for "Run workflow" and select the branch you wish to deploy
- Choose your deploy target (client, server, both)
- Click "Run workflow"
The client app will deploy to the preview url, and the server will deploy to your staging worker.
pnpm run dev
: Start the development serverpnpm run lint
: Run ESLintpnpm run test
: Run unit tests with Vitestpnpm run typecheck
: Run TypeScript type checkingpnpm run format
: Format code with Prettierpnpm run e2e
: Run end-to-end tests with Playwright
Some convenience scripts for shortcuts:
pnpm run clean
: Execute a clean install of package dependenciespnpm run client <script>
: Run a script within the client package onlypnpm run server <script>
: Run a script within the server package only
Sometimes the server fails to shutdown, leaving an instance listening to port 8787. The next time you run pnpm run server dev
, it will start a new instance and listen to a random port. Running killall workerd
does not seem to fix it. Instead, get any workerd
process ID listening to port 8787 (there may be several) and kill it. On macOS:
lsof -i :8787
kill -9 <pid>
- React SPA for the client-side application
- Tailwind CSS for styling
- Tanstack Query for client http request state management
- Hono server-side api framework
- Prettier for code formatting
- ESLint for linting
- Vitest for unit testing
- Playwright for end-to-end testing
- TypeScript for type checking
- Cloudflare Pages for hosting the client, Worker with KV storage for hosting the server
- GitHub workflows for CI and staging deployment
- Sentry integration for client-side error tracking
- pnpm for performant monorepo package management