Part 1 continued: ChatGPT Snake code
The chat app gives you a component that you still need to place inside a project. The snake-chatgpt App.jsx file shows what the generated code looks like after you move it into Vite.
The whole game sits in src/App.jsx. That makes the first version easy to
paste and run, and it also gives you one file to review before asking for
the next change.
State and constants
The component starts with a few constants and four pieces of React state:
import React, { useEffect, useState, useRef } from 'react';
const BOARD_SIZE = 20;
const INITIAL_SNAKE = [{ x: 8, y: 8 }];
const INITIAL_DIRECTION = { x: 1, y: 0 };
const SPEED = 200;
export default function SnakeGame() {
const [snake, setSnake] = useState(INITIAL_SNAKE);
const [food, setFood] = useState(generateFood);
const [direction, setDirection] = useState(INITIAL_DIRECTION);
const [gameOver, setGameOver] = useState(false);
const boardRef = useRef(null);
This is a good first answer because the moving parts are visible. The snake, food, direction, and game-over flag are all state values, so React rerenders the board when one of them changes.
The boardRef is attached to the board later, but the file does not use it
for any behavior. That is a normal thing to catch when reviewing generated
code: the app can work while still carrying unused pieces.
Keyboard handling
The keyboard handler listens for arrow keys and blocks direct reversal. The guard prevents the snake from colliding with itself immediately after it grows.
useEffect(() => {
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowUp': if (direction.y === 0) setDirection({ x: 0, y: -1 }); break;
case 'ArrowDown': if (direction.y === 0) setDirection({ x: 0, y: 1 }); break;
case 'ArrowLeft': if (direction.x === 0) setDirection({ x: -1, y: 0 }); break;
case 'ArrowRight': if (direction.x === 0) setDirection({ x: 1, y: 0 }); break;
default: break;
}
};
The effect registers the listener and removes it when React reruns the effect or unmounts the component:
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [direction]);
The dependency on direction means the handler always sees the latest
direction. For this small game, that is easy to understand and good enough
for the comparison.
Movement and collisions
The game loop runs every SPEED milliseconds while the game is active:
useEffect(() => {
if (gameOver) return;
const interval = setInterval(moveSnake, SPEED);
return () => clearInterval(interval);
}, [snake, direction, gameOver]);
This effect is worth checking. It depends on snake, so React recreates the
interval after each movement. That is acceptable in this demo, but an IDE
assistant could later move the timing logic into a hook or use a functional
state update.
The movement function calculates a new head and ends the game if the head hits a wall or the existing snake:
function moveSnake() {
const newHead = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
if (
newHead.x < 0 || newHead.x >= BOARD_SIZE ||
newHead.y < 0 || newHead.y >= BOARD_SIZE ||
snake.some(segment => segment.x === newHead.x && segment.y === newHead.y)
) {
setGameOver(true);
return;
}
After that, the function either grows the snake by keeping the new tail or moves normally by dropping the last segment:
const newSnake = [newHead, ...snake];
if (newHead.x === food.x && newHead.y === food.y) {
setFood(generateFood);
} else {
newSnake.pop();
}
setSnake(newSnake);
}
The design is direct and readable. The limitation is that all behavior stays inside the component, which makes future tests harder unless you extract the game rules.
Food and restart
Food generation keeps picking random coordinates until it finds a cell that is not occupied by the snake:
function generateFood() {
let newFood;
do {
newFood = {
x: Math.floor(Math.random() * BOARD_SIZE),
y: Math.floor(Math.random() * BOARD_SIZE),
};
} while (snake.some(segment => segment.x === newFood.x && segment.y === newFood.y));
return newFood;
}
The restart function resets the original state:
function restartGame() {
setSnake(INITIAL_SNAKE);
setDirection(INITIAL_DIRECTION);
setFood(generateFood);
setGameOver(false);
}
There is no score in this version. That gives you a natural next prompt for a coding assistant: add score, show it above the board, and reset it when the game restarts.
Board rendering
The board is a CSS grid with 20 rows and 20 columns. Each cell decides whether it should look like snake, food, or empty space:
<div
ref={boardRef}
className="grid"
style={{
gridTemplateColumns: `repeat(${BOARD_SIZE}, 20px)`,
gridTemplateRows: `repeat(${BOARD_SIZE}, 20px)`,
}}
>
The render loop maps over all 400 cells:
{[...Array(BOARD_SIZE * BOARD_SIZE)].map((_, index) => {
const x = index % BOARD_SIZE;
const y = Math.floor(index / BOARD_SIZE);
const isSnake = snake.some(segment => segment.x === x && segment.y === y);
const isFood = food.x === x && food.y === y;
return (
<div
key={index}
className={`w-5 h-5 border border-gray-800 ${
isSnake ? 'bg-green-500' : isFood ? 'bg-red-500' : 'bg-gray-700'
}`}
></div>
);
})}
The game-over UI appears only after collision:
{gameOver && (
<div className="mt-4">
<p className="text-red-500">Game Over</p>
<button
onClick={restartGame}
className="mt-2 px-4 py-2 bg-blue-600 rounded hover:bg-blue-700"
>
Restart
</button>
</div>
)}
Run the app and test the basics before comparing tools further:
npm run dev
Use the arrow keys, hit a wall, restart, and eat food. That manual check is small, but it tells you whether the generated component is a usable baseline or only a code-shaped answer.
Starter leftovers
The snake-chatgpt/src/App.css file still contains default Vite starter
styles such as .logo, .card, and .read-the-docs. The generated game
does not use those classes.
That is worth noticing because chat apps often leave project cleanup to you. A coding assistant can help with that cleanup because it can look through which files and classes are still referenced.