skip to Main Content

In react I tried to recreate the well known game "minesweeper" and in my journey I wanted to randomly generate where the bombs will spawn, so I got a 12*12 matrix and 40 bombs for which I randomly generate x and y that stand for the coordinates, but to not be unfair I generate the bombs only after the first square is selected, now the code isn’t complete but I guess because after the first phase of the game(mentioned earlier) I do a re-render by pressing the submit button the variable bomb is never decremented to 0 because it will always be declared again as 40 ,tried to declare it outside of the function and I have even tried to set up a useState for that value but it keeps getting incremented back and creating an infinite loop . How can I declare an unchangeable variable?

import { useState } from 'react'
import './App.css'

const square = {
  bomb: 0,
  viz: 0,
}
let nrbomb = 40

function App() {
  const n = 12
  let v = []
  for (let i = 0; i < n; i++) {
    let arr = []
    for (let j = 0; j < n; j++) arr.push(square)
    v.push(arr)
  }

  const [inc, change_inc] = useState(0)
  const [mine_grid, change_mine_grid] = useState(v)
  const [cx, change_cx] = useState(0)
  const [cy, change_cy] = useState(0)
  const [rendergame, change_render_game] = useState(0)

  if (inc == 0) {
    return (
      <div className="all">
        <div className="grid_container">
          {mine_grid.map((arr) => {
            return arr.map((e) => {
              return (
                <div
                  style={{
                    background: 'green',
                    width: '50px',
                    height: '50px',
                    margin: '5px',
                  }}
                ></div>
              )
            })
          })}
        </div>
        <h6>for x:</h6>
        <input
          value={cx}
          onChange={(e) => {
            change_cx(e.target.value)
          }}
        ></input>
        <h6>for y:</h6>
        <input
          value={cy}
          onChange={(e) => {
            change_cy(e.target.value)
          }}
        ></input>
        <button
          type="button"
          onClick={() => {
            let new_grid = mine_grid
            new_grid[cx][cy].viz = 1
            change_mine_grid(new_grid)
            change_inc(1)
          }}
        >
          Submit move
        </button>
      </div>
    )
  }
  
  if (inc == 1 && rendergame == 0) {
    while (nrbomb != 0) {
      let x = Math.floor(Math.random() * n)
      let y = Math.floor(Math.random() * n)
      console.log(x, y, nrbomb)
      if (mine_grid[x][y].bomb === 0 && mine_grid[x][y].viz == 0) {
        nrbomb = nrbomb - 1
        let new_grid = mine_grid
        new_grid[x][y].bomb = 1
        change_mine_grid(new_grid)
      }
    }
  }
  
  return <div className="grid_container"></div>
}

export default App

2

Answers


  1. Instead of a while loop, use a for:

    import { useState } from 'react'
    import './App.css'
    
    const square = {
      bomb: 0,
      viz: 0,
    }
    let nrbomb = 40
    
    function App() {
      const n = 12
      let v = []
      for (let i = 0; i < n; i++) {
        let arr = []
        for (let j = 0; j < n; j++) arr.push(square)
        v.push(arr)
      }
    
      const [inc, change_inc] = useState(0)
      const [mine_grid, change_mine_grid] = useState(v)
      const [cx, change_cx] = useState(0)
      const [cy, change_cy] = useState(0)
      const [rendergame, change_render_game] = useState(0)
    
      if (inc == 0) {
        return (
          <div className="all">
            <div className="grid_container">
              {mine_grid.map((arr) => {
                return arr.map((e) => {
                  return (
                    <div
                      style={{
                        background: 'green',
                        width: '50px',
                        height: '50px',
                        margin: '5px',
                      }}
                    ></div>
                  )
                })
              })}
            </div>
            <h6>for x:</h6>
            <input
              value={cx}
              onChange={(e) => {
                change_cx(e.target.value)
              }}
            ></input>
            <h6>for y:</h6>
            <input
              value={cy}
              onChange={(e) => {
                change_cy(e.target.value)
              }}
            ></input>
            <button
              type="button"
              onClick={() => {
                let new_grid = mine_grid
                new_grid[cx][cy].viz = 1
                change_mine_grid(new_grid)
                change_inc(1)
              }}
            >
              Submit move
            </button>
          </div>
        )
      }
      
      if (inc == 1 && rendergame == 0) {
        for (let i = 0; i < nrbomb; i++) {
          let x = Math.floor(Math.random() * n)
          let y = Math.floor(Math.random() * n)
          console.log(x, y, nrbomb)
    
          if (!mine_grid[x][y].bomb === 0 || mine_grid[x][y].viz != 0) {
            // In case it's not placing the bomb, decrease "i" back and skip
            i--
            continue
          }
            let new_grid = mine_grid
            new_grid[x][y].bomb = 1
            change_mine_grid(new_grid)
        }
      }
      
      return <div className="grid_container"></div>
    }
    
    export default App
    
    Login or Signup to reply.
  2. This is a refactor of what you originally created, with the major differences being:

    1. When the mine grid is updated we forcefully recreate the mine grid object (array of arrays) to ensure that React recognizes the state change
    2. When mines are not currently placed, the onClick handler for a mine cell will handle creating mines on the first click via the createMines function. This way state is only ever changed as a result of an action, and not on every render.

    Sample:

    const totalBombs = 40;
    const square = { bomb: 0, viz: 0 };
    const boardSideSize = 12;
    // this is an easy way for us to recreate the mine_grid
    const createDefaultGrid = () =>
      new Array(boardSideSize)
        .fill(0)
        .map(() => new Array(boardSideSize).fill(0).map(() => ({ ...square })));
    
    // we call this once we want to initialize the game and place bombs
    const createMineGridFromPositions = (minePositions) => {
      const grid = createDefaultGrid();
      minePositions.forEach(({ x, y }) => {
        grid[x][y].bomb = 1;
      });
      return grid;
    };
    
    // this is used to change mine_grid state. we create a new grid and return it
    const createMineGrid = (grid) => {
      const new_grid = createDefaultGrid();
      new_grid.forEach((row, x) =>
        row.forEach((cell, y) => {
          cell.bomb = grid[x][y].bomb;
          cell.viz = grid[x][y].viz;
        })
      );
      return new_grid;
    };
    
    function App() {
      const [mine_grid, change_mine_grid] = useState(createDefaultGrid());
      const [mines_placed, change_mines_placed] = useState(false);
      const [game_over, set_game_over] = useState(false);
    
      const createMines = (firstX, firstY) => {
        const totalMines = [];
        while (totalMines.length < totalBombs) {
          const x = Math.floor(Math.random() * boardSideSize);
          const y = Math.floor(Math.random() * boardSideSize);
          console.log(x, y, totalMines);
          const gridPos = mine_grid[x][y];
          const sameAsFirstPos = x === firstX && y === firstY;
          if (
            gridPos.bomb === 0 &&
            gridPos.viz === 0 &&
            !sameAsFirstPos &&
            !totalMines.find((mines) => mines.x === x && mines.y === y)
          ) {
            totalMines.push({ x, y });
          }
        }
        const newGrid = createMineGridFromPositions(totalMines);
        newGrid[firstX][firstY].viz = 1;
        change_mine_grid(newGrid);
      };
    
      const clickHandler = (cell, x, y) => {
        if (cell.viz) {
          // if visible already this is a no-op. do nothing
          return;
        }
    
        if (!mines_placed) {
          createMines(x, y);
          change_mines_placed(true);
        } else {
          const new_mine_grid = createMineGrid(mine_grid);
          new_mine_grid[x][y].viz = 1;
          if (new_mine_grid[x][y].bomb) {
            set_game_over(true);
          }
          change_mine_grid(new_mine_grid);
        }
      }
    
      if (game_over) {
        // can render something different in this game state
        return null;
      }
    
      return (
        <div className="all">
          <div className="grid_container">
            {mine_grid.map((row, x) =>
              row.map((cell, y) => (
                <div
                  key={`${x}-${y}`}
                  style={{
                    background:
                      cell.bomb && cell.viz ? "red" : cell.viz ? "green" : "gray",
                    width: "50px",
                    height: "50px",
                    margin: "5px",
                  }}
                  onClick={() => clickHandler(cell, x, y)}
                />
              ))
            )}
          </div>
        </div>
      );
    }
    

    If you refactor this further you might find it’s actually easier to use the minePositions approach for handling state for visible/ bomb cells instead of maintaining state for the entire grid.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search