added presets
This commit is contained in:
parent
3127575eaf
commit
3c29265b79
|
|
@ -12,7 +12,8 @@
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-confetti-boom": "^1.0.0",
|
"react-confetti-boom": "^1.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"use-sound": "^4.0.3"
|
"use-sound": "^4.0.3",
|
||||||
|
"zustand": "^4.5.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
|
|
@ -1003,13 +1004,13 @@
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.5",
|
"version": "18.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
|
||||||
"integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
|
"integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
|
|
@ -1416,7 +1417,7 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
|
|
@ -2527,6 +2528,14 @@
|
||||||
"react": ">=16.8"
|
"react": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.5",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.5.tgz",
|
||||||
|
|
@ -2621,6 +2630,33 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "4.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz",
|
||||||
|
"integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -3130,13 +3166,13 @@
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "18.3.5",
|
"version": "18.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
|
||||||
"integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
|
"integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
|
|
@ -3407,7 +3443,7 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
|
|
@ -4174,6 +4210,12 @@
|
||||||
"howler": "^2.1.3"
|
"howler": "^2.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"vite": {
|
"vite": {
|
||||||
"version": "5.4.5",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.5.tgz",
|
||||||
|
|
@ -4206,6 +4248,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"zustand": {
|
||||||
|
"version": "4.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz",
|
||||||
|
"integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==",
|
||||||
|
"requires": {
|
||||||
|
"use-sync-external-store": "1.2.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-confetti-boom": "^1.0.0",
|
"react-confetti-boom": "^1.0.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"use-sound": "^4.0.3"
|
"use-sound": "^4.0.3",
|
||||||
|
"zustand": "^4.5.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
|
|
|
||||||
16
src/App.tsx
16
src/App.tsx
|
|
@ -1,30 +1,28 @@
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { updateGame, useGame } from "./GameContext";
|
|
||||||
import Timer from "./Timer";
|
import Timer from "./Timer";
|
||||||
import Options from "./Options";
|
import Options from "./Options";
|
||||||
|
import useGameStore from "./GameState";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const game = useGame();
|
const game = useGameStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<h1>Minesweeper</h1>
|
<h1>Minesweeper</h1>
|
||||||
<Options />
|
<Options />
|
||||||
<button onClick={() => updateGame((game) => game?.quickStart())}>
|
<button onClick={() => game.quickStart()}>Quick Start</button>
|
||||||
Quick Start
|
|
||||||
</button>
|
|
||||||
<div className="game-wrapper">
|
<div className="game-wrapper">
|
||||||
<div>
|
<div>
|
||||||
<Timer />
|
<Timer />
|
||||||
<div
|
<div
|
||||||
className="game-board"
|
className="game-board"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${game?.getWidth()}, 1fr)`,
|
gridTemplateColumns: `repeat(${game.getWidth()}, 1fr)`,
|
||||||
gridTemplateRows: `repeat(${game?.getHeight()}, 1fr)`,
|
gridTemplateRows: `repeat(${game.getHeight()}, 1fr)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{game?.mines[0].map((_, y) =>
|
{game.mines[0].map((_, y) =>
|
||||||
game?.mines.map((_, x) => (
|
game.mines.map((_, x) => (
|
||||||
<Button key={`${x},${y}`} x={x} y={y} />
|
<Button key={`${x},${y}`} x={x} y={y} />
|
||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { updateGame, useGame } from "./GameContext";
|
|
||||||
import { Bomb, Flag } from "lucide-react";
|
import { Bomb, Flag } from "lucide-react";
|
||||||
|
import useGameStore from "./GameState";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -19,15 +19,15 @@ export const colorMap: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = ({ x, y }: ButtonProps) => {
|
export const Button = ({ x, y }: ButtonProps) => {
|
||||||
const game = useGame();
|
const game = useGameStore();
|
||||||
|
|
||||||
let content: ReactNode = "";
|
let content: ReactNode = "";
|
||||||
|
|
||||||
if (game?.isRevealed[x][y]) {
|
if (game.isRevealed[x][y]) {
|
||||||
content = game?.isMine(x, y) ? <Bomb /> : game?.getValue(x, y).toString();
|
content = game.isMine(x, y) ? <Bomb /> : game.getValue(x, y).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game?.isFlagged[x][y]) {
|
if (game.isFlagged[x][y]) {
|
||||||
content = <Flag fill="red" />;
|
content = <Flag fill="red" />;
|
||||||
}
|
}
|
||||||
if (content === "0") content = "";
|
if (content === "0") content = "";
|
||||||
|
|
@ -36,50 +36,43 @@ export const Button = ({ x, y }: ButtonProps) => {
|
||||||
<div
|
<div
|
||||||
className="mine-button"
|
className="mine-button"
|
||||||
style={{
|
style={{
|
||||||
background: game?.isRevealed[x][y] ? "#444" : undefined,
|
background: game.isRevealed[x][y] ? "#444" : undefined,
|
||||||
borderRight: !game?.isRevealed[x][y] ? "3px solid black" : undefined,
|
borderRight: !game.isRevealed[x][y] ? "3px solid black" : undefined,
|
||||||
borderTop: !game?.isRevealed[x][y] ? "3px solid #999" : undefined,
|
borderTop: !game.isRevealed[x][y] ? "3px solid #999" : undefined,
|
||||||
borderLeft: !game?.isRevealed[x][y] ? "3px solid #999" : undefined,
|
borderLeft: !game.isRevealed[x][y] ? "3px solid #999" : undefined,
|
||||||
borderBottom: !game?.isRevealed[x][y] ? "3px solid black" : undefined,
|
borderBottom: !game.isRevealed[x][y] ? "3px solid black" : undefined,
|
||||||
color: game?.isRevealed[x][y]
|
color: game.isRevealed[x][y]
|
||||||
? colorMap[String(content)] ?? "#eee"
|
? colorMap[String(content)] ?? "#eee"
|
||||||
: undefined,
|
: undefined,
|
||||||
fontSize: Number(content) > 0 ? "1.75rem" : undefined,
|
fontSize: Number(content) > 0 ? "1.75rem" : undefined,
|
||||||
cursor: game?.isRevealed[x][y] ? "default" : "pointer",
|
cursor: game.isRevealed[x][y] ? "default" : "pointer",
|
||||||
}}
|
}}
|
||||||
onMouseUp={(e) => {
|
onMouseUp={(e) => {
|
||||||
if (game?.getHasWon() || game?.isGameOver) {
|
if (game.getHasWon() || game.isGameOver) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
// Left click
|
// Left click
|
||||||
if (!game?.isRevealed[x][y] && !game?.isFlagged[x][y]) {
|
if (!game.isRevealed[x][y] && !game.isFlagged[x][y]) {
|
||||||
updateGame((game) => game?.reveal(x, y));
|
game.reveal(x, y);
|
||||||
} else {
|
} else {
|
||||||
const neighborFlagCount = game
|
const neighborFlagCount = game
|
||||||
?.getNeighborFlags(x, y)
|
?.getNeighborFlags(x, y)
|
||||||
.filter((n) => n).length;
|
.filter((n) => n).length;
|
||||||
const value = game?.getValue(x, y);
|
const value = game.getValue(x, y);
|
||||||
if (neighborFlagCount === value) {
|
if (neighborFlagCount === value) {
|
||||||
updateGame((game) => {
|
if (!game.isFlagged[x - 1]?.[y]) game.reveal(x - 1, y);
|
||||||
if (!game?.isFlagged[x - 1]?.[y]) game?.reveal(x - 1, y);
|
if (!game.isFlagged[x - 1]?.[y - 1]) game.reveal(x - 1, y - 1);
|
||||||
if (!game?.isFlagged[x - 1]?.[y - 1])
|
if (!game.isFlagged[x - 1]?.[y + 1]) game.reveal(x - 1, y + 1);
|
||||||
game?.reveal(x - 1, y - 1);
|
if (!game.isFlagged[x]?.[y - 1]) game.reveal(x, y - 1);
|
||||||
if (!game?.isFlagged[x - 1]?.[y + 1])
|
if (!game.isFlagged[x]?.[y + 1]) game.reveal(x, y + 1);
|
||||||
game?.reveal(x - 1, y + 1);
|
if (!game.isFlagged[x + 1]?.[y - 1]) game.reveal(x + 1, y - 1);
|
||||||
if (!game?.isFlagged[x]?.[y - 1]) game?.reveal(x, y - 1);
|
if (!game.isFlagged[x + 1]?.[y]) game.reveal(x + 1, y);
|
||||||
if (!game?.isFlagged[x]?.[y + 1]) game?.reveal(x, y + 1);
|
if (!game.isFlagged[x + 1]?.[y + 1]) game.reveal(x + 1, y + 1);
|
||||||
if (!game?.isFlagged[x + 1]?.[y - 1])
|
|
||||||
game?.reveal(x + 1, y - 1);
|
|
||||||
if (!game?.isFlagged[x + 1]?.[y]) game?.reveal(x + 1, y);
|
|
||||||
if (!game?.isFlagged[x + 1]?.[y + 1])
|
|
||||||
game?.reveal(x + 1, y + 1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.button === 2 && !game?.isRevealed[x][y]) {
|
} else if (e.button === 2 && !game.isRevealed[x][y]) {
|
||||||
// Right click
|
game.flag(x, y);
|
||||||
updateGame((game) => game?.flag(x, y));
|
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import {
|
import { createContext, ReactNode, useContext, useEffect } from "react";
|
||||||
createContext,
|
|
||||||
ReactNode,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Game } from "./Game";
|
import { Game } from "./Game";
|
||||||
import useSound from "use-sound";
|
import useSound from "use-sound";
|
||||||
import explosion from "./sound/explosion.mp3";
|
import explosion from "./sound/explosion.mp3";
|
||||||
|
import useGameStore from "./GameState";
|
||||||
|
|
||||||
const GameContext = createContext<Game | null>(null);
|
const GameContext = createContext<Game | null>(null);
|
||||||
const useGame = () => useContext(GameContext);
|
const useGame = () => useContext(GameContext);
|
||||||
|
|
@ -16,39 +11,17 @@ let updateGame: (cb: (game: Game) => void) => void;
|
||||||
let resetGame: (width: number, height: number, mines: number) => void;
|
let resetGame: (width: number, height: number, mines: number) => void;
|
||||||
|
|
||||||
const GameProvider = ({ children }: { children: ReactNode }) => {
|
const GameProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const game = useGameStore();
|
||||||
const [counter, setCounter] = useState(0);
|
|
||||||
const [playSound] = useSound(explosion, {
|
const [playSound] = useSound(explosion, {
|
||||||
volume: 0.5,
|
volume: 0.5,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (game?.isGameOver) {
|
if (game.isGameOver) {
|
||||||
playSound();
|
playSound();
|
||||||
}
|
}
|
||||||
}, [game?.isGameOver]);
|
}, [game.isGameOver]);
|
||||||
|
return children;
|
||||||
useEffect(() => {
|
|
||||||
const game = new Game(30, 20, 100);
|
|
||||||
setGame(game);
|
|
||||||
updateGame = (cb: (game: Game) => void) => {
|
|
||||||
cb(game);
|
|
||||||
setCounter((c) => c + 1);
|
|
||||||
};
|
|
||||||
resetGame = (width: number, height: number, mines: number) => {
|
|
||||||
const game = new Game(width, height, mines);
|
|
||||||
setGame(game);
|
|
||||||
updateGame = (cb: (game: Game) => void) => {
|
|
||||||
cb(game);
|
|
||||||
setCounter((c) => c + 1);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<GameContext.Provider key={counter} value={game}>
|
|
||||||
{children}
|
|
||||||
</GameContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { GameProvider, useGame, updateGame, resetGame };
|
export { GameProvider, useGame, updateGame, resetGame };
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
interface GameState {
|
||||||
|
mines: boolean[][];
|
||||||
|
minesCount: number;
|
||||||
|
isRevealed: boolean[][];
|
||||||
|
isFlagged: boolean[][];
|
||||||
|
isGameOver: boolean;
|
||||||
|
startTime: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
initializeGame: (width: number, height: number, mines: number) => void;
|
||||||
|
flag: (x: number, y: number) => void;
|
||||||
|
reveal: (x: number, y: number) => void;
|
||||||
|
getValue: (x: number, y: number) => number;
|
||||||
|
getHasWon: () => boolean;
|
||||||
|
getMinesLeft: () => number;
|
||||||
|
quickStart: () => void;
|
||||||
|
isValid: (x: number, y: number) => boolean;
|
||||||
|
resetGame: (width: number, height: number, mines: number) => void;
|
||||||
|
isMine: (x: number, y: number) => boolean;
|
||||||
|
getNeighborMines: (x: number, y: number) => boolean[];
|
||||||
|
getNeighborFlags: (x: number, y: number) => boolean[];
|
||||||
|
getWidth: () => number;
|
||||||
|
getHeight: () => number;
|
||||||
|
isTouched: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGameStore = create<GameState>((set, get) => ({
|
||||||
|
mines: [[]],
|
||||||
|
minesCount: 0,
|
||||||
|
isRevealed: [[]],
|
||||||
|
isFlagged: [[]],
|
||||||
|
isGameOver: false,
|
||||||
|
startTime: Date.now(),
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
|
||||||
|
initializeGame: (width, height, mines) => {
|
||||||
|
mines = Math.min(mines, width * height);
|
||||||
|
|
||||||
|
const minesArray = Array.from({ length: width }, () =>
|
||||||
|
new Array(height).fill(false),
|
||||||
|
);
|
||||||
|
const isRevealedArray = Array.from({ length: width }, () =>
|
||||||
|
new Array(height).fill(false),
|
||||||
|
);
|
||||||
|
const isFlaggedArray = Array.from({ length: width }, () =>
|
||||||
|
new Array(height).fill(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
let remainingMines = mines;
|
||||||
|
while (remainingMines > 0) {
|
||||||
|
const x = Math.floor(Math.random() * width);
|
||||||
|
const y = Math.floor(Math.random() * height);
|
||||||
|
if (!minesArray[x][y]) {
|
||||||
|
minesArray[x][y] = true;
|
||||||
|
remainingMines--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
mines: minesArray,
|
||||||
|
isRevealed: isRevealedArray,
|
||||||
|
isFlagged: isFlaggedArray,
|
||||||
|
minesCount: mines,
|
||||||
|
isGameOver: false,
|
||||||
|
startTime: Date.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
flag: (x, y) => {
|
||||||
|
set((state) => {
|
||||||
|
const isFlagged = [...state.isFlagged];
|
||||||
|
isFlagged[x][y] = !isFlagged[x][y];
|
||||||
|
return { isFlagged };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reveal: (x, y) => {
|
||||||
|
const { mines, isRevealed, isGameOver, getValue } = get();
|
||||||
|
if (isGameOver || !get().isValid(x, y) || isRevealed[x][y]) return;
|
||||||
|
|
||||||
|
const newRevealed = [...isRevealed];
|
||||||
|
newRevealed[x][y] = true;
|
||||||
|
|
||||||
|
if (mines[x][y]) {
|
||||||
|
set({ isGameOver: true, isRevealed: newRevealed });
|
||||||
|
} else {
|
||||||
|
set({ isRevealed: newRevealed });
|
||||||
|
const value = getValue(x, y);
|
||||||
|
if (value === 0) {
|
||||||
|
const revealNeighbors = (nx: number, ny: number) => {
|
||||||
|
if (get().isValid(nx, ny) && !isRevealed[nx]?.[ny]) {
|
||||||
|
get().reveal(nx, ny);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
revealNeighbors(x - 1, y - 1);
|
||||||
|
revealNeighbors(x, y - 1);
|
||||||
|
revealNeighbors(x + 1, y - 1);
|
||||||
|
revealNeighbors(x - 1, y);
|
||||||
|
revealNeighbors(x + 1, y);
|
||||||
|
revealNeighbors(x - 1, y + 1);
|
||||||
|
revealNeighbors(x, y + 1);
|
||||||
|
revealNeighbors(x + 1, y + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getValue: (x, y) => {
|
||||||
|
const { mines } = get();
|
||||||
|
const neighbors = [
|
||||||
|
mines[x - 1]?.[y - 1],
|
||||||
|
mines[x]?.[y - 1],
|
||||||
|
mines[x + 1]?.[y - 1],
|
||||||
|
mines[x - 1]?.[y],
|
||||||
|
mines[x + 1]?.[y],
|
||||||
|
mines[x - 1]?.[y + 1],
|
||||||
|
mines[x]?.[y + 1],
|
||||||
|
mines[x + 1]?.[y + 1],
|
||||||
|
];
|
||||||
|
return neighbors.filter((n) => n).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
getHasWon: () => {
|
||||||
|
const { mines, isRevealed, isFlagged, isGameOver, width, height } = get();
|
||||||
|
if (isGameOver) return false;
|
||||||
|
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
if (!isRevealed[i][j] && !isFlagged[i][j]) return false;
|
||||||
|
if (mines[i][j] && !isFlagged[i][j]) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMinesLeft: () => {
|
||||||
|
const { minesCount, isFlagged } = get();
|
||||||
|
return minesCount - isFlagged.flat().filter((flag) => flag).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
quickStart: () => {
|
||||||
|
const { width, height, mines, getValue, reveal } = get();
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
const value = getValue(i, j);
|
||||||
|
if (value === 0 && !mines[i][j]) {
|
||||||
|
reveal(i, j);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isValid: (x: number, y: number) => {
|
||||||
|
const { width, height } = get();
|
||||||
|
return x >= 0 && x < width && y >= 0 && y < height;
|
||||||
|
},
|
||||||
|
resetGame: (width: number, height: number, mines: number) => {
|
||||||
|
if (mines > width * height) {
|
||||||
|
throw new Error("Too many mines");
|
||||||
|
}
|
||||||
|
|
||||||
|
const minesArray = Array.from({ length: width }, () =>
|
||||||
|
new Array(height).fill(false),
|
||||||
|
);
|
||||||
|
const isRevealedArray = Array.from({ length: width }, () =>
|
||||||
|
new Array(height).fill(false),
|
||||||
|
);
|
||||||
|
const isFlaggedArray = Array.from({ length: width }, () =>
|
||||||
|
new Array(height).fill(false),
|
||||||
|
);
|
||||||
|
|
||||||
|
let remainingMines = mines;
|
||||||
|
while (remainingMines > 0) {
|
||||||
|
const x = Math.floor(Math.random() * width);
|
||||||
|
const y = Math.floor(Math.random() * height);
|
||||||
|
if (!minesArray[x][y]) {
|
||||||
|
minesArray[x][y] = true;
|
||||||
|
remainingMines--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
mines: minesArray,
|
||||||
|
isRevealed: isRevealedArray,
|
||||||
|
isFlagged: isFlaggedArray,
|
||||||
|
minesCount: mines,
|
||||||
|
isGameOver: false,
|
||||||
|
startTime: Date.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isMine: (x: number, y: number) => {
|
||||||
|
const { mines } = get();
|
||||||
|
return mines[x][y];
|
||||||
|
},
|
||||||
|
getNeighborMines: (x: number, y: number) => {
|
||||||
|
const { mines } = get();
|
||||||
|
const neighbors = [
|
||||||
|
mines[x - 1]?.[y - 1],
|
||||||
|
mines[x]?.[y - 1],
|
||||||
|
mines[x + 1]?.[y - 1],
|
||||||
|
mines[x - 1]?.[y],
|
||||||
|
mines[x + 1]?.[y],
|
||||||
|
mines[x - 1]?.[y + 1],
|
||||||
|
mines[x]?.[y + 1],
|
||||||
|
mines[x + 1]?.[y + 1],
|
||||||
|
];
|
||||||
|
return neighbors;
|
||||||
|
},
|
||||||
|
getNeighborFlags: (x: number, y: number) => {
|
||||||
|
const { isFlagged } = get();
|
||||||
|
const neighbors = [
|
||||||
|
isFlagged[x - 1]?.[y - 1],
|
||||||
|
isFlagged[x]?.[y - 1],
|
||||||
|
isFlagged[x + 1]?.[y - 1],
|
||||||
|
isFlagged[x - 1]?.[y],
|
||||||
|
isFlagged[x + 1]?.[y],
|
||||||
|
isFlagged[x - 1]?.[y + 1],
|
||||||
|
isFlagged[x]?.[y + 1],
|
||||||
|
isFlagged[x + 1]?.[y + 1],
|
||||||
|
];
|
||||||
|
return neighbors;
|
||||||
|
},
|
||||||
|
getWidth: () => {
|
||||||
|
const { width } = get();
|
||||||
|
return width;
|
||||||
|
},
|
||||||
|
getHeight: () => {
|
||||||
|
const { height } = get();
|
||||||
|
return height;
|
||||||
|
},
|
||||||
|
isTouched: () => {
|
||||||
|
const { isRevealed, isFlagged } = get();
|
||||||
|
return (
|
||||||
|
isRevealed.flat().filter((flag) => flag).length > 0 ||
|
||||||
|
isFlagged.flat().filter((flag) => flag).length > 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useGameStore;
|
||||||
|
|
@ -1,13 +1,36 @@
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { resetGame, useGame } from "./GameContext";
|
import useGameStore from "./GameState";
|
||||||
|
|
||||||
|
const presets = {
|
||||||
|
Easy: { width: 10, height: 10, mines: 20 },
|
||||||
|
Medium: { width: 16, height: 16, mines: 32 },
|
||||||
|
Expert: { width: 30, height: 16, mines: 99 },
|
||||||
|
"Max Mode": { width: 40, height: 40, mines: 350 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
function Options() {
|
function Options() {
|
||||||
const game = useGame();
|
const game = useGameStore();
|
||||||
const [width, setWidth] = useState(game?.getWidth() || 20);
|
const [width, setWidth] = useState(16);
|
||||||
const [height, setHeight] = useState(game?.getHeight() || 20);
|
const [height, setHeight] = useState(16);
|
||||||
const [mines, setMines] = useState(game?.minesCount || 20);
|
const [mines, setMines] = useState(32);
|
||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fixWidth = Math.min(40, width);
|
||||||
|
const fixHeight = Math.min(40, height);
|
||||||
|
setWidth(fixWidth);
|
||||||
|
setHeight(fixHeight);
|
||||||
|
}, [width, height]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!game.isTouched()) {
|
||||||
|
if (width <= 0 || height <= 0 || mines <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
game.resetGame(width, height, mines);
|
||||||
|
}
|
||||||
|
}, [width, height, mines]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => setShowOptions(!showOptions)}>
|
<button onClick={() => setShowOptions(!showOptions)}>
|
||||||
|
|
@ -15,6 +38,24 @@ function Options() {
|
||||||
</button>
|
</button>
|
||||||
{showOptions && (
|
{showOptions && (
|
||||||
<>
|
<>
|
||||||
|
<p>
|
||||||
|
Presets:{" "}
|
||||||
|
{(Object.keys(presets) as Array<keyof typeof presets>).map(
|
||||||
|
(key) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
const { width, height, mines } = presets[key];
|
||||||
|
setWidth(width);
|
||||||
|
setHeight(height);
|
||||||
|
setMines(mines);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Width:{" "}
|
Width:{" "}
|
||||||
<input
|
<input
|
||||||
|
|
@ -28,7 +69,6 @@ function Options() {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={height}
|
value={height}
|
||||||
max="40"
|
|
||||||
onChange={(e) => setHeight(Number(e.target.value))}
|
onChange={(e) => setHeight(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -37,7 +77,6 @@ function Options() {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={mines}
|
value={mines}
|
||||||
max="40"
|
|
||||||
onChange={(e) => setMines(Number(e.target.value))}
|
onChange={(e) => setMines(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -45,7 +84,7 @@ function Options() {
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetGame(width, height, mines);
|
game.resetGame(width, height, mines);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useGame } from "./GameContext";
|
|
||||||
import Confetti from "react-confetti-boom";
|
import Confetti from "react-confetti-boom";
|
||||||
|
import useGameStore from "./GameState";
|
||||||
|
|
||||||
const Timer = () => {
|
const Timer = () => {
|
||||||
const game = useGame();
|
const game = useGameStore();
|
||||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (game?.isGameOver || game?.getHasWon()) {
|
if (game.isGameOver || game.getHasWon()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|
@ -15,21 +15,21 @@ const Timer = () => {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, [game.isGameOver, game.getHasWon()]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="timer">
|
<div className="timer">
|
||||||
<p style={{ width: "100px" }}>{game?.getMinesLeft()}</p>
|
<p style={{ width: "100px" }}>{game.getMinesLeft()}</p>
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
fontSize: "2rem",
|
fontSize: "2rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{game?.getHasWon() ? "😎" : game?.isGameOver ? "😢" : "😐"}
|
{game.getHasWon() ? "😎" : game.isGameOver ? "😢" : "😐"}
|
||||||
{game?.getHasWon() && <Confetti mode="boom" particleCount={301} />}
|
{game.getHasWon() && <Confetti mode="boom" particleCount={301} />}
|
||||||
</p>
|
</p>
|
||||||
<p style={{ width: "100px", textAlign: "right" }}>
|
<p style={{ width: "100px", textAlign: "right" }}>
|
||||||
{Math.max(0, Math.floor((currentTime - (game?.startTime || 0)) / 1000))}
|
{Math.max(0, Math.floor((currentTime - (game.startTime || 0)) / 1000))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue