# 웹 카탄 게임 만들기 - 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)*