Skip to content
React Front-End
JavaScript CSS HTML
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
public
src
.gitignore
README.md
package-lock.json
package.json
static.json

README.md

React-Minesweeper

Minesweeper clone built in React- utilizing state to represent game boards. Users can play on three different levels of difficulty. Utilizes a Ruby on Rails backend to allow users to submit won games to a database and see the leaderboard updated in real time to compare their times to other players!

How It Works

The heavy-lifting of the game is done through a GameBoard React component that contains a two-dimensional array as state to represent a grid of tiles with X,Y coordinates (ie grid[0][0] represents the top left tile on the board) In addition the state keeps track of various high level game information to determine whether a game is won or lost:

class GameBoard extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      grid: [], //the 2d array that represents the tiles on the board
      mines: 0, //number of mines left on the board
      revealed: 0, //number of revealed tiles
      time: 0, //time to "sweep" board is a user's final score
      activeTimer: false, //when the timer needs to stop incrementing
      gameOver: false, //whether or not the game is in progress
      won: false, //whether a user actually won the game (prompts for scoreboard entry)
      difficulty: '' //passed in as props from main menu and used to set up the board
    }
  }

  componentDidMount() {
    this.determineBoard(this.props.difficulty) //where the intial board/grid is set up- sized corresponds to difficulty
  }

When a user selects their difficulty a new array of a corresponding size (ie Easy is 16x16) is created and each element of this array is filled with an instance of the GameTile class- which contains data representing the various values a tile can take on or represent throughout a game of Minesweeper. This array then is set as the "grid" state of the GameBoard. When the board is actually rendered on the screen- each of these elements passes their data as props to a Square component that renders it's CSS/apperance based on that data.

class GameTile { constructor() { this.isFlagged = false; //Has this tile been flagged? this.isRevealed = false; //has this tile been clicked on or revealed as the result of clicking a blank tile? this.isBomb = false; //Is this tile a bomb (actually a mine)? this.adjacentCount = -1; //The number of mines in the neighboring tiles } isClickable() { return !this.isFlagged && !this.isRevealed; //Stops users from clicking tiles that have been flagged or revealed } }

Once the size has been determined- a few callback functions are executed that go through and 'seed' the grid before the game can begin. First random X,Y coordinates are generated- and then set the isBomb of the GameTile at that coordinate to true. This process is repeated until the all the mines for that diffculty have been placed. Once the mines are seeded- the array is then iterated through- with each tile checking all its possible surrounding tiles and incrementing its adjacentCount for each mine found.

  generatePossibilities(x, y) {
    let all = [
      [x - 1, y - 1],
      [x - 1, y],
      [x - 1, y + 1],
      [x, y + 1],
      [x, y - 1],
      [x + 1, y - 1],
      [x + 1, y],
      [x + 1, y + 1]
    ]
    return all.filter((coords, i) => {
      let xx = all[i][0]
      let yy = all[i][1]
      return (xx >= 0 && yy >= 0 && xx < this.state.grid.length && yy < this.state.grid.length)
    })
  }

As a last step- the Gameboard has it's state values for mines and revealed set. Revealed is equal to the size of the grid minus the number of mines. This is how the core logic of whether or not a game is won is set up-- A game is only won when the number of mines left is 0 AND the number of tiles left to reveal is 0.

The wincheck function that runs on user clicks: Note that flagging a tile reduces the mine count and revealing a tile reduces the revealed count. If these are both 0 then the game is won and the timer is stopped.

   winCheck = () => {
      if (this.state.mines === 0 && this.state.revealed === 0) {
        this.setState({ activeTimer: false, gameOver: true, won: true })
        return true
      }
      return false;
    }

-- With this the game board is fully seeded and ready to play.

The final aspect to highlight is the actual logic of what happens when a user clicks/interacts with a tile. There are functions in place to handle a mine click (instantly lose the game) and to handle flagging a square with a right click (decrement the mine count) but the most interesting is the processing of a "normal" click of a tile. Clicking on a tile with some number of neighboring mines simply reveals it to the user while clicking on a "blank" tile (one that has 0 mines in it's neighboring tiles) prompts a wave a tiles being revealed- where neighboring numbered tiles are simply revealed and neghboring blank tiles reveal their neighbors and so on. In order to handle this process we implemented a breadth first search-- starting with the tile clicked and then adding all of it's neighbors into a queue to be checked. Once a tile has been visited it is added to an object that stores that tile's coordinates so it is never checked again. Along the way the number of revealed tiles is decremented appropriately as each tile is visited.

 processNonMineClick = (coords) => {
   let copyGrid = [...this.state.grid]
   let revealed = this.state.revealed
   let visited = {}
   let queue = [coords];
   
  while (queue.length > 0) {
     let currCoords = queue.pop();
     let currTile = copyGrid[currCoords[0]][currCoords[1]];
     if (!currTile.isFlagged && !visited[currCoords]) {
       visited[currCoords] = true;
       if (!currTile.isRevealed) {
         currTile.isRevealed = true;
         revealed--;
         
       }
       if (currTile.adjacentCount === 0) {
         //grab all possibile neighboring tiles
         let poss = this.generatePossibilities(currCoords[0], currCoords[1])
         poss.forEach(neighbor => {
           if (!visited[neighbor]) {
             queue.push(neighbor);
           }
         })
       }
     }
   }
   this.setState({
     grid: copyGrid,
     revealed
   }
     , () => this.winCheck())
 }

Built With

  • React.js
  • Ruby on Rails

Authors

Built by Claire Muller and Logan Wohlers

You can’t perform that action at this time.