# 웹 카탄 게임 만들기 - Part 1: 프로젝트 소개 및 기본 구조 ## 시리즈 소개 이 튜토리얼 시리즈에서는 순수 HTML, CSS, JavaScript만을 사용하여 인기 보드게임 "카탄의 개척자(Settlers of Catan)"의 웹 버전을 만들어 보겠습니다. AI 상대와 대전할 수 있는 싱글 플레이어 게임을 목표로 합니다. ### 완성 후 기능 - 1~4명의 AI 상대 선택 - 쉬움/보통/어려움 난이도 설정 - 완전한 카탄 규칙 구현 (자원 수집, 거래, 건설, 개발 카드) - 직관적인 UI와 실시간 게임 상태 업데이트 - 게임 재시작 및 피드백 기능 --- ## 카탄 게임 규칙 개요 카탄을 처음 접하는 분들을 위해 기본 규칙을 정리합니다. ### 게임 목표 **10점을 먼저 획득한 플레이어가 승리합니다.** ### 점수 획득 방법 - 정착지: 1점 - 도시: 2점 - 최장 도로 (5개 이상 연속 도로): 2점 - 최대 기사단 (3장 이상 기사 카드 사용): 2점 - 승점 개발 카드: 1점 ### 5가지 자원 | 자원 | 타일 | 용도 | |------|------|------| | 🌾 밀 (Grain) | 밭 | 정착지, 도시, 개발카드 | | 🧱 벽돌 (Brick) | 점토 | 도로, 정착지 | | 🪵 목재 (Lumber) | 숲 | 도로, 정착지 | | 🐑 양모 (Wool) | 목초지 | 정착지, 개발카드 | | �ite 광석 (Ore) | 산 | 도시, 개발카드 | ### 건설 비용 ``` 도로: 벽돌 1 + 목재 1 정착지: 벽돌 1 + 목재 1 + 밀 1 + 양모 1 도시: 밀 2 + 광석 3 개발카드: 밀 1 + 양모 1 + 광석 1 ``` ### 턴 진행 1. **주사위 굴리기**: 나온 숫자의 타일에 인접한 건물 소유자가 자원 획득 2. **거래**: 다른 플레이어 또는 은행과 자원 교환 3. **건설**: 자원을 사용하여 도로, 정착지, 도시 건설 또는 개발카드 구매 4. **턴 종료**: 다음 플레이어에게 턴 넘김 ### 도둑 (숫자 7) - 주사위 합이 7이면 자원 8장 이상 보유한 플레이어는 절반 버림 - 현재 플레이어가 도둑을 이동시키고 해당 타일의 플레이어에게서 자원 1장 훔침 --- ## 프로젝트 구조 ``` catan-game/ ├── index.html # 메인 HTML 파일 ├── css/ │ ├── style.css # 전역 스타일 │ ├── board.css # 게임 보드 스타일 │ ├── ui.css # UI 컴포넌트 스타일 │ └── animations.css # 애니메이션 (최소한) ├── js/ │ ├── main.js # 앱 진입점 │ ├── game/ │ │ ├── Game.js # 게임 로직 메인 클래스 │ │ ├── Board.js # 보드 생성 및 관리 │ │ ├── Player.js # 플레이어 클래스 │ │ ├── Tile.js # 타일 클래스 │ │ └── Building.js # 건물 클래스 │ ├── ai/ │ │ ├── AI.js # AI 기본 클래스 │ │ ├── EasyAI.js # 쉬움 난이도 │ │ ├── MediumAI.js # 보통 난이도 │ │ └── HardAI.js # 어려움 난이도 │ ├── ui/ │ │ ├── UIManager.js # UI 총괄 │ │ ├── BoardRenderer.js # 보드 렌더링 │ │ └── ActionPanel.js # 액션 버튼 관리 │ └── utils/ │ ├── constants.js # 상수 정의 │ └── helpers.js # 유틸리티 함수 └── assets/ └── images/ # 이미지 리소스 (필요시) ``` --- ## Step 1: HTML 기본 구조 먼저 `index.html` 파일을 작성합니다. ```html 카탄 - 웹 보드게임

🏝️ 카탄의 개척자

웹 보드게임

게임 로그
``` --- ## Step 2: 상수 정의 (constants.js) 게임 전체에서 사용할 상수들을 정의합니다. ```javascript // js/utils/constants.js // 자원 타입 const RESOURCES = { GRAIN: 'grain', // 밀 BRICK: 'brick', // 벽돌 LUMBER: 'lumber', // 목재 WOOL: 'wool', // 양모 ORE: 'ore' // 광석 }; // 타일 타입 const TILE_TYPES = { FIELD: { resource: RESOURCES.GRAIN, name: '밭', color: '#f4d03f' }, HILL: { resource: RESOURCES.BRICK, name: '점토', color: '#c0392b' }, FOREST: { resource: RESOURCES.LUMBER, name: '숲', color: '#27ae60' }, PASTURE: { resource: RESOURCES.WOOL, name: '목초지', color: '#a8e6cf' }, MOUNTAIN: { resource: RESOURCES.ORE, name: '산', color: '#7f8c8d' }, DESERT: { resource: null, name: '사막', color: '#f5cba7' } }; // 건설 비용 const BUILD_COSTS = { ROAD: { [RESOURCES.BRICK]: 1, [RESOURCES.LUMBER]: 1 }, SETTLEMENT: { [RESOURCES.BRICK]: 1, [RESOURCES.LUMBER]: 1, [RESOURCES.GRAIN]: 1, [RESOURCES.WOOL]: 1 }, CITY: { [RESOURCES.GRAIN]: 2, [RESOURCES.ORE]: 3 }, DEV_CARD: { [RESOURCES.GRAIN]: 1, [RESOURCES.WOOL]: 1, [RESOURCES.ORE]: 1 } }; // 개발 카드 종류 const DEV_CARDS = { KNIGHT: { name: '기사', count: 14 }, VICTORY_POINT: { name: '승점', count: 5 }, ROAD_BUILDING: { name: '도로 건설', count: 2 }, YEAR_OF_PLENTY: { name: '풍년', count: 2 }, MONOPOLY: { name: '독점', count: 2 } }; // 플레이어 색상 const PLAYER_COLORS = ['#e74c3c', '#3498db', '#f39c12', '#9b59b6']; // AI 난이도 const AI_DIFFICULTY = { EASY: 'easy', MEDIUM: 'medium', HARD: 'hard' }; // 게임 상태 const GAME_PHASES = { SETUP: 'setup', // 초기 배치 ROLL_DICE: 'roll_dice', // 주사위 굴리기 대기 ROBBER: 'robber', // 도둑 이동 MAIN: 'main', // 메인 턴 (거래, 건설) GAME_OVER: 'game_over' // 게임 종료 }; // 표준 카탄 보드 숫자 배치 const STANDARD_NUMBERS = [5, 2, 6, 3, 8, 10, 9, 12, 11, 4, 8, 10, 9, 4, 5, 6, 3, 11]; // 표준 카탄 보드 타일 배치 (총 19개) const STANDARD_TILES = [ 'MOUNTAIN', 'PASTURE', 'FOREST', 'FIELD', 'HILL', 'PASTURE', 'HILL', 'FIELD', 'FOREST', 'DESERT', 'FOREST', 'MOUNTAIN', 'FOREST', 'MOUNTAIN', 'FIELD', 'PASTURE', 'HILL', 'FIELD', 'PASTURE' ]; // 헥사곤 보드 레이아웃 (각 행의 타일 수) const BOARD_LAYOUT = [3, 4, 5, 4, 3]; // 승점 목표 const VICTORY_POINTS_TO_WIN = 10; ``` --- ## Step 3: 유틸리티 함수 (helpers.js) ```javascript // js/utils/helpers.js // 배열 셔플 (Fisher-Yates) function shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } // 주사위 굴리기 (1-6) function rollDice() { return Math.floor(Math.random() * 6) + 1; } // 두 개의 주사위 굴리기 function rollTwoDice() { return { dice1: rollDice(), dice2: rollDice(), get total() { return this.dice1 + this.dice2; } }; } // 자원 충분한지 확인 function hasEnoughResources(playerResources, cost) { for (const [resource, amount] of Object.entries(cost)) { if ((playerResources[resource] || 0) < amount) { return false; } } return true; } // 자원 차감 function subtractResources(playerResources, cost) { const newResources = { ...playerResources }; for (const [resource, amount] of Object.entries(cost)) { newResources[resource] = (newResources[resource] || 0) - amount; } return newResources; } // 자원 추가 function addResources(playerResources, resources) { const newResources = { ...playerResources }; for (const [resource, amount] of Object.entries(resources)) { newResources[resource] = (newResources[resource] || 0) + amount; } return newResources; } // 총 자원 수 계산 function getTotalResourceCount(resources) { return Object.values(resources).reduce((sum, count) => sum + count, 0); } // 주사위 이모지 반환 function getDiceEmoji(value) { const emojis = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅']; return emojis[value - 1] || '⚀'; } // 자원 이모지 반환 function getResourceEmoji(resource) { const emojis = { [RESOURCES.GRAIN]: '🌾', [RESOURCES.BRICK]: '🧱', [RESOURCES.LUMBER]: '🪵', [RESOURCES.WOOL]: '🐑', [RESOURCES.ORE]: '�ite' }; return emojis[resource] || '❓'; } // 자원 한글명 반환 function getResourceName(resource) { const names = { [RESOURCES.GRAIN]: '밀', [RESOURCES.BRICK]: '벽돌', [RESOURCES.LUMBER]: '목재', [RESOURCES.WOOL]: '양모', [RESOURCES.ORE]: '광석' }; return names[resource] || resource; } // 게임 로그에 메시지 추가 function logMessage(message, type = 'info') { const logContent = document.getElementById('log-content'); if (logContent) { const entry = document.createElement('div'); entry.className = `log-entry log-${type}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; logContent.appendChild(entry); logContent.scrollTop = logContent.scrollHeight; } } // 딜레이 함수 (AI 턴 등에 사용) function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 랜덤 요소 선택 function randomChoice(array) { return array[Math.floor(Math.random() * array.length)]; } ``` --- ## Step 4: 기본 CSS 스타일 (style.css) ```css /* css/style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary-color: #2c3e50; --secondary-color: #3498db; --accent-color: #e74c3c; --background-color: #1a252f; --panel-color: #2c3e50; --text-color: #ecf0f1; --text-muted: #95a5a6; --success-color: #27ae60; --warning-color: #f39c12; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); color: var(--text-color); min-height: 100vh; overflow-x: hidden; } /* 화면 전환 */ .screen { display: none; width: 100%; min-height: 100vh; } .screen.active { display: flex; } /* 버튼 스타일 */ button { cursor: pointer; border: none; padding: 10px 20px; border-radius: 5px; font-size: 14px; transition: all 0.2s ease; } button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } button:disabled { opacity: 0.5; cursor: not-allowed; } .primary-btn { background-color: var(--secondary-color); color: white; font-size: 18px; padding: 15px 40px; } .secondary-btn { background-color: transparent; color: var(--text-color); border: 2px solid var(--text-muted); } .action-btn { background-color: var(--panel-color); color: var(--text-color); border: 1px solid var(--text-muted); width: 100%; margin-bottom: 8px; text-align: left; padding: 12px 15px; } .action-btn:hover:not(:disabled) { background-color: var(--secondary-color); border-color: var(--secondary-color); } /* 모달 */ .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); justify-content: center; align-items: center; z-index: 1000; } .modal.active { display: flex; } .modal-content { background-color: var(--panel-color); padding: 30px; border-radius: 10px; max-width: 600px; max-height: 80vh; overflow-y: auto; position: relative; } .close-btn { position: absolute; top: 10px; right: 15px; font-size: 24px; cursor: pointer; color: var(--text-muted); } .close-btn:hover { color: var(--text-color); } /* 플로팅 버튼 */ .floating-btn { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; border-radius: 50%; background-color: var(--secondary-color); color: white; font-size: 20px; display: flex; align-items: center; justify-content: center; z-index: 100; } /* 피드백 텍스트 영역 */ #feedback-text { width: 100%; height: 150px; padding: 10px; border-radius: 5px; border: 1px solid var(--text-muted); background-color: var(--background-color); color: var(--text-color); resize: vertical; margin-bottom: 15px; } ``` --- ## 다음 단계 예고 Part 2에서는 **헥사곤 게임 보드**를 구현합니다: - SVG를 활용한 헥사곤 타일 그리기 - 카탄 보드 레이아웃 생성 - 타일에 숫자 토큰 배치 - 정점(vertex)과 변(edge) 위치 계산 --- ## 참고 자료 - [카탄 공식 규칙서](https://www.catan.com/understand-catan/game-rules) - [CSS Hexagon Grid Guide](https://www.redblobgames.com/grids/hexagons/) --- *다음 파트: [Part 2 - 헥사곤 게임 보드 구현](make-web-katan-game-v2.md)*