-
Notifications
You must be signed in to change notification settings - Fork 710
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
doc: Explain some more confusing aspects
- Loading branch information
Showing
2 changed files
with
39 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# React Demo Game | ||
|
||
The demo game is present to help you test your [Adagrams implementation](../adagrams.js). At its core, the demo game is a bunch of UI that is built around the Adagrams API, which consists of the four methods `drawLetters`, `usesAvailableLetters`, `scoreWord`, and `highestScoreFrom`. | ||
|
||
As a result, when you first start the demo game, before you've implemented any of the Waves, the demo game won't function correctly! Specifically, it starts off thinking that every hand of letters is `["H", "E", "L", "L", "O", "W", "O", "R", "L", "D"]`, that any word at all "uses" those letters, and that everything is worth 0 points. As you implement the Adagrams functions (and pass its tests), you make it so the demo game functions correctly. | ||
|
||
## The Proxy Pattern | ||
|
||
The way the demo game functions without your implementation is by applying the [Proxy Pattern](https://en.wikipedia.org/wiki/Proxy_pattern) to the methods of your [adagrams.js](../adagrams.js). The Proxy in this case is an object defined in [adagrams-proxy.js](./adagrams-proxy.js). This proxy object implements the same "interface" as your real Adagrams -- that is, it defines the same four functions, with the same names, parameters, and return types -- and provides default behavior for any cases where the real Adagrams returns `undefined` -- that's the default return value for any JavaScript function. When you start implementing your Adagrams, and the functions stop returning `undefined`, the Proxy automatically switches to using your implementation for the function instead of its default behavior. | ||
|
||
The traditional definition of the Proxy Pattern explains that two "concrete" classes will inherit from an "interface". In other languages besides JavaScript, an interface is a way to explicitly specify the names, parameters, and return values of a class's methods without providing any implementation for them. An interface is usually described as a "contract" that code in a function expects an instance object of a class that implements the interface to fulfill. JavaScript doesn't have a way to explicitly define an interface in code; you call a method, and deal with whatever the result is. (A return value of `undefined` or a runtime error might be that result!) So when we implement the Proxy Pattern in JavaScript, our Proxy object fulfills the "implicit" interface for our real object. In this case, we know what the interface is because there are tests, other functions outside the module, and documents describing it. The Proxy and Real Adagrams objects implement the same "implicit" interface because they satisfy the expectations of the code that uses them. If the idea of an implicit interface makes you feel uncomfortable, then you might like TypeScript. | ||
|
||
## How is the game structured? | ||
The demo game is built with a framework called [Ink](https://github.com/vadimdemedes/ink#readme), which makes it so you can develop terminal applications using [React](https://reactjs.org/). This means you will find React concepts -- props, state, jsx, etc. -- used throughout the demo game code. | ||
|
||
- [components/](./components/): React components that can be reused. This includes simple display-only components like [Button](./components/button.js), complex input-handling components like [NumberField](./components/number-field.js), and more-esoteric components such as the context-providing [GameStateStore](./components/gamestate-context.js). | ||
- [gamestate/](./gamestate/): Reducers, actions, and "middleware", following patterns that are like redux but implemented with [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer). | ||
- [screens/](./screens/): React components that represent the various "screens" that players move through during the game. The [ScreenDisplayer](./screens/index.js) chooses the screen based on the current state. | ||
|
||
## History | ||
This is not the first incarnation of the JS Adagrams demo game! An [Architectural Decision Record](./docs/adr.md) describes the latest iteration as well as reasoning behind its development. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# gamestate | ||
This is where you find functions controlling the game's underlying state, which includes things like what screen the game is displaying, how many rounds have been requested, what players have guessed, etc. There are a few types of things here that you will commonly find in code following a [state reducer](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers) pattern: | ||
- Actions | ||
- Reducers | ||
- Middleware | ||
- Selectors | ||
|
||
## Actions | ||
An action is just an object. It's expected to have a `type` field with a string value that tells you what type of action it is. Beyond that, it's completely up to you what's in the action object, but most people put the data it contains in a field called `payload`. The point of an action is to indicate, via its type, which part of the reducer should run and provide whatever data is needed to calculate the next state. | ||
|
||
## Reducers | ||
The `reducer` at the bottom of [reducer.js](./reducer.js) looks complicated. Fundamentally, it's a function that is just like the callback you pass to [Array.reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). In fact, you can imagine React, under the covers, taking an array of action objects that it has been given via `dispatch`, and literally calling `reduce` on that array using your reducer function. The `reduce` function will use your reducer to calculate the new `state`, starting with `initialState`, for every action that it was provided. (You do have to imagine this, though. React doesn't actually use `reduce`. It needs to prioritize, defer, and occasionally discard work it has done to update state, so the mechanism it uses to loop over actions is more complicated.) Once the actions are applied, you end up with a brand new state object that is the result of previous state and action running through your reducer function. | ||
|
||
## Middleware | ||
So why is the `reducer` surrounded by a bunch of other function calls? These are "middleware". They apply the [Decorator Pattern](https://blog.logrocket.com/understanding-javascript-decorators/) to modify the core behavior of `gameStateReducer`. For example, while `gameStateReducer` will happily set `desiredPlayers` to whatever the action payload's value is, it is wrapped in [validateOptionsInput](./options.js), which will swap the action out for an error action if the payload isn't in a valid range. You could achieve the same result with a big, monolithic reducer function, but using the decorator pattern gives you more flexibility to change the reducer without modifying its existing implementation. (That's the O in [SOLID](https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/), open/closed: the reducer is open for extension, but closed for modification.) The separation of middleware also presents opportunity to test the middleware independently, though the current suite of tests simply tests the reducer with all the middleware wrapping it. | ||
|
||
## Selectors | ||
There aren't many selectors; the purpose of selectors is to take the `state` object and retrieve information from it. Most places in the application just read what they need directly from `state`, but the win screen has some more complicated calculations to do. Those have gone in [WinScreenInfo](./win-selectors.js). |