* 자동 재시작이 안되고, 브라우저 크기 변경시 초기화되는 버그가 있음. 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 |