Skip to content

Commit

Permalink
Merge pull request #52 from nickovchinnikov/nick/gameWithRedux
Browse files Browse the repository at this point in the history
Fix referential transparency
  • Loading branch information
nickovchinnikov committed Oct 13, 2021
2 parents 6aa3e8f + b98ddac commit e9797ee
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 84 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,6 @@
[slides](./slides/PureFunctions.md)

[Pull request](https://github.com/nickovchinnikov/minesweeper/pull/51/files)

###
### Redux basic example
16 changes: 16 additions & 0 deletions src/core/copyField.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
3 changes: 3 additions & 0 deletions src/core/copyField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Field } from './Field';

export const copyField = (field: Field): Field => field.map((row) => [...row]);
16 changes: 15 additions & 1 deletion src/core/openCell.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;

Expand All @@ -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);
}
}
}
Expand Down
38 changes: 10 additions & 28 deletions src/core/setFlag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand All @@ -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([
[
Expand Down
15 changes: 9 additions & 6 deletions src/core/setFlag.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CellState, Coords, Field } from './Field';
import { detectSolvedPuzzle } from './detectSolvedPullze';
import { copyField } from './copyField';

/**
* Set flag to the cell
Expand All @@ -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];
};
80 changes: 33 additions & 47 deletions src/modules/GameWithHooks/useGame.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand All @@ -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(() => {
Expand Down Expand Up @@ -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]);
});
}
}
Expand All @@ -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]);
});
}
}
Expand All @@ -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]);
});
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/modules/GameWithHooks/useGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@ export const useGame = (): ReturnType => {
setGameLoose();
}
},
[isGameStarted, isGameOver, isWin, level, flagCounter]
[
isGameStarted,
isGameOver,
isWin,
level,
flagCounter,
playerField,
gameField,
]
);

const onContextMenu = useCallback(
Expand All @@ -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]) => {
Expand Down

0 comments on commit e9797ee

Please sign in to comment.