点击查看html编辑器说明文档

React 写的扫雷游戏edit icon

|
|
Fork(复制)
|
|
作者:
穿越者X57

👉 新版编辑器已上线,点击进行体验吧!

BUG反馈
嵌入
设置
下载
HTML
格式化
支持Emmet,输入 p 后按 Tab键试试吧!
<head> ...
展开
</head>
<body>
            
            <!-- 引入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>
        
</body>
SCSS
格式化
            
            $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);
  }
}
        
JS
格式化
            
            
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"));

        
预览
控制台
清空