- This file contains the analysis which I did before coding the solution.
- Basically its explanation of my thought process while reaching to the solution.
- NOTE: this was written before coding the solution so there will be little differences in actual implementation
- Demo link: Memory Game (using hooks & Context API) demo
- Cards should be laid out on a 6x6 grid (can be configured using
config
object{rows: 6, cols: 6}
), all face down initially (i.e. numbers not showing) - There should be a total of 36 cards with the numbers 1-18 (two of each), placed randomly on the grid
-
- Clicking a card should 'reveal' it - showing the hidden number of the card
- Clicking a second card should reveal that card
- If the second card has the same number as the first card, both cards should be removed from the board after 1 seconds
- If the second card has a different number to the first card, both cards should be 'hidden' again after 1 seconds (i.e. turned face down)
- The user shouldn't be able to turn over any more cards until the 3 second timer completes and the two revealed cards are either removed (if they matched), or hidden again (if they didn't)
- Once all cards are removed from the board, the game is over and the 'Play again' button should be shown
- Clicking 'Play again' should generate a new, random set of cards on the grid
thoughts on following areas 👇:
- What data structure to use for storing
Grid
&Card
data - What pieces of data should be kept in state
- Different components & How to structure/compose those components
- Provider pattern using
Context API
&useReducer
(GameProvider
to manage global state of Game) - custom hooks:
useGame()
,useCards()
etc - Different possible user actions & dispatchers corresponding to those actions
- eg:
startGame()
,openCard(card)
,closeCards([cards])
- eg:
const cards = Card[][]
- 2D array to hold 6x6 grid- each
Card
has
type Card {
value: number,
isOpen: boolean,
isMatched: boolean, // when card is matched
index: number // position of card
}
type Position {
row: number,
column: number
}
openedCards[]
- at max only 2 cards can be opened at a time- if two cards are opened & they do not match then they will be closed after 1 seconds
- when
(openCards.length >= 2)
(and not matching) thensetTimeout(closeCards, 1000)
// NOTE: sideEffect
- should not dispatch anything when openCards.length === 2
dispatch(openCard(card))
- NOTE: effect:
- when
(openCards.length >= 2)
!matchFound
thensetTimeout(closeCards, 1000)
// NOTE: sideEffectmatchFound
thensetTimeout(() => dispatch(removeMatchedCards(c1,c2)), 1000)
- when
GameProvider
Component- will have following state
type GameState {
cards: Card[][],
openedCards: Card[],
matchFound: boolean,
moveCount: 0,
isGameOver: false,
status: "RUNNING" | "OVER" | "COMPLETED" | null,
rows: number,
cols: number,
timeTaken: string // when game completes we capture timeTaken & show in results screen
}
Card
Component
<Card isMatched value={} isOpen onReveal={fn} />
// onClick of closed Card it invokes onReveal()
{type: 'START_GAME' payload: card}
{type: 'OPEN_CARD' payload: card}
// can be invoked when we want single or multiple cards to be closed
{type: 'CLOSE_CARDS' payload: [card]}
// dispatched when `matchFound` & after 1 second we want to REMOVE_MATCHED_CARD by marking those cards as `isMatched=true`
{type: 'REMOVE_MATCHED_CARDS' payload: {card1, card2}}
// NOTE: might not be needed, as this can be computed when second card is opened
{type: 'CARDS_MATCHED' payload: {card1, card2}}
<GameProvider>
<Board>
<Card />
<Card />
.
.
.
</Board>
</GameProvider>
// hooks
const cards = useCards()
const openCards = useOpenCards()
const gameState = useGame()
const dispatch = useGameDispatch()
// helper fns that dispatch appropriate actions
openCard(card)
closeCards([card])
removeMatchedCards()
const cards = createRandomCards(row, cols)
const getRandomNumber = buildGenerator(1, 18)
ScoreBar
shown at top which displays following data- number Of moves: {count}
- Time Taken