Skip to content

Commit

Permalink
refactor: functions into pure function
Browse files Browse the repository at this point in the history
  • Loading branch information
hhuang-rayark committed Apr 5, 2021
1 parent 7dbf0d1 commit cbe5651
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 107 deletions.
164 changes: 57 additions & 107 deletions src/component/Board.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";

import Cell from "component/Cell";
import {
getAdjacentCells,
handleFirstBomb,
openAdjacentSafeCells,
openCell,
openBomb,
getState,
doSideEffect,
pipe,
} from "helper";

const DEFAULT_CELL_STATE = {
opened: false,
isBomb: false,
Expand All @@ -23,28 +33,7 @@ const Board = ({
const [boardState, setBoardState] = useState(
new Array(height).fill(new Array(width).fill(DEFAULT_CELL_STATE))
);

const modifyBombSideEffect = (row, col, add = true) => {
// purpose: record the adjacent cells and update adjBombNum later
if (add) bombCount.current++;
//place bomb
else bombCount.current--; //remove bomb (only called by handleFirstBomb)

let sideEffects = [];
if (row - 1 >= 0) {
sideEffects.push({ row: row - 1, col }); //top
if (col - 1 >= 0) sideEffects.push({ row: row - 1, col: col - 1 }); //left-top
if (col + 1 < width) sideEffects.push({ row: row - 1, col: col + 1 }); //right-Top
}
if (row + 1 < height) {
sideEffects.push({ row: row + 1, col }); //bottom
if (col - 1 >= 0) sideEffects.push({ row: row + 1, col: col - 1 }); //left-Bottom
if (col + 1 < width) sideEffects.push({ row: row + 1, col: col + 1 }); //right-Bottom
}
if (col - 1 >= 0) sideEffects.push({ row, col: col - 1 }); //left
if (col + 1 < width) sideEffects.push({ row, col: col + 1 }); //right
return sideEffects;
};
const [countToAdd, setCountToAdd] = useState(0);

useEffect(() => {
const placeBomb = () => {
Expand All @@ -56,7 +45,8 @@ const Board = ({
for (let colIdx = 0; colIdx < width; colIdx++) {
if (Math.random() < bombProbability) {
newState[rowIdx][colIdx].isBomb = true;
sideEffects = [...sideEffects, ...modifyBombSideEffect(rowIdx, colIdx)];
bombCount.current++;
sideEffects = [...sideEffects, ...getAdjacentCells(rowIdx, colIdx, width, height)];
}
}
}
Expand All @@ -70,109 +60,69 @@ const Board = ({
placeBomb();
}, []);

const handleFirstBomb = (row, col) => {
//purpose: when user clicking on the bomb on first step, modify it to normal cell.
const sideEffects = modifyBombSideEffect(row, col, false);
setBoardState((prevState) => {
let newState = JSON.parse(JSON.stringify(prevState));
newState[row][col].isBomb = false;

for (let cell of sideEffects) {
newState[cell.row][cell.col].adjBombNum--;
}
return newState;
});
};

const findAdjacentSafeCells = (row, col, visited) => {
// purpose: Clicking a square with no adjacent mine clears that square and clicks all adjacent squares.
if (
row < 0 ||
col < 0 ||
row > height - 1 ||
col > width - 1 ||
boardState[row][col].opened === true ||
visited[row][col] ||
boardState[row][col].isBomb === true
)
//stop condition
return;
else if (boardState[row][col].adjBombNum > 0) {
visited[row][col] = true;
return;
}
visited[row][col] = true;
findAdjacentSafeCells(row - 1, col, visited); // top
findAdjacentSafeCells(row - 1, col - 1, visited); // left-top
findAdjacentSafeCells(row - 1, col + 1, visited); // right-top
findAdjacentSafeCells(row + 1, col, visited); // bottom
findAdjacentSafeCells(row + 1, col + 1, visited); //right-bottom
findAdjacentSafeCells(row + 1, col - 1, visited); //left-bottom
findAdjacentSafeCells(row, col + 1, visited); // right
findAdjacentSafeCells(row, col - 1, visited); // left
};

const handleClickCell = (row, col, e) => {
//TODO rise flag on right click
//TODO performance: long time click handler
if (boardState[row][col].opened || boardState[row][col].flagged) return;
if (boardState[row][col].isBomb) {
if (openedCount.current === 0) {
handleFirstBomb(row, col);
// condition: first step, open a bomb
bombCount.current--;
if (boardState[row][col].adjBombNum === 0) {
// action: open adjacent cells
setBoardState((boardState) =>
pipe(
handleFirstBomb,
openAdjacentSafeCells,
doSideEffect((args) => {
// should not add
setCountToAdd(args.count);
}),
getState
)({ row, col, boardState, showLog })
);
} else {
// open cell
setBoardState((boardState) =>
pipe(handleFirstBomb, openCell, getState)({ row, col, boardState, showLog })
);
openedCount.current += 1;
}
} else {
setBoardState((prevState) => {
let newState = JSON.parse(JSON.stringify(prevState));

showLog && console.log(`Oops! Clicked a Bomb on [${row},${col}]`);
newState[row][col] = { ...newState[row][col], opened: true, backgroundColor: "red" };
return newState;
});
//condition: not first step, open a bomb
setBoardState((boardState) => pipe(openBomb, getState)({ row, col, boardState, showLog }));
endGameCallback(false);
}
} else if (boardState[row][col].adjBombNum === 0) {
let visited = new Array(height).fill(1).map(() => new Array(width).fill(false));

findAdjacentSafeCells(row, col, visited);
const adjacentSafeCells = visited.reduce(
(result, row, rowIdx) => [
...result,
...row.reduce((result, cellVisited, colIdx) => {
if (cellVisited === true) return [...result, { row: rowIdx, col: colIdx }];
else return [...result];
}, []),
],
[]
setBoardState((boardState) =>
pipe(
openAdjacentSafeCells,
doSideEffect((args) => {
setCountToAdd(args.count);
}),
getState
)({ row, col, boardState, showLog })
);
setBoardState((prevState) => {
let newState = JSON.parse(JSON.stringify(prevState));

showLog && console.log("Found adjacentSafeCells", adjacentSafeCells);
adjacentSafeCells.forEach((cell) => {
newState[cell.row][cell.col].opened = true;
});
return newState;
});

openedCount.current += adjacentSafeCells.length;
} else {
setBoardState((prevState) => {
let newRow = prevState[row];
newRow[col].opened = true;
showLog && console.log(`Open a Cell at [${row},${col}]`);

return [...prevState.slice(0, row), newRow, ...prevState.slice(row + 1)];
});
openedCount.current++;
setBoardState((boardState) => pipe(openCell, getState)({ row, col, boardState, showLog }));
openedCount.current += 1;
}
};

useEffect(() => {
if (countToAdd > 0) {
openedCount.current += countToAdd;
setCountToAdd(0);
}
}, [countToAdd]);

useEffect(() => {
const isAllOpened = width * height - bombCount.current === openedCount.current;
if (isAllOpened) endGameCallback(true);
}, [openedCount.current]);

return (
<div>
<>
{boardState.map((row, idxRow) => (
<div style={{ display: "flex" }} key={`row_${idxRow}`}>
{row.map((cell, idxCell) => (
Expand All @@ -188,7 +138,7 @@ const Board = ({
))}
</div>
))}
</div>
</>
);
};

Expand Down
118 changes: 118 additions & 0 deletions src/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const getBoardWH = (boardState) => ({ width: boardState[0].length, height: boardState.length });

export const getAdjacentCells = (row, col, width, height) => {
let adjacentCells = [];
if (row - 1 >= 0) {
adjacentCells.push({ row: row - 1, col }); //top
if (col - 1 >= 0) adjacentCells.push({ row: row - 1, col: col - 1 }); //left-top
if (col + 1 < width) adjacentCells.push({ row: row - 1, col: col + 1 }); //right-Top
}
if (row + 1 < height) {
adjacentCells.push({ row: row + 1, col }); //bottom
if (col - 1 >= 0) adjacentCells.push({ row: row + 1, col: col - 1 }); //left-Bottom
if (col + 1 < width) adjacentCells.push({ row: row + 1, col: col + 1 }); //right-Bottom
}
if (col - 1 >= 0) adjacentCells.push({ row, col: col - 1 }); //left
if (col + 1 < width) adjacentCells.push({ row, col: col + 1 }); //right

return adjacentCells;
};

export const handleFirstBomb = ({ row, col, boardState, showLog }) => {
showLog && console.log(`first click on [${row},${col}] is a bomb. Remove it!`);
const { width, height } = getBoardWH(boardState);
const adjacentCells = getAdjacentCells(row, col, width, height);
let newState = JSON.parse(JSON.stringify(boardState));
newState[row][col].isBomb = false;
for (let cell of adjacentCells) {
newState[cell.row][cell.col].adjBombNum--;
}
showLog &&
console.log(`Bomb number of adjacent cells (adjBombNum) are also updated.`, adjacentCells);

return { row, col, boardState: newState };
};

export const openCell = ({ row, col, boardState, showLog }) => {
let newRow = boardState[row];
newRow[col].opened = true;
showLog && console.log(`Open a Cell at [${row},${col}]`);

return {
row,
col,
boardState: [...boardState.slice(0, row), newRow, ...boardState.slice(row + 1)],
};
};

const findAdjacentSafeCells = (row, col, visited, boardState) => {
const { width, height } = getBoardWH(boardState);
// purpose: Clicking a square with no adjacent mine clears that square and clicks all adjacent squares.
if (
boardState[row][col].opened === true ||
visited[row][col] ||
boardState[row][col].isBomb === true
)
//stop condition
return;
else if (boardState[row][col].adjBombNum > 0) {
visited[row][col] = true;
return;
}
visited[row][col] = true;

const adjacentCells = getAdjacentCells(row, col, width, height);
adjacentCells.forEach((cell) => {
findAdjacentSafeCells(cell.row, cell.col, visited, boardState);
});
};

export const openAdjacentSafeCells = ({ row, col, boardState, showLog }) => {
const { width, height } = getBoardWH(boardState);
let visited = new Array(height).fill(1).map(() => new Array(width).fill(false));
findAdjacentSafeCells(row, col, visited, boardState);
const adjacentSafeCells = visited.reduce(
(result, row, rowIdx) => [
...result,
...row.reduce((result, cellVisited, colIdx) => {
if (cellVisited === true) return [...result, { row: rowIdx, col: colIdx }];
else return [...result];
}, []),
],
[]
);
let newState = JSON.parse(JSON.stringify(boardState));
showLog && console.log("Found adjacentSafeCells", adjacentSafeCells);
adjacentSafeCells.forEach((cell) => {
newState[cell.row][cell.col].opened = true;
});

return {
row,
col,
boardState: newState,
count: adjacentSafeCells.length,
};
};

export const openBomb = ({ row, col, boardState, showLog }) => {
let newRow = boardState[row];
newRow[col] = { ...newRow[col], opened: true, backgroundColor: "red" };
showLog && console.log(`Oops! Clicked a Bomb on [${row},${col}]`);

return {
row,
col,
boardState: [...boardState.slice(0, row), newRow, ...boardState.slice(row + 1)],
};
};

export const doSideEffect = (fn) => (args) => {
console.log("doSideEffect");
fn(args);
return args;
};

export const getState = ({ boardState }) => boardState;

export const pipe = (...functions) => (args) => functions.reduce((arg, fn) => fn(arg), args);

0 comments on commit cbe5651

Please sign in to comment.