# 웹 카탄 게임 만들기 - Part 6: 개발 카드 시스템 ## 이번 파트에서 구현할 내용 - 개발 카드 덱 생성 및 셔플 - 기사 카드 (Knight) - 도둑 이동 - 도로 건설 카드 (Road Building) - 풍년 카드 (Year of Plenty) - 독점 카드 (Monopoly) - 승점 카드 (Victory Point) --- ## Step 1: 개발 카드 클래스 ```javascript // js/game/DevCard.js class DevCard { constructor(type) { this.type = type; this.turnBought = null; // 구매한 턴 this.used = false; } getInfo() { const cardInfo = { KNIGHT: { name: '기사', emoji: '⚔️', description: '도둑을 이동시키고 해당 타일의 플레이어에게서 자원 1장을 가져옵니다.' }, VICTORY_POINT: { name: '승점', emoji: '🏆', description: '즉시 1점을 획득합니다. (공개하지 않아도 됩니다)' }, ROAD_BUILDING: { name: '도로 건설', emoji: '🛤️', description: '무료로 도로 2개를 건설합니다.' }, YEAR_OF_PLENTY: { name: '풍년', emoji: '🌾', description: '은행에서 원하는 자원 2개를 가져옵니다.' }, MONOPOLY: { name: '독점', emoji: '💰', description: '자원 하나를 선택하면 모든 플레이어에게서 해당 자원을 전부 가져옵니다.' } }; return cardInfo[this.type]; } } // 개발 카드 덱 관리 class DevCardDeck { constructor() { this.cards = []; this.initialize(); } initialize() { this.cards = []; // 카드 생성 for (let i = 0; i < DEV_CARDS.KNIGHT.count; i++) { this.cards.push(new DevCard('KNIGHT')); } for (let i = 0; i < DEV_CARDS.VICTORY_POINT.count; i++) { this.cards.push(new DevCard('VICTORY_POINT')); } for (let i = 0; i < DEV_CARDS.ROAD_BUILDING.count; i++) { this.cards.push(new DevCard('ROAD_BUILDING')); } for (let i = 0; i < DEV_CARDS.YEAR_OF_PLENTY.count; i++) { this.cards.push(new DevCard('YEAR_OF_PLENTY')); } for (let i = 0; i < DEV_CARDS.MONOPOLY.count; i++) { this.cards.push(new DevCard('MONOPOLY')); } // 셔플 this.shuffle(); } shuffle() { this.cards = shuffleArray(this.cards); } draw() { if (this.cards.length === 0) { return null; } return this.cards.pop(); } getRemainingCount() { return this.cards.length; } } ``` --- ## Step 2: DevCardManager 클래스 ```javascript // js/game/DevCardManager.js class DevCardManager { constructor(game) { this.game = game; this.deck = new DevCardDeck(); this.cardUsedThisTurn = false; } // 개발 카드 구매 buyCard(playerId) { const player = this.game.players[playerId]; // 검증 if (!player.hasResources(BUILD_COSTS.DEV_CARD)) { throw new Error('자원이 부족합니다.'); } const card = this.deck.draw(); if (!card) { throw new Error('개발 카드가 모두 소진되었습니다.'); } // 자원 차감 player.removeResources(BUILD_COSTS.DEV_CARD); // 카드 추가 (이번 턴에는 사용 불가) card.turnBought = this.game.turnNumber; player.newDevCards.push(card); const cardInfo = card.getInfo(); logMessage(`${player.name}이(가) 개발 카드를 구매했습니다.`); // 인간 플레이어에게만 카드 종류 표시 if (!player.isAI) { logMessage(` → ${cardInfo.emoji} ${cardInfo.name}`); } return card; } // 개발 카드 사용 가능 여부 canUseCard(playerId, cardIndex) { const player = this.game.players[playerId]; if (cardIndex >= player.devCards.length) { return false; } const card = player.devCards[cardIndex]; // 승점 카드는 사용하는 것이 아님 if (card.type === 'VICTORY_POINT') { return false; } // 이번 턴에 이미 사용했으면 불가 if (this.cardUsedThisTurn) { return false; } return true; } // 개발 카드 사용 async useCard(playerId, cardIndex) { const player = this.game.players[playerId]; const card = player.devCards[cardIndex]; if (!this.canUseCard(playerId, cardIndex)) { throw new Error('이 카드를 사용할 수 없습니다.'); } const cardInfo = card.getInfo(); logMessage(`${player.name}이(가) ${cardInfo.emoji} ${cardInfo.name} 카드를 사용했습니다.`); // 카드 타입별 처리 switch (card.type) { case 'KNIGHT': await this.useKnight(playerId); player.usedKnights++; this.game.checkLargestArmy(); break; case 'ROAD_BUILDING': await this.useRoadBuilding(playerId); break; case 'YEAR_OF_PLENTY': await this.useYearOfPlenty(playerId); break; case 'MONOPOLY': await this.useMonopoly(playerId); break; } // 카드 사용 완료 card.used = true; player.devCards.splice(cardIndex, 1); this.cardUsedThisTurn = true; } // 기사 카드 - 도둑 이동 async useKnight(playerId) { return new Promise((resolve) => { if (this.game.players[playerId].isAI) { // AI 처리 const ai = this.game.getAIForPlayer(this.game.players[playerId]); const { tileId, victimId } = ai.chooseRobberPlacement(this.game.board); this.game.moveRobber(tileId, victimId); resolve(); } else { // 인간 플레이어: 도둑 이동 UI this.game.uiManager.startRobberPlacement(resolve); } }); } // 도로 건설 카드 - 무료 도로 2개 async useRoadBuilding(playerId) { const player = this.game.players[playerId]; for (let i = 0; i < 2; i++) { if (player.roads >= player.maxRoads) break; await new Promise((resolve) => { if (player.isAI) { const ai = this.game.getAIForPlayer(player); const validLocations = this.game.buildingManager.getValidRoadLocations(playerId); if (validLocations.length > 0) { const chosen = ai.chooseBuildLocation('road', validLocations); this.game.buildingManager.buildRoad(chosen, playerId, true); } resolve(); } else { logMessage(`도로 ${i + 1}/2을(를) 배치하세요.`); this.game.uiManager.startRoadPlacement(playerId, true, resolve); } }); } } // 풍년 카드 - 자원 2개 선택 async useYearOfPlenty(playerId) { const player = this.game.players[playerId]; return new Promise((resolve) => { if (player.isAI) { // AI: 가장 필요한 자원 2개 선택 const ai = this.game.getAIForPlayer(player); const resources = ai.chooseYearOfPlentyResources(); resources.forEach(r => player.addResources({ [r]: 1 })); logMessage(` → ${resources.map(r => getResourceEmoji(r)).join(', ')} 획득`); resolve(); } else { // 인간 플레이어: 자원 선택 UI this.game.uiManager.showResourcePicker(2, (selected) => { selected.forEach(r => player.addResources({ [r]: 1 })); logMessage(` → ${selected.map(r => getResourceEmoji(r)).join(', ')} 획득`); resolve(); }); } }); } // 독점 카드 - 특정 자원 모두 가져오기 async useMonopoly(playerId) { const player = this.game.players[playerId]; return new Promise((resolve) => { if (player.isAI) { // AI: 가장 많이 가져올 수 있는 자원 선택 const ai = this.game.getAIForPlayer(player); const resource = ai.chooseMonopolyResource(this.game.players); this.executeMonopoly(playerId, resource); resolve(); } else { // 인간 플레이어: 자원 선택 UI this.game.uiManager.showResourcePicker(1, (selected) => { this.executeMonopoly(playerId, selected[0]); resolve(); }, '독점할 자원을 선택하세요'); } }); } executeMonopoly(playerId, resource) { const player = this.game.players[playerId]; let totalStolen = 0; this.game.players.forEach((p, index) => { if (index === playerId) return; const amount = p.resources[resource]; if (amount > 0) { p.resources[resource] = 0; totalStolen += amount; logMessage(` → ${p.name}에게서 ${getResourceEmoji(resource)}×${amount} 획득`); } }); player.resources[resource] += totalStolen; logMessage(` 총 ${getResourceEmoji(resource)}×${totalStolen} 획득!`); } // 턴 종료 시 리셋 onTurnEnd() { this.cardUsedThisTurn = false; } } ``` --- ## Step 3: 최대 기사단 확인 ```javascript // Game 클래스에 추가 class Game { // 최대 기사단 확인 checkLargestArmy() { let maxKnights = 2; // 최소 3장 이상이어야 함 let leader = null; this.players.forEach(player => { if (player.usedKnights > maxKnights) { maxKnights = player.usedKnights; leader = player; } }); // 최대 기사단 업데이트 this.players.forEach(player => { const hadIt = player.hasLargestArmy; player.hasLargestArmy = (player === leader); if (!hadIt && player.hasLargestArmy) { logMessage(`⚔️ ${player.name}이(가) 최대 기사단을 획득했습니다!`); } }); } } ``` --- ## Step 4: 개발 카드 UI ```javascript // js/ui/DevCardUI.js class DevCardUI { constructor(game, uiManager) { this.game = game; this.uiManager = uiManager; this.bindEvents(); } bindEvents() { document.getElementById('btn-buy-card').addEventListener('click', () => { this.buyCard(); }); } buyCard() { try { this.game.devCardManager.buyCard(0); this.uiManager.updateUI(); this.updateDevCardsDisplay(); } catch (error) { logMessage(`⚠️ ${error.message}`); } } updateDevCardsDisplay() { const container = document.getElementById('my-dev-cards'); const player = this.game.players[0]; container.innerHTML = ''; if (player.devCards.length === 0 && player.newDevCards.length === 0) { container.innerHTML = '