diff --git a/README.md b/README.md index 17e3a5b..412fcf9 100644 --- a/README.md +++ b/README.md @@ -291,3 +291,6 @@ [slides](./slides/PureFunctions.md) [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/51/files) + +### +### Redux basic example \ No newline at end of file diff --git a/src/core/copyField.test.ts b/src/core/copyField.test.ts new file mode 100644 index 0000000..01fafcf --- /dev/null +++ b/src/core/copyField.test.ts @@ -0,0 +1,16 @@ +import { copyField } from './copyField'; + +import { Field } from './Field'; + +import { fieldGenerator } from './__mocks__/Field'; + +describe('Check copyField', () => { + it('Object.is should be different, data is the same', () => { + const prevField = fieldGenerator(9) as Field; + + const nextField = copyField(prevField); + + expect(prevField).not.toBe(nextField); + expect(prevField).toEqual(nextField); + }); +}); diff --git a/src/core/copyField.ts b/src/core/copyField.ts new file mode 100644 index 0000000..387badf --- /dev/null +++ b/src/core/copyField.ts @@ -0,0 +1,3 @@ +import { Field } from './Field'; + +export const copyField = (field: Field): Field => field.map((row) => [...row]); diff --git a/src/core/openCell.ts b/src/core/openCell.ts index 5a23c97..ac82179 100644 --- a/src/core/openCell.ts +++ b/src/core/openCell.ts @@ -1,6 +1,7 @@ import { CellState, Coords, Field } from './Field'; import { checkItemInField, getNeigboursItems } from './CellsManipulator'; import { detectSolvedPuzzle } from './detectSolvedPullze'; +import { copyField } from './copyField'; /** * Open cell in the player field using game field info @@ -13,6 +14,19 @@ export const openCell = ( coords: Coords, playerField: Field, gameField: Field +): [Field, boolean] => + openCellRecursively(coords, copyField(playerField), gameField); + +/** + * @param {Coords} coords + * @param {Field} playerField + * @param {Field} gameField + * @returns {[Field, boolean, number]} + */ +export const openCellRecursively = ( + coords: Coords, + playerField: Field, + gameField: Field ): [Field, boolean] => { const { empty, hidden, bomb, weakFlag, flag } = CellState; @@ -35,7 +49,7 @@ export const openCell = ( for (const [y, x] of Object.values(items)) { if (checkItemInField([y, x], gameField)) { - [playerField] = openCell([y, x], playerField, gameField); + [playerField] = openCellRecursively([y, x], playerField, gameField); } } } diff --git a/src/core/setFlag.test.ts b/src/core/setFlag.test.ts index d09b8a8..0377154 100644 --- a/src/core/setFlag.test.ts +++ b/src/core/setFlag.test.ts @@ -38,43 +38,25 @@ describe('Set flag action', () => { [h, h, h], ]; - const [playerFieldAfterFirstClick] = setFlag( - [0, 0], - playerField, - gameField, - 0, - 3 - ); + const result = setFlag([0, 0], playerField, gameField, 0, 3); - expect(playerFieldAfterFirstClick).toStrictEqual([ + expect(result[0]).toStrictEqual([ [f, h, h], [h, h, h], [h, h, h], ]); - const [playerFieldAfterSecondClick] = setFlag( - [0, 0], - playerField, - gameField, - 0, - 3 - ); + const result2 = setFlag([0, 0], result[0], gameField, 0, 3); - expect(playerFieldAfterSecondClick).toStrictEqual([ + expect(result2[0]).toStrictEqual([ [w, h, h], [h, h, h], [h, h, h], ]); - const [playerFieldAfterThirdClick] = setFlag( - [0, 0], - playerField, - gameField, - 0, - 3 - ); + const result3 = setFlag([0, 0], result2[0], gameField, 0, 3); - expect(playerFieldAfterThirdClick).toStrictEqual([ + expect(result3[0]).toStrictEqual([ [h, h, h], [h, h, h], [h, h, h], @@ -187,11 +169,11 @@ describe('Set flag action', () => { [f, h, h], ]; - setFlag([0, 0], playerField, gameField, 2, 2); - const result = setFlag([0, 0], playerField, gameField, 2, 2); - expect(result).toStrictEqual([ + const result1 = setFlag([0, 0], result[0], gameField, 2, 2); + + expect(result1).toStrictEqual([ [ [h, h, h], [h, h, h], @@ -201,7 +183,7 @@ describe('Set flag action', () => { 1, ]); - const result2 = setFlag([0, 0], playerField, gameField, 1, 2); + const result2 = setFlag([0, 0], result1[0], gameField, 1, 2); expect(result2).toStrictEqual([ [ diff --git a/src/core/setFlag.ts b/src/core/setFlag.ts index b76dd37..03e2bea 100644 --- a/src/core/setFlag.ts +++ b/src/core/setFlag.ts @@ -1,5 +1,6 @@ import { CellState, Coords, Field } from './Field'; import { detectSolvedPuzzle } from './detectSolvedPullze'; +import { copyField } from './copyField'; /** * Set flag to the cell @@ -18,25 +19,27 @@ export const setFlag = ( bombs: number ): [Field, boolean, number] => { const [y, x] = coords; - const cell = playerField[y][x]; + const newPlayerField = copyField(playerField); + + const cell = newPlayerField[y][x]; const { flag, weakFlag, hidden } = CellState; switch (cell) { case flag: - playerField[y][x] = weakFlag; + newPlayerField[y][x] = weakFlag; break; case weakFlag: - playerField[y][x] = hidden; + newPlayerField[y][x] = hidden; break; case hidden: if (prevFlagCounter < bombs) { - playerField[y][x] = flag; + newPlayerField[y][x] = flag; } break; } - const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + const [isSolved, flagCounter] = detectSolvedPuzzle(newPlayerField, gameField); - return [playerField, isSolved, flagCounter]; + return [newPlayerField, isSolved, flagCounter]; }; diff --git a/src/modules/GameWithHooks/useGame.test.ts b/src/modules/GameWithHooks/useGame.test.ts index 394fde0..6834039 100644 --- a/src/modules/GameWithHooks/useGame.test.ts +++ b/src/modules/GameWithHooks/useGame.test.ts @@ -93,51 +93,41 @@ describe('useGame test cases', () => { describe('OnClick with OnChangeGameLevel', () => { it('Check click to the cell when the level is changed', () => { const { result } = renderHook(useGame); - const { playerField, onChangeLevel } = result.current; + expect(result.current.playerField).toHaveLength(9); - expect(playerField).toHaveLength(9); - - act(() => onChangeLevel(intermediate)); + act(() => result.current.onChangeLevel(intermediate)); - const { - playerField: intermediatePlayerField, - onClick: onClickIntermediate, - } = result.current; + act(() => result.current.onClick([15, 15])); - act(() => onClickIntermediate([15, 15])); + expect(result.current.playerField).toHaveLength(16); + expect(flatWithFilter(result.current.playerField, e)).toHaveLength(2); - expect(intermediatePlayerField).toHaveLength(16); - expect(flatWithFilter(intermediatePlayerField, e)).toHaveLength(2); + act(() => result.current.onChangeLevel(expert)); - act(() => onChangeLevel(expert)); + act(() => result.current.onClick([21, 21])); - const { playerField: expertPlayerField, onClick: onClickExpert } = - result.current; - - act(() => onClickExpert([21, 21])); - - expect(expertPlayerField).toHaveLength(22); - expect(flatWithFilter(expertPlayerField, e)).toHaveLength(1); - expect(flatWithFilter(expertPlayerField, 1)).toHaveLength(2); - expect(flatWithFilter(expertPlayerField, 2)).toHaveLength(1); + expect(result.current.playerField).toHaveLength(22); + expect(flatWithFilter(result.current.playerField, e)).toHaveLength(1); + expect(flatWithFilter(result.current.playerField, 1)).toHaveLength(2); + expect(flatWithFilter(result.current.playerField, 2)).toHaveLength(1); }); it('onReset game handler', () => { const { result } = renderHook(useGame); - const { playerField, onClick, onReset, onContextMenu } = result.current; + // const { playerField, onClick, onReset, onContextMenu } = result.current; - expect(playerField).toHaveLength(9); + expect(result.current.playerField).toHaveLength(9); - act(() => onClick([0, 8])); - act(() => onContextMenu([8, 8])); + act(() => result.current.onClick([0, 8])); + act(() => result.current.onContextMenu([8, 8])); - expect(flatWithFilter(playerField, 1)).toHaveLength(1); + expect(flatWithFilter(result.current.playerField, 1)).toHaveLength(1); - act(() => onClick([0, 0])); - const { playerField: newPlayerField } = result.current; + act(() => result.current.onClick([0, 0])); - expect(flatWithFilter(newPlayerField, e)).toHaveLength(18); + expect(flatWithFilter(result.current.playerField, e)).toHaveLength(18); + + act(result.current.onReset); - act(onReset); const { playerField: finalPlayerField, isWin, @@ -160,9 +150,7 @@ describe('useGame test cases', () => { jest.useFakeTimers(); const { result } = renderHook(useGame); - const { playerField, onClick } = result.current; - - act(() => onClick([0, 8])); + act(() => result.current.onClick([0, 8])); const timeMustPass = 5; @@ -174,13 +162,13 @@ describe('useGame test cases', () => { expect(result.current.time).toBe(5); - expect(flatWithFilter(playerField, 1)).toHaveLength(1); + expect(flatWithFilter(result.current.playerField, 1)).toHaveLength(1); - act(() => onClick([0, 0])); + act(() => result.current.onClick([0, 0])); - expect(flatWithFilter(playerField, e)).toHaveLength(18); + expect(flatWithFilter(result.current.playerField, e)).toHaveLength(18); - act(() => onClick([0, 7])); + act(() => result.current.onClick([0, 7])); for (let i = 0; i < timeMustPass; i++) { act(() => { @@ -213,13 +201,13 @@ describe('useGame test cases', () => { it('Player win a game when open the last cell', () => { const { result } = renderHook(useGame); - const { gameField, onClick, onContextMenu } = result.current; + const { gameField } = result.current; for (const y of gameField.keys()) { for (const x of gameField[y].keys()) { const gameCell = gameField[y][x]; act(() => { - gameCell === b && onContextMenu([y, x]); + gameCell === b && result.current.onContextMenu([y, x]); }); } } @@ -228,26 +216,24 @@ describe('useGame test cases', () => { for (const x of gameField[y].keys()) { const gameCell = gameField[y][x]; act(() => { - gameCell !== b && onClick([y, x]); + gameCell < b && result.current.onClick([y, x]); }); } } - const { isGameOver, isWin } = result.current; - - expect(isWin).toBe(true); - expect(isGameOver).toBe(true); + expect(result.current.isWin).toBe(true); + expect(result.current.isGameOver).toBe(true); }); it('Player win the game when setup flag to the last cell', () => { const { result } = renderHook(useGame); - const { gameField, onClick, onContextMenu } = result.current; + const { gameField } = result.current; for (const y of gameField.keys()) { for (const x of gameField[y].keys()) { const gameCell = gameField[y][x]; act(() => { - gameCell !== b && onClick([y, x]); + gameCell !== b && result.current.onClick([y, x]); }); } } @@ -256,7 +242,7 @@ describe('useGame test cases', () => { for (const x of gameField[y].keys()) { const gameCell = gameField[y][x]; act(() => { - gameCell === b && onContextMenu([y, x]); + gameCell === b && result.current.onContextMenu([y, x]); }); } } diff --git a/src/modules/GameWithHooks/useGame.ts b/src/modules/GameWithHooks/useGame.ts index 3d93ae0..63ae025 100644 --- a/src/modules/GameWithHooks/useGame.ts +++ b/src/modules/GameWithHooks/useGame.ts @@ -79,7 +79,15 @@ export const useGame = (): ReturnType => { setGameLoose(); } }, - [isGameStarted, isGameOver, isWin, level, flagCounter] + [ + isGameStarted, + isGameOver, + isWin, + level, + flagCounter, + playerField, + gameField, + ] ); const onContextMenu = useCallback( @@ -98,7 +106,15 @@ export const useGame = (): ReturnType => { } setPlayerField([...newPlayerField]); }, - [isGameStarted, isGameOver, isWin, level, flagCounter] + [ + isGameStarted, + isGameOver, + isWin, + level, + flagCounter, + playerField, + gameField, + ] ); const resetHandler = ([size, bombs]: [number, number]) => {