# 웹 카탄 게임 만들기 - Part 3: 자원 시스템 및 주사위 ## 이번 파트에서 구현할 내용 - Player 클래스와 자원 관리 - 주사위 굴리기 시스템 - 자원 분배 로직 - UI에 자원 표시 - 숫자 7과 도둑 처리 --- ## Step 1: Player 클래스 구현 ```javascript // js/game/Player.js class Player { constructor(id, name, color, isAI = false, aiDifficulty = null) { this.id = id; this.name = name; this.color = color; this.isAI = isAI; this.aiDifficulty = aiDifficulty; // 자원 this.resources = { [RESOURCES.GRAIN]: 0, [RESOURCES.BRICK]: 0, [RESOURCES.LUMBER]: 0, [RESOURCES.WOOL]: 0, [RESOURCES.ORE]: 0 }; // 개발 카드 this.devCards = []; // 보유 카드 this.usedKnights = 0; // 사용한 기사 카드 수 this.newDevCards = []; // 이번 턴에 구매한 카드 (사용 불가) // 건물 수 this.settlements = 0; this.cities = 0; this.roads = 0; // 특별 점수 this.hasLongestRoad = false; this.hasLargestArmy = false; // 최대 건물 수 this.maxSettlements = 5; this.maxCities = 4; this.maxRoads = 15; } // 총 자원 수 getTotalResources() { return Object.values(this.resources).reduce((sum, count) => sum + count, 0); } // 승점 계산 getVictoryPoints() { let points = 0; points += this.settlements; // 정착지 1점 points += this.cities * 2; // 도시 2점 if (this.hasLongestRoad) points += 2; // 최장 도로 2점 if (this.hasLargestArmy) points += 2; // 최대 기사단 2점 // 승점 개발 카드 this.devCards.forEach(card => { if (card.type === 'VICTORY_POINT') points += 1; }); return points; } // 자원 추가 addResources(resources) { for (const [resource, amount] of Object.entries(resources)) { this.resources[resource] = (this.resources[resource] || 0) + amount; } } // 자원 차감 removeResources(resources) { for (const [resource, amount] of Object.entries(resources)) { this.resources[resource] = Math.max(0, (this.resources[resource] || 0) - amount); } } // 자원 충분한지 확인 hasResources(cost) { for (const [resource, amount] of Object.entries(cost)) { if ((this.resources[resource] || 0) < amount) { return false; } } return true; } // 건설 가능 여부 확인 canBuild(buildingType) { switch (buildingType) { case 'road': return this.roads < this.maxRoads && this.hasResources(BUILD_COSTS.ROAD); case 'settlement': return this.settlements < this.maxSettlements && this.hasResources(BUILD_COSTS.SETTLEMENT); case 'city': return this.settlements > 0 && this.cities < this.maxCities && this.hasResources(BUILD_COSTS.CITY); case 'devCard': return this.hasResources(BUILD_COSTS.DEV_CARD); default: return false; } } // 랜덤하게 자원 하나 가져가기 (도둑용) stealRandomResource() { const availableResources = []; for (const [resource, count] of Object.entries(this.resources)) { for (let i = 0; i < count; i++) { availableResources.push(resource); } } if (availableResources.length === 0) return null; const stolen = randomChoice(availableResources); this.resources[stolen]--; return stolen; } // 7이 나왔을 때 자원 절반 버리기 discardHalfResources() { const total = this.getTotalResources(); if (total <= 7) return {}; const toDiscard = Math.floor(total / 2); const discarded = {}; let discardedCount = 0; // AI는 가장 많은 자원부터 버림 while (discardedCount < toDiscard) { const maxResource = Object.entries(this.resources) .filter(([_, count]) => count > 0) .sort((a, b) => b[1] - a[1])[0]; if (!maxResource) break; const [resource] = maxResource; this.resources[resource]--; discarded[resource] = (discarded[resource] || 0) + 1; discardedCount++; } return discarded; } // 턴 종료 시 새 개발카드를 사용 가능하게 이동 endTurn() { this.devCards.push(...this.newDevCards); this.newDevCards = []; } } ``` --- ## Step 2: 주사위 시스템 ```javascript // js/game/Dice.js class DiceManager { constructor() { this.dice1 = 1; this.dice2 = 1; this.isRolling = false; } // 주사위 굴리기 roll() { this.dice1 = Math.floor(Math.random() * 6) + 1; this.dice2 = Math.floor(Math.random() * 6) + 1; return this.getTotal(); } getTotal() { return this.dice1 + this.dice2; } // 애니메이션과 함께 굴리기 async rollWithAnimation(dice1El, dice2El) { if (this.isRolling) return null; this.isRolling = true; const diceEmojis = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅']; // 애니메이션 클래스 추가 dice1El.classList.add('rolling'); dice2El.classList.add('rolling'); // 애니메이션 동안 랜덤하게 표시 const animationDuration = 500; const frameRate = 50; const frames = animationDuration / frameRate; for (let i = 0; i < frames; i++) { dice1El.textContent = diceEmojis[Math.floor(Math.random() * 6)]; dice2El.textContent = diceEmojis[Math.floor(Math.random() * 6)]; await delay(frameRate); } // 최종 결과 this.roll(); dice1El.textContent = diceEmojis[this.dice1 - 1]; dice2El.textContent = diceEmojis[this.dice2 - 1]; // 애니메이션 클래스 제거 dice1El.classList.remove('rolling'); dice2El.classList.remove('rolling'); this.isRolling = false; return this.getTotal(); } } ``` --- ## Step 3: 자원 분배 로직 ```javascript // Game 클래스에 추가할 메서드들 class Game { constructor() { this.board = new Board(); this.players = []; this.currentPlayerIndex = 0; this.phase = GAME_PHASES.SETUP; this.diceManager = new DiceManager(); this.turnNumber = 0; } // 주사위 결과에 따른 자원 분배 distributeResources(diceRoll) { if (diceRoll === 7) { return this.handleRobber(); } const production = {}; // 해당 숫자의 타일 찾기 this.board.tiles.forEach(tile => { if (tile.number !== diceRoll || tile.hasRobber) return; const resource = tile.getInfo().resource; if (!resource) return; // 타일에 인접한 건물 확인 tile.vertexIds.forEach(vertexId => { const vertex = this.board.vertices[vertexId]; if (!vertex.building) return; const playerId = vertex.building.playerId; const amount = vertex.building.type === 'city' ? 2 : 1; // 자원 추가 this.players[playerId].addResources({ [resource]: amount }); // 로그용 if (!production[playerId]) production[playerId] = {}; production[playerId][resource] = (production[playerId][resource] || 0) + amount; }); }); // 로그 출력 for (const [playerId, resources] of Object.entries(production)) { const player = this.players[playerId]; const resourceList = Object.entries(resources) .map(([r, a]) => `${getResourceEmoji(r)} ${a}`) .join(', '); logMessage(`${player.name}이(가) ${resourceList}을(를) 획득했습니다.`); } return production; } // 7이 나왔을 때 처리 handleRobber() { // 1. 8장 이상 가진 플레이어는 절반 버림 this.players.forEach(player => { if (player.getTotalResources() > 7) { const discarded = player.discardHalfResources(); const discardList = Object.entries(discarded) .map(([r, a]) => `${getResourceEmoji(r)} ${a}`) .join(', '); logMessage(`${player.name}이(가) ${discardList}을(를) 버렸습니다.`); } }); // 2. 도둑 이동 단계로 this.phase = GAME_PHASES.ROBBER; return { robber: true }; } // 도둑 이동 처리 moveRobber(tileId, victimId = null) { this.board.moveRobber(tileId); // 피해자에게서 자원 훔치기 if (victimId !== null && victimId !== this.currentPlayerIndex) { const victim = this.players[victimId]; const stolen = victim.stealRandomResource(); if (stolen) { this.players[this.currentPlayerIndex].addResources({ [stolen]: 1 }); logMessage( `${this.getCurrentPlayer().name}이(가) ` + `${victim.name}에게서 ${getResourceEmoji(stolen)}을(를) 훔쳤습니다.` ); } } this.phase = GAME_PHASES.MAIN; } getCurrentPlayer() { return this.players[this.currentPlayerIndex]; } } ``` --- ## Step 4: UI 자원 표시 ```javascript // js/ui/UIManager.js class UIManager { constructor(game) { this.game = game; this.boardRenderer = null; } initialize() { this.boardRenderer = new BoardRenderer(this.game.board, 'game-board'); this.boardRenderer.initialize(); this.boardRenderer.render(); this.bindEvents(); this.updateUI(); } // 자원 표시 업데이트 updateResourcesDisplay() { const container = document.getElementById('my-resources'); const player = this.game.players[0]; // 인간 플레이어 container.innerHTML = ''; const resourceList = [ { key: RESOURCES.GRAIN, emoji: '🌾', name: '밀' }, { key: RESOURCES.BRICK, emoji: '🧱', name: '벽돌' }, { key: RESOURCES.LUMBER, emoji: '🪵', name: '목재' }, { key: RESOURCES.WOOL, emoji: '🐑', name: '양모' }, { key: RESOURCES.ORE, emoji: '�ite', name: '광석' } ]; resourceList.forEach(({ key, emoji, name }) => { const div = document.createElement('div'); div.className = 'resource-item'; div.innerHTML = ` ${emoji} ${player.resources[key]} ${name} `; container.appendChild(div); }); } // 플레이어 정보 패널 업데이트 updatePlayersPanel() { const container = document.getElementById('players-info'); container.innerHTML = ''; this.game.players.forEach((player, index) => { const isCurrentTurn = index === this.game.currentPlayerIndex; const div = document.createElement('div'); div.className = `player-info ${isCurrentTurn ? 'current-turn' : ''}`; div.innerHTML = `