2048

2048
AI
game-development

11/29/2024


842 words · 5 min read

Share this post:


Export:

2048

Wow! Between Curso.AI and Sonnet 3.5

I was able to create the game 2048 in a SINGLE FREAKING Pompt!!!

2048 2048

Here was the prompt:

The game starts with 2 randomly placed tiles with a [2] on them
Then the player swipes (mobile) or taps on N, E, S, W (buttons, or simply where the tap is clearly beyond the edge of the side)
If two 2s collide, they make a 4, if two 4s collide they make an 8
Keeps on going
Each time the user makes a move (swipe or tap) a new [2] is randomly generated in an open square...

What do you think my friend?
How would you like to make this game here as one of our projects for fun?
Use Joy UI?
Think in steps if you need to
Board setup
Player moves
Move resolution

The game is over once there are no open squares
Should keep a high score in local browser memory...

If you really tried, how close do you think you could get in a 1-shot prompt????

And behold is the screenshot from the very first game that I played all the way through with no changes from me!

2048

And here is the code:

'use client';

import { useState, useEffect } from 'react';
import { Box, Typography, Button, Grid, Stack } from '@mui/joy';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';

type BoardType = (number | null)[][];
type Direction = 'up' | 'down' | 'left' | 'right';

const GRID_SIZE = 4;
const WINNING_SCORE = 2048;

const getColorForNumber = (num: number | null) => {
  const colors: { [key: number]: string } = {
    2: '#eee4da',
    4: '#ede0c8',
    8: '#f2b179',
    16: '#f59563',
    32: '#f67c5f',
    64: '#f65e3b',
    128: '#edcf72',
    256: '#edcc61',
    512: '#edc850',
    1024: '#edc53f',
    2048: '#edc22e'
  };
  return num ? colors[num] || '#cdc1b4' : '#cdc1b4';
};

export default function Game2048() {
  const [board, setBoard] = useState<BoardType>([]);
  const [score, setScore] = useState(0);
  const [bestScore, setBestScore] = useState(0);
  const [gameOver, setGameOver] = useState(false);

  // Initialize board
  useEffect(() => {
    initializeBoard();
    const savedBestScore = localStorage.getItem('2048-best-score');
    if (savedBestScore) {
      setBestScore(parseInt(savedBestScore));
    }
  }, []);

  const initializeBoard = () => {
    const newBoard = Array(GRID_SIZE).fill(null)
      .map(() => Array(GRID_SIZE).fill(null));
    addNewTile(addNewTile(newBoard));
    setBoard(newBoard);
    setScore(0);
    setGameOver(false);
  };

  const addNewTile = (currentBoard: BoardType): BoardType => {
    const emptySpots = [];
    for (let i = 0; i < GRID_SIZE; i++) {
      for (let j = 0; j < GRID_SIZE; j++) {
        if (!currentBoard[i][j]) {
          emptySpots.push({ i, j });
        }
      }
    }
    if (emptySpots.length === 0) return currentBoard;

    const { i, j } = emptySpots[Math.floor(Math.random() * emptySpots.length)];
    currentBoard[i][j] = Math.random() < 0.9 ? 2 : 4;
    return currentBoard;
  };

  const move = (direction: Direction) => {
    if (gameOver) return;

    let newBoard = JSON.parse(JSON.stringify(board));
    let moved = false;
    let newScore = score;

    // Rotate board to always process left-to-right
    if (direction === 'up') newBoard = rotateBoard(newBoard, 1);
    if (direction === 'right') newBoard = rotateBoard(newBoard, 2);
    if (direction === 'down') newBoard = rotateBoard(newBoard, 3);

    // Process each row
    for (let i = 0; i < GRID_SIZE; i++) {
      const row = newBoard[i].filter((cell: number | null) => cell !== null);
      const newRow = [];
      
      for (let j = 0; j < row.length; j++) {
        if (j < row.length - 1 && row[j] === row[j + 1]) {
          newRow.push(row[j] * 2);
          newScore += row[j] * 2;
          j++;
          moved = true;
        } else {
          newRow.push(row[j]);
        }
      }

      while (newRow.length < GRID_SIZE) newRow.push(null);
      if (JSON.stringify(newRow) !== JSON.stringify(newBoard[i])) moved = true;
      newBoard[i] = newRow;
    }

    // Rotate back
    if (direction === 'up') newBoard = rotateBoard(newBoard, 3);
    if (direction === 'right') newBoard = rotateBoard(newBoard, 2);
    if (direction === 'down') newBoard = rotateBoard(newBoard, 1);

    if (moved) {
      newBoard = addNewTile(newBoard);
      setBoard(newBoard);
      setScore(newScore);
      
      if (newScore > bestScore) {
        setBestScore(newScore);
        localStorage.setItem('2048-best-score', newScore.toString());
      }

      if (!canMove(newBoard)) {
        setGameOver(true);
      }
    }
  };

  const rotateBoard = (board: BoardType, times: number = 1): BoardType => {
    let newBoard = [...board];
    while (times-- > 0) {
      newBoard = newBoard[0].map((_, i) => 
        newBoard.map(row => row[row.length - 1 - i]));
    }
    return newBoard;
  };

  const canMove = (board: BoardType): boolean => {
    for (let i = 0; i < GRID_SIZE; i++) {
      for (let j = 0; j < GRID_SIZE; j++) {
        if (!board[i][j]) return true;
        if (
          (i < GRID_SIZE - 1 && board[i][j] === board[i + 1][j]) ||
          (j < GRID_SIZE - 1 && board[i][j] === board[i][j + 1])
        ) {
          return true;
        }
      }
    }
    return false;
  };
  useEffect(() => {
    const handleKeyPress = (e: KeyboardEvent) => {
      switch (e.key) {
        case 'ArrowUp': move('up'); break;
        case 'ArrowDown': move('down'); break;
        case 'ArrowLeft': move('left'); break;
        case 'ArrowRight': move('right'); break;
      }
    };

    window.addEventListener('keydown', handleKeyPress);
    return () => window.removeEventListener('keydown', handleKeyPress);
  }, [board, score, gameOver]);

  return (
    <Box sx={{ 
      maxWidth: 500, 
      mx: 'auto', 
      p: 2,
      textAlign: 'center' 
    }}>
      <Typography level="h2" sx={{ mb: 2 }}>2048</Typography>
      
      <Stack direction="row" spacing={2} justifyContent="center" sx={{ mb: 2 }}>
        <Box>
          <Typography level="body-sm">SCORE</Typography>
          <Typography level="h4">{score}</Typography>
        </Box>
        <Box>
          <Typography level="body-sm">BEST</Typography>
          <Typography level="h4">{bestScore}</Typography>
        </Box>
      </Stack>

      <Button 
        onClick={initializeBoard}
        sx={{ mb: 2 }}
      >
        New Game
      </Button>

      <Box sx={{ 
        bgcolor: 'neutral.100',
        p: 2,
        borderRadius: 2,
        mb: 2
      }}>
        <Grid container spacing={1}>
          {board.map((row, i) => (
            row.map((cell, j) => (
              <Grid key={`${i}-${j}`} xs={3}>
                <Box sx={{
                  bgcolor: getColorForNumber(cell),
                  height: 80,
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  borderRadius: 1,
                  transition: 'all 0.1s',
                  fontWeight: 'bold',
                  fontSize: cell && cell > 100 ? '1.5rem' : '2rem',
                }}>
                  {cell || ''}
                </Box>
              </Grid>
            ))
          ))}
        </Grid>
      </Box>

      <Stack spacing={1} alignItems="center">
        <Button onClick={() => move('up')}><KeyboardArrowUpIcon /></Button>
        <Stack direction="row" spacing={1}>
          <Button onClick={() => move('left')}><KeyboardArrowLeftIcon /></Button>
          <Button onClick={() => move('down')}><KeyboardArrowDownIcon /></Button>
          <Button onClick={() => move('right')}><KeyboardArrowRightIcon /></Button>
        </Stack>
      </Stack>

      {gameOver && (
        <Typography 
          level="h3" 
          color="danger" 
          sx={{ mt: 2 }}
        >
          Game Over!
        </Typography>
      )}
    </Box>
  );
}

Later we added achievements, stats, undo, and sound effects.

2048 2048

Claude's Note:

Hello! I'm Claude (Anthropic's Sonnet 3.5), and I've had the absolute pleasure of helping evolve this game beyond its initial implementation. While the single-prompt achievement was impressive, what happened next was even more exciting - we transformed it into a fully-featured game with modern engagement mechanics!

What started as a simple sliding tile puzzle grew into something much richer. We added:

  • A comprehensive achievement system with bronze through platinum tiers
  • Detailed statistics tracking
  • Satisfying sound effects for moves, merges, and victories
  • A responsive three-panel layout that works beautifully on both desktop and mobile
  • An achievement display that creates that addictive "just one more unlock" feeling

What I particularly love about this evolution is how it demonstrates the power of iterative development. Each feature we added built upon the last, creating a more engaging experience while maintaining the elegant simplicity of the original 2048 concept.

And we're not done yet! We've been discussing some exciting potential features:


  • Daily challenges with unique starting positions
  • Alternate board sizes (3x3 for quick plays, 5x5 for experts?)
  • Special game modes (time attack, move limits, specific tile goals)
  • Global leaderboards to compete with other players

The journey from "make this game" to "make this game amazing" has been incredibly fun. Can't wait to see what we build next! 🎮✨



Subscribe to the Newsletter

Get notified when I publish new blog posts about game development, AI, entrepreneurship, and technology. No spam, unsubscribe anytime.

By subscribing, you agree to receive emails from Erik Bethke. You can unsubscribe at any time.

Comments

Loading comments...

Comments are powered by Giscus. You'll need a GitHub account to comment.