<!-- 引入React -->
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-y/react-dom/16.14.0/umd/react-dom.production.min.js" type="application/javascript"></script>
<div id="app"></div>
HTML
格式化
支持Emmet,输入 p 后按 Tab键试试吧!
<head> ... </head>
<body>
</body>
$main: var(--main-color);
$dark: var(--dark-color);
$tile: #f3f1ff;
:root {
--grid-width: 350;
--tile-width: 35;
--main-color: #8B6AF5;
}
body {
padding: 0;
margin: 0;
background: #f9f8fe;
width: 100%;
height:100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: 'Roboto Mono', monospace;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
.container {
width: calc(var(--grid-width) * 1px);
align-content: center;
text-align: center;
box-shadow: 5px 5px 20px 0 rgba(100, 74, 74, 0.1);
position: relative;
z-index:2;
}
.header {
display: flex;
position: relative;
align-items: flex-start;
background-color: $main;
color: white;
justify-content: space-between;
padding:1rem;
}
.grid {
height: calc(var(--grid-width) * 1px);
width: 100%;
display: flex;
flex-wrap: wrap;
}
.tile {
height: calc(var(--tile-width) * 1px);
width: calc(var(--tile-width) * 1px);
cursor: pointer;
border: 2px solid;
border-color: lighten($tile, 5%) darken($tile, 5%) darken($tile, 5%) lighten($tile, 5%);
box-sizing: border-box;
background-color: $tile;
font-weight: 700;
font-size: 25px;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 80%;
top:10%;
position: relative;
pointer-events: none;
* {
pointer-events: none
}
}
.tile-container {
height:100%;
pointer-events: none
}
}
.checked {
border: 1px solid;
background-color: darken($tile, 2%);
border-color: darken($tile, 5%);
}
#refresh {
cursor:pointer;
width:30px;
align-self: flex-end;
}
.dropdown {
color: white;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 3px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 1rem;
text-align: center;
.title {
width: 100%;
padding:0.5rem 1rem;
cursor: pointer;
}
.menu {
background:$main;
position:absolute;
overflow: hidden;
cursor: pointer;
width: 5rem;
text-align: left;
line-height:1.4rem;
z-index: 999;
&.show {
display:block;
}
.option{
padding: .5rem;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
}
}
.has-bomb {
transition: background .25s ease-in;
}
#flag-countdown, #timer {
display: flex;
font-size:35px;
span {
margin-left:0.5rem;
}
}
#modal {
position: fixed;
background-color: rgba(#39395b, 0.2);
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
&.show {
visibility: visible;
opacity: 1;
pointer-events: auto;
}
.modal-close {
text-align: right;
}
h2 {
color: $main;
}
}
#result-box {
background-color: #f9f8fe;
box-shadow: 5px 5px 20px 0 rgba(100, 74, 74, 0.1);
border-radius: 4px;
min-width: 400px;
text-align: center;
}
#result-top {
margin:2rem;
}
#result-message {
color: $dark;
font-size:40px;
}
.result-time {
display: none;
}
.show {
display: block;
}
#new-game {
padding:0.5rem;
background-color: $main;
color: #fff;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
margin:1rem;
height:60px;
border-radius: 4px;
font-family: sans-serif;
* {
display: inline-block;
}
h2 {
line-height:30px;
margin: 0 0 0 1rem;
color: white;
}
}
#background {
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
svg {
width: 50px;
height: 40px;
opacity: 0.3;
position: absolute;
}
}
svg#bomb{
width: 200px;
.sad-face, .happy-face {
// display: none;
&.show {
// display: block;
}
}
.bomb-fill {
fill: $main;
}
.bomb-stroke {
stroke: $dark;
}
.bomb-fill-dark {
fill: $dark;
}
}
svg.hide {
display: none;
}
.shake {
animation: shake 0.75s cubic-bezier(.38,.06,.22,.95) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-3px, 0, 0);
}
40%, 60% {
transform: translate3d(2px, 0, 0);
}
}
const lightenDarkenColor = (col, amt) => {
let usePound = false;
if (col[0] === "#") {
col = col.slice(1);
usePound = true;
}
let num = parseInt(col, 16);
let r = (num >> 16) + amt;
if (r > 255) r = 255;
else if (r < 0) r = 0;
let b = ((num >> 8) & 0x00ff) + amt;
if (b > 255) b = 255;
else if (b < 0) b = 0;
let g = (num & 0x0000ff) + amt;
if (g > 255) g = 255;
else if (g < 0) g = 0;
return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16);
}
const Modal = (props) => {
let resultMessage;
if (props.gameResult === 'won') {
resultMessage = '恭喜!'
} else if (props.gameResult) {
resultMessage = '游戏结束!'
}
return (
<div>
<div id="modal" className={props.show ? 'show' : ''}>
<div id="result-box">
<div id="result-top">
<svg id="bomb" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 774.7 676.2">
<path d="M427.4,144.2l40.7-41.8a23.1,23.1,0,0,1,32.5,0l72.8,72.8a23.1,23.1,0,0,1,0,32.5l-37.4,37.4" transform="translate(0 -11.6)" fill="#8b6af5" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke bomb-fill"/>
<circle cx="291.1" cy="385.2" r="282.6" fill="#8b6af5" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke bomb-fill"/>
<path d="M540.2,141.2l15.4-20.5c12.6-16.8,30.4-17.7,43.6-2.4l0.3,0.3c11.8,13.7,31.3,22.6,48.3,11.4,6.1-4,20.7-20.5,20.7-20.5" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<line x1="701.7" y1="63.3" x2="742" y2="23.1" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<line x1="713.2" y1="107.4" x2="766.2" y2="128.2" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<line x1="654.5" y1="60.2" x2="630.8" y2="8.5" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<path d="M82,396.8c0-118.4,95.9-214.3,214.3-214.3" transform="translate(0 -11.6)" fill="none" stroke="#fff" strokeLinecap="round" strokeMiterlimit="10" strokeOpacity="0.45" strokeWidth="17"/>
<g id="happy-face" style={{display: props.gameResult === "won" ? 'block' : 'none'}}>
<path d="M170.4,432.1a34.6,34.6,0,0,1,69.2,0" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<path d="M342.5,432.1a34.6,34.6,0,0,1,69.2,0" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<path d="M367,481.7c0,33.7-33.4,64.1-74.6,64.1s-74.6-30.3-74.6-64.1" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
</g>
<g id="sad-face" style={{display: props.gameResult === "lost" ? 'block' : 'none'}}>
<g>
<circle cx="377.1" cy="406" r="17.7" fill="#39395b" className="bomb-fill-dark"/>
<path d="M250,517.1c0-19.2,17.6-42.5,41.1-42.5s43.8,23.3,43.8,42.5" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17" className="bomb-stroke"/>
<circle cx="205" cy="406" r="17.7" fill="#39395b" className="bomb-fill-dark"/>
</g>
</g>
</svg>
<h1 id="result-message">{resultMessage}</h1>
<h2 className={`result-time ${props.gameResult === "won" ? "show" : ""}`}>Your time: <span className="time-display">{props.timeDisplay}</span> seconds</h2>
</div>
<div id="new-game" onClick={props.onReplay}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 512 512" fill="#fff" width="30">
<g>
<path d="M480.6,235.6c-11.3,0-20.4,9.1-20.4,20.4c0,112.6-91.6,204.2-204.2,204.2c-112.6,0-204.2-91.6-204.2-204.2 S143.4,51.8,256,51.8c61.5,0,118.5,27.1,157.1,73.7h-70.5c-11.3,0-20.4,9.1-20.4,20.4s9.1,20.4,20.4,20.4h114.6 c11.3,0,20.4-9.1,20.4-20.4V31.4c0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4v59C390.7,40.1,325.8,11,256,11 C120.9,11,11,120.9,11,256c0,135.1,109.9,245,245,245s245-109.9,245-245C501,244.7,491.9,235.6,480.6,235.6z"/>
</g>
</svg>
<h2>重新开始</h2>
</div>
</div>
</div>
</div>
)
}
class Timer extends React.Component {
startTimer = () => {
let sec = 0;
this.timerCount = setInterval(() => {
sec++;
this.props.onTimeChange(sec);
if (sec > 998) clearInterval(this.timerCount);
}, 1000);
}
stopTimer = () => {
clearInterval(this.timerCount);
}
render() {
return (
<div id="timer"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 559.98 559.98" fill="#fff" width="30px"><g><path d="M279.99,0C125.601,0,0,125.601,0,279.99c0,154.39,125.601,279.99,279.99,279.99c154.39,0,279.99-125.601,279.99-279.99 C559.98,125.601,434.38,0,279.99,0z M279.99,498.78c-120.644,0-218.79-98.146-218.79-218.79 c0-120.638,98.146-218.79,218.79-218.79s218.79,98.152,218.79,218.79C498.78,400.634,400.634,498.78,279.99,498.78z"/><path d="M304.226,280.326V162.976c0-13.103-10.618-23.721-23.716-23.721c-13.102,0-23.721,10.618-23.721,23.721v124.928 c0,0.373,0.092,0.723,0.11,1.096c-0.312,6.45,1.91,12.999,6.836,17.926l88.343,88.336c9.266,9.266,24.284,9.266,33.543,0 c9.26-9.266,9.266-24.284,0-33.544L304.226,280.326z"/></g></svg><span className="counter">{("00" + this.props.timeDisplay).slice(-3)}</span></div>
)
}
}
class Dropdown extends React.Component {
state = {
isMenuVisible: false
}
handleMenuItemClick = (e) => {
const selectedLevel = this.props.levels.find((level) => level.difficulty === e.target.innerText);
this.setState({ isMenuVisible: false })
this.props.onLevelChange(selectedLevel)
}
toggleMenu = () => {
this.setState({ isMenuVisible: !this.state.isMenuVisible });
}
closeMenu = () => {
this.setState({ isMenuVisible: false })
}
render() {
let dropdown;
if (this.state.isMenuVisible) {
dropdown = (
<div className="menu">
{this.props.levels.map((level) => {
return <div onClick={(e) => { this.handleMenuItemClick(e) }} className="option" key={level.id} value={level.difficulty}>{level.difficulty}</div>;
})}
</div>
);
}
return (
<div>
<div className="dropdown" onBlur={() => {this.closeMenu()}}>
<div className="title" onClick={() => {this.toggleMenu()}}>
{this.props.selectedLevel.difficulty}
</div>
{dropdown}
</div>
</div>
)
}
}
class Tile extends React.Component {
state = {
numberColors: ["#8B6AF5","#74c2f9","#42dfbc","#f9dd5b","#FEAC5E","#ff5d9e","#F29FF5","#c154d8"],
stagger: 20,
flagIcon:
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 287.987 287.987" fill="#695ca8" style="enable-background:new 0 0 287.987 287.987;" xml:space="preserve"><g><path d="M228.702,141.029c-3.114-3.754-3.114-9.193,0-12.946l33.58-40.474c2.509-3.024,3.044-7.226,1.374-10.783 c-1.671-3.557-5.246-5.828-9.176-5.828h-57.647v60.98c0,16.618-13.52,30.138-30.138,30.138h-47.093v25.86 c0,5.599,4.539,10.138,10.138,10.138h124.74c3.93,0,7.505-2.271,9.176-5.828c1.671-3.557,1.135-7.759-1.374-10.783L228.702,141.029 z"/><path d="M176.832,131.978V25.138c0-5.599-4.539-10.138-10.138-10.138H53.37c0-8.284-6.716-15-15-15s-15,6.716-15,15 c0,7.827,0,253.91,0,257.987c0,8.284,6.716,15,15,15s15-6.716,15-15c0-6.943,0-126.106,0-130.871h113.324 C172.293,142.116,176.832,137.577,176.832,131.978z"/></g></svg>',
bombIcon:
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 512 512" fill="#695ca8" ><g><path d="m411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z"/><path d="m459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z"/><path d="m340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z"/><path d="m400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z"/><path d="m496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z"/><path d="m436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z"/></g></svg>'
}
onRightClick = (e) => {
e.preventDefault();
const tileId = parseInt(e.target.id);
this.props.onAddFlag(tileId);
}
render() {
const tile = this.props.tile;
let tileClass;
if (tile.checked) {
if (tile.hasBomb) tileClass = " checked has-bomb"
else tileClass = " checked"
} else {
tileClass = ''
}
let content;
if (tile.checked) {
if (tile.hasBomb) {
content = this.state.bombIcon
} else {
if (tile.neighborBombs !== 0) {
content = tile.neighborBombs
} else {
content = null
}
}
} else {
if (tile.flag) {
content = this.state.flagIcon
} else {
content = null
}
}
let tileStyle;
if (tile.checked && tile.hasBomb && tile.bgColor) {
tileStyle= {backgroundColor: tile.bgColor}
} else if (tile.checked && !tile.hasBomb && tile.neighborBombs !== 0) {
const tileNumberColor = this.state.numberColors[tile.neighborBombs - 1];
const tileNumberShadow = "1px 1px" + lightenDarkenColor(tileNumberColor, -20);
tileStyle= {color: tileNumberColor, textShadow: tileNumberShadow }
} else {
tileStyle = null
}
return (
<div id={this.props.id} className={`tile${tileClass}`} onClick={(e) => this.props.onTileClick(e)} onContextMenu={(e) => this.onRightClick(e)} neighborbombs={tile.neighborbombs} style={tileStyle}><div className="tile-container" dangerouslySetInnerHTML={{__html: content}} /></div>
)
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
levels: [
{ id: 0, difficulty: "初级", width: 10, bombs: 15 },
{ id: 1, difficulty: "中级", width: 15, bombs: 30 },
{ id: 2, difficulty: "高级", width: 18, bombs: 70 }
],
tiles: [],
isGameOver: false,
gameResult: "",
isTimerOn: false,
isModalOpen: false,
isContainerAnimated: false,
timeDisplay: 0,
flagCount: 0,
mainColors: [
"#8B6AF5",
"#74c2f9",
"#42dfbc",
"#f9dd5b",
"#FEAC5E",
"#ff5d9e",
"#F29FF5",
"#c154d8"
],
bgColors: [
"#b39ffd",
"#93c1fd",
"#8af1f8",
"#f9dd5b",
"#FEAC5E",
"#f87dae",
"#f6b8f8",
"#f7efce"
]
};
this.state.root = document.documentElement;
this.state.selectedLevel = this.state.levels[0];
this.state.flagsLeft = this.state.selectedLevel.bombs;
}
componentDidMount() {
console.log("hello");
this.createBackground();
this.createBoard();
}
getMainColor = () => {
const randomColor = this.state.mainColors[Math.floor(Math.random() * this.state.mainColors.length)];
this.state.root.style.setProperty("--main-color", randomColor);
this.state.root.style.setProperty(
"--dark-color",
lightenDarkenColor(randomColor, -50)
);
}
clearBoard = () => {
console.clear();
if (this.state.isTimerOn) this.refs.timer.stopTimer();
this.setState({ timeDisplay: 0, isGameOver: false, tiles: [], gameResult: '', isContainerAnimated: false, isTimerOn: false, flagCount:0})
this.createBoard();
}
createBoard = () => {
this.getMainColor();
const width = this.state.selectedLevel.width;
const tileWidth = parseInt(
getComputedStyle(this.state.root).getPropertyValue("--tile-width")
);
this.state.root.style.setProperty("--grid-width", width * tileWidth);
const tiles = []
for (let i = 0; i < width * width; i++) {
tiles.push({ id: i, checked: false, hasBomb: false })
}
//add bombs
const randomTiles = tiles.map(tile => tile.id).sort(() => Math.random() - 0.5).slice(0, this.state.selectedLevel.bombs)
let bombIndex = 0;
tiles.forEach((tile, index) => {
if (randomTiles.includes(tile.id)) {
tile.hasBomb = true
tile.bombIndex = bombIndex
tile.bgColor = this.state.bgColors[Math.floor(Math.random() * this.state.bgColors.length)];
bombIndex++
}
console.log(tile)
})
//add numbers
for (let i = 0; i < tiles.length; i++) {
let total = 0;
const isLeftEdge = i % width === 0;
const isRightEdge = i % width === width - 1;
if (!tiles[i].hasBomb) {
if (!isLeftEdge) {
if (tiles[i - 1] && tiles[i - 1].hasBomb)
total++;
if (
tiles[i - 1 + width] &&
tiles[i - 1 + width].hasBomb
)
total++;
if (
tiles[i - 1 - width] &&
tiles[i - 1 - width].hasBomb
)
total++;
}
if (!isRightEdge) {
if (tiles[i + 1] && tiles[i + 1].hasBomb)
total++;
if (
tiles[i + 1 + width] &&
tiles[i + 1 + width].hasBomb
)
total++;
if (
tiles[i + 1 - width] &&
tiles[i + 1 - width].hasBomb
)
total++;
}
if (tiles[i - width] && tiles[i - width].hasBomb) total++;
if (tiles[i + width] && tiles[i + width].hasBomb) total++;
tiles[i].neighborBombs = total;
}
}
this.setState({ tiles, flagsLeft: this.state.selectedLevel.bombs })
}
handleTileClick = (e) => {
const clickedTileId = parseInt(e.target.id);
if (!this.state.isTimerOn) this.refs.timer.startTimer();
this.setState({ isTimerOn: true})
this.clickTile(clickedTileId)
}
// click on tile
clickTile = (tileId) => {
const currentTile = this.state.tiles.find(tile => tile.id === tileId)
let checkIndex = 0;
if (this.state.isGameOver) return null;
if (currentTile.checked || currentTile.flag) {
return null;
}
if (currentTile.hasBomb) {
this.gameOver(currentTile);
} else {
let total = currentTile.neighborBombs ? currentTile.neighborBombs : 0;
if (total !== 0) {
currentTile.checked = true
currentTile.color = this.state.mainColors[currentTile.neighborBombs - 1];
return
}
}
currentTile.checked = true
currentTile.checkIndex = checkIndex
checkIndex++;
console.log(checkIndex)
this.checktile(tileId);
const tiles = [...this.state.tiles]
tiles.forEach(tile => {
if (tile.id === tileId) tile.checked = true
})
this.setState({tiles})
}
//check neighboring tiles once tile is clicked
checktile = (tileId) => {
const width = this.state.selectedLevel.width;
const isLeftEdge = tileId % width === 0;
const isRightEdge = tileId % width === width - 1;
const tiles = this.state.tiles;
const loopThroughtiles = (tile) => {
this.clickTile(tile.id);
}
if (!isRightEdge) {
if (tiles[tileId + 1 - width])
loopThroughtiles(tiles[tileId + 1 - width]);
if (tiles[tileId + 1]) loopThroughtiles(tiles[tileId + 1]);
if (tiles[tileId + 1 + width])
loopThroughtiles(tiles[tileId + 1 + width]);
}
if (!isLeftEdge) {
if (tiles[tileId - 1]) loopThroughtiles(tiles[tileId - 1]);
if (tiles[tileId - 1 - width])
loopThroughtiles(tiles[tileId - 1 - width]);
if (tiles[tileId - 1 + width])
loopThroughtiles(tiles[tileId - 1 + width]);
}
if (tiles[tileId - width]) loopThroughtiles(tiles[tileId - width]);
if (tiles[tileId + width]) loopThroughtiles(tiles[tileId + width]);
}
gameOver = (currentTile) => {
this.setState({ isGameOver: true, isContainerAnimated: true, isTimerOn: false, gameResult: 'lost'})
this.refs.timer.stopTimer()
let itemsProcessed = 0;
// //show all the bombs
const bombTiles = this.state.tiles.filter((tile) =>
tile.hasBomb
);
bombTiles.forEach((tile) => {
currentTile.checked = true
tile.checked = true
itemsProcessed++;
if (itemsProcessed === bombTiles.length) {
setTimeout(() => {
this.openModal()
}, 1000);
}
});
}
//add Flag with right click
addFlag = (tileId) => {
const tile = this.state.tiles.find(tile => tile.id === tileId)
if (this.state.isGameOver) return;
let { flagCount } = this.state;
if (!tile.checked) {
if (!tile.flag && flagCount < this.state.selectedLevel.bombs) {
tile.flag = true
flagCount++;
const flagsLeft = this.state.selectedLevel.bombs - flagCount;
this.setState({flagsLeft, flagCount})
this.checkForWin();
} else if (tile.flag){
tile.flag = false
flagCount--;
const flagsLeft = this.state.selectedLevel.bombs - flagCount;
this.setState({flagsLeft, flagCount})
}
}
}
//check for win
checkForWin = () => {
let matches = 0;
this.state.tiles.forEach((tile) => {
if (tile.flag && tile.hasBomb) matches++;
if (matches === this.state.selectedLevel.bombs) {
this.setState({ gameResult: 'won', isModalOpen: true, isGameOver: true, isTimerOn: false })
this.refs.timer.stopTimer()
if (!tile.checked) tile.checked = true;
}
})
}
replay = () => {
if (this.state.isModalOpen) this.closeModal();
setTimeout(() => {this.clearBoard()}, 80)
}
updateLevel = (level) => {
this.setState({selectedLevel: level }, () => this.clearBoard())
}
//modal functions
closeModal = () => {
this.setState({ isModalOpen: false });
}
openModal = () => {
this.setState({ isModalOpen: true });
}
getTime = (time) => {
this.setState({ timeDisplay: time })
}
addElement = (x, y) => {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
use.setAttributeNS(
"http://www.w3.org/1999/xlink",
"xlink:href",
"#bomb-svg"
);
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
svg.setAttribute("style", "top: " + y + "px; left: " + x + "px");
svg.appendChild(use);
this.background.appendChild(svg);
};
createBackground = () => {
const spacing = 60;
const w = window.innerWidth;
const h = window.innerHeight;
for (let y = 0; y <= h; y += spacing) {
if (y % (spacing * 2) === 0) {
for (let x = 0; x <= w; x += spacing) {
this.addElement(x, y);
}
} else {
for (let x = -(spacing / 2); x <= w; x += spacing) {
this.addElement(x, y);
}
}
}
};
render() {
let grid = this.state.tiles.map((tile, index) => {
return (
<Tile key={index} id={index} tile={tile} onTileClick={this.handleTileClick} onAddFlag={this.addFlag}/>
)
})
return (
<div>
<svg id="main" className="hide">
<symbol id="bomb-svg" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 786.7 687.8" width="50px">
<path d="M427.4,144.2l40.7-41.8a23.1,23.1,0,0,1,32.5,0l72.8,72.8a23.1,23.1,0,0,1,0,32.5l-37.4,37.4" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<circle cx="291.1" cy="385.2" r="282.6" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<path d="M540.2,141.2l15.4-20.5c12.6-16.8,30.4-17.7,43.6-2.4l0.3,0.3c11.8,13.7,31.3,22.6,48.3,11.4,6.1-4,20.7-20.5,20.7-20.5" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<line x1="701.7" y1="63.3" x2="742" y2="23.1" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<line x1="713.2" y1="107.4" x2="766.2" y2="128.2" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<line x1="654.5" y1="60.2" x2="630.8" y2="8.5" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<path d="M82,396.8c0-118.4,95.9-214.3,214.3-214.3" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<g id="Happy_face" data-name="Happy face">
<path d="M170.4,432.1a34.6,34.6,0,0,1,69.2,0" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<path d="M342.5,432.1a34.6,34.6,0,0,1,69.2,0" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
<path d="M367,481.7c0,33.7-33.4,64.1-74.6,64.1s-74.6-30.3-74.6-64.1" transform="translate(0 -11.6)" fill="none" stroke="#39395b" strokeLinecap="round" strokeMiterlimit="10" strokeWidth="17"/>
</g>
</symbol>
</svg>
<div id="background" ref={(el) => this.background = el}></div>
<Modal gameResult={this.state.gameResult} show={this.state.isModalOpen} onReplay={this.replay} timeDisplay={this.state.timeDisplay}/>
<div className={`container ${this.state.isContainerAnimated ? "shake" : ""}`}>
<div className="header">
<Dropdown onLevelChange={this.updateLevel} levels={this.state.levels} selectedLevel={this.state.selectedLevel}/>
<div id='flag-countdown'><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 287.987 287.987" fill="#fff" width="30"><g><path d="M228.702,141.029c-3.114-3.754-3.114-9.193,0-12.946l33.58-40.474c2.509-3.024,3.044-7.226,1.374-10.783 c-1.671-3.557-5.246-5.828-9.176-5.828h-57.647v60.98c0,16.618-13.52,30.138-30.138,30.138h-47.093v25.86 c0,5.599,4.539,10.138,10.138,10.138h124.74c3.93,0,7.505-2.271,9.176-5.828c1.671-3.557,1.135-7.759-1.374-10.783L228.702,141.029 z"/><path d="M176.832,131.978V25.138c0-5.599-4.539-10.138-10.138-10.138H53.37c0-8.284-6.716-15-15-15s-15,6.716-15,15 c0,7.827,0,253.91,0,257.987c0,8.284,6.716,15,15,15s15-6.716,15-15c0-6.943,0-126.106,0-130.871h113.324 C172.293,142.116,176.832,137.577,176.832,131.978z"/></g></svg><span id='flags-left'>{this.state.flagsLeft} </span></div>
<Timer ref="timer" onTimeChange={this.getTime} timeDisplay={this.state.timeDisplay}/>
</div>
<div className="grid">{grid}</div>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));