I've pasted here a basic implementation I've done of a Minesweeper game. This is not the full functionality, only the populating/revealing of squares. The idea is that the user inputs rows,cols,list-of-bombs
as a string, and the game generates a board of the input dimensions, with bombs at the input indices.
I'm specifically wondering about maintaining the state of all these cells. I originally stored the board and visibility list as 2d arrays, but that didn't seem right.
I would really like to learn if my strategy for state management was good. I had a similar question on a take home for a trading desk role at a HFT firm, and I failed, so I'm trying to see if I can learn from my experience.
I'm also wondering why my <Cell />
components keep rerendering each time a cell is toggled.
// LIB
const generateMap = <vals = number | boolean,>(
rows: number,
cols: number,
defVal: vals
) => range(rows * cols).reduce((acc, curr) => ({ ...acc, [curr]: defVal }), {});
const range = (num: number) => {
const res = [];
let count = 0;
while (count < num) {
res.push(count++);
}
return res;
};
const addBombs = (values: Record<string, number>, bombs: string[]) => ({
...values,
...bombs.reduce((acc, curr) => ({ ...acc, [curr]: -1 }), {}),
});
const addValues = (
values: Record<string, number>,
row: number,
col: number
) => {
const getCount = (key: number) => (values[key] == -1 ? 1 : 0);
for (const key in values) {
if (values[key] == -1) {
continue;
}
const keyNum = Number(key);
const rowCount = Math.floor(keyNum / row);
const colCount = keyNum % col;
const left = colCount > 0 ? getCount(keyNum - 1) : 0;
const right = colCount < col - 1 ? getCount(keyNum + 1) : 0;
const up = rowCount > 0 ? getCount(keyNum - col) : 0;
const down = rowCount < row - 1 ? getCount(keyNum + col) : 0;
const topLeft =
colCount > 0 && rowCount > 0 ? getCount(keyNum - col - 1) : 0;
const topRight =
colCount < col - 1 && rowCount > 0 ? getCount(keyNum - col + 1) : 0;
const downLeft =
colCount > 0 && rowCount < row - 1 ? getCount(keyNum + col - 1) : 0;
const downRight =
colCount < col - 1 && rowCount < row - 1 ? getCount(keyNum + col + 1) : 0;
values[key] =
left + right + up + down + topLeft + topRight + downLeft + downRight;
}
return values;
};
// COMPONENTS
const Input = ({ handlePlay }: { handlePlay: (val: string) => void }) => {
const [val, setVal] = useState("");
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 20,
}}
>
<input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
<button onClick={() => handlePlay(val)}>Play</button>
</div>
);
};
const Cell = React.memo(
({
visible,
value,
handleClick,
}: {
visible: boolean;
value: number;
handleClick: () => void;
}) => {
console.log('cell rendering')
return (
<div
onClick={() => handleClick()}
style={{
padding: 10,
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
border: "1px solid grey",
backgroundColor: visible ? "white" : "lightgrey",
}}
>
{visible && (value > -1 ? value : "💣")}
</div>
);
}
);
const Grid = ({
seen,
values,
rows,
cols,
handleCellClick,
}: {
seen: Record<string, boolean>;
values: Record<string, number>;
rows: number;
cols: number;
handleCellClick: (key: string) => void;
}) => {
const key = (row: number, col: number) => (row * cols + col).toString();
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{range(rows).map((r) => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{range(cols).map((c) => (
<Cell
key={key(r, c)}
handleClick={() => handleCellClick(key(r, c))}
value={values[key(r, c)]}
visible={seen[key(r, c)]}
/>
))}
</div>
))}
</div>
);
};
function App() {
const [values, setValues] = useState<Record<string, number>>({});
const [seen, setSeen] = useState<Record<string, boolean>>({});
const [[rows, cols], setRowCol] = useState<number[]>([]);
const handlePlay = (val: string) => {
const [row, col, ...bombs] = val.split(",");
const valuesMap = generateMap(Number(row), Number(col), 0);
const seenMap = generateMap(Number(row), Number(col), false);
setValues(addValues(addBombs(valuesMap, bombs), Number(row), Number(col)));
setSeen(seenMap);
setRowCol([Number(row), Number(col)]);
};
const handleCellClick = useCallback(
(key: string) => {
setSeen({
...seen,
[key]: true,
});
},
[seen]
);
return (
<div>
<Input handlePlay={handlePlay} />
<Grid
rows={rows}
cols={cols}
seen={seen}
values={values}
handleCellClick={handleCellClick}
/>
</div>
);
}
export default App;