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. 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 first answer keeps the moving parts visible. The snake, food, direction, and game-over flag are all state values. React rerenders the board when one of them changes.
The boardRef is attached to the board later, but the file doesn't use it
for any behavior. You catch this kind of thing when reviewing generated
code: the app can work while still holding unused pieces.
Keyboard handling
The keyboard handler listens for arrow keys and blocks direct reversal. The guard prevents the snake from colliding with its own body 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's easy to follow 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 depends on snake, so React recreates the interval after each
movement. That holds up in this demo, and 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, then 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.
Normal movement drops the last segment:
const newSnake = [newHead, ...snake];
if (newHead.x === food.x && newHead.y === food.y) {
setFood(generateFood);
} else {
newSnake.pop();
}
setSnake(newSnake);
}
This code reads directly and stays easy to follow. All behavior sits 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 isn't 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);
}
This version has no score, which gives you a natural next prompt for a coding assistant. Ask it to add score, show it above the board, and reset it when the game restarts.
Board rendering
The board renders as 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. 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
doesn't use those classes.
Notice this, because chat apps often leave project cleanup to you. A coding assistant can help, since it can look through which files and classes are still referenced.