IT 관련/기타

Claude 가 만들어 준 Tetris (웹버젼)

islet2 2025. 2. 18. 17:08

index.html
0.00MB
script.js
0.01MB
style.css
0.00MB

 

                                            테트리스 실행  

실행 모습

 

* 자동 재시작이 안되고, 브라우저 크기 변경시 초기화되는 버그가 있음.  script를 몰라서 못 고침. ㅎ

index.html

<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>Tetris</title>
        <link rel="stylesheet" href="style.css">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <h1>Tetris</h1>
        <div class="container">
            <canvas id="tetris"></canvas>
            <div class="side-panel">
                <h2 class="small-title">Next Block</h2>
                <canvas id="next" width="80" height="55"></canvas>
                <h2 class="small-title">Hold</h2>
                <canvas id="hold" width="80" height="55"></canvas>
                <p id="hold-message"></p>
                <p id="ghost-message"></p>
                <h2>SCORE</h2>
                <p id="score">0</p>
                <h2>Level</h2>
                <p id="level">1</p>
                <h2>Lines</h2>
                <p id="lines">0</p>
            </div>
        </div>
    
        <script src="script.js"></script>
    </body>
    </html>

 

style.css

body {
    font-family: Arial, sans-serif;
}
h1{
    align-items: center
}
.container {
    display: flex;
    justify-content: center;
    align-items: flex-start;
}

#tetris {
    border: 3px solid #000;
}

.side-panel {
    margin-left: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.side-panel h2 {
    margin: 5px 0; /* 간격 */
}

.side-panel p {
    margin: 5px 0; /* 간격 */
}

.small-title {
    font-size: 16px; 
}

 

script.js

const canvas = document.getElementById('tetris');
const context = canvas.getContext('2d');
const nextCanvas = document.getElementById('next');
const nextContext = nextCanvas.getContext('2d');
const holdCanvas = document.getElementById('hold');
const holdContext = holdCanvas.getContext('2d');
const holdMessage = document.getElementById('hold-message');
const ghostMessage = document.getElementById('ghost-message');
const scoreElement = document.getElementById('score');
const levelElement = document.getElementById('level');
const linesElement = document.getElementById('lines');

let grid = [];
let gridWidth = 10; // 가로 10 블록
let gridHeight = 20; // 세로 20 블록
let blockSize = 0; // blockSize는 초기화 시점에 계산됨

let currentPiece = null;
let nextPiece = null;
let holdPiece = null;
let canHold = true; // 홀드 가능 여부
let score = 0;
let level = 1;
let lines = 0;
let gameOver = false;
let dropInterval = 1000; // ms
let lastDropTime = 0;
let isPaused = false;
let ghost = false; //고스트기능 온오프

// 테트리스 블록 모양과 색상 정의
const pieces = [
  { shape: [[1,1,1,1]], color: 'cyan' },       // I
  { shape: [[1,1,1],[0,1,0]], color: 'purple' },   // T
  { shape: [[1,1],[1,1]], color: 'gray' },      // O
  { shape: [[0,1,1],[1,1,0]], color: 'green' },    // S
  { shape: [[1,1,0],[0,1,1]], color: 'red' },      // Z
  { shape: [[1,1,1],[1,0,0]], color: 'blue' },     // L
  { shape: [[1,1,1],[0,0,1]], color: 'orange' }   // J
];

// 게임 초기화 및 캔버스 크기 조정
function init() {
  // 캔버스 크기 조정 (화면 크기에 따라 동적으로 변경)
  const containerWidth = document.querySelector('.container').offsetWidth;
  const containerHeight = window.innerHeight * 0.8; // 예: 화면 높이의 80%
  const maxCanvasWidth = containerWidth * 0.6; // 예: container width 의 60%
  const maxCanvasHeight = containerHeight * 0.8; // 예: container height 의 80%

  let canvasWidth = Math.min(maxCanvasWidth, maxCanvasHeight / 2); // grid 비율을 1:2 로 유지
  let canvasHeight = canvasWidth * 2;

  canvas.width = canvasWidth;
  canvas.height = canvasHeight;

  blockSize = canvasWidth / gridWidth; // 블록 크기 계산

  // grid 초기화
  grid = [];
  for (let y = 0; y < gridHeight; y++) {
    grid[y] = [];
    for (let x = 0; x < gridWidth; x++) {
      grid[y][x] = 0;
    }
  }

  currentPiece = getRandomPiece();
  nextPiece = getRandomPiece();
  drawNextPiece();
  lastDropTime = Date.now();
  gameOver = false; // 게임 오버 상태 초기화
  score = 0;
  level = 1;
  lines = 0;
  holdMessage.textContent = "홀드 사용(H)";
  ghostMessage.textContent= "Ghost 토글(G)";
  updateScoreDisplay();
  updateLevel();
  gameLoop();
}

// 랜덤 블록 생성
function getRandomPiece() {
  const index = Math.floor(Math.random() * pieces.length);
  return {
    ...pieces[index],
    x: Math.floor(gridWidth / 2) - Math.ceil(pieces[index].shape[0].length / 2),
    y: 0
  };
}

// 다음 블록 그리기
function drawNextPiece() {
  nextContext.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
  const piece = nextPiece;
  const offsetX = (nextCanvas.width / blockSize - piece.shape[0].length) / 2;
  const offsetY = (nextCanvas.height / blockSize - piece.shape.length) / 2;

  piece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        nextContext.fillStyle = piece.color;
        nextContext.fillRect((x + offsetX) * blockSize, (y + offsetY) * blockSize, blockSize, blockSize);
      }
    });
  });
}

// 홀드 블록 그리기
function drawHoldPiece() {
  holdContext.clearRect(0, 0, holdCanvas.width, holdCanvas.height);
  if (holdPiece) {
    const piece = holdPiece;
    const offsetX = (holdCanvas.width / blockSize - piece.shape[0].length) / 2;
    const offsetY = (holdCanvas.height / blockSize - piece.shape.length) / 2;

    piece.shape.forEach((row, y) => {
      row.forEach((value, x) => {
        if (value) {
          holdContext.fillStyle = piece.color;
          holdContext.fillRect((x + offsetX) * blockSize, (y + offsetY) * blockSize, blockSize, blockSize);
        }
      });
    });
  }
}

// 블록 그리기
function drawPiece() {
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        context.fillStyle = currentPiece.color;
        context.fillRect((currentPiece.x + x) * blockSize, (currentPiece.y + y) * blockSize, blockSize, blockSize);
      }
    });
  });
}

// 게임 보드 그리기
function drawGrid() {
  for (let y = 0; y < gridHeight; y++) {
    for (let x = 0; x < gridWidth; x++) {
      if (grid[y][x]) {
        context.fillStyle = grid[y][x];
        context.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
      }
    }
  }
}

// 고스트 피스 그리기
function drawGhostPiece() {
  if (!ghost) return;
  let ghostY = currentPiece.y;
  while (isValidMove(0, 1, currentPiece.shape, currentPiece.x, ghostY)) {
    ghostY++;
  }
  ghostY--;

  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        context.fillStyle = currentPiece.color;
        context.globalAlpha = 0.1; // 반투명하게
        context.fillRect((currentPiece.x + x) * blockSize, (ghostY + y) * blockSize, blockSize, blockSize);
        context.globalAlpha = 1; // 투명도 복원
      }
    });
  });
}

// 화면 업데이트
function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);
  drawGrid();
  drawGhostPiece(); // 고스트 피스 먼저 그림
  drawPiece();

  if (isPaused) {
    context.fillStyle = 'rgba(0, 0, 0, 0.5)';
    context.fillRect(0, 0, canvas.width, canvas.height);
    context.fillStyle = 'white';
    context.font = '24px Arial';
    context.textAlign = 'center';
    context.fillText('일시 정지', canvas.width / 2, canvas.height / 2);
  }
}

// 블록 이동 가능 여부 확인
function isValidMove(xOffset, yOffset, pieceShape, startX, startY) {
  const shape = pieceShape || currentPiece.shape;
  const x = startX !== undefined ? startX : currentPiece.x;
  const y = startY !== undefined ? startY : currentPiece.y;

  for (let row = 0; row < shape.length; row++) {
    for (let col = 0; col < shape[row].length; col++) {
      if (shape[row][col]) {
        let newX = x + col + xOffset;
        let newY = y + row + yOffset;

        if (newX < 0 || newX >= gridWidth || newY < 0 || newY >= gridHeight || (grid[newY] && grid[newY][newX])) {
          return false;
        }
      }
    }
  }
  return true;
}


// 블록 회전 (왼쪽 회전)
function rotatePiece() {
  const shape = currentPiece.shape;
  const rows = shape.length;
  const cols = shape[0].length;

  const rotatedShape = [];
  for (let x = 0; x < cols; x++) {
    rotatedShape[x] = [];
    for (let y = 0; y < rows; y++) {
      rotatedShape[x][y] = shape[y][cols - 1 - x]; // Modified for left rotation
    }
  }

  // 회전 후 이동 가능 여부 확인 (벽 충돌 방지)
  if (isValidMove(0, 0, rotatedShape)) {
    currentPiece.shape = rotatedShape;
  }
}

// 블록 고정
function solidifyPiece() {
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        grid[currentPiece.y + y][currentPiece.x + x] = currentPiece.color;
      }
    });
  });

  // 라인 제거
  removeFullLines();

  // 새 블록 생성
  currentPiece = nextPiece;
  nextPiece = getRandomPiece();
  drawNextPiece();
  currentPiece.x = Math.floor(gridWidth / 2) - Math.ceil(currentPiece.shape[0].length / 2);
  currentPiece.y = 0;
  canHold = true;  //새로운 블록이 생성되면 홀드 가능하도록 설정
  holdMessage.textContent = "홀드 사용(H)";
  ghostMessage.textContent= "Ghost 토글(G)";

  // 게임 오버 확인
  if (!isValidMove(0, 0)) {
    gameOver = true;
    alert('게임 오버! 점수: ' + score);
  }
}

// 라인 제거
function removeFullLines() {
  let linesRemoved = 0;
  for (let y = 0; y < gridHeight; y++) {
    let isFull = true;
    for (let x = 0; x < gridWidth; x++) {
      if (!grid[y][x]) {
        isFull = false;
        break;
      }
    }
    if (isFull) {
      // 라인 제거 (위에 있는 라인을 아래로 이동)
      for (let i = y; i > 0; i--) {
        grid[i] = grid[i - 1].slice(); // 깊은 복사
      }
      grid[0] = Array(gridWidth).fill(0);
      linesRemoved++;
      lines++;
    }
  }

  if (linesRemoved > 0) {
    score += calculateScore(linesRemoved, level);
    updateScoreDisplay();
    updateLevel();
  }
}

// 스코어 계산
function calculateScore(linesRemoved, level) {
  switch (linesRemoved) {
    case 1: return 40 * level;
    case 2: return 100 * level;
    case 3: return 300 * level;
    case 4: return 1200 * level;
    default: return 0;
  }
}

// 레벨 업데이트
function updateLevel() {
  level = Math.floor(lines / 10) + 1;
  levelElement.textContent = level;
  dropInterval = 1000 - (level - 1) * 50; // 레벨이 올라갈수록 속도 증가
}

// 스코어 표시 업데이트
function updateScoreDisplay() {
  scoreElement.textContent = score;
  linesElement.textContent = lines;
}

// 블록 아래로 이동
function movePieceDown() {
  if (isValidMove(0, 1)) {
    currentPiece.y++;
  } else {
    solidifyPiece();
  }
}

// 빠른 하강
function dropPiece() {
  while (isValidMove(0, 1)) {
    currentPiece.y++;
  }
  solidifyPiece();
}

// 홀드 기능
function holdCurrentPiece() {
  if (canHold) {
    if (holdPiece === null) {
      // 홀드 슬롯이 비어있는 경우
      holdPiece = currentPiece;
      currentPiece = nextPiece;
      nextPiece = getRandomPiece();
      drawNextPiece();
      holdPiece.x = Math.floor(gridWidth / 2) - Math.ceil(holdPiece.shape[0].length / 2);
      holdPiece.y = 0;

    } else {
      // 홀드 슬롯에 블록이 있는 경우, 교체
      const tempPiece = currentPiece;
      currentPiece = holdPiece;
      holdPiece = tempPiece;
      holdPiece.x = Math.floor(gridWidth / 2) - Math.ceil(holdPiece.shape[0].length / 2);
      holdPiece.y = 0;
    }

    currentPiece.x = Math.floor(gridWidth / 2) - Math.ceil(currentPiece.shape[0].length / 2);
    currentPiece.y = 0;
    drawHoldPiece();
    canHold = false; // 한번 홀드하면 더 이상 홀드 불가능
    holdMessage.textContent = "홀드 사용";
    ghostMessage.textContent= "Ghost 토글(G)";

    // 게임 오버 확인
    if (!isValidMove(0, 0)) {
      gameOver = true;
      alert('게임 오버! 점수: ' + score);
    }
  } else {
    holdMessage.textContent = "홀드 불가능";
  }
}

// 게임 루프
function gameLoop() {
  if (gameOver) return;

  if (isPaused) {
    draw(); // paused 상태에서도 화면을 그려줌
    return; // Pause 상태일 때는 아무것도 하지 않음
  }

  const currentTime = Date.now();
  const deltaTime = currentTime - lastDropTime;

  if (deltaTime > dropInterval) {
    movePieceDown();
    lastDropTime = currentTime;
  }

  draw();
  requestAnimationFrame(gameLoop);
}

// 키 입력 처리
document.addEventListener('keydown', (event) => {
  switch (event.key) {
    case 'ArrowLeft':
      if (isValidMove(-1, 0)) {
        currentPiece.x--;
      }
      break;
    case 'ArrowRight':
      if (isValidMove(1, 0)) {
        currentPiece.x++;
      }
      break;
    case 'ArrowUp': // 왼쪽 회전
      rotatePiece();
      break;
    case 'ArrowDown': // 빠른 이동
      dropInterval = 50; // interval 감소시켜 빠르게 움직이도록
      break;
    case ' ': // 스페이스바: 바로 하강
      dropPiece();
      break;
    case 'p': // p 키: 일시정지/재개
    case 'P':
    case 'Escape':
      isPaused = !isPaused;
      if (!isPaused) {
        lastDropTime = Date.now(); // Pause 해제 시 시간 갱신
        gameLoop(); // 게임 재개
      } else {
        draw(); // pause 상태에서 멈춘 화면을 그림
      }
      break;
    case 'h': // h 키: 홀드
    case 'H':
      holdCurrentPiece();
      break;
    case 'g': // g 키: 고스트 피스 켜기/끄기
    case 'G':
      ghost = !ghost;
      break;  
  }
});

// 키 떼기 처리 (화살표 아래)
document.addEventListener('keyup', (event) => {
  if (event.key === 'ArrowDown') {
    dropInterval = 1000 - (level - 1) * 50; // 원래 속도로 복구
  }
});

// 윈도우 크기가 변경될 때 init() 함수를 다시 호출하여 캔버스 크기를 업데이트합니다.
window.addEventListener('resize', init);

// 게임 시작
init();

 

'IT 관련 > 기타' 카테고리의 다른 글

세계시간 코드 설명  (0) 2025.03.14
세계시간  (0) 2025.03.14
Python에서 외부 모듈을 가져오는 방법 정리  (0) 2025.03.11
간단 버전 flappy bird (html)  (0) 2025.02.28
Claude 가 만들어 준 Tetris(python)  (0) 2025.02.18