Amazon Q CLI로 테트리스 만들기

넘실넘실·2025년 6월 29일
post-thumbnail

1. Amazon Q CLI 설치 - Windows

1-1. glibc 버전 확인

ldd --version

1-2. 설치 파일 다운로드

표준 버전(glibc 2.34 이상)

Linux x86-64

curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip" -o "q.zip"

Linux ARM (aarch64)

curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-aarch64-linux.zip" -o "q.zip"

Musl 버전(glibc < 2.34의 경우)

Linux x86-64와 musl

curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux-musl.zip" -o "q.zip"

Linux ARM(aarch64)과 musl

curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-aarch64-linux-musl.zip" -o "q.zip"

1-3. Amazon Q CLI 설치

  1. 설치 프로그램 압축 해제
unzip q.zip
  1. 설치 프로그램 실행
./q/install.sh
  1. 기본적으로 파일은 ~/.local/bin에 설치된다. q라는 명령어를 입력하거나 명령어를 찾을 수 없다고 하면 ~/.local/bin 경로로 이동하여 명령어를 입력한다.

2. 테트리스 게임 만들기

https://youtu.be/fuaYzGFanYA 영상의 프롬프트를 참고하였다

  1. 먼저 HTML와 JavaScript로 테트리스 게임을 만들어달라고 명령해보자
🧑: I'll create a complete web-based Tetris game using HTML and JavaScript. This will be a fully functional game with all
the classic Tetris mechanics.
<<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tetris Game</title>
    <style>
        body {
            margin: 0;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            font-family: 'Arial', sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            color: white;
        }

        .game-container {
            display: flex;
            gap: 30px;
            align-items: flex-start;
        }

        .game-board {
            background: rgba(0, 0, 0, 0.8);
            border: 3px solid #fff;
            border-radius: 10px;
            padding: 10px;
        }

        canvas {
            display: block;
            background: #000;
            border-radius: 5px;
        }

        .game-info {
            background: rgba(0, 0, 0, 0.8);
            border: 3px solid #fff;
            border-radius: 10px;
            padding: 20px;
            min-width: 200px;
        }

        .score-display {
            font-size: 18px;
            margin-bottom: 15px;
        }

        .next-piece {
            margin-top: 20px;
        }

        .next-canvas {
            background: #333;
            border: 2px solid #666;
            border-radius: 5px;
            margin-top: 10px;
        }

        .controls {
            margin-top: 30px;
            font-size: 14px;
            line-height: 1.6;
        }

        .game-over {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.9);
            color: white;
            padding: 30px;
            border-radius: 10px;
            text-align: center;
            font-size: 24px;
            display: none;
        }

        .restart-btn {
            background: #667eea;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            margin-top: 15px;
        }

        .restart-btn:hover {
            background: #5a6fd8;
        }

        h1 {
            text-align: center;
            margin-bottom: 30px;
            font-size: 2.5em;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
        }
    </style>
</head>
<body>
    <div>
        <h1>🎮 TETRIS</h1>
        <div class="game-container">
            <div class="game-board">
                <canvas id="gameCanvas" width="300" height="600"></canvas>
            </div>
            <div class="game-info">
                <div class="score-display">
                    <div>Score: <span id="score">0</span></div>
                    <div>Lines: <span id="lines">0</span></div>
                    <div>Level: <span id="level">1</span></div>
                </div>
                <div class="next-piece">
                    <div>Next Piece:</div>
                    <canvas id="nextCanvas" class="next-canvas" width="120" height="120"></canvas>
                </div>
                <div class="controls">
                    <strong>Controls:</strong><br>
                    ← → Move<br>
                    ↓ Soft Drop<br>
                    ↑ Rotate<br>
                    Space: Hard Drop<br>
                    P: Pause
                </div>
            </div>
        </div>
        <div id="gameOver" class="game-over">
            <div>Game Over!</div>
            <div>Final Score: <span id="finalScore">0</span></div>
            <button class="restart-btn" onclick="restartGame()">Play Again</button>
        </div>
    </div>

    <script>
        // Game constants
        const BOARD_WIDTH = 10;
        const BOARD_HEIGHT = 20;
        const BLOCK_SIZE = 30;
        
        // Canvas setup
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const nextCanvas = document.getElementById('nextCanvas');
        const nextCtx = nextCanvas.getContext('2d');
        
        // Game state
        let board = [];
        let currentPiece = null;
        let nextPiece = null;
        let score = 0;
        let lines = 0;
        let level = 1;
        let gameRunning = false;
        let isPaused = false;
        let dropTime = 0;
        let dropInterval = 1000; // milliseconds
        
        // Tetris pieces (tetrominoes)
        const PIECES = [
            // I-piece
            {
                shape: [
                    [1, 1, 1, 1]
                ],
                color: '#00f0f0'
            },
            // O-piece
            {
                shape: [
                    [1, 1],
                    [1, 1]
                ],
                color: '#f0f000'
            },
            // T-piece
            {
                shape: [
                    [0, 1, 0],
                    [1, 1, 1]
                ],
                color: '#a000f0'
            },
            // S-piece
            {
                shape: [
                    [0, 1, 1],
                    [1, 1, 0]
                ],
                color: '#00f000'
            },
            // Z-piece
            {
                shape: [
                    [1, 1, 0],
                    [0, 1, 1]
                ],
                color: '#f00000'
            },
            // J-piece
            {
                shape: [
                    [1, 0, 0],
                    [1, 1, 1]
                ],
                color: '#0000f0'
            },
            // L-piece
            {
                shape: [
                    [0, 0, 1],
                    [1, 1, 1]
                ],
                color: '#f0a000'
            }
        ];
        
        // Initialize game board
        function initBoard() {
            board = [];
            for (let y = 0; y < BOARD_HEIGHT; y++) {
                board[y] = [];
                for (let x = 0; x < BOARD_WIDTH; x++) {
                    board[y][x] = 0;
                }
            }
        }
        
        // Create a new piece
        function createPiece() {
            const pieceIndex = Math.floor(Math.random() * PIECES.length);
            const piece = JSON.parse(JSON.stringify(PIECES[pieceIndex])); // Deep copy
            piece.x = Math.floor(BOARD_WIDTH / 2) - Math.floor(piece.shape[0].length / 2);
            piece.y = 0;
            return piece;
        }
        
        // Draw a block
        function drawBlock(ctx, x, y, color) {
            ctx.fillStyle = color;
            ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
            ctx.strokeStyle = '#333';
            ctx.lineWidth = 1;
            ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
        }
        
        // Draw the game board
        function drawBoard() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // Draw placed pieces
            for (let y = 0; y < BOARD_HEIGHT; y++) {
                for (let x = 0; x < BOARD_WIDTH; x++) {
                    if (board[y][x]) {
                        drawBlock(ctx, x, y, board[y][x]);
                    }
                }
            }
            
            // Draw current piece
            if (currentPiece) {
                drawPiece(ctx, currentPiece);
            }
            
            // Draw grid lines
            ctx.strokeStyle = '#333';
            ctx.lineWidth = 0.5;
            for (let x = 0; x <= BOARD_WIDTH; x++) {
                ctx.beginPath();
                ctx.moveTo(x * BLOCK_SIZE, 0);
                ctx.lineTo(x * BLOCK_SIZE, canvas.height);
                ctx.stroke();
            }
            for (let y = 0; y <= BOARD_HEIGHT; y++) {
                ctx.beginPath();
                ctx.moveTo(0, y * BLOCK_SIZE);
                ctx.lineTo(canvas.width, y * BLOCK_SIZE);
                ctx.stroke();
            }
        }
        
        // Draw a piece
        function drawPiece(ctx, piece) {
            for (let y = 0; y < piece.shape.length; y++) {
                for (let x = 0; x < piece.shape[y].length; x++) {
                    if (piece.shape[y][x]) {
                        drawBlock(ctx, piece.x + x, piece.y + y, piece.color);
                    }
                }
            }
        }
        
        // Draw next piece
        function drawNextPiece() {
            nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
            if (nextPiece) {
                const offsetX = (nextCanvas.width / BLOCK_SIZE - nextPiece.shape[0].length) / 2;
                const offsetY = (nextCanvas.height / BLOCK_SIZE - nextPiece.shape.length) / 2;
                
                for (let y = 0; y < nextPiece.shape.length; y++) {
                    for (let x = 0; x < nextPiece.shape[y].length; x++) {
                        if (nextPiece.shape[y][x]) {
                            nextCtx.fillStyle = nextPiece.color;
                            nextCtx.fillRect(
                                (offsetX + x) * BLOCK_SIZE,
                                (offsetY + y) * BLOCK_SIZE,
                                BLOCK_SIZE,
                                BLOCK_SIZE
                            );
                            nextCtx.strokeStyle = '#333';
                            nextCtx.strokeRect(
                                (offsetX + x) * BLOCK_SIZE,
                                (offsetY + y) * BLOCK_SIZE,
                                BLOCK_SIZE,
                                BLOCK_SIZE
                            );
                        }
                    }
                }
            }
        }
        
        // Check if piece can be placed at position
        function canPlacePiece(piece, dx = 0, dy = 0, newShape = null) {
            const shape = newShape || piece.shape;
            const newX = piece.x + dx;
            const newY = piece.y + dy;
            
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x]) {
                        const boardX = newX + x;
                        const boardY = newY + y;
                        
                        if (boardX < 0 || boardX >= BOARD_WIDTH || 
                            boardY >= BOARD_HEIGHT || 
                            (boardY >= 0 && board[boardY][boardX])) {
                            return false;
                        }
                    }
                }
            }
            return true;
        }
        
        // Place piece on board
        function placePiece() {
            for (let y = 0; y < currentPiece.shape.length; y++) {
                for (let x = 0; x < currentPiece.shape[y].length; x++) {
                    if (currentPiece.shape[y][x]) {
                        const boardY = currentPiece.y + y;
                        const boardX = currentPiece.x + x;
                        if (boardY >= 0) {
                            board[boardY][boardX] = currentPiece.color;
                        }
                    }
                }
            }
        }
        
        // Clear completed lines
        function clearLines() {
            let linesCleared = 0;
            
            for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {
                let isLineFull = true;
                for (let x = 0; x < BOARD_WIDTH; x++) {
                    if (!board[y][x]) {
                        isLineFull = false;
                        break;
                    }
                }
                
                if (isLineFull) {
                    board.splice(y, 1);
                    board.unshift(new Array(BOARD_WIDTH).fill(0));
                    linesCleared++;
                    y++; // Check the same line again
                }
            }
            
            if (linesCleared > 0) {
                lines += linesCleared;
                score += linesCleared * 100 * level;
                level = Math.floor(lines / 10) + 1;
                dropInterval = Math.max(50, 1000 - (level - 1) * 50);
                updateDisplay();
            }
        }
        
        // Rotate piece
        function rotatePiece() {
            const rotated = [];
            const shape = currentPiece.shape;
            
            for (let x = 0; x < shape[0].length; x++) {
                rotated[x] = [];
                for (let y = shape.length - 1; y >= 0; y--) {
                    rotated[x][shape.length - 1 - y] = shape[y][x];
                }
            }
            
            if (canPlacePiece(currentPiece, 0, 0, rotated)) {
                currentPiece.shape = rotated;
            }
        }
        
        // Move piece
        function movePiece(dx, dy) {
            if (canPlacePiece(currentPiece, dx, dy)) {
                currentPiece.x += dx;
                currentPiece.y += dy;
                return true;
            }
            return false;
        }
        
        // Hard drop
        function hardDrop() {
            while (movePiece(0, 1)) {
                score += 2;
            }
            updateDisplay();
        }
        
        // Update display
        function updateDisplay() {
            document.getElementById('score').textContent = score;
            document.getElementById('lines').textContent = lines;
            document.getElementById('level').textContent = level;
        }
        
        // Game over
        function gameOver() {
            gameRunning = false;
            document.getElementById('finalScore').textContent = score;
            document.getElementById('gameOver').style.display = 'block';
        }
        
        // Restart game
        function restartGame() {
            initBoard();
            score = 0;
            lines = 0;
            level = 1;
            dropInterval = 1000;
            currentPiece = createPiece();
            nextPiece = createPiece();
            gameRunning = true;
            isPaused = false;
            document.getElementById('gameOver').style.display = 'none';
            updateDisplay();
            drawNextPiece();
        }
        
        // Game loop
        function gameLoop(timestamp) {
            if (!gameRunning || isPaused) {
                requestAnimationFrame(gameLoop);
                return;
            }
            
            if (timestamp - dropTime > dropInterval) {
                if (!movePiece(0, 1)) {
                    placePiece();
                    clearLines();
                    currentPiece = nextPiece;
                    nextPiece = createPiece();
                    drawNextPiece();
                    
                    if (!canPlacePiece(currentPiece)) {
                        gameOver();
                        return;
                    }
                }
                dropTime = timestamp;
            }
            
            drawBoard();
            requestAnimationFrame(gameLoop);
        }
        
        // Keyboard controls
        document.addEventListener('keydown', (e) => {
            if (!gameRunning || isPaused) {
                if (e.code === 'KeyP') {
                    isPaused = !isPaused;
                }
                return;
            }
            
            switch (e.code) {
                case 'ArrowLeft':
                    movePiece(-1, 0);
                    break;
                case 'ArrowRight':
                    movePiece(1, 0);
                    break;
                case 'ArrowDown':
                    if (movePiece(0, 1)) {
                        score += 1;
                        updateDisplay();
                    }
                    break;
                case 'ArrowUp':
                    rotatePiece();
                    break;
                case 'Space':
                    e.preventDefault();
                    hardDrop();
                    break;
                case 'KeyP':
                    isPaused = !isPaused;
                    break;
            }
        });
        
        // Initialize and start game
        function startGame() {
            initBoard();
            currentPiece = createPiece();
            nextPiece = createPiece();
            gameRunning = true;
            updateDisplay();
            drawNextPiece();
            requestAnimationFrame(gameLoop);
        }
        
        // Start the game when page loads
        window.addEventListener('load', startGame);
    </script>
</body>
</html>
  1. 바닥에 떨어지고 있는 블록의 모양이 보이는 기능을 추가하라고 명령해보자
🧑: Could you implement projection so that player can see where the block would land beforehand?
<<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tetris Game</title>
    <style>
        body {
            margin: 0;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            font-family: 'Arial', sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            color: white;
        }

        .game-container {
            display: flex;
            gap: 30px;
            align-items: flex-start;
        }

        .game-board {
            background: rgba(0, 0, 0, 0.8);
            border: 3px solid #fff;
            border-radius: 10px;
            padding: 10px;
        }

        canvas {
            display: block;
            background: #000;
            border-radius: 5px;
        }

        .game-info {
            background: rgba(0, 0, 0, 0.8);
            border: 3px solid #fff;
            border-radius: 10px;
            padding: 20px;
            min-width: 200px;
        }

        .score-display {
            font-size: 18px;
            margin-bottom: 15px;
        }

        .next-piece {
            margin-top: 20px;
        }

        .next-canvas {
            background: #333;
            border: 2px solid #666;
            border-radius: 5px;
            margin-top: 10px;
        }

        .controls {
            margin-top: 30px;
            font-size: 14px;
            line-height: 1.6;
        }

        .game-over {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.9);
            color: white;
            padding: 30px;
            border-radius: 10px;
            text-align: center;
            font-size: 24px;
            display: none;
        }

        .restart-btn {
            background: #667eea;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            margin-top: 15px;
        }

        .restart-btn:hover {
            background: #5a6fd8;
        }

        h1 {
            text-align: center;
            margin-bottom: 30px;
            font-size: 2.5em;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
        }
    </style>
</head>
<body>
    <div>
        <h1>🎮 TETRIS</h1>
        <div class="game-container">
            <div class="game-board">
                <canvas id="gameCanvas" width="300" height="600"></canvas>
            </div>
            <div class="game-info">
                <div class="score-display">
                    <div>Score: <span id="score">0</span></div>
                    <div>Lines: <span id="lines">0</span></div>
                    <div>Level: <span id="level">1</span></div>
                </div>
                <div class="next-piece">
                    <div>Next Piece:</div>
                    <canvas id="nextCanvas" class="next-canvas" width="120" height="120"></canvas>
                </div>
                <div class="controls">
                    <strong>Controls:</strong><br>
                    ← → Move<br>
                    ↓ Soft Drop<br>
                    ↑ Rotate<br>
                    Space: Hard Drop<br>
                    P: Pause<br><br>
                    <small>Ghost piece shows where block will land</small>
                </div>
            </div>
        </div>
        <div id="gameOver" class="game-over">
            <div>Game Over!</div>
            <div>Final Score: <span id="finalScore">0</span></div>
            <button class="restart-btn" onclick="restartGame()">Play Again</button>
        </div>
    </div>

    <script>
        // Game constants
        const BOARD_WIDTH = 10;
        const BOARD_HEIGHT = 20;
        const BLOCK_SIZE = 30;
        
        // Canvas setup
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const nextCanvas = document.getElementById('nextCanvas');
        const nextCtx = nextCanvas.getContext('2d');
        
        // Game state
        let board = [];
        let currentPiece = null;
        let nextPiece = null;
        let score = 0;
        let lines = 0;
        let level = 1;
        let gameRunning = false;
        let isPaused = false;
        let dropTime = 0;
        let dropInterval = 1000; // milliseconds
        
        // Tetris pieces (tetrominoes)
        const PIECES = [
            // I-piece
            {
                shape: [
                    [1, 1, 1, 1]
                ],
                color: '#00f0f0'
            },
            // O-piece
            {
                shape: [
                    [1, 1],
                    [1, 1]
                ],
                color: '#f0f000'
            },
            // T-piece
            {
                shape: [
                    [0, 1, 0],
                    [1, 1, 1]
                ],
                color: '#a000f0'
            },
            // S-piece
            {
                shape: [
                    [0, 1, 1],
                    [1, 1, 0]
                ],
                color: '#00f000'
            },
            // Z-piece
            {
                shape: [
                    [1, 1, 0],
                    [0, 1, 1]
                ],
                color: '#f00000'
            },
            // J-piece
            {
                shape: [
                    [1, 0, 0],
                    [1, 1, 1]
                ],
                color: '#0000f0'
            },
            // L-piece
            {
                shape: [
                    [0, 0, 1],
                    [1, 1, 1]
                ],
                color: '#f0a000'
            }
        ];
        
        // Initialize game board
        function initBoard() {
            board = [];
            for (let y = 0; y < BOARD_HEIGHT; y++) {
                board[y] = [];
                for (let x = 0; x < BOARD_WIDTH; x++) {
                    board[y][x] = 0;
                }
            }
        }
        
        // Create a new piece
        function createPiece() {
            const pieceIndex = Math.floor(Math.random() * PIECES.length);
            const piece = JSON.parse(JSON.stringify(PIECES[pieceIndex])); // Deep copy
            piece.x = Math.floor(BOARD_WIDTH / 2) - Math.floor(piece.shape[0].length / 2);
            piece.y = 0;
            return piece;
        }
        
        // Draw a block
        function drawBlock(ctx, x, y, color, isGhost = false) {
            if (isGhost) {
                // Draw ghost/projection block with transparency
                ctx.fillStyle = color + '40'; // Add transparency
                ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
                ctx.strokeStyle = color + '80';
                ctx.lineWidth = 2;
                ctx.setLineDash([5, 5]); // Dashed border
                ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
                ctx.setLineDash([]); // Reset line dash
            } else {
                ctx.fillStyle = color;
                ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
                ctx.strokeStyle = '#333';
                ctx.lineWidth = 1;
                ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
            }
        }
        
        // Calculate projection position
        function getProjectionPosition(piece) {
            let projectionY = piece.y;
            
            // Keep moving down until we can't place the piece anymore
            while (canPlacePiece(piece, 0, projectionY - piece.y + 1)) {
                projectionY++;
            }
            
            return projectionY;
        }
        
        // Draw projection/ghost piece
        function drawProjection(ctx, piece) {
            const projectionY = getProjectionPosition(piece);
            
            // Only draw projection if it's different from current position
            if (projectionY !== piece.y) {
                for (let y = 0; y < piece.shape.length; y++) {
                    for (let x = 0; x < piece.shape[y].length; x++) {
                        if (piece.shape[y][x]) {
                            drawBlock(ctx, piece.x + x, projectionY + y, piece.color, true);
                        }
                    }
                }
            }
        }
        function drawBoard() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            // Draw placed pieces
            for (let y = 0; y < BOARD_HEIGHT; y++) {
                for (let x = 0; x < BOARD_WIDTH; x++) {
                    if (board[y][x]) {
                        drawBlock(ctx, x, y, board[y][x]);
                    }
                }
            }
            
            // Draw projection/ghost piece first (so it appears behind the current piece)
            if (currentPiece) {
                drawProjection(ctx, currentPiece);
            }
            
            // Draw current piece
            if (currentPiece) {
                drawPiece(ctx, currentPiece);
            }
            
            // Draw grid lines
            ctx.strokeStyle = '#333';
            ctx.lineWidth = 0.5;
            for (let x = 0; x <= BOARD_WIDTH; x++) {
                ctx.beginPath();
                ctx.moveTo(x * BLOCK_SIZE, 0);
                ctx.lineTo(x * BLOCK_SIZE, canvas.height);
                ctx.stroke();
            }
            for (let y = 0; y <= BOARD_HEIGHT; y++) {
                ctx.beginPath();
                ctx.moveTo(0, y * BLOCK_SIZE);
                ctx.lineTo(canvas.width, y * BLOCK_SIZE);
                ctx.stroke();
            }
        }
        
        // Draw a piece
        function drawPiece(ctx, piece) {
            for (let y = 0; y < piece.shape.length; y++) {
                for (let x = 0; x < piece.shape[y].length; x++) {
                    if (piece.shape[y][x]) {
                        drawBlock(ctx, piece.x + x, piece.y + y, piece.color);
                    }
                }
            }
        }
        
        // Draw next piece
        function drawNextPiece() {
            nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
            if (nextPiece) {
                const offsetX = (nextCanvas.width / BLOCK_SIZE - nextPiece.shape[0].length) / 2;
                const offsetY = (nextCanvas.height / BLOCK_SIZE - nextPiece.shape.length) / 2;
                
                for (let y = 0; y < nextPiece.shape.length; y++) {
                    for (let x = 0; x < nextPiece.shape[y].length; x++) {
                        if (nextPiece.shape[y][x]) {
                            nextCtx.fillStyle = nextPiece.color;
                            nextCtx.fillRect(
                                (offsetX + x) * BLOCK_SIZE,
                                (offsetY + y) * BLOCK_SIZE,
                                BLOCK_SIZE,
                                BLOCK_SIZE
                            );
                            nextCtx.strokeStyle = '#333';
                            nextCtx.strokeRect(
                                (offsetX + x) * BLOCK_SIZE,
                                (offsetY + y) * BLOCK_SIZE,
                                BLOCK_SIZE,
                                BLOCK_SIZE
                            );
                        }
                    }
                }
            }
        }
        
        // Check if piece can be placed at position
        function canPlacePiece(piece, dx = 0, dy = 0, newShape = null) {
            const shape = newShape || piece.shape;
            const newX = piece.x + dx;
            const newY = piece.y + dy;
            
            for (let y = 0; y < shape.length; y++) {
                for (let x = 0; x < shape[y].length; x++) {
                    if (shape[y][x]) {
                        const boardX = newX + x;
                        const boardY = newY + y;
                        
                        if (boardX < 0 || boardX >= BOARD_WIDTH || 
                            boardY >= BOARD_HEIGHT || 
                            (boardY >= 0 && board[boardY][boardX])) {
                            return false;
                        }
                    }
                }
            }
            return true;
        }
        
        // Place piece on board
        function placePiece() {
            for (let y = 0; y < currentPiece.shape.length; y++) {
                for (let x = 0; x < currentPiece.shape[y].length; x++) {
                    if (currentPiece.shape[y][x]) {
                        const boardY = currentPiece.y + y;
                        const boardX = currentPiece.x + x;
                        if (boardY >= 0) {
                            board[boardY][boardX] = currentPiece.color;
                        }
                    }
                }
            }
        }
        
        // Clear completed lines
        function clearLines() {
            let linesCleared = 0;
            
            for (let y = BOARD_HEIGHT - 1; y >= 0; y--) {
                let isLineFull = true;
                for (let x = 0; x < BOARD_WIDTH; x++) {
                    if (!board[y][x]) {
                        isLineFull = false;
                        break;
                    }
                }
                
                if (isLineFull) {
                    board.splice(y, 1);
                    board.unshift(new Array(BOARD_WIDTH).fill(0));
                    linesCleared++;
                    y++; // Check the same line again
                }
            }
            
            if (linesCleared > 0) {
                lines += linesCleared;
                score += linesCleared * 100 * level;
                level = Math.floor(lines / 10) + 1;
                dropInterval = Math.max(50, 1000 - (level - 1) * 50);
                updateDisplay();
            }
        }
        
        // Rotate piece
        function rotatePiece() {
            const rotated = [];
            const shape = currentPiece.shape;
            
            for (let x = 0; x < shape[0].length; x++) {
                rotated[x] = [];
                for (let y = shape.length - 1; y >= 0; y--) {
                    rotated[x][shape.length - 1 - y] = shape[y][x];
                }
            }
            
            if (canPlacePiece(currentPiece, 0, 0, rotated)) {
                currentPiece.shape = rotated;
            }
        }
        
        // Move piece
        function movePiece(dx, dy) {
            if (canPlacePiece(currentPiece, dx, dy)) {
                currentPiece.x += dx;
                currentPiece.y += dy;
                return true;
            }
            return false;
        }
        
        // Hard drop
        function hardDrop() {
            while (movePiece(0, 1)) {
                score += 2;
            }
            updateDisplay();
        }
        
        // Update display
        function updateDisplay() {
            document.getElementById('score').textContent = score;
            document.getElementById('lines').textContent = lines;
            document.getElementById('level').textContent = level;
        }
        
        // Game over
        function gameOver() {
            gameRunning = false;
            document.getElementById('finalScore').textContent = score;
            document.getElementById('gameOver').style.display = 'block';
        }
        
        // Restart game
        function restartGame() {
            initBoard();
            score = 0;
            lines = 0;
            level = 1;
            dropInterval = 1000;
            currentPiece = createPiece();
            nextPiece = createPiece();
            gameRunning = true;
            isPaused = false;
            document.getElementById('gameOver').style.display = 'none';
            updateDisplay();
            drawNextPiece();
        }
        
        // Game loop
        function gameLoop(timestamp) {
            if (!gameRunning || isPaused) {
                requestAnimationFrame(gameLoop);
                return;
            }
            
            if (timestamp - dropTime > dropInterval) {
                if (!movePiece(0, 1)) {
                    placePiece();
                    clearLines();
                    currentPiece = nextPiece;
                    nextPiece = createPiece();
                    drawNextPiece();
                    
                    if (!canPlacePiece(currentPiece)) {
                        gameOver();
                        return;
                    }
                }
                dropTime = timestamp;
            }
            
            drawBoard();
            requestAnimationFrame(gameLoop);
        }
        
        // Keyboard controls
        document.addEventListener('keydown', (e) => {
            if (!gameRunning || isPaused) {
                if (e.code === 'KeyP') {
                    isPaused = !isPaused;
                }
                return;
            }
            
            switch (e.code) {
                case 'ArrowLeft':
                    movePiece(-1, 0);
                    break;
                case 'ArrowRight':
                    movePiece(1, 0);
                    break;
                case 'ArrowDown':
                    if (movePiece(0, 1)) {
                        score += 1;
                        updateDisplay();
                    }
                    break;
                case 'ArrowUp':
                    rotatePiece();
                    break;
                case 'Space':
                    e.preventDefault();
                    hardDrop();
                    break;
                case 'KeyP':
                    isPaused = !isPaused;
                    break;
            }
        });
        
        // Initialize and start game
        function startGame() {
            initBoard();
            currentPiece = createPiece();
            nextPiece = createPiece();
            gameRunning = true;
            updateDisplay();
            drawNextPiece();
            requestAnimationFrame(gameLoop);
        }
        
        // Start the game when page loads
        window.addEventListener('load', startGame);
    </script>
</body>
</html>

3. 배포하기

https://youtu.be/fuaYzGFanYA 영상의 프롬프트를 참고하였다

  1. S3 Static Hosting로 배포하는 Cloudformation Template를 생성해달라고 명령해보자
🧑:이 게임을 S3 static hosting으로 배포하고 싶어. 파라미터에서 입력한 값을 버킷명으로 하는 S3 버킷을 만들어주고 S3 Static Hosting 기능을 활성화 한 다음에 필요한 설정값 (예를 들어서 Public Access 허용 등)을 설정하는 YML 기반의 Cloudformation
Template 파일을 만들어줘
AWSTemplateFormatVersion: '2010-09-09'
Description: 'S3 Static Website Hosting for Tetris Game'

Parameters:
  BucketName:
    Type: String
    Description: 'Name of the S3 bucket for static website hosting'
    MinLength: 3
    MaxLength: 63
    AllowedPattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$'
    ConstraintDescription: 'Bucket name must be 3-63 characters, lowercase letters, numbers, and hyphens only'

Resources:
  # S3 Bucket for static website hosting
  StaticWebsiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      WebsiteConfiguration:
        IndexDocument: tetris.html
        ErrorDocument: error.html
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false
      VersioningConfiguration:
        Status: Enabled
      Tags:
        - Key: Purpose
          Value: StaticWebsiteHosting
        - Key: Project
          Value: TetrisGame

  # Bucket Policy to allow public read access
  StaticWebsiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref StaticWebsiteBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: PublicReadGetObject
            Effect: Allow
            Principal: '*'
            Action: 's3:GetObject'
            Resource: !Sub '${StaticWebsiteBucket}/*'

  # CloudFront Distribution (Optional but recommended for better performance)
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: S3Origin
            DomainName: !GetAtt StaticWebsiteBucket.RegionalDomainName
            CustomOriginConfig:
              HTTPPort: 80
              HTTPSPort: 443
              OriginProtocolPolicy: http-only
        Enabled: true
        DefaultRootObject: tetris.html
        Comment: !Sub 'CloudFront Distribution for ${BucketName}'
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          Compress: true
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
          MinTTL: 0
          DefaultTTL: 86400
          MaxTTL: 31536000
        PriceClass: PriceClass_100
        ViewerCertificate:
          CloudFrontDefaultCertificate: true

Outputs:
  WebsiteURL:
    Description: 'URL of the static website'
    Value: !GetAtt StaticWebsiteBucket.WebsiteURL
    Export:
      Name: !Sub '${AWS::StackName}-WebsiteURL'

  BucketName:
    Description: 'Name of the S3 bucket'
    Value: !Ref StaticWebsiteBucket
    Export:
      Name: !Sub '${AWS::StackName}-BucketName'

  CloudFrontURL:
    Description: 'CloudFront Distribution URL'
    Value: !Sub 'https://${CloudFrontDistribution.DomainName}'
    Export:
      Name: !Sub '${AWS::StackName}-CloudFrontURL'

  BucketDomainName:
    Description: 'Regional domain name of the bucket'
    Value: !GetAtt StaticWebsiteBucket.RegionalDomainName
    Export:
      Name: !Sub '${AWS::StackName}-BucketDomainName'
  1. 배포 중 에러가 발생하여 다음과 같이 프롬프트 명령어를 입력하여 수정하였다
🧑: 논리적 ID StaticWebsiteBucketPolicy에서 다음과 같은 오류가 발생했어 Resource handler returned message: "Policy has invalid resource (Service: S3, Status Code: 400...
🤖: 이 오류는 S3 버킷 정책에서 리소스 참조 방식에 문제가 있을 때 발생합니다. CloudFormation 템플릿을 수정해드리겠습니다.

🛠️  Using tool: fs_write (trusted)
 ⋮
 ● Path: s3-static-hosting.yml

  35, 35:   # Bucket Policy to allow public read access
  36, 36:   StaticWebsiteBucketPolicy:
  37, 37:     Type: AWS::S3::BucketPolicy
  38, 38:     Properties:
  39, 39:       Bucket: !Ref StaticWebsiteBucket
  40, 40:       PolicyDocument:
  41, 41:         Version: '2012-10-17'
  42, 42:         Statement:
  43, 43:           - Sid: PublicReadGetObject
  44, 44:             Effect: Allow
  45, 45:             Principal: '*'
  46, 46:             Action: 's3:GetObject'
- 47    :             Resource: !Sub '${StaticWebsiteBucket}/*'
+     47:             Resource: !Join
+     48:               - ''
+     49:               - - 'arn:aws:s3:::'
+     50:                 - !Ref StaticWebsiteBucket
+     51:                 - '/*'

Updating: s3-static-hosting.yml

 ⋮
 ● Completed in 0.5s

또한 CloudFront 설정도 S3 정적 웹사이트 호스팅과 더 잘 호환되도록 수정하겠습니다:


Tool validation failed:
Failed to validate tool parameters: missing field `command`. The model has either suggested tool parameters which are incompatible with the existing tools, or has suggested one or more tool that does not exist in the list of known tools.



🛠️  Using tool: fs_write (trusted)
 ⋮
 ● Path: s3-static-hosting.yml

  53, 53:   # CloudFront Distribution (Optional but recommended for better performance)
  54, 54:   CloudFrontDistribution:
  55, 55:     Type: AWS::CloudFront::Distribution
  56, 56:     Properties:
  57, 57:       DistributionConfig:
  58, 58:         Origins:
  59, 59:           - Id: S3Origin
- 60    :             DomainName: !GetAtt StaticWebsiteBucket.RegionalDomainName
+     60:             DomainName: !Select [2, !Split ["/", !GetAtt StaticWebsiteBucket.WebsiteURL]]
  61, 61:             CustomOriginConfig:
  62, 62:               HTTPPort: 80
  63, 63:               HTTPSPort: 443
  64, 64:               OriginProtocolPolicy: http-only
  65, 65:         Enabled: true
  66, 66:         DefaultRootObject: tetris.html
  67, 67:         Comment: !Sub 'CloudFront Distribution for ${BucketName}'
  68, 68:         DefaultCacheBehavior:
  69, 69:           AllowedMethods:
  70, 70:             - GET
  71, 71:             - HEAD
+     72:             - OPTIONS
  72, 73:           Compress: true
  73, 74:           TargetOriginId: S3Origin
  74, 75:           ForwardedValues:
  75, 76:             QueryString: false
  76, 77:             Cookies:
  77, 78:               Forward: none
  78, 79:           ViewerProtocolPolicy: redirect-to-https
  79, 80:           MinTTL: 0
  80, 81:           DefaultTTL: 86400
  81, 82:           MaxTTL: 31536000
  82, 83:         PriceClass: PriceClass_100
  83, 84:         ViewerCertificate:
- 84    :           CloudFrontDefaultCertificate: true
+     85:           CloudFrontDefaultCertificate: true
+     86:         CustomErrorResponses:
+     87:           - ErrorCode: 404
+     88:             ResponseCode: 404
+     89:             ResponsePagePath: /error.html

Updating: s3-static-hosting.yml

 ⋮
 ● Completed in 0.0s

이제 수정된 CloudFormation 템플릿의 주요 변경사항을 설명드리겠습니다:

## 🔧 수정된 내용

### 1. S3 버킷 정책 리소스 참조 수정
yaml
# 이전 (오류 발생)
Resource: !Sub '${StaticWebsiteBucket}/*'

# 수정 후 (정상 작동)
Resource: !Join
  - ''
  - - 'arn:aws:s3:::'
    - !Ref StaticWebsiteBucket
    - '/*'


### 2. CloudFront Origin 설정 개선
• S3 정적 웹사이트 호스팅 엔드포인트를 올바르게 참조하도록 수정
• 404 에러 페이지 처리 추가

### 3. 오류 해결 방법

원인: !Sub 함수가 S3 버킷 리소스를 올바른 ARN 형식으로 변환하지 못했습니다.

해결: !Join 함수를 사용하여 명시적으로 ARN을 구성했습니다.
AWSTemplateFormatVersion: '2010-09-09'
Description: 'S3 Static Website Hosting for Tetris Game'

Parameters:
  BucketName:
    Type: String
    Description: 'Name of the S3 bucket for static website hosting'
    MinLength: 3
    MaxLength: 63
    AllowedPattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$'
    ConstraintDescription: 'Bucket name must be 3-63 characters, lowercase letters, numbers, and hyphens only'

Resources:
  # S3 Bucket for static website hosting
  StaticWebsiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      WebsiteConfiguration:
        IndexDocument: tetris.html
        ErrorDocument: error.html
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false
      VersioningConfiguration:
        Status: Enabled
      Tags:
        - Key: Purpose
          Value: StaticWebsiteHosting
        - Key: Project
          Value: TetrisGame

  # Bucket Policy to allow public read access
  StaticWebsiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref StaticWebsiteBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: PublicReadGetObject
            Effect: Allow
            Principal: '*'
            Action: 's3:GetObject'
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref StaticWebsiteBucket
                - '/*'

  # CloudFront Distribution (Optional but recommended for better performance)
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: S3Origin
            DomainName: !Select [2, !Split ["/", !GetAtt StaticWebsiteBucket.WebsiteURL]]
            CustomOriginConfig:
              HTTPPort: 80
              HTTPSPort: 443
              OriginProtocolPolicy: http-only
        Enabled: true
        DefaultRootObject: tetris.html
        Comment: !Sub 'CloudFront Distribution for ${BucketName}'
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          Compress: true
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
          MinTTL: 0
          DefaultTTL: 86400
          MaxTTL: 31536000
        PriceClass: PriceClass_100
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
        CustomErrorResponses:
          - ErrorCode: 404
            ResponseCode: 404
            ResponsePagePath: /error.html

Outputs:
  WebsiteURL:
    Description: 'URL of the static website'
    Value: !GetAtt StaticWebsiteBucket.WebsiteURL
    Export:
      Name: !Sub '${AWS::StackName}-WebsiteURL'

  BucketName:
    Description: 'Name of the S3 bucket'
    Value: !Ref StaticWebsiteBucket
    Export:
      Name: !Sub '${AWS::StackName}-BucketName'

  CloudFrontURL:
    Description: 'CloudFront Distribution URL'
    Value: !Sub 'https://${CloudFrontDistribution.DomainName}'
    Export:
      Name: !Sub '${AWS::StackName}-CloudFrontURL'

  BucketDomainName:
    Description: 'Regional domain name of the bucket'
    Value: !GetAtt StaticWebsiteBucket.RegionalDomainName
    Export:
      Name: !Sub '${AWS::StackName}-BucketDomainName'

Reference

profile
어쩌다보니 데이터쟁이

0개의 댓글