Skip to content

Commit 8a0ac37

Browse files
feat: solve 2nd stage of the Hyperskill project "Minesweeper"
1 parent ff61c0c commit 8a0ac37

16 files changed

+434
-0
lines changed

hyperskill/05_minesweeper/02/.gitkeep

Whitespace-only changes.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
2+
3+
* {
4+
box-sizing: border-box;
5+
}
6+
7+
.App {
8+
text-align: center;
9+
}
10+
11+
.App-logo {
12+
height: 40vmin;
13+
pointer-events: none;
14+
}
15+
16+
@media (prefers-reduced-motion: no-preference) {
17+
.App-logo {
18+
animation: App-logo-spin infinite 20s linear;
19+
}
20+
}
21+
22+
.App-header {
23+
background-color: #282c34;
24+
min-height: 100vh;
25+
display: flex;
26+
flex-direction: column;
27+
align-items: center;
28+
justify-content: center;
29+
font-size: calc(10px + 2vmin);
30+
color: white;
31+
}
32+
33+
.App-link {
34+
color: #61dafb;
35+
}
36+
37+
@keyframes App-logo-spin {
38+
from {
39+
transform: rotate(0deg);
40+
}
41+
to {
42+
transform: rotate(360deg);
43+
}
44+
}
45+
46+
.App p {
47+
font-family: 'Roboto', sans-serif;
48+
}
49+
50+
.minesweeper {
51+
display: flex;
52+
flex-direction: column;
53+
align-items: center;
54+
justify-content: center;
55+
font-family: 'Roboto', sans-serif;
56+
margin: 20px;
57+
border: none;
58+
border-radius: 20px;
59+
box-shadow: 0px 0px 20px red;
60+
width: 60vmin;
61+
}
62+
63+
.game-title {
64+
font-size: 36px;
65+
padding: 10px;
66+
}
67+
68+
.game-title img {
69+
height: 24px;
70+
}
71+
72+
.control-panel {
73+
display: flex;
74+
flex-direction: row;
75+
align-items: center;
76+
justify-content: space-around;
77+
margin: 0 20px 20px;
78+
width: 100%;
79+
}
80+
81+
.flags-counter {
82+
font-size: 24px;
83+
}
84+
85+
.reset-button {
86+
font-size: 24px;
87+
cursor: pointer;
88+
background-color: inherit;
89+
border: none;
90+
border-radius: 25%;
91+
}
92+
93+
.reset-button:hover {
94+
cursor: pointer;
95+
}
96+
97+
.timer {
98+
font-size: 24px;
99+
}
100+
101+
.field {
102+
display: grid;
103+
gap: 10px;
104+
width: 60vmin;
105+
height: 60vmin;
106+
padding: 20px;
107+
}
108+
109+
.cell {
110+
width: 48px;
111+
height: 48px;
112+
justify-self: center;
113+
align-self: center;
114+
background-color: black;
115+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import './App.css';
3+
import Minesweeper from "./components/Minesweeper";
4+
import Spinner from "./components/Spinner";
5+
6+
function App() {
7+
const isReady = true;
8+
9+
return (
10+
<div className="App">
11+
<header className="App-header">
12+
{isReady ? <Minesweeper /> : <Spinner />}
13+
</header>
14+
</div>
15+
);
16+
}
17+
18+
export default App;
Lines changed: 30 additions & 0 deletions
Loading
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
3+
const Cell = ({cell, onCellClick, onCellContext}) => {
4+
return (
5+
<div
6+
className="cell"
7+
onClick={() => onCellClick(cell.x, cell.y)}
8+
onContextMenu={(e) => {
9+
e.preventDefault();
10+
onCellContext(cell.x, cell.y);
11+
}}
12+
>
13+
{cell.isRevealed && !cell.isMine && cell.neighbour === 0
14+
? ""
15+
: cell.isRevealed && !cell.isMine
16+
? cell.neighbour
17+
: cell.isRevealed && cell.isMine
18+
? "💣"
19+
: cell.isFlagged
20+
? "🚩"
21+
: ""}
22+
</div>
23+
);
24+
}
25+
26+
export default Cell;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from "react";
2+
3+
import FlagsCounter from "./FlagsCounter";
4+
import ResetButton from "./ResetButton";
5+
import Timer from "./Timer";
6+
7+
const ControlPanel = ({flags, time, onResetGame}) => {
8+
return (
9+
<div className="control-panel">
10+
<FlagsCounter flags={flags}/>
11+
<ResetButton onResetGame={onResetGame}/>
12+
<Timer time={time}/>
13+
</div>
14+
);
15+
}
16+
17+
export default ControlPanel;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
3+
import Row from "./Row";
4+
5+
const Field = ({field, onCellClick, onCellContext}) => {
6+
const rows = field.length;
7+
const columns = field[0].length;
8+
9+
return (
10+
<div className="field" style={{
11+
gridTemplateColumns: `repeat(${columns}, 1fr)`,
12+
gridTemplateRows: `repeat(${rows}, 1fr)`,
13+
}}>
14+
{field.map((row, rowIndex) => (
15+
<Row
16+
key={rowIndex}
17+
row={row}
18+
onCellClick={onCellClick}
19+
onCellContext={onCellContext}
20+
/>
21+
))}
22+
</div>
23+
);
24+
}
25+
26+
export default Field;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from "react";
2+
3+
const FlagsCounter = ({flags}) => {
4+
return (
5+
<div className="flags-counter"><span role="img" aria-label="flag">🚩</span> {flags}</div>
6+
);
7+
}
8+
9+
export default FlagsCounter;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
3+
import bomb from "../bomb.svg";
4+
5+
const GameTitle = () => {
6+
return (
7+
<div className="game-title">
8+
<p>Minesweeper <span role="img"><img src={bomb} alt="logo"/></span></p>
9+
</div>
10+
);
11+
}
12+
13+
export default GameTitle;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, {useEffect, useState} from "react";
2+
3+
import Field from "./Field";
4+
import ControlPanel from "./ControlPanel";
5+
import GameTitle from "./GameTitle";
6+
7+
const Minesweeper = () => {
8+
const rows = 9;
9+
const columns = 8;
10+
const minesQuantity = 10;
11+
12+
const [flags, setFlags] = useState(minesQuantity);
13+
const [time, setTime] = useState(0);
14+
15+
// const timeIntervalInMilliseconds = 1000;
16+
// useEffect(() => {
17+
// const intervalId = setInterval(() => {
18+
// setTime(prevTime => {
19+
// if (prevTime <= 0) {
20+
// // TODO: Game over
21+
// clearInterval(intervalId);
22+
// return 0;
23+
// } else {
24+
// return prevTime - timeIntervalInMilliseconds;
25+
// }
26+
// });
27+
// }, timeIntervalInMilliseconds);
28+
//
29+
// // Clear the interval when the component unmounts
30+
// return () => clearInterval(intervalId);
31+
// }, []);
32+
33+
const placeMines = (field, minesQuantity) => {
34+
let i = 0;
35+
while (i < minesQuantity) {
36+
const x = Math.floor(Math.random() * rows);
37+
const y = Math.floor(Math.random() * columns);
38+
if (!field[x][y].isMine) {
39+
field[x][y].isMine = true;
40+
i++;
41+
}
42+
}
43+
}
44+
45+
const generateField = (rows, columns, minesQuantity) => {
46+
const field = [];
47+
for (let i = 0; i < rows; i++) {
48+
field[i] = [];
49+
for (let j = 0; j < columns; j++) {
50+
field[i][j] = {
51+
isRevealed: false, isMine: false, neighbour: 0, isFlagged: false, value: "empty", x: i + 1, y: j + 1
52+
};
53+
}
54+
}
55+
56+
placeMines(field, minesQuantity)
57+
58+
return field;
59+
}
60+
61+
const field = generateField(rows, columns, minesQuantity);
62+
63+
const onResetGame = () => {
64+
console.log("reset game");
65+
};
66+
67+
const onCellClick = (x, y) => {
68+
console.log(`clicked on cell ${x} ${y}`);
69+
};
70+
71+
const onCellContext = (x, y) => {
72+
console.log(`context on cell ${x} ${y}`);
73+
};
74+
75+
return (
76+
<div className="minesweeper">
77+
<GameTitle />
78+
<ControlPanel flags={flags} time={time} onResetGame={onResetGame}/>
79+
<Field field={field} onCellClick={onCellClick} onCellContext={onCellContext}/>
80+
</div>
81+
);
82+
}
83+
84+
export default Minesweeper;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
3+
const ResetButton = ({onResetGame}) => {
4+
return (
5+
<button className="reset-button" onClick={onResetGame}>
6+
<span role="img" aria-label="reset button">
7+
🔄
8+
</span>
9+
</button>
10+
);
11+
}
12+
13+
export default ResetButton;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from "react";
2+
3+
import Cell from "./Cell";
4+
5+
const Row = ({ row, onCellClick, onCellContext }) => {
6+
return (
7+
<>
8+
{row.map((cell, _) => (
9+
<Cell
10+
key={cell.x * row.length + cell.y}
11+
cell={cell}
12+
onCellClick={onCellClick}
13+
onCellContext={onCellContext}
14+
/>
15+
))}
16+
</>
17+
);
18+
}
19+
20+
export default Row;

0 commit comments

Comments
 (0)