From 05bffdf59ec10d087e842fd5f7f6292cb0da542e Mon Sep 17 00:00:00 2001 From: Nikita Ovchinnikov Date: Thu, 19 Aug 2021 17:48:34 +0300 Subject: [PATCH 1/5] Add DetectSolvedPuzzle function --- src/helpers/DetectSolvedPullze.ts | 47 +++++++++++ src/helpers/DetectSolvedPuzzle.test.ts | 105 +++++++++++++++++++++++++ tsconfig.json | 3 +- 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/helpers/DetectSolvedPullze.ts create mode 100644 src/helpers/DetectSolvedPuzzle.test.ts diff --git a/src/helpers/DetectSolvedPullze.ts b/src/helpers/DetectSolvedPullze.ts new file mode 100644 index 0000000..0157da0 --- /dev/null +++ b/src/helpers/DetectSolvedPullze.ts @@ -0,0 +1,47 @@ +import { CellState, Field } from './Field'; + +/** + * Detect solved puzzle based on the player and game fields coorelation + * @param {Field} playerField + * @param {Field} gameField + * @returns {[boolean, number]} + */ +export const detectSolvedPuzzle = ( + playerField: Field, + gameField: Field +): [boolean, number] => { + const { hidden, bomb, flag, weakFlag } = CellState; + + let bombsCounter = 0; + let flagCounter = 0; + let detectedBombsCounter = 0; + let hiddenCounter = 0; + + for (const y of gameField.keys()) { + for (const x of gameField[y].keys()) { + const gameCell = gameField[y][x]; + const playerCell = playerField[y][x]; + + if (playerCell === hidden) { + hiddenCounter++; + } + + if ([flag, weakFlag].includes(playerCell)) { + flagCounter++; + } + + if (gameCell === bomb) { + bombsCounter++; + + if (playerCell === flag) { + detectedBombsCounter++; + } + } + } + } + + const isPuzzleSolved = + bombsCounter === detectedBombsCounter && hiddenCounter === 0; + + return [isPuzzleSolved, flagCounter]; +}; diff --git a/src/helpers/DetectSolvedPuzzle.test.ts b/src/helpers/DetectSolvedPuzzle.test.ts new file mode 100644 index 0000000..46ca692 --- /dev/null +++ b/src/helpers/DetectSolvedPuzzle.test.ts @@ -0,0 +1,105 @@ +import { CellState, Field } from './Field'; +import { detectSolvedPuzzle } from './DetectSolvedPullze'; + +const { empty: e, hidden: h, bomb: b, flag: f, weakFlag: w } = CellState; + +describe('Detect solved puzzle function test cases', () => { + it('Simplest 3*3 case', () => { + const gameField: Field = [ + [1, 1, e], + [b, 1, e], + [1, 1, e], + ]; + + const playerField: Field = [ + [1, 1, e], + [f, 1, e], + [1, 1, e], + ]; + + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + expect(flagCounter).toBe(1); + expect(isSolved).toBe(true); + }); + it('Wrong 3*3 hidden cells case', () => { + const gameField: Field = [ + [1, 1, e], + [b, 1, e], + [1, 1, e], + ]; + + const playerField: Field = [ + [1, 1, h], + [h, 1, h], + [1, 1, h], + ]; + + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + expect(flagCounter).toBe(0); + expect(isSolved).toBe(false); + }); + it('Wrong 3*3 hidden cell case', () => { + const gameField: Field = [ + [1, 1, e], + [b, 1, e], + [1, 1, e], + ]; + + const playerField: Field = [ + [1, h, e], + [f, 1, e], + [1, 1, e], + ]; + + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + expect(flagCounter).toBe(1); + expect(isSolved).toBe(false); + }); + it('5*5 with hidden cells', () => { + const gameField: Field = [ + [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], + ]; + + const playerField: Field = [ + [f, f, 1, h, h], + [f, 3, 1, h, h], + [1, 1, h, h, h], + [1, h, h, h, h], + [2, h, h, h, h], + ]; + + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + expect(flagCounter).toBe(3); + expect(isSolved).toStrictEqual(false); + }); + it('5*5 solved case', () => { + const gameField: Field = [ + [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], + ]; + + const playerField: Field = [ + [f, f, 1, 1, 2], + [f, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, f], + [2, 1, 0, 1, 0], + ]; + + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + expect(flagCounter).toBe(4); + expect(isSolved).toStrictEqual(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index a33d4d3..c4154aa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "downlevelIteration": true, } } From 0b62c268c9e77ca05bbb83ade693d6972a03e2f4 Mon Sep 17 00:00:00 2001 From: Nikita Ovchinnikov Date: Thu, 19 Aug 2021 18:27:38 +0300 Subject: [PATCH 2/5] Add solved puzzle handler --- ...tSolvedPullze.ts => detectSolvedPullze.ts} | 0 ...zle.test.ts => detectSolvedPuzzle.test.ts} | 2 +- src/helpers/openCell.test.ts | 41 ++++++++++++++++--- src/helpers/openCell.ts | 11 +++-- src/helpers/setFlag.test.ts | 39 ++++++++++++++++-- src/helpers/setFlag.ts | 9 ++-- src/modules/GameWithHooks/useGame.ts | 24 +++++++++-- 7 files changed, 106 insertions(+), 20 deletions(-) rename src/helpers/{DetectSolvedPullze.ts => detectSolvedPullze.ts} (100%) rename src/helpers/{DetectSolvedPuzzle.test.ts => detectSolvedPuzzle.test.ts} (97%) diff --git a/src/helpers/DetectSolvedPullze.ts b/src/helpers/detectSolvedPullze.ts similarity index 100% rename from src/helpers/DetectSolvedPullze.ts rename to src/helpers/detectSolvedPullze.ts diff --git a/src/helpers/DetectSolvedPuzzle.test.ts b/src/helpers/detectSolvedPuzzle.test.ts similarity index 97% rename from src/helpers/DetectSolvedPuzzle.test.ts rename to src/helpers/detectSolvedPuzzle.test.ts index 46ca692..7992b97 100644 --- a/src/helpers/DetectSolvedPuzzle.test.ts +++ b/src/helpers/detectSolvedPuzzle.test.ts @@ -1,5 +1,5 @@ import { CellState, Field } from './Field'; -import { detectSolvedPuzzle } from './DetectSolvedPullze'; +import { detectSolvedPuzzle } from './detectSolvedPullze'; const { empty: e, hidden: h, bomb: b, flag: f, weakFlag: w } = CellState; diff --git a/src/helpers/openCell.test.ts b/src/helpers/openCell.test.ts index a1ce5ec..7b7d1f9 100644 --- a/src/helpers/openCell.test.ts +++ b/src/helpers/openCell.test.ts @@ -1,7 +1,7 @@ import { CellState } from './Field'; import { openCell } from './openCell'; -const { empty: e, hidden: h, bomb: b } = CellState; +const { empty: e, hidden: h, bomb: b, flag: f } = CellState; describe('Open cell action', () => { describe('Simple cases with loose', () => { @@ -23,7 +23,7 @@ describe('Open cell action', () => { }); describe('Open cell with number', () => { it('Open cell with state == 1', () => { - const playerField = openCell( + const [playerField] = openCell( [1, 1], [ [h, h, h], @@ -43,7 +43,7 @@ describe('Open cell action', () => { ]); }); it('Open cell with state == 3', () => { - const playerField = openCell( + const [playerField] = openCell( [1, 1], [ [h, h, h], @@ -65,7 +65,7 @@ describe('Open cell action', () => { }); describe('Open empty cell', () => { it('Open empty cell, simple 3*3 case', () => { - const playerField = openCell( + const [playerField] = openCell( [1, 2], [ [h, h, h], @@ -85,7 +85,7 @@ describe('Open cell action', () => { ]); }); it('Open empty cell 5*5 case', () => { - const playerField = openCell( + const [playerField] = openCell( [2, 2], [ [h, h, h, h, h], @@ -111,4 +111,35 @@ describe('Open cell action', () => { ]); }); }); + describe('Detect win state', () => { + it('5*5 solved case', () => { + const [playerField, isSolved, flagCounter] = openCell( + [4, 0], + [ + [f, f, 1, 1, 2], + [f, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, f], + [h, 1, 0, 1, 0], + ], + [ + [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(flagCounter).toBe(4); + expect(isSolved).toStrictEqual(true); + expect(playerField).toStrictEqual([ + [f, f, 1, 1, 2], + [f, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, f], + [2, 1, 0, 1, 0], + ]); + }); + }); }); diff --git a/src/helpers/openCell.ts b/src/helpers/openCell.ts index edb1b41..12285b9 100644 --- a/src/helpers/openCell.ts +++ b/src/helpers/openCell.ts @@ -1,18 +1,19 @@ import { CellState, Coords, Field } from './Field'; import { checkItemInField, getNeigboursItems } from './CellsManipulator'; +import { detectSolvedPuzzle } from './detectSolvedPullze'; /** * Open cell in the player field using game field info * @param {Coords} coords * @param {Field} playerField * @param {Field} gameField - * @returns {Field} + * @returns {[Field, boolean, number]} */ export const openCell = ( coords: Coords, playerField: Field, gameField: Field -): Field => { +): [Field, boolean, number] => { const { empty, hidden, bomb } = CellState; const [y, x] = coords; @@ -33,7 +34,7 @@ export const openCell = ( const gameCell = gameField[y][x]; if (playerCell === hidden && gameCell !== bomb) { - playerField = openCell([y, x], playerField, gameField); + [playerField] = openCell([y, x], playerField, gameField); } } } @@ -41,5 +42,7 @@ export const openCell = ( playerField[y][x] = gameCell; - return playerField; + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + return [playerField, isSolved, flagCounter]; }; diff --git a/src/helpers/setFlag.test.ts b/src/helpers/setFlag.test.ts index 3377c62..9138bb4 100644 --- a/src/helpers/setFlag.test.ts +++ b/src/helpers/setFlag.test.ts @@ -17,7 +17,7 @@ describe('Set flag action', () => { [h, h, h], ]; - const newPlayerField = setFlag([0, 0], playerField, gameField); + const [newPlayerField] = setFlag([0, 0], playerField, gameField); expect(newPlayerField).toStrictEqual([ [1, h, h], @@ -38,7 +38,7 @@ describe('Set flag action', () => { [h, h, h], ]; - const playerFieldAfterFirstClick = setFlag( + const [playerFieldAfterFirstClick] = setFlag( [0, 0], playerField, gameField @@ -50,7 +50,7 @@ describe('Set flag action', () => { [h, h, h], ]); - const playerFieldAfterSecondClick = setFlag( + const [playerFieldAfterSecondClick] = setFlag( [0, 0], playerField, gameField @@ -62,7 +62,7 @@ describe('Set flag action', () => { [h, h, h], ]); - const playerFieldAfterThirdClick = setFlag( + const [playerFieldAfterThirdClick] = setFlag( [0, 0], playerField, gameField @@ -75,4 +75,35 @@ describe('Set flag action', () => { ]); }); }); + describe('Detect win state', () => { + it('5*5 solved case', () => { + const [playerField, isSolved, flagCounter] = setFlag( + [1, 0], + [ + [f, f, 1, 1, 2], + [h, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, f], + [2, 1, 0, 1, 0], + ], + [ + [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(flagCounter).toBe(4); + expect(isSolved).toStrictEqual(true); + expect(playerField).toStrictEqual([ + [f, f, 1, 1, 2], + [f, 3, 1, 0, 0], + [1, 1, 0, 1, 1], + [1, 0, 0, 1, f], + [2, 1, 0, 1, 0], + ]); + }); + }); }); diff --git a/src/helpers/setFlag.ts b/src/helpers/setFlag.ts index 8384834..26e1114 100644 --- a/src/helpers/setFlag.ts +++ b/src/helpers/setFlag.ts @@ -1,17 +1,18 @@ import { CellState, Coords, Field } from './Field'; +import { detectSolvedPuzzle } from './detectSolvedPullze'; /** * Set flag to the cell * @param {Coords} coords * @param {Field} playerField * @param {Field} gameField - * @returns {[Field, FlagCounter]} + * @returns {[Field, boolean, number]} */ export const setFlag = ( coords: Coords, playerField: Field, gameField: Field -): Field => { +): [Field, boolean, number] => { const [y, x] = coords; const cell = playerField[y][x]; @@ -29,5 +30,7 @@ export const setFlag = ( break; } - return playerField; + const [isSolved, flagCounter] = detectSolvedPuzzle(playerField, gameField); + + return [playerField, isSolved, flagCounter]; }; diff --git a/src/modules/GameWithHooks/useGame.ts b/src/modules/GameWithHooks/useGame.ts index e091f92..8c8a76d 100644 --- a/src/modules/GameWithHooks/useGame.ts +++ b/src/modules/GameWithHooks/useGame.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Field, @@ -41,9 +41,19 @@ export const useGame = (): ReturnType => { fieldGenerator(size, bombs / (size * size)) ); + useMemo(() => console.log(gameField), []); + const onClick = (coords: Coords) => { try { - const newPlayerField = openCell(coords, playerField, gameField); + const [newPlayerField, isSolved, flagCounter] = openCell( + coords, + playerField, + gameField + ); + if (isSolved) { + setIsWin(true); + setIsGameOver(true); + } setPlayerField([...newPlayerField]); } catch (e) { setPlayerField([...gameField]); @@ -52,7 +62,15 @@ export const useGame = (): ReturnType => { }; const onContextMenu = (coords: Coords) => { - const newPlayerField = setFlag(coords, playerField, gameField); + const [newPlayerField, isSolved, flagCounter] = setFlag( + coords, + playerField, + gameField + ); + if (isSolved) { + setIsWin(true); + setIsGameOver(true); + } setPlayerField([...newPlayerField]); }; From 614466192a9aa5ab5c88b7c55d1fb00c98713bf7 Mon Sep 17 00:00:00 2001 From: Nikita Ovchinnikov Date: Fri, 20 Aug 2021 14:27:45 +0300 Subject: [PATCH 3/5] Edit readme.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index fadb970..161eadb 100644 --- a/README.md +++ b/README.md @@ -195,3 +195,8 @@ ### Set flag action [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/32/files) + +### Solved puzzle detector +### Create win game state handler + +[Pull request](https://github.com/nickovchinnikov/minesweeper/pull/33/files) From 5d162bfebb57abc3a3f21c8cd20146bf668d905e Mon Sep 17 00:00:00 2001 From: Nikita Ovchinnikov Date: Fri, 20 Aug 2021 14:48:36 +0300 Subject: [PATCH 4/5] Add useGame test cases for the win state --- src/modules/GameWithHooks/useGame.test.ts | 19 +++++++++++++++++++ src/modules/GameWithHooks/useGame.ts | 13 ++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/modules/GameWithHooks/useGame.test.ts b/src/modules/GameWithHooks/useGame.test.ts index 0a4e152..2574584 100644 --- a/src/modules/GameWithHooks/useGame.test.ts +++ b/src/modules/GameWithHooks/useGame.test.ts @@ -186,5 +186,24 @@ describe('useGame test cases', () => { expect(flatWithFilter(latestPlayerField, h)).toHaveLength(81); }); + it('Player win the game', () => { + const { result } = renderHook(useGame); + + const { gameField, onClick, onContextMenu } = 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]) : onContextMenu([y, x]); + }); + } + } + + const { isGameOver, isWin } = result.current; + + expect(isWin).toBe(true); + expect(isGameOver).toBe(true); + }); }); }); diff --git a/src/modules/GameWithHooks/useGame.ts b/src/modules/GameWithHooks/useGame.ts index 8c8a76d..f6c4f93 100644 --- a/src/modules/GameWithHooks/useGame.ts +++ b/src/modules/GameWithHooks/useGame.ts @@ -31,6 +31,11 @@ export const useGame = (): ReturnType => { const [isGameOver, setIsGameOver] = useState(false); const [isWin, setIsWin] = useState(false); + const setGameOver = (isSolved = false) => { + setIsGameOver(true); + setIsWin(isSolved); + }; + const [size, bombs] = GameSettings[level]; const [playerField, setPlayerField] = useState( @@ -51,13 +56,12 @@ export const useGame = (): ReturnType => { gameField ); if (isSolved) { - setIsWin(true); - setIsGameOver(true); + setGameOver(isSolved); } setPlayerField([...newPlayerField]); } catch (e) { setPlayerField([...gameField]); - setIsGameOver(true); + setGameOver(false); } }; @@ -68,8 +72,7 @@ export const useGame = (): ReturnType => { gameField ); if (isSolved) { - setIsWin(true); - setIsGameOver(true); + setGameOver(isSolved); } setPlayerField([...newPlayerField]); }; From 1b1904b0d1f998ea781e0435a1f95d155dae37c0 Mon Sep 17 00:00:00 2001 From: Nikita Ovchinnikov Date: Fri, 20 Aug 2021 15:58:42 +0300 Subject: [PATCH 5/5] Edit Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 161eadb..e61cab2 100644 --- a/README.md +++ b/README.md @@ -198,5 +198,6 @@ ### Solved puzzle detector ### Create win game state handler +### Add test cases for the useGame hook win state [Pull request](https://github.com/nickovchinnikov/minesweeper/pull/33/files)