diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..addc2dd --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/plugin-proposal-private-property-in-object"] +} \ No newline at end of file diff --git a/package.json b/package.json index 357c47e..1d717da 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,11 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "node-sass": "^4.14.1", "react": "^17.0.1", "react-dom": "^17.0.1", - "react-scripts": "4.0.1", + "react-scripts": "4.0.3", + "styled-components": "^5.1.1", "web-vitals": "^0.2.4" }, "scripts": { diff --git a/public/apple.png b/public/apple.png new file mode 100755 index 0000000..d24fc80 Binary files /dev/null and b/public/apple.png differ diff --git a/public/avocado.png b/public/avocado.png new file mode 100755 index 0000000..0991e07 Binary files /dev/null and b/public/avocado.png differ diff --git a/public/banana.png b/public/banana.png new file mode 100755 index 0000000..575fe41 Binary files /dev/null and b/public/banana.png differ diff --git a/public/corn.png b/public/corn.png new file mode 100755 index 0000000..5fdc6bd Binary files /dev/null and b/public/corn.png differ diff --git a/public/lemon.png b/public/lemon.png new file mode 100755 index 0000000..c18a651 Binary files /dev/null and b/public/lemon.png differ diff --git a/public/lettuce.png b/public/lettuce.png new file mode 100755 index 0000000..b600f79 Binary files /dev/null and b/public/lettuce.png differ diff --git a/public/onion.png b/public/onion.png new file mode 100755 index 0000000..f5e5e70 Binary files /dev/null and b/public/onion.png differ diff --git a/public/strawberry.png b/public/strawberry.png new file mode 100755 index 0000000..e164b71 Binary files /dev/null and b/public/strawberry.png differ diff --git a/src/App.js b/src/App.js index 8c97871..80e8852 100644 --- a/src/App.js +++ b/src/App.js @@ -1,3 +1,225 @@ +// import styled from 'styled-components'; +/* eslint-disable no-eval */ +import React from 'react'; +import { + GAME_STATE_NEW, + GAME_STATE_RUNNING, + GAME_STATE_FINISHED, + CARD_STATE_IDLE, + CARD_STATE_ACTIVE, + CARD_STATE_FOUND, + FRUIT_APPLE, + FRUIT_AVOCADO, + FRUIT_BANANA, + FRUIT_CORN, + FRUIT_LEMON, + FRUIT_LETTUCE, + FRUIT_ONION, + FRUIT_STRAWBERRY, + TIME_CARD_ACTIVE +} from './constants' +import './App.scss'; + export default function Memory() { - return
Votre Memory ici
; + const [gameState, setGameState] = React.useState(GAME_STATE_NEW); + + const [seconds, setSeconds] = React.useState(0); + const [minutes, setMinutes] = React.useState(0); + + const [cardsShowed, setCardsShowed] = React.useState(0); + const [hitsNumber, setHitsNumber] = React.useState(0); + + /* eslint-disable no-unused-vars */ + const [caseA1State, setCaseA1State] = React.useState({ name: 'A1', state: CARD_STATE_IDLE, value: FRUIT_LETTUCE }); + const [caseA2State, setCaseA2State] = React.useState({ name: 'A2', state: CARD_STATE_IDLE, value: FRUIT_ONION }); + const [caseA3State, setCaseA3State] = React.useState({ name: 'A3', state: CARD_STATE_IDLE, value: FRUIT_AVOCADO }); + const [caseA4State, setCaseA4State] = React.useState({ name: 'A4', state: CARD_STATE_IDLE, value: FRUIT_LEMON }); + + const [caseB1State, setCaseB1State] = React.useState({ name: 'B1', state: CARD_STATE_IDLE, value: FRUIT_LETTUCE }); + const [caseB2State, setCaseB2State] = React.useState({ name: 'B2', state: CARD_STATE_IDLE, value: FRUIT_ONION }); + const [caseB3State, setCaseB3State] = React.useState({ name: 'B3', state: CARD_STATE_IDLE, value: FRUIT_APPLE }); + const [caseB4State, setCaseB4State] = React.useState({ name: 'B4', state: CARD_STATE_IDLE, value: FRUIT_LEMON }); + + const [caseC1State, setCaseC1State] = React.useState({ name: 'C1', state: CARD_STATE_IDLE, value: FRUIT_STRAWBERRY }); + const [caseC2State, setCaseC2State] = React.useState({ name: 'C2', state: CARD_STATE_IDLE, value: FRUIT_BANANA }); + const [caseC3State, setCaseC3State] = React.useState({ name: 'C3', state: CARD_STATE_IDLE, value: FRUIT_STRAWBERRY }); + const [caseC4State, setCaseC4State] = React.useState({ name: 'C4', state: CARD_STATE_IDLE, value: FRUIT_CORN }); + + const [caseD1State, setCaseD1State] = React.useState({ name: 'D1', state: CARD_STATE_IDLE, value: FRUIT_CORN }); + const [caseD2State, setCaseD2State] = React.useState({ name: 'D2', state: CARD_STATE_IDLE, value: FRUIT_BANANA }); + const [caseD3State, setCaseD3State] = React.useState({ name: 'D3', state: CARD_STATE_IDLE, value: FRUIT_APPLE }); + const [caseD4State, setCaseD4State] = React.useState({ name: 'D4', state: CARD_STATE_IDLE, value: FRUIT_AVOCADO }); + /* eslint-enable no-unused-vars */ + + const allCases = [ + caseA1State, + caseA2State, + caseA3State, + caseA4State, + caseB1State, + caseB2State, + caseB3State, + caseB4State, + caseC1State, + caseC2State, + caseC3State, + caseC4State, + caseD1State, + caseD2State, + caseD3State, + caseD4State + ]; + + // increment second each second + React.useEffect(() => { + if (gameState === GAME_STATE_RUNNING) { + const interval = setInterval(() => { + setSeconds(seconds => { + if (seconds === 59) { + return 0; + } else { + return seconds + 1; + } + }); + }, 1000); + + return () => clearInterval(interval); + } + }, [gameState]); + + // increment minutes each minute + React.useEffect(() => { + if (gameState === GAME_STATE_RUNNING) { + const minutesInterval = setInterval(() => { + setMinutes(minutes => minutes + 1); + }, 60000); + return () => clearInterval(minutesInterval); + } + }, [gameState]); + + // set game status finished if all pairs found + React.useEffect(() => { + let gameFinished = true; + allCases.forEach(oneCase => { + if (oneCase.state !== CARD_STATE_FOUND) { + gameFinished = false; + } + }); + + gameFinished && setGameState(GAME_STATE_FINISHED); + }, allCases); // eslint-disable-line react-hooks/exhaustive-deps + + const reset = () => { + setSeconds(0); + setMinutes(0); + setHitsNumber(0); + setGameState(GAME_STATE_NEW); + + // map all cases to set card state idle + // ex. setCaseA1State({...caseA1State, state: CARD_STATE_IDLE}); + allCases.forEach(oneCase => { + eval(`setCase${oneCase.name}State`)({ ...eval(`case${oneCase.name}State`), state: CARD_STATE_IDLE}) + }); + } + + + const memoryTableStatusClassname = (status) => { + switch (status.state) { + case CARD_STATE_IDLE: + return 'memoryTableItemIdle'; + case CARD_STATE_ACTIVE: + return 'memoryTableItemActive'; + case CARD_STATE_FOUND: + return 'memoryTableItemFound'; + default: + return 'memoryTableItemIdle'; + } + } + + const displayCard = (setCaseState, caseState) => { + if (gameState === GAME_STATE_NEW) { + setGameState(GAME_STATE_RUNNING); + } + + setCaseState({...caseState, state: CARD_STATE_ACTIVE}); + + if (cardsShowed + 1 < 2) { + setCardsShowed(cardsShowed + 1); + + } else { + setCardsShowed(0); + setHitsNumber(hitsNumber + 1); + + let pairFound = false + + // map all cases to set card state found if pairs + allCases.forEach(oneCase => { + const oneCaseState = eval(`case${oneCase.name}State`); + const setOneCaseState = eval(`setCase${oneCase.name}State`); + + // if other card active and not the actual one + if (oneCaseState.state === CARD_STATE_ACTIVE && oneCaseState.name !== caseState.name) { + // if values match, set status to found for the two cards + if (oneCaseState.value === caseState.value) { + setTimeout(() => { setOneCaseState({...oneCaseState, state: CARD_STATE_FOUND}); }, TIME_CARD_ACTIVE); + setTimeout(() => { setCaseState({...caseState, state: CARD_STATE_FOUND}); }, TIME_CARD_ACTIVE); + pairFound = true; + } else { + // else, set card to idle + setTimeout(() => { setOneCaseState({...oneCaseState, state: CARD_STATE_IDLE}); }, TIME_CARD_ACTIVE); + } + } + }); + + // if no pair found, set actual card to idle + !pairFound && setTimeout(() => { setCaseState({...caseState, state: CARD_STATE_IDLE}) }, TIME_CARD_ACTIVE); + } + } + + return ( +
+
+
+
+

+ Temps +

+

+ {minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')} +

+
+
+

+ Nombre de coups +

+

+ {hitsNumber} +

+
+
+

+ Cliquez sur une carte pour commencer +

+
+
+ {allCases.map(oneCase => { + const oneCaseState = eval(`case${oneCase.name}State`); + const setOneCaseState = eval(`setCase${oneCase.name}State`); + + // bind all cases + return ( + + ) + })} +
+ +
+ ); } diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 0000000..4efdc8d --- /dev/null +++ b/src/App.scss @@ -0,0 +1,94 @@ +$border-radius: 3px; + +.header { + position: absolute; + top: 0; +} + +.header { + &Infos { + display: flex; + width: 98vw; + justify-content: space-between; + + &Time { + &Title { + color: grey; + font-size: 1.25rem; + } + + &Clock { + font-size: 1.75rem; + font-weight: bold; + } + } + + &Hits { + &Title { + color: grey; + font-size: 1.25rem; + } + + &Number { + font-size: 1.75rem; + font-weight: bold; + } + } + } + + &Instructions { + text-align: center; + } +} + +.memoryTable { + margin-top: 15vh; + margin-left: auto; + margin-right: auto; + display: grid; + grid-template-columns: 10rem 10rem 10rem 10rem; + grid-template-rows: 10rem 10rem 10rem 10rem; + gap: 2%; + &Item { + cursor: pointer; + padding: 10%; + background-color: white; + border-radius: $border-radius; + border: none; + + &:disabled { + cursor: not-allowed; + } + + &Idle { + img { + display: none; + } + } + + &Active { + img { + display: block; + } + } + + &Found { + opacity: 0.6; + } + + img { + width: 8.5rem; + height: 8.5rem; + } + } +} + +.resetButton { + margin-top: 6vh; + background-color: rgb(9, 211, 172); + border: none; + padding: 7.5px; + width: 7.5rem; + font-size: 1rem; + border-radius: $border-radius; +} \ No newline at end of file diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..42f9d7e --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,126 @@ +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/dom'; +import App from './App'; +import {render} from '@testing-library/react' +import '@testing-library/jest-dom' + +it('render layout with all cards', async () => { + const { baseElement } = render() + + const header = baseElement.querySelector('.header'); + const memoryTable = baseElement.querySelector('.memoryTable'); + const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem'); + + await expect(header).toBeTruthy(); + await expect(memoryTable).toBeTruthy(); + await expect(memoryTableItems).toHaveLength(16); + + await expect(baseElement).toMatchSnapshot(); +}); + +it('display active cards then display found cards when we find a pair', async () => { + const { baseElement } = render() + + const memoryTable = baseElement.querySelector('.memoryTable'); + const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem'); + + await expect(memoryTable).toBeTruthy(); + await expect(memoryTableItems).toBeTruthy(); + + fireEvent.click(memoryTableItems[0]); + fireEvent.click(memoryTableItems[4]); + + const memoryTableItemsActive = baseElement.querySelectorAll('.memoryTableItemActive'); + expect(memoryTableItemsActive).toHaveLength(2); + + await waitFor(() => { + const memoryTableItemsFound = baseElement.querySelectorAll('.memoryTableItemFound'); + expect(memoryTableItemsFound).toHaveLength(2); + }); + + + await expect(baseElement).toMatchSnapshot(); +}); + +it('display active cards then display idle cards when we dont find a pair', async () => { + const { baseElement } = render() + + const memoryTable = baseElement.querySelector('.memoryTable'); + const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem'); + + await expect(memoryTable).toBeTruthy(); + await expect(memoryTableItems).toBeTruthy(); + + fireEvent.click(memoryTableItems[0]); + fireEvent.click(memoryTableItems[1]); + + const memoryTableItemsActive = baseElement.querySelectorAll('.memoryTableItemActive'); + expect(memoryTableItemsActive).toHaveLength(2); + + await waitFor(() => { + const memoryTableItemsFound = baseElement.querySelectorAll('.memoryTableItemIdle'); + expect(memoryTableItemsFound).toHaveLength(16); + }); + + + await expect(baseElement).toMatchSnapshot(); +}); + +it('start clock when playing and count hits', async () => { + const { baseElement } = render() + + const memoryTable = baseElement.querySelector('.memoryTable'); + const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem'); + const headerInfosTimeClock = baseElement.querySelector('.headerInfosTimeClock'); + const headerInfosHitsNumber = baseElement.querySelector('.headerInfosHitsNumber'); + + await expect(memoryTable).toBeTruthy(); + await expect(memoryTableItems).toBeTruthy(); + + expect(headerInfosTimeClock.innerHTML).toEqual('00:00'); + expect(headerInfosHitsNumber.innerHTML).toEqual('0'); + + fireEvent.click(memoryTableItems[0]); + fireEvent.click(memoryTableItems[1]); + + await waitFor(() => { + expect(headerInfosTimeClock.innerHTML).not.toEqual('00:00'); + expect(headerInfosHitsNumber.innerHTML).toEqual('1'); + }); + + + await expect(baseElement).toMatchSnapshot(); +}); + +it('can reset game', async () => { + const { baseElement } = render() + + const memoryTable = baseElement.querySelector('.memoryTable'); + const memoryTableItems = baseElement.querySelectorAll('.memoryTableItem'); + const headerInfosTimeClock = baseElement.querySelector('.headerInfosTimeClock'); + const headerInfosHitsNumber = baseElement.querySelector('.headerInfosHitsNumber'); + const resetButton = baseElement.querySelector('.resetButton'); + + await expect(memoryTable).toBeTruthy(); + await expect(memoryTableItems).toBeTruthy(); + + expect(headerInfosTimeClock.innerHTML).toEqual('00:00'); + expect(headerInfosHitsNumber.innerHTML).toEqual('0'); + + fireEvent.click(memoryTableItems[0]); + fireEvent.click(memoryTableItems[4]); + + await waitFor(() => { + expect(headerInfosTimeClock.innerHTML).not.toEqual('00:00'); + expect(headerInfosHitsNumber.innerHTML).toEqual('1'); + }); + + fireEvent.click(resetButton); + + await waitFor(() => { + expect(headerInfosTimeClock.innerHTML).toEqual('00:00'); + expect(headerInfosHitsNumber.innerHTML).toEqual('0'); + }); + + await expect(baseElement).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/src/__snapshots__/App.test.js.snap b/src/__snapshots__/App.test.js.snap new file mode 100644 index 0000000..25d8841 --- /dev/null +++ b/src/__snapshots__/App.test.js.snap @@ -0,0 +1,948 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`can reset game 1`] = ` + +
+
+
+
+
+

+ Temps +

+

+ 00 + : + 00 +

+
+
+

+ Nombre de coups +

+

+ 0 +

+
+
+

+ Cliquez sur une carte pour commencer +

+
+
+ + + + + + + + + + + + + + + + +
+ +
+
+ +`; + +exports[`display active cards then display found cards when we find a pair 1`] = ` + +
+
+
+
+
+

+ Temps +

+

+ 00 + : + 01 +

+
+
+

+ Nombre de coups +

+

+ 1 +

+
+
+

+ Cliquez sur une carte pour commencer +

+
+
+ + + + + + + + + + + + + + + + +
+ +
+
+ +`; + +exports[`display active cards then display idle cards when we dont find a pair 1`] = ` + +
+
+
+
+
+

+ Temps +

+

+ 00 + : + 01 +

+
+
+

+ Nombre de coups +

+

+ 1 +

+
+
+

+ Cliquez sur une carte pour commencer +

+
+
+ + + + + + + + + + + + + + + + +
+ +
+
+ +`; + +exports[`render layout with all cards 1`] = ` + +
+
+
+
+
+

+ Temps +

+

+ 00 + : + 00 +

+
+
+

+ Nombre de coups +

+

+ 0 +

+
+
+

+ Cliquez sur une carte pour commencer +

+
+
+ + + + + + + + + + + + + + + + +
+ +
+
+ +`; + +exports[`start clock when playing and count hits 1`] = ` + +
+
+
+
+
+

+ Temps +

+

+ 00 + : + 01 +

+
+
+

+ Nombre de coups +

+

+ 1 +

+
+
+

+ Cliquez sur une carte pour commencer +

+
+
+ + + + + + + + + + + + + + + + +
+ +
+
+ +`; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..5fd7afb --- /dev/null +++ b/src/constants.js @@ -0,0 +1,23 @@ +// game states const +export const GAME_STATE_NEW = "new"; +export const GAME_STATE_RUNNING = "running"; +export const GAME_STATE_FINISHED = "finished"; + +// card states const +export const CARD_STATE_IDLE = "idle"; +export const CARD_STATE_ACTIVE = "active"; +export const CARD_STATE_FOUND = "found"; + +// fruit values +export const FRUIT_APPLE = 'apple' +export const FRUIT_AVOCADO = 'avocado' +export const FRUIT_BANANA = 'banana' +export const FRUIT_CORN = 'corn' +export const FRUIT_LEMON = 'lemon' +export const FRUIT_LETTUCE = 'lettuce' +export const FRUIT_ONION = 'onion' +export const FRUIT_STRAWBERRY = 'strawberry' + +export const TIME_CARD_ACTIVE = 1000; + +