admin 管理员组

文章数量: 1086019

I'm attempting to build a board game similar to Rubik's Race. The board gets randomly generated each time the game loads. This is what I have so far:

interface GameBoardProps {
  initialConfiguration: GamePiece[];
}

function GameBoard({ initialConfiguration }: GameBoardProps) {
  console.log("Drawing board");

  const [gamePieces, setGamePieces] = useState(initialConfiguration);

  for (let gamePiece of gamePieces) {
    console.log(gamePiece.color);
  }
  const gamePieceSize = gamePieces[0].size;

  const isEmptySpace = (index: number): boolean => {
    return gamePieces[index].color === "black";
  };

  const isAtLeftEdge = (index: number): boolean => {
    return index % 5 === 0;
  };

  const isAtRightEdge = (index: number): boolean => {
    return index % 5 === 4;
  };

  const isAtTopEdge = (index: number): boolean => {
    return index < 5;
  };

  const isAtBottomEdge = (index: number): boolean => {
    return index >= 20;
  };

  const shiftLeft = (leftIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[leftIndex];
      for (; leftIndex < index; leftIndex++)
        gamePiecesCopy[leftIndex] = gamePiecesCopy[leftIndex + 1];

      gamePiecesCopy[leftIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const shiftRight = (rightIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[rightIndex];
      for (; rightIndex > index; rightIndex--)
        gamePiecesCopy[rightIndex] = gamePiecesCopy[rightIndex - 1];

      gamePiecesCopy[rightIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const shiftUp = (topIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[topIndex];
      for (; topIndex < index; topIndex += 5)
        gamePiecesCopy[topIndex] = gamePiecesCopy[topIndex + 5];

      gamePiecesCopy[topIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const shiftDown = (bottomIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[bottomIndex];
      for (; bottomIndex > index; bottomIndex -= 5)
        gamePiecesCopy[bottomIndex] = gamePiecesCopy[bottomIndex - 5];

      gamePiecesCopy[bottomIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const handleClick = (index: number) => {
    let left = index,
      right = index,
      top = index,
      bottom = index;

    let shifted = false;
    console.log("Clicked");
    while (!(shifted || isAtLeftEdge(left))) {
      console.log("Checking left side");

      left -= 1;
      if (isEmptySpace(left)) {
        shiftLeft(left, index);
        shifted = true;
      }
    }

    while (!(shifted || isAtRightEdge(right))) {
      console.log("Checking right side");

      right += 1;
      if (isEmptySpace(right)) {
        shiftRight(right, index);
        shifted = true;
      }
    }

    while (!(shifted || isAtTopEdge(top))) {
      console.log("Checking top side");

      top -= 5;
      if (isEmptySpace(top)) {
        shiftUp(top, index);
        shifted = true;
      }
    }

    while (!(shifted || isAtBottomEdge(bottom))) {
      console.log("Checking bottom side");

      bottom += 5;
      if (isEmptySpace(bottom)) {
        shiftDown(bottom, index);
        shifted = true;
      }
    }
  };

  return (
    <div
      className="game-board"
      style={{
        gridTemplate: `repeat(5, ${gamePieceSize}) / repeat(5, ${gamePieceSize})`,
      }}
    >
      {gamePieces.map((piece, index) => (
        <Square
          key={piece.id}
          size={piece.size}
          color={piece.color}
          onClick={() => handleClick(index)}
        />
      ))}
    </div>
  );
}

GameBoard is initialized from the App component like so:

const randomizePieces = (gamePieceSize: string) => {
  console.log("randomizing board");

  const gameBoard: GamePiece[] = [
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "blue" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "white" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "green" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "orange" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "red" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "yellow" })),
    { id: 0, size: gamePieceSize, color: "black" },
  ];

  for (let i = 0; i < 15; i++) {
    const randA = Math.floor(Math.random() * gameBoard.length),
      randB = Math.floor(Math.random() * gameBoard.length);

    const temp = gameBoard[randA];
    gameBoard[randA] = gameBoard[randB];
    gameBoard[randB] = temp;
  }

  for (let i = 0; i < gameBoard.length; i++) gameBoard[i].id = i;

  return gameBoard;
};

const gamePieces = randomizePieces("50px");

function App() {
  return <GameBoard initialConfiguration={gamePieces} />;
}

The unexpected behavior I'm noticing is this: Initially after loading the game board, game pieces are able to shift around the first time a valid board piece is clicked, but they do not shift around on consecutive clicks. I notice on consecutive clicks that the logs are being printed, indicating that the appropriate functions are being called, but nothing is getting redrawn. This is only happening in StrictMode though; when I disable StrictMode the board behaves as expected (shifting valid pieces into the "empty" space). Anyone know what's happening and how I can change the code to work in development, using StrictMode?

I'm attempting to build a board game similar to Rubik's Race. The board gets randomly generated each time the game loads. This is what I have so far:

interface GameBoardProps {
  initialConfiguration: GamePiece[];
}

function GameBoard({ initialConfiguration }: GameBoardProps) {
  console.log("Drawing board");

  const [gamePieces, setGamePieces] = useState(initialConfiguration);

  for (let gamePiece of gamePieces) {
    console.log(gamePiece.color);
  }
  const gamePieceSize = gamePieces[0].size;

  const isEmptySpace = (index: number): boolean => {
    return gamePieces[index].color === "black";
  };

  const isAtLeftEdge = (index: number): boolean => {
    return index % 5 === 0;
  };

  const isAtRightEdge = (index: number): boolean => {
    return index % 5 === 4;
  };

  const isAtTopEdge = (index: number): boolean => {
    return index < 5;
  };

  const isAtBottomEdge = (index: number): boolean => {
    return index >= 20;
  };

  const shiftLeft = (leftIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[leftIndex];
      for (; leftIndex < index; leftIndex++)
        gamePiecesCopy[leftIndex] = gamePiecesCopy[leftIndex + 1];

      gamePiecesCopy[leftIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const shiftRight = (rightIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[rightIndex];
      for (; rightIndex > index; rightIndex--)
        gamePiecesCopy[rightIndex] = gamePiecesCopy[rightIndex - 1];

      gamePiecesCopy[rightIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const shiftUp = (topIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[topIndex];
      for (; topIndex < index; topIndex += 5)
        gamePiecesCopy[topIndex] = gamePiecesCopy[topIndex + 5];

      gamePiecesCopy[topIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const shiftDown = (bottomIndex: number, index: number) => {
    setGamePieces((gamePieces) => {
      const gamePiecesCopy = [...gamePieces];

      var tempNullPiece = gamePiecesCopy[bottomIndex];
      for (; bottomIndex > index; bottomIndex -= 5)
        gamePiecesCopy[bottomIndex] = gamePiecesCopy[bottomIndex - 5];

      gamePiecesCopy[bottomIndex] = tempNullPiece;

      return gamePiecesCopy;
    });
  };

  const handleClick = (index: number) => {
    let left = index,
      right = index,
      top = index,
      bottom = index;

    let shifted = false;
    console.log("Clicked");
    while (!(shifted || isAtLeftEdge(left))) {
      console.log("Checking left side");

      left -= 1;
      if (isEmptySpace(left)) {
        shiftLeft(left, index);
        shifted = true;
      }
    }

    while (!(shifted || isAtRightEdge(right))) {
      console.log("Checking right side");

      right += 1;
      if (isEmptySpace(right)) {
        shiftRight(right, index);
        shifted = true;
      }
    }

    while (!(shifted || isAtTopEdge(top))) {
      console.log("Checking top side");

      top -= 5;
      if (isEmptySpace(top)) {
        shiftUp(top, index);
        shifted = true;
      }
    }

    while (!(shifted || isAtBottomEdge(bottom))) {
      console.log("Checking bottom side");

      bottom += 5;
      if (isEmptySpace(bottom)) {
        shiftDown(bottom, index);
        shifted = true;
      }
    }
  };

  return (
    <div
      className="game-board"
      style={{
        gridTemplate: `repeat(5, ${gamePieceSize}) / repeat(5, ${gamePieceSize})`,
      }}
    >
      {gamePieces.map((piece, index) => (
        <Square
          key={piece.id}
          size={piece.size}
          color={piece.color}
          onClick={() => handleClick(index)}
        />
      ))}
    </div>
  );
}

GameBoard is initialized from the App component like so:

const randomizePieces = (gamePieceSize: string) => {
  console.log("randomizing board");

  const gameBoard: GamePiece[] = [
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "blue" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "white" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "green" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "orange" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "red" })),
    ...Array(4)
      .fill(null)
      .map(() => ({ id: 0, size: gamePieceSize, color: "yellow" })),
    { id: 0, size: gamePieceSize, color: "black" },
  ];

  for (let i = 0; i < 15; i++) {
    const randA = Math.floor(Math.random() * gameBoard.length),
      randB = Math.floor(Math.random() * gameBoard.length);

    const temp = gameBoard[randA];
    gameBoard[randA] = gameBoard[randB];
    gameBoard[randB] = temp;
  }

  for (let i = 0; i < gameBoard.length; i++) gameBoard[i].id = i;

  return gameBoard;
};

const gamePieces = randomizePieces("50px");

function App() {
  return <GameBoard initialConfiguration={gamePieces} />;
}

The unexpected behavior I'm noticing is this: Initially after loading the game board, game pieces are able to shift around the first time a valid board piece is clicked, but they do not shift around on consecutive clicks. I notice on consecutive clicks that the logs are being printed, indicating that the appropriate functions are being called, but nothing is getting redrawn. This is only happening in StrictMode though; when I disable StrictMode the board behaves as expected (shifting valid pieces into the "empty" space). Anyone know what's happening and how I can change the code to work in development, using StrictMode?

Share Improve this question asked Mar 27 at 18:31 Oni BarolliOni Barolli 931 gold badge1 silver badge9 bronze badges 1
  • Step 1: ignore React, do all of it in plain JS and then only as almost an after thought, turn the resulting JS data structure into a bunch of JSX elements. Because this is one of those things where it's completely irrelevant what framework you're using, you first generate your data, then in as little code as possible you map that data to UI elements. Anything done "inside" a React component that isn't about recording state updates and turning finalized data into UI elements is code that doesn't belong there. – Mike 'Pomax' Kamermans Commented Mar 27 at 18:48
Add a comment  | 

1 Answer 1

Reset to default 0

StrictMode calls your state setters twice (but only keeps the result of the second call). Because of this your setters have to be idempotent, but yours have a tricky side-effect!

In your shift handlers, e.g., shiftUp, your setter, setGamePieces(...), is capturing the arguments passed, topIndex and index, then inside the setter, you are modifying topIndex in the for loop. So here's what happens:

  1. StrictMode calls setGamePieces() the first time with topIndex = 12 and index = 22 (for example)
  2. In the setter, your for loop updates topIndex to 22 while it shifts the pieces correctly
  3. Your setter returns the correct updated state
  4. StrictMode discards this result, and calls the setter a second time
  5. This time, topIndex is already 22, so it doesn't make any changes because it is already >= index, and your setter returns the state effectively unchanged.
  6. StrictMode accepts this result and re-renders

To fix it, use a new indexing variable instead of modifying the topIndex directly, e.g.,

var i = topIndex;
var tempNullPiece = gamePiecesCopy[i];
for (; i < index; i += 5)
  gamePiecesCopy[topIndex] = gamePiecesCopy[i + 5];

gamePiecesCopy[i] = tempNullPiece;

本文标签: reactjsHow to create a randomly generated grid of colored squares in ReactStack Overflow