diff --git a/README.md b/README.md index c102d3e..fadb970 100644 --- a/README.md +++ b/README.md @@ -191,3 +191,7 @@ ### Test refactoring [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/31/files) + +### Set flag action + +[Pull request](https://github.com/nickovchinnikov/minesweeper/pull/32/files) diff --git a/src/helpers/CellsManipulator.test.ts b/src/helpers/CellsManipulator.test.ts index 2078522..fef61bf 100644 --- a/src/helpers/CellsManipulator.test.ts +++ b/src/helpers/CellsManipulator.test.ts @@ -3,7 +3,6 @@ import { incrementNeibours, getNeigboursItems, checkItemInField, - openCell, } from './CellsManipulator'; const { empty: e, hidden: h, bomb: b } = CellState; @@ -221,113 +220,3 @@ describe('Check Increment Neibours', () => { }); }); }); - -describe('Open cell action', () => { - describe('Simple cases with loose', () => { - it('Open cell with the bomb', () => { - expect(() => - openCell( - [1, 1], - [ - [h, h], - [h, h], - ], - [ - [1, 1], - [1, b], - ] - ) - ).toThrow('Game Over'); - }); - }); - describe('Open cell with number', () => { - it('Open cell with state == 1', () => { - const playerField = openCell( - [1, 1], - [ - [h, h, h], - [h, h, h], - [h, h, h], - ], - [ - [1, 1, 0], - [9, 1, 0], - [1, 1, 0], - ] - ); - expect(playerField).toStrictEqual([ - [h, h, h], - [h, 1, h], - [h, h, h], - ]); - }); - it('Open cell with state == 3', () => { - const playerField = openCell( - [1, 1], - [ - [h, h, h], - [h, h, h], - [h, h, h], - ], - [ - [9, 2, 0], - [9, 3, 0], - [9, 2, 0], - ] - ); - expect(playerField).toStrictEqual([ - [h, h, h], - [h, 3, h], - [h, h, h], - ]); - }); - }); - describe('Open empty cell', () => { - it('Open empty cell, simple 3*3 case', () => { - const playerField = openCell( - [1, 2], - [ - [h, h, h], - [h, h, h], - [h, h, h], - ], - [ - [1, 1, 0], - [9, 1, 0], - [1, 1, 0], - ] - ); - expect(playerField).toStrictEqual([ - [h, 1, 0], - [h, 1, 0], - [h, 1, 0], - ]); - }); - it('Open empty cell 5*5 case', () => { - const playerField = openCell( - [2, 2], - [ - [h, h, h, h, h], - [h, h, h, h, h], - [h, h, h, h, h], - [h, h, h, h, h], - [h, h, h, h, h], - ], - [ - [9, 9, 1, 1, 2], - [9, 3, 1, 0, 0], - [1, 1, 0, 1, 1], - [1, 0, 0, 1, 9], - [2, 1, 0, 1, 0], - ] - ); - expect(playerField).toStrictEqual([ - [h, h, 1, 1, 2], - [h, 3, 1, 0, 0], - [1, 1, 0, 1, 1], - [1, 0, 0, 1, h], - [2, 1, 0, 1, h], - ]); - }); - }); -}); diff --git a/src/helpers/CellsManipulator.ts b/src/helpers/CellsManipulator.ts index e7fa007..3838add 100644 --- a/src/helpers/CellsManipulator.ts +++ b/src/helpers/CellsManipulator.ts @@ -1,4 +1,4 @@ -import { Cell, CellState, Coords, Field } from './Field'; +import { Cell, Coords, Field } from './Field'; /** * Get neighbour cells indexes @@ -48,46 +48,3 @@ export const incrementNeibours = (coords: Coords, field: Field): Field => { return field; }; - -/** - * Open cell in the player field using game field info - * @param {Coords} coords - * @param {Field} playerField - * @param {Field} gameField - * @returns {Field} - */ -export const openCell = ( - coords: Coords, - playerField: Field, - gameField: Field -): Field => { - const { empty, hidden, bomb } = CellState; - - const [y, x] = coords; - const gameCell = gameField[y][x]; - - if (gameCell === bomb) { - throw new Error('Game Over'); - } - - if (gameCell === empty) { - playerField[y][x] = gameCell; - - const items = getNeigboursItems(coords); - - for (const [y, x] of Object.values(items)) { - if (checkItemInField([y, x], gameField)) { - const playerCell = playerField[y][x]; - const gameCell = gameField[y][x]; - - if (playerCell === hidden && gameCell !== bomb) { - playerField = openCell([y, x], playerField, gameField); - } - } - } - } - - playerField[y][x] = gameCell; - - return playerField; -}; diff --git a/src/helpers/openCell.test.ts b/src/helpers/openCell.test.ts new file mode 100644 index 0000000..a1ce5ec --- /dev/null +++ b/src/helpers/openCell.test.ts @@ -0,0 +1,114 @@ +import { CellState } from './Field'; +import { openCell } from './openCell'; + +const { empty: e, hidden: h, bomb: b } = CellState; + +describe('Open cell action', () => { + describe('Simple cases with loose', () => { + it('Open cell with the bomb', () => { + expect(() => + openCell( + [1, 1], + [ + [h, h], + [h, h], + ], + [ + [1, 1], + [1, b], + ] + ) + ).toThrow('Game Over'); + }); + }); + describe('Open cell with number', () => { + it('Open cell with state == 1', () => { + const playerField = openCell( + [1, 1], + [ + [h, h, h], + [h, h, h], + [h, h, h], + ], + [ + [1, 1, 0], + [9, 1, 0], + [1, 1, 0], + ] + ); + expect(playerField).toStrictEqual([ + [h, h, h], + [h, 1, h], + [h, h, h], + ]); + }); + it('Open cell with state == 3', () => { + const playerField = openCell( + [1, 1], + [ + [h, h, h], + [h, h, h], + [h, h, h], + ], + [ + [9, 2, 0], + [9, 3, 0], + [9, 2, 0], + ] + ); + expect(playerField).toStrictEqual([ + [h, h, h], + [h, 3, h], + [h, h, h], + ]); + }); + }); + describe('Open empty cell', () => { + it('Open empty cell, simple 3*3 case', () => { + const playerField = openCell( + [1, 2], + [ + [h, h, h], + [h, h, h], + [h, h, h], + ], + [ + [1, 1, 0], + [9, 1, 0], + [1, 1, 0], + ] + ); + expect(playerField).toStrictEqual([ + [h, 1, 0], + [h, 1, 0], + [h, 1, 0], + ]); + }); + it('Open empty cell 5*5 case', () => { + const playerField = openCell( + [2, 2], + [ + [h, h, h, h, h], + [h, h, h, h, h], + [h, h, h, h, h], + [h, h, h, h, h], + [h, h, h, h, h], + ], + [ + [9, 9, 1, 1, 2], + [9, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, 9], + [2, 1, 0, 1, 0], + ] + ); + expect(playerField).toStrictEqual([ + [h, h, 1, 1, 2], + [h, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, h], + [2, 1, 0, 1, h], + ]); + }); + }); +}); diff --git a/src/helpers/openCell.ts b/src/helpers/openCell.ts new file mode 100644 index 0000000..edb1b41 --- /dev/null +++ b/src/helpers/openCell.ts @@ -0,0 +1,45 @@ +import { CellState, Coords, Field } from './Field'; +import { checkItemInField, getNeigboursItems } from './CellsManipulator'; + +/** + * Open cell in the player field using game field info + * @param {Coords} coords + * @param {Field} playerField + * @param {Field} gameField + * @returns {Field} + */ +export const openCell = ( + coords: Coords, + playerField: Field, + gameField: Field +): Field => { + const { empty, hidden, bomb } = CellState; + + const [y, x] = coords; + const gameCell = gameField[y][x]; + + if (gameCell === bomb) { + throw new Error('Game Over'); + } + + if (gameCell === empty) { + playerField[y][x] = gameCell; + + const items = getNeigboursItems(coords); + + for (const [y, x] of Object.values(items)) { + if (checkItemInField([y, x], gameField)) { + const playerCell = playerField[y][x]; + const gameCell = gameField[y][x]; + + if (playerCell === hidden && gameCell !== bomb) { + playerField = openCell([y, x], playerField, gameField); + } + } + } + } + + playerField[y][x] = gameCell; + + return playerField; +}; diff --git a/src/helpers/setFlag.test.ts b/src/helpers/setFlag.test.ts new file mode 100644 index 0000000..3377c62 --- /dev/null +++ b/src/helpers/setFlag.test.ts @@ -0,0 +1,78 @@ +import { CellState, Field } from './Field'; +import { setFlag } from './setFlag'; + +const { empty: e, hidden: h, bomb: b, flag: f, weakFlag: w } = CellState; + +describe('Set flag action', () => { + describe('Set flag to the cell check', () => { + it('Set flag to the non hidden cell should be ignored', () => { + const gameField: Field = [ + [1, 1, e], + [b, 1, e], + [1, 1, e], + ]; + const playerField: Field = [ + [1, h, h], + [h, h, h], + [h, h, h], + ]; + + const newPlayerField = setFlag([0, 0], playerField, gameField); + + expect(newPlayerField).toStrictEqual([ + [1, h, h], + [h, h, h], + [h, h, h], + ]); + }); + it('Set Flag action, simple 3*3 case', () => { + const gameField: Field = [ + [1, 1, e], + [b, 1, e], + [1, 1, e], + ]; + + const playerField: Field = [ + [h, h, h], + [h, h, h], + [h, h, h], + ]; + + const playerFieldAfterFirstClick = setFlag( + [0, 0], + playerField, + gameField + ); + + expect(playerFieldAfterFirstClick).toStrictEqual([ + [f, h, h], + [h, h, h], + [h, h, h], + ]); + + const playerFieldAfterSecondClick = setFlag( + [0, 0], + playerField, + gameField + ); + + expect(playerFieldAfterSecondClick).toStrictEqual([ + [w, h, h], + [h, h, h], + [h, h, h], + ]); + + const playerFieldAfterThirdClick = setFlag( + [0, 0], + playerField, + gameField + ); + + expect(playerFieldAfterThirdClick).toStrictEqual([ + [h, h, h], + [h, h, h], + [h, h, h], + ]); + }); + }); +}); diff --git a/src/helpers/setFlag.ts b/src/helpers/setFlag.ts new file mode 100644 index 0000000..8384834 --- /dev/null +++ b/src/helpers/setFlag.ts @@ -0,0 +1,33 @@ +import { CellState, Coords, Field } from './Field'; + +/** + * Set flag to the cell + * @param {Coords} coords + * @param {Field} playerField + * @param {Field} gameField + * @returns {[Field, FlagCounter]} + */ +export const setFlag = ( + coords: Coords, + playerField: Field, + gameField: Field +): Field => { + const [y, x] = coords; + const cell = playerField[y][x]; + + const { flag, weakFlag, hidden } = CellState; + + switch (cell) { + case flag: + playerField[y][x] = weakFlag; + break; + case weakFlag: + playerField[y][x] = hidden; + break; + case hidden: + playerField[y][x] = flag; + break; + } + + return playerField; +}; diff --git a/src/modules/GameWithHooks/GameWithHooks.test.tsx b/src/modules/GameWithHooks/GameWithHooks.test.tsx index 1e8c937..1347d69 100644 --- a/src/modules/GameWithHooks/GameWithHooks.test.tsx +++ b/src/modules/GameWithHooks/GameWithHooks.test.tsx @@ -7,6 +7,7 @@ import { GameWithHooks } from './GameWithHooks'; const mockOnClick = jest.fn(); const mockOnChangeLevel = jest.fn(); const mockOnReset = jest.fn(); +const mockOnContextMenu = jest.fn(); jest.mock('./useGame', () => ({ __esModule: true, @@ -20,6 +21,7 @@ jest.mock('./useGame', () => ({ [10, 10], ], onClick: mockOnClick, + onContextMenu: mockOnContextMenu, onChangeLevel: mockOnChangeLevel, onReset: mockOnReset, }), @@ -39,6 +41,11 @@ describe('GameWithHooks test cases', () => { userEvent.click(screen.getByTestId('0,0')); expect(mockOnClick).toHaveBeenCalled(); }); + it('Context menu handler on a cell works fine', () => { + render(); + userEvent.click(screen.getByTestId('0,0'), { button: 2 }); + expect(mockOnContextMenu).toHaveBeenCalled(); + }); it('Reset handler works fine', () => { render(); userEvent.click(screen.getByRole('button')); diff --git a/src/modules/GameWithHooks/GameWithHooks.tsx b/src/modules/GameWithHooks/GameWithHooks.tsx index b87ae87..19490d2 100644 --- a/src/modules/GameWithHooks/GameWithHooks.tsx +++ b/src/modules/GameWithHooks/GameWithHooks.tsx @@ -17,6 +17,7 @@ export const GameWithHooks: FC = () => { settings, playerField, onClick, + onContextMenu, onChangeLevel, onReset, } = useGame(); @@ -42,7 +43,7 @@ export const GameWithHooks: FC = () => { onReset={onReset} /> {isGameOver && } - null}> + {playerField} diff --git a/src/modules/GameWithHooks/useGame.test.ts b/src/modules/GameWithHooks/useGame.test.ts index b545443..0a4e152 100644 --- a/src/modules/GameWithHooks/useGame.test.ts +++ b/src/modules/GameWithHooks/useGame.test.ts @@ -65,6 +65,17 @@ describe('useGame test cases', () => { expect(flatWithFilter(newPlayerField, e)).toHaveLength(18); }); + it('Context menu handler', () => { + const { result } = renderHook(useGame); + + const { onContextMenu } = result.current; + + act(() => onContextMenu([0, 0])); + + const { playerField: newPlayerField } = result.current; + + expect(flatWithFilter(newPlayerField, f)).toHaveLength(1); + }); it('Click to the non-empty cells area', () => { const { result } = renderHook(useGame); diff --git a/src/modules/GameWithHooks/useGame.ts b/src/modules/GameWithHooks/useGame.ts index 125759c..e091f92 100644 --- a/src/modules/GameWithHooks/useGame.ts +++ b/src/modules/GameWithHooks/useGame.ts @@ -7,7 +7,8 @@ import { fieldGenerator, Coords, } from '@/helpers/Field'; -import { openCell } from '@/helpers/CellsManipulator'; +import { openCell } from '@/helpers/openCell'; +import { setFlag } from '@/helpers/setFlag'; import { LevelNames, GameSettings } from '@/modules/GameSettings'; @@ -19,6 +20,7 @@ interface ReturnType { playerField: Field; gameField: Field; onClick: (coords: Coords) => void; + onContextMenu: (coords: Coords) => void; onChangeLevel: (level: LevelNames) => void; onReset: () => void; } @@ -49,6 +51,11 @@ export const useGame = (): ReturnType => { } }; + const onContextMenu = (coords: Coords) => { + const newPlayerField = setFlag(coords, playerField, gameField); + setPlayerField([...newPlayerField]); + }; + const resetHandler = ([size, bombs]: [number, number]) => { const newGameField = fieldGenerator(size, bombs / (size * size)); const newPlayerField = generateFieldWithDefaultState( @@ -78,6 +85,7 @@ export const useGame = (): ReturnType => { playerField, gameField, onClick, + onContextMenu, onChangeLevel, onReset, };