windsurf 에디터를 이용해 만든 테트리스가 추가 되었습니다. > AI

AI

windsurf 에디터를 이용해 만든 테트리스가 추가 되었습니다. 정보

windsurf 에디터를 이용해 만든 테트리스가 추가 되었습니다.

본문

https://sir.kr/javascript/tetris/

 

총 이틀에 걸쳐서 만들었고

 

집중한 시간은 한 2~3시간 되는것 같습니다.

 

생산속도가 엄청 빨라 지겠네요.

 

g 키를 누르면 고스트 블록을 켜고, 끄고 할수 있습니다.

 

(현재 떨어지고 있는 블록이 땅에 닿았을 때 어디에 위치하게 될지 미리 보여주는 기능)

 

 

src/app/tetris/page.js

 


'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import styled from '@emotion/styled';
 
// Tetromino shapes
const TETROMINOES = {
  I: [[1], [1], [1], [1]],
  O: [[1, 1], [1, 1]],
  T: [[0, 1, 0], [1, 1, 1]],
  S: [[0, 1, 1], [1, 1, 0]],
  Z: [[1, 1, 0], [0, 1, 1]],
  J: [[1, 0], [1, 0], [1, 1]],
  L: [[0, 1], [0, 1], [1, 1]]
};
 
const COLORS = {
  I: '#00f0f0',
  O: '#f0f000',
  T: '#a000f0',
  S: '#00f000',
  Z: '#f00000',
  J: '#0000f0',
  L: '#f0a000'
};
 
// Board size
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
 
// Styled components
const Container = styled.div`
  position: fixed;
  top: 120px;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  background: #111;
  color: white;
  font-family: Arial, sans-serif;
  overflow: hidden;
`;
 
const GameWrapper = styled.div`
  display: flex;
  gap: 20px;
  align-items: flex-start;
  justify-content: center;
  width: 100%;
  max-width: 800px;
  margin-top: 40px;
 
  @media (max-width: 768px) {
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 0 10px;
  }
`;
 
const GameInfo = styled.div`
  display: flex;
  flex-direction: column;
  gap: 10px;
 
  h1 {
    font-size: 24px;
    margin: 0;
  }
 
  div {
    min-height: 20px;
    font-size: 16px;
  }
 
  .status-info {
    min-width: 150px;
    display: flex;
    justify-content: space-between;
  }
 
  .next-piece {
    margin-top: 20px;
 
    h2 {
      font-size: 18px;
      margin: 0 0 10px 0;
    }
 
    .preview {
      background: #000;
      padding: 5px;
      border-radius: 4px;
      display: grid;
      grid-template-columns: repeat(4, 20px);
      grid-template-rows: repeat(4, 20px);
      gap: 1px;
    }
  }
 
  @media (max-width: 768px) {
    width: min(90vw, 300px);
    padding: 10px;
    flex-direction: row;
    justify-content: space-between;
    align-items: flex-start;
  }
`;
 
const GameBoard = styled.div`
  display: grid;
  grid-template-rows: repeat(${BOARD_HEIGHT}, 1fr);
  grid-template-columns: repeat(${BOARD_WIDTH}, 1fr);
  gap: 1px;
  background: #222;
  padding: 10px;
  border-radius: 5px;
  width: min(85vw, 240px);
  height: min(60vh, 480px);
  box-sizing: border-box;
`;
 
const Cell = styled.div`
  background: ${props => props.color || '#000'};
  border: 1px solid #333;
  aspect-ratio: 1;
  transition: all 0.2s ease;
  ${props => props.isClearing && `
    animation: clearAnimation 0.5s;
  `}
  ${props => props.ghost && `
    background: ${props.color}22;
    border: 2px solid ${props.color}44;
  `}
 
  @keyframes clearAnimation {
    0% {
      transform: scale(1);
      opacity: 1;
    }
    50% {
      transform: scale(1.1);
      opacity: 0.5;
    }
    100% {
      transform: scale(0);
      opacity: 0;
    }
  }
`;
 
const GhostBlock = styled.div`
  position: absolute;
  width: 20px;
  height: 20px;
  background: ${props => props.color}22;
  border: 2px solid ${props => props.color}44;
`;
 
const Controls = styled.div`
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-top: 20px;
  width: min(90vw, 250px);
 
  @media (min-width: 768px) {
    display: none;
  }
`;
 
const Button = styled.button`
  padding: 10px;
  font-size: 16px;
  border: none;
  border-radius: 5px;
  background: #444;
  color: white;
  cursor: pointer;
  &:hover {
    background: #555;
  }
`;
 
export default function TetrisGame() {
  const [board, setBoard] = useState(createEmptyBoard());
  const [piece, setPiece] = useState(null);
  const [nextPiece, setNextPiece] = useState(null);
  const [score, setScore] = useState(0);
  const [gameOver, setGameOver] = useState(false);
  const [gameStarted, setGameStarted] = useState(false);
  const [clearingRows, setClearingRows] = useState([]);
  const [speed, setSpeed] = useState(120); // 120% is base speed
  const baseInterval = 1000; // 1초를 기본 속도로 설정
  const [showGhost, setShowGhost] = useState(true); // 고스트 블록 표시 여부
 
  // Create empty board
  function createEmptyBoard() {
    return Array(BOARD_HEIGHT).fill().map(() => Array(BOARD_WIDTH).fill(null));
  }
 
  // Create new piece
  function createNewPiece() {
    const shapes = Object.keys(TETROMINOES);
    const shape = shapes[Math.floor(Math.random() * shapes.length)];
    return {
      shape: TETROMINOES[shape],
      color: COLORS[shape],
      x: Math.floor(BOARD_WIDTH / 2) - Math.floor(TETROMINOES[shape][0].length / 2),
      y: 0
    };
  }
 
  // Check collision
  function hasCollision(piece, board, dx = 0, dy = 0) {
    return piece.shape.some((row, y) =>
      row.some((cell, x) => {
        if (!cell) return false;
        const newX = piece.x + x + dx;
        const newY = piece.y + y + dy;
        return (
          newX < 0 ||
          newX >= BOARD_WIDTH ||
          newY >= BOARD_HEIGHT ||
          (newY >= 0 && board[newY][newX] !== null)
        );
      })
    );
  }
 
  // Merge piece with board
  function mergePiece(piece, boardState) {
    const newBoard = boardState.map(row => [...row]);
    piece.shape.forEach((row, y) => {
      row.forEach((cell, x) => {
        if (cell && piece.y + y >= 0) {
          newBoard[piece.y + y][piece.x + x] = piece.color;
        }
      });
    });
    return newBoard;
  }
 
  // Clear completed rows
  function clearRows(boardState) {
    let clearedRows = [];
    const newBoard = boardState.map((row, index) => {
      if (row.every(cell => cell !== null)) {
        clearedRows.push(index);
        return null;
      }
      return [...row];
    });
 
    if (clearedRows.length > 0) {
      setClearingRows(clearedRows);
      setSpeed(prevSpeed => Math.min(200, prevSpeed + (clearedRows.length * 2))); // 2%씩 속도 증가
 
      setTimeout(() => {
        setClearingRows([]);
        const finalBoard = newBoard
          .filter(row => row !== null)
          .map(row => [...row]);
 
        while (finalBoard.length < BOARD_HEIGHT) {
          finalBoard.unshift(Array(BOARD_WIDTH).fill(null));
        }
 
        setBoard(finalBoard);
        setScore(prev => prev + clearedRows.length * 100);
      }, 500);
 
      return boardState;
    }
 
    return newBoard.filter(row => row !== null);
  }
 
  // Hard drop
  const hardDrop = useCallback(() => {
    if (!piece || gameOver) return;
 
    let dropDistance = 0;
    while (!hasCollision(piece, board, 0, dropDistance + 1)) {
      dropDistance++;
    }
 
    if (dropDistance > 0) {
      setPiece(prev => ({
        ...prev,
        y: prev.y + dropDistance
      }));
    }
  }, [piece, board, gameOver, hasCollision]);
 
  // Move piece
  const movePiece = useCallback((dx, dy) => {
    if (!piece || gameOver) return;
 
    if (!hasCollision(piece, board, dx, dy)) {
      setPiece(prev => ({
        ...prev,
        x: prev.x + dx,
        y: prev.y + dy
      }));
    } else if (dy > 0) {
      // Piece has landed
      const newBoard = mergePiece(piece, board);
      const clearedBoard = clearRows(newBoard);
      setBoard(clearedBoard);
 
      // Use next piece and create new next piece
      setPiece(nextPiece);
      setNextPiece(createNewPiece());
     
      if (hasCollision(nextPiece, clearedBoard)) {
        setGameOver(true);
      }
    }
  }, [piece, nextPiece, board, gameOver, hasCollision, mergePiece, clearRows]);
 
  // Rotate piece
  const rotatePiece = useCallback(() => {
    if (!piece || gameOver) return;
 
    const rotated = piece.shape[0].map((_, i) =>
      piece.shape.map(row => row[i]).reverse()
    );
 
    const newPiece = {
      ...piece,
      shape: rotated
    };
 
    if (!hasCollision(newPiece, board)) {
      setPiece(newPiece);
    }
  }, [piece, board, gameOver, hasCollision]);
 
  // Get ghost piece position
  const getGhostPosition = useCallback((currentPiece) => {
    if (!currentPiece) return null;
   
    let ghostY = currentPiece.y;
    while (!hasCollision(
      { ...currentPiece, y: ghostY + 1 },
      board
    )) {
      ghostY++;
    }
   
    return {
      ...currentPiece,
      y: ghostY
    };
  }, [board]);
 
  // Handle keyboard controls
  useEffect(() => {
    const handleKeyPress = (e) => {
      if (!gameStarted || gameOver) return;
 
      switch (e.code) {
        case 'ArrowLeft':
          movePiece(-1, 0);
          break;
        case 'ArrowRight':
          movePiece(1, 0);
          break;
        case 'ArrowDown':
          movePiece(0, 1);
          break;
        case 'ArrowUp':
          rotatePiece();
          break;
        case 'Space':
          hardDrop();
          break;
        case 'KeyG': // G 키로 고스트 블록 토글
          setShowGhost(prev => !prev);
          break;
      }
    };
 
    window.addEventListener('keydown', handleKeyPress);
    return () => window.removeEventListener('keydown', handleKeyPress);
  }, [gameStarted, gameOver, movePiece, rotatePiece, hardDrop, setShowGhost]);
 
  // Initialize game
  useEffect(() => {
    if (!piece && !gameOver) {
      const newPiece = createNewPiece();
      const newNextPiece = createNewPiece();
      setPiece(newPiece);
      setNextPiece(newNextPiece);
    }
  }, [piece, gameOver]);
 
  // Game loop
  useEffect(() => {
    let dropTimer;
 
    if (gameStarted && !gameOver) {
      if (!piece) {
        const newPiece = createNewPiece();
        const newNextPiece = createNewPiece();
        setPiece(newPiece);
        setNextPiece(newNextPiece);
      }
 
      dropTimer = setInterval(() => {
        movePiece(0, 1);
      }, baseInterval * (100 / speed)); // 속도에 따라 인터벌 조정
    }
 
    return () => clearInterval(dropTimer);
  }, [gameStarted, gameOver, piece, movePiece, speed]);
 
  // Render next piece preview
  const renderNextPiece = () => {
    if (!nextPiece) return null;
 
    const grid = Array(4).fill().map(() => Array(4).fill(null));
   
    // Center the piece in the preview grid
    const offsetY = Math.floor((4 - nextPiece.shape.length) / 2);
    const offsetX = Math.floor((4 - nextPiece.shape[0].length) / 2);
   
    nextPiece.shape.forEach((row, y) => {
      row.forEach((cell, x) => {
        if (cell) {
          grid[y + offsetY][x + offsetX] = nextPiece.color;
        }
      });
    });
 
    return grid.map((row, y) =>
      row.map((cell, x) => (
        <Cell
          key={`next-${y}-${x}`}
          color={cell || '#333'}
        />
      ))
    );
  };
 
  // Render game board with current piece
  const renderBoard = () => {
    const displayBoard = board.map(row => [...row]);
 
    if (piece) {
      // Add ghost piece first
      const ghostPiece = getGhostPosition(piece);
      if (ghostPiece && showGhost) {
        ghostPiece.shape.forEach((row, dy) => {
          row.forEach((cell, dx) => {
            if (cell === 1) {
              const y = ghostPiece.y + dy;
              const x = ghostPiece.x + dx;
              if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) {
                displayBoard[y][x] = { color: piece.color, ghost: true };
              }
            }
          });
        });
      }
 
      // Add current piece on top
      piece.shape.forEach((row, dy) => {
        row.forEach((cell, dx) => {
          if (cell === 1) {
            const y = piece.y + dy;
            const x = piece.x + dx;
            if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) {
              displayBoard[y][x] = { color: piece.color, ghost: false };
            }
          }
        });
      });
    }
 
    return displayBoard.map((row, y) =>
      row.map((cell, x) => (
        <Cell
          key={`${y}-${x}`}
          color={typeof cell === 'string' ? cell : cell?.color}
          ghost={cell?.ghost}
          isClearing={clearingRows.includes(y)}
        />
      ))
    );
  };
 
  // Start new game
  const startGame = () => {
    setBoard(createEmptyBoard());
    setPiece(null);
    setNextPiece(null);
    setScore(0);
    setGameOver(false);
    setGameStarted(true);
    setSpeed(120); // 시작 속도를 120%로 설정
  };
 
  return (
    <Container>
      <GameWrapper>
        <GameInfo>
          <h1>Tetris</h1>
          <div className="status-info">
            <span>Score:</span>
            <span>{score}</span>
          </div>
          <div className="status-info">
            <span>Speed:</span>
            <span>{speed}%</span>
          </div>
          <div className="status-info">
            <span>Ghost:</span>
            <span>{showGhost ? 'ON' : 'OFF'}</span>
          </div>
          <div className="next-piece">
            <h2>Next Block</h2>
            <div className="preview">
              {renderNextPiece()}
            </div>
          </div>
        </GameInfo>
       
        <div>
          <GameBoard>{renderBoard()}</GameBoard>
         
          {!gameStarted ? (
            <Button onClick={startGame}>Start Game</Button>
          ) : gameOver ? (
            <div style={{ textAlign: 'center', marginTop: '10px' }}>
              <h2>Game Over!</h2>
              <Button onClick={startGame}>Play Again</Button>
            </div>
          ) : (
            <Controls>
              <Button onClick={() => movePiece(-1, 0)}>←</Button>
              <Button onClick={() => rotatePiece()}>↻</Button>
              <Button onClick={() => movePiece(1, 0)}>→</Button>
              <div></div>
              <Button onClick={() => movePiece(0, 1)}>↓</Button>
              <Button onClick={hardDrop}>⤓</Button>
            </Controls>
          )}
        </div>
      </GameWrapper>
    </Container>
  );
}
추천
3
  • 복사

댓글 2개

@멸천도 아 이틀정도 걸리셨다고 하셨었군요.

잘못보고 집중시간 2~3시간을 총 작업시간으로 인식했네요.

어쨌든 AI의 발전이 대단한거같습니다.

 

© SIRSOFT
현재 페이지 제일 처음으로