Ear Sharpener is a simple game for practicing pitch perception. While ear training probably won't give you absolute or perfect pitch, it may improve your relative pitch. Try it for yourself!
https://ryanatkn.github.io/ear-sharpener
- Made with TypeScript, React, Redux, Immutable.js, React Router, PostCSS, Mocha, Chai, Sinon, Enzyme, Webpack, and the Redux DevTools Extension.
- I wrote about my experience with TypeScript on this game here on Reddit.
- Redux actions are type safe in reducers using the discriminated union types
available in the pre-2.0 TypeScript nightly.
The actions are defined in
src/types.ts
. The old ununsed hacky action implementation is insrc/utils/actions
. - Friction between Redux and the needs of this game:
- There's a lot of complexity in the
gameActions
to get the desired UX. The four main sources of complexity include time-sequenced actions and side effects, disabling input when appropriate, canceling async audio that should no longer be played due to user input, and sequencing actions in the combo game differently than in standalone games. - The Redux actions and reducers are tested together, not in isolation.
This is not the recommended best practice,
but this game has thunk action creators like
gameActions.guess
andgameActions.present
that sequence multiple dispatched actions and side effects based on changing store state. Mocking the store with something like redux-mock-store is possible but impractical because these action creators read the state changes to behave correctly, so mocking the store would require mocking all state changes after each dispatch, which duplicates reducer logic and adds significant complexity. Given this dependency of actions on reducers, I wrote the action tests to include the entire action/reducer flow and assert the state changes rather than the dispatched actions, and the reducers have simple smoke tests. - Redux reducers should be pure,
so presenting a game (playing its audio) is not performed in reducers.
However presenting also requires data transformations,
so the
PresentingAction
andPresentedAction
create the new states. Doing this unfortunately fragments some of the logic. For example, it would follow the rest of the app's conventions thatpresent(game)
increments thepresentCount
and plays the audio, but now they are separate code paths. This design is more verbose and error prone than it feels like it should be.
- There's a lot of complexity in the
- There's a boundary between Immutable.js and plain JS objects/arrays in the models before the heavier data transformations. The app state is represented fully with Immutable.js data structures, but many supporting model functions use plain JS arrays/objects because Immutable.js collections are unwieldy.
- The React idea of the UI being a pure projection of state is somewhat at odds with event-triggered time-dependent effects like the feedback animations after guesses. This lead to some strange workarounds like tracking action counters in the game state that get used in React component keys to get the desired animations.
- All non-container React components implement a shallow compare for
shouldComponentUpdate
, made possible by Redux and immutable app state.
npm install
npm install -g tsd
tsd install
npm test
npm start
# browse to http://localhost:8080
- more sounds than just piano, like other instruments and generated synths
- expose game variables in the ui so the player can configure things to their liking
- let the player lock the current difficulty level
- more game types? variations of existing games?
- better support for mobile and keyboard controls
- display audio load failures to the player instead of an unending loading animation
- ...? share your ideas!
Uploaded by user beskhu at freesound.org. (public domain license) Transformed with Audacity into 3 second mp3s that fade out.
MIT