<div id="app"></div>
body {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
  background-color: #0093E9;
  background-image: linear-gradient(160deg, #0093E9 0%, #80D0C7 100%);
}

#app {
  font-family: Arial, sans-serif;
}
.game-title {
  color: #D9AFD9;
}
.restart-button {
    background-image: linear-gradient(to right, #314755 0%, #26a0da 51%, #314755 100%);
    background-color: transparent;
    color: #fff;
    cursor: pointer;
    outline: none;
    border: none;
    box-shadow: 0 0 20px #eee;
    border-radius: 10px;
    display: block;
    margin-bottom: 10px;
    padding: 5px 20px;
    text-transform: uppercase;
    -webkit-appearance: button;
}
/*
* https://frontendeval.com/questions/snake
*
* Create an HTML/CSS/JS version of Snake
*/
const { useState, useEffect, useRef } = React;
const ROWS = 15;
const COLS = 15;
const Direction = {
  UP: 'UP',
  DOWN: 'DOWN',
  LEFT: 'LEFT',
  RIGHT: 'RIGHT'
};
const getRandomPosition = () => {
  return {
    x: Math.floor(Math.random() * COLS),
    y: Math.floor(Math.random() * ROWS)
  }
};

const initialSnake = [
  { x : 7, y : 7 },
  { x : 7, y : 8 },
  { x : 7, y : 9 },
];

const SnakeGame = () => {
  const [snake, setSnake] = useState(initialSnake);
  const [apple, setApple] = useState(getRandomPosition());
  const [direction, setDirection] = useState(Direction.RIGHT);
  const [gameStarted, setGameStarted] = useState(false);
  const [gameOver, setGameOver] = useState(false);
  const [score, setScore] = useState(0);
  const gameRef = useRef(null);
  
  const restartGame = () => {
    setSnake(initialSnake);
    setDirection(Direction.RIGHT);
    setGameOver(false);
    setScore(0);
    setApple(getRandomPosition());
  }
  
  useEffect(() => {
    const handleKeyPress = (event) => {
      if (!gameStarted) setGameStarted(true);
      switch (event.key) {
        case 'ArrowUp':
          if (direction !== Direction.DOWN) setDirection(Direction.UP);
          break;
        case 'ArrowDown':
          if (direction !== Direction.UP) setDirection(Direction.DOWN);
          break;
        case 'ArrowLeft':
          if (direction !== Direction.RIGHT) setDirection(Direction.LEFT);
          break;
        case 'ArrowRight':
          if (direction !== Direction.LEFT) setDirection(Direction.RIGHT);
          break;
        default:
          break;
      }
    };

    window.addEventListener('keydown', handleKeyPress);

    return () => window.removeEventListener('keydown', handleKeyPress);
  }, [direction]);
  useEffect(() => {
    const moveSnake = () => {
      if (gameOver) return;
      if (!gameStarted) return;

      const newSnake = [...snake];
      const head = { ...newSnake[0] };

      switch (direction) {
        case Direction.UP:
          head.y -= 1;
          break;
        case Direction.DOWN:
          head.y += 1;
          break;
        case Direction.LEFT:
          head.x -= 1;
          break;
        case Direction.RIGHT:
          head.x += 1;
          break;
        default:
          break;
      }

      newSnake.unshift(head);

      if (head.x === apple.x && head.y === apple.y) {
        setApple(getRandomPosition());
        setScore(score + 1);
      } else {
        newSnake.pop();
      }
      if (
        head.x < 0 ||
        head.x >= COLS ||
        head.y < 0 ||
        head.y >= ROWS ||
        newSnake.slice(1).some((segment) => segment.x === head.x && segment.y === head.y)
      ) {
        setGameOver(true);
      }

      setSnake(newSnake);
    };

    const gameInterval = setInterval(moveSnake, 150);
    gameRef.current = gameInterval;

    return () => clearInterval(gameInterval);
  }, [snake, direction, apple, gameOver, score, gameStarted]);

  return (
    <div>
      <h1 className="game-title">Snake Game</h1>
      <button className="restart-button" onClick={restartGame}>Restart</button>
      <div>
        {gameOver ? (
          <div>Game Over! Your Score: {score}</div>
        ) : (
          <div>Score: {score}</div>
        )}
        <div style={{ display: 'grid', gridTemplateColumns: `repeat(${COLS}, 20px)` }}>
          {[...Array(ROWS)].map((_, rowIndex) =>
            [...Array(COLS)].map((_, colIndex) => (
              <div
                key={`${rowIndex}-${colIndex}`}
                style={{
                  width: '20px',
                  height: '20px',
                  border: '1px solid #ccc',
                  backgroundColor: snake.some((seg) => seg.x === colIndex && seg.y === rowIndex)
                    ? '#333'
                    : apple.x === colIndex && apple.y === rowIndex
                    ? 'red'
                    : 'transparent',
                }}
              ></div>
            ))
          )}
        </div>
      </div>
    </div>
  );
}


ReactDOM.render(<SnakeGame />, document.getElementById('app'));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js