====== 🧠 λ°”λ‘‘ κ²Œμž„ 둜직 ====== Phaser Baduk Metaverse ν”„λ‘œμ νŠΈμ˜ λ°”λ‘‘ κ²Œμž„ 둜직과 κ·œμΉ™ κ΅¬ν˜„μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. ===== πŸ“‹ κ°œμš” ===== λ°”λ‘‘ κ²Œμž„μ˜ 핡심 κ·œμΉ™κ³Ό λ‘œμ§μ„ JavaScript둜 κ΅¬ν˜„ν•˜μ—¬ μ •ν™•ν•œ λ°”λ‘‘ κ²Œμž„μ„ μ œκ³΅ν•©λ‹ˆλ‹€. {{https://api.dreamofenc.com/uploads/20250728171755_image.png?600}} ===== 🎯 κΈ°λ³Έ κ·œμΉ™ ===== ''λ°”λ‘‘μ˜ κΈ°λ³Έ κ·œμΉ™:'' * ''착수'': ν‘λŒλΆ€ν„° μ‹œμž‘ν•˜μ—¬ λ²ˆκ°ˆμ•„κ°€λ©° λŒμ„ λ†“μŠ΅λ‹ˆλ‹€. * ''μžμœ λ„'': λŒμ€ μ΅œμ†Œ ν•˜λ‚˜μ˜ μžμœ λ„λ₯Ό κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€. * ''착수'': μƒλŒ€λ°© 돌의 μžμœ λ„λ₯Ό λͺ¨λ‘ μ—†μ• λ©΄ κ·Έ λŒμ„ μ œκ±°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. * ''자좩'': μžμ‹ μ˜ λŒμ„ μžμœ λ„ 없이 놓을 수 μ—†μŠ΅λ‹ˆλ‹€. * ''계가'': κ²Œμž„ μ’…λ£Œ ν›„ 점수λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€. ---- ==== 1. κ²Œμž„ μƒνƒœ 관리 ==== 이 μ„Ήμ…˜μ—μ„œλŠ” λ°”λ‘‘ κ²Œμž„μ˜ ν˜„μž¬ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³  μ €μž₯ν•˜λŠ” ''BadukGameState'' ν΄λž˜μŠ€μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. 이 ν΄λž˜μŠ€λŠ” λ°”λ‘‘νŒμ˜ 돌 배치, ν˜„μž¬ ν”Œλ ˆμ΄μ–΄, μž‘μ€ 돌의 수, κ²Œμž„ 단계 λ“± κ²Œμž„μ˜ λͺ¨λ“  μ€‘μš”ν•œ 정보λ₯Ό ν¬ν•¨ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. === 1) μƒμ„±μž (Constructor) - κ²Œμž„ μ΄ˆκΈ°ν™” === 이 μ½”λ“œλŠ” ''BadukGameState'' 객체가 생성될 λ•Œ 호좜되며, κ²Œμž„μ˜ 초기 μƒνƒœλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€. λ°”λ‘‘νŒμ„ 19x19 격자둜 μ΄ˆκΈ°ν™”ν•˜κ³ , ν˜„μž¬ ν”Œλ ˆμ΄μ–΄λ₯Ό ν‘λŒλ‘œ μ„€μ •ν•˜λ©°, μž‘μ€ 돌의 μˆ˜μ™€ μ˜μ—­ 점수λ₯Ό 0으둜 λ§Œλ“­λ‹ˆλ‹€. κ²Œμž„ λ‹¨κ³„λŠ” 'playing'으둜 μ‹œμž‘ν•©λ‹ˆλ‹€. export default class BadukGameState { constructor() { this.board = Array(19).fill().map(() => Array(19).fill(null)); this.currentPlayer = 'black'; // 'black' λ˜λŠ” 'white' this.capturedStones = { black: 0, white: 0 }; this.territory = { black: 0, white: 0 }; this.gamePhase = 'playing'; // 'playing', 'counting', 'finished' this.moveHistory = []; this.lastMove = null; } === 2) μƒνƒœ μž¬μ„€μ • (Resetting State) === ''reset()'' λ©”μ„œλ“œλŠ” κ²Œμž„ μƒνƒœλ₯Ό 처음 μ‹œμž‘ν–ˆμ„ λ•Œμ™€ λ™μΌν•œ 초기 μƒνƒœλ‘œ λ˜λŒλ¦½λ‹ˆλ‹€. μƒˆλ‘œμš΄ κ²Œμž„μ„ μ‹œμž‘ν•  λ•Œλ‚˜ κ²Œμž„μ„ λ‹€μ‹œ ν”Œλ ˆμ΄ν•  λ•Œ μœ μš©ν•˜κ²Œ μ‚¬μš©λ©λ‹ˆλ‹€. // κ²Œμž„ μƒνƒœ μ΄ˆκΈ°ν™” reset() { this.board = Array(19).fill().map(() => Array(19).fill(null)); this.currentPlayer = 'black'; this.capturedStones = { black: 0, white: 0 }; this.territory = { black: 0, white: 0 }; this.gamePhase = 'playing'; this.moveHistory = []; this.lastMove = null; } === 3) μƒνƒœ 직렬화 (Serialization - toJSON) === ''toJSON()'' λ©”μ„œλ“œλŠ” ν˜„μž¬ κ²Œμž„ μƒνƒœλ₯Ό 일반 JavaScript 객체둜 λ³€ν™˜ν•©λ‹ˆλ‹€. μ΄λŠ” κ²Œμž„ μƒνƒœλ₯Ό μ €μž₯ν•˜κ±°λ‚˜ λ„€νŠΈμ›Œν¬λ₯Ό 톡해 전솑할 λ•Œ μœ μš©ν•©λ‹ˆλ‹€. λ³΅μž‘ν•œ 클래슀 μΈμŠ€ν„΄μŠ€ λŒ€μ‹  κ°„λ‹¨ν•œ 데이터 ꡬ쑰둜 λ³€ν™˜ν•˜μ—¬ μ‰½κ²Œ μ²˜λ¦¬ν•  수 있게 ν•©λ‹ˆλ‹€. // ν˜„μž¬ μƒνƒœλ₯Ό JSON으둜 직렬화 toJSON() { return { board: this.board, currentPlayer: this.currentPlayer, capturedStones: this.capturedStones, territory: this.territory, gamePhase: this.gamePhase, moveHistory: this.moveHistory, lastMove: this.lastMove }; } === 4) μƒνƒœ 볡원 (Deserialization - fromJSON) === ''fromJSON()'' λ©”μ„œλ“œλŠ” ''toJSON()''으둜 μ§λ ¬ν™”λœ 데이터 객체λ₯Ό λ°›μ•„ ''BadukGameState'' μΈμŠ€ν„΄μŠ€μ˜ μƒνƒœλ₯Ό ν•΄λ‹Ή λ°μ΄ν„°λ‘œ μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€. μ €μž₯된 κ²Œμž„μ„ λΆˆλŸ¬μ˜€κ±°λ‚˜ λ‹€λ₯Έ ν΄λΌμ΄μ–ΈνŠΈλ‘œλΆ€ν„° κ²Œμž„ μƒνƒœλ₯Ό 동기화할 λ•Œ μ‚¬μš©λ©λ‹ˆλ‹€. // JSONμ—μ„œ μƒνƒœ 볡원 fromJSON(data) { this.board = data.board; this.currentPlayer = data.currentPlayer; this.capturedStones = data.capturedStones; this.territory = data.territory; this.gamePhase = data.gamePhase; this.moveHistory = data.moveHistory; this.lastMove = data.lastMove; } } ---- ==== 2. 이동 검증 ==== 이 μ„Ήμ…˜μ—μ„œλŠ” ν”Œλ ˆμ΄μ–΄μ˜ μ°©μˆ˜κ°€ μœ νš¨ν•œμ§€ ν™•μΈν•˜λŠ” ''BadukMoveValidator'' ν΄λž˜μŠ€μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. λ°”λ‘‘μ˜ λ³΅μž‘ν•œ κ·œμΉ™(μžμœ λ„, 자좩, μ½” λ“±)을 μ μš©ν•˜μ—¬ μ˜¬λ°”λ₯Έ 착수만 ν—ˆμš©ν•˜λ„λ‘ ν•©λ‹ˆλ‹€. === 1) μƒμ„±μž (Constructor) === ''BadukMoveValidator''λŠ” ν˜„μž¬ κ²Œμž„ μƒνƒœ(''gameState'')λ₯Ό μ°Έμ‘°ν•˜μ—¬ μœ νš¨μ„± 검사λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€. 이λ₯Ό 톡해 λ°”λ‘‘νŒμ˜ ν˜„μž¬ 상황을 기반으둜 착수의 μœ νš¨μ„±μ„ νŒλ‹¨ν•  수 μžˆμŠ΅λ‹ˆλ‹€. export default class BadukMoveValidator { constructor(gameState) { this.gameState = gameState; } === 2) μœ νš¨ν•œ 이동 확인 (isValidMove) === ''isValidMove(x, y, player)'' λ©”μ„œλ“œλŠ” μ£Όμ–΄μ§„ μ’Œν‘œ ''(x, y)''에 ''player''κ°€ λŒμ„ λ†“μ•˜μ„ λ•Œ μœ νš¨ν•œ μ°©μˆ˜μΈμ§€ μ—¬λΆ€λ₯Ό ν™•μΈν•˜λŠ” 핡심 λ‘œμ§μž…λ‹ˆλ‹€. λ°”λ‘‘μ˜ μ—¬λŸ¬ κ·œμΉ™(λ°”λ‘‘νŒ λ²”μœ„, 이미 돌이 놓인 κ³³, 자좩, μ½” κ·œμΉ™)을 순차적으둜 κ²€μ‚¬ν•©λ‹ˆλ‹€. // 이동이 μœ νš¨ν•œμ§€ 확인 isValidMove(x, y, player) { // 1. λ°”λ‘‘νŒ λ²”μœ„ 확인 if (!this.isInBounds(x, y)) { return { valid: false, reason: 'λ°”λ‘‘νŒ λ²”μœ„λ₯Ό λ²—μ–΄λ‚¬μŠ΅λ‹ˆλ‹€.' }; } // 2. 이미 돌이 놓인 μœ„μΉ˜μΈμ§€ 확인 if (this.gameState.board[y][x] !== null) { return { valid: false, reason: '이미 돌이 놓인 μœ„μΉ˜μž…λ‹ˆλ‹€.' }; } // 3. 자좩 κ·œμΉ™ 확인 if (this.isSuicide(x, y, player)) { return { valid: false, reason: 'μžμΆ©μ€ ν—ˆμš©λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.' }; } // 4. μ½” κ·œμΉ™ 확인 (같은 μœ„μΉ˜μ— μ—°μ†μœΌλ‘œ 놓을 수 μ—†μŒ) if (this.isKoViolation(x, y)) { return { valid: false, reason: 'μ½” κ·œμΉ™ μœ„λ°˜μž…λ‹ˆλ‹€.' }; } return { valid: true, reason: 'μœ νš¨ν•œ μ΄λ™μž…λ‹ˆλ‹€.' }; } === 3) λ°”λ‘‘νŒ λ²”μœ„ 확인 (isInBounds) === 이 κ°„λ‹¨ν•œ μœ ν‹Έλ¦¬ν‹° λ©”μ„œλ“œλŠ” μ£Όμ–΄μ§„ μ’Œν‘œ ''(x, y)''κ°€ 19x19 λ°”λ‘‘νŒμ˜ μœ νš¨ν•œ λ²”μœ„ 내에 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. λͺ¨λ“  착수 및 μ£Όλ³€ 탐색 전에 ν•„μˆ˜μ μœΌλ‘œ μˆ˜ν–‰λ©λ‹ˆλ‹€. // λ°”λ‘‘νŒ λ²”μœ„ 확인 isInBounds(x, y) { return x >= 0 && x < 19 && y >= 0 && y < 19; } === 4) 자좩 확인 (isSuicide) === μžμΆ©μ€ λ°”λ‘‘μ—μ„œ μžμ‹ μ˜ λŒμ„ λ‘μ—ˆμ„ λ•Œ κ·Έ 돌이 μ¦‰μ‹œ 작히게 λ˜λŠ” 수λ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” μ£Όμ–΄μ§„ μœ„μΉ˜μ— μž„μ‹œλ‘œ λŒμ„ 놓아보고, ν•΄λ‹Ή 돌이 μžμœ λ„λ₯Ό κ°€μ§€λŠ”μ§€ λ˜λŠ” μƒλŒ€λ°© λŒμ„ μž‘μ„ 수 μžˆλŠ”μ§€ ν™•μΈν•˜μ—¬ 자좩 μ—¬λΆ€λ₯Ό νŒλ‹¨ν•©λ‹ˆλ‹€. 확인 ν›„μ—λŠ” λ°”λ‘‘νŒμ„ μ›λž˜ μƒνƒœλ‘œ λ˜λŒλ¦½λ‹ˆλ‹€. // 자좩 확인 isSuicide(x, y, player) { // μž„μ‹œλ‘œ λŒμ„ 놓아보고 μžμœ λ„ 확인 this.gameState.board[y][x] = player; const hasLiberties = this.hasLiberties(x, y); const canCapture = this.canCaptureOpponent(x, y, player); // μ›λž˜ μƒνƒœλ‘œ 볡원 this.gameState.board[y][x] = null; // μžμœ λ„κ°€ μ—†κ³  μƒλŒ€λ°©μ„ μž‘μ„ 수 μ—†λ‹€λ©΄ 자좩 return !hasLiberties && !canCapture; } === 5) 돌 그룹의 μžμœ λ„ 확인 (hasLiberties & checkGroupLiberties) === λ°”λ‘‘μ—μ„œ 돌이 μ‚΄κΈ° μœ„ν•΄μ„œλŠ” 'μžμœ λ„'(숨ꡬ멍)κ°€ ν•„μš”ν•©λ‹ˆλ‹€. ''hasLiberties()''λŠ” νŠΉμ • 돌이 μ†ν•œ κ·Έλ£Ή 전체가 μžμœ λ„λ₯Ό κ°€μ§€κ³  μžˆλŠ”μ§€ ν™•μΈν•˜λŠ” μ‹œμž‘μ μ΄λ©°, ''checkGroupLiberties()''λŠ” μž¬κ·€μ μœΌλ‘œ λ˜λŠ” μŠ€νƒμ„ μ‚¬μš©ν•˜μ—¬ 돌 그룹을 νƒμƒ‰ν•˜λ©° 빈 곡간(μžμœ λ„)을 μ°ΎμŠ΅λ‹ˆλ‹€. ''visited'' 집합은 이미 λ°©λ¬Έν•œ λŒμ„ μΆ”μ ν•˜μ—¬ λ¬΄ν•œ 루프λ₯Ό λ°©μ§€ν•©λ‹ˆλ‹€. // μžμœ λ„ 확인 hasLiberties(x, y) { const color = this.gameState.board[y][x]; if (!color) return false; const visited = new Set(); return this.checkGroupLiberties(x, y, color, visited); } // 그룹의 μžμœ λ„ 확인 checkGroupLiberties(x, y, color, visited) { const key = `${x},${y}`; if (visited.has(key)) return false; visited.add(key); const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; for (const [dx, dy] of directions) { const nx = x + dx; const ny = y + dy; if (!this.isInBounds(nx, ny)) continue; if (this.gameState.board[ny][nx] === null) { return true; // μžμœ λ„ 발견 } if (this.gameState.board[ny][nx] === color) { if (this.checkGroupLiberties(nx, ny, color, visited)) { return true; } } } return false; } === 6) μƒλŒ€λ°© 돌 μž‘μ„ 수 μžˆλŠ”μ§€ 확인 (canCaptureOpponent) === 이 λ©”μ„œλ“œλŠ” νŠΉμ • μœ„μΉ˜μ— λŒμ„ λ†“μ•˜μ„ λ•Œ, κ·Έ 돌 주변에 μžˆλŠ” μƒλŒ€λ°© 돌 그룹의 μžμœ λ„κ°€ λͺ¨λ‘ 사라져 μž‘μ„ 수 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. 착수 직후 μƒλŒ€λ°© λŒμ„ μ œκ±°ν•˜λŠ” 데 μ‚¬μš©λ˜λŠ” μ€‘μš”ν•œ λ‘œμ§μž…λ‹ˆλ‹€. // μƒλŒ€λ°© λŒμ„ μž‘μ„ 수 μžˆλŠ”μ§€ 확인 canCaptureOpponent(x, y, player) { const opponent = player === 'black' ? 'white' : 'black'; const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; for (const [dx, dy] of directions) { const nx = x + dx; const ny = y + dy; if (!this.isInBounds(nx, ny)) continue; if (this.gameState.board[ny][nx] === opponent) { if (!this.hasLiberties(nx, ny)) { return true; } } } return false; } === 7) μ½” κ·œμΉ™ μœ„λ°˜ 확인 (isKoViolation) === 'μ½”'λŠ” λ°”λ‘‘μ—μ„œ 같은 ν˜•νƒœκ°€ λ°˜λ³΅λ˜λŠ” 것을 λ°©μ§€ν•˜λŠ” κ·œμΉ™μž…λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” λ§ˆμ§€λ§‰ 이동이 μƒλŒ€λ°©μ˜ λŒμ„ ν•˜λ‚˜ μž‘λŠ” 'λ‹¨μˆ˜'μ˜€μ„ 경우, κ·Έ μœ„μΉ˜μ— μ¦‰μ‹œ λ‹€μ‹œ λŒμ„ λ†“λŠ” 것을 κΈˆμ§€ν•˜μ—¬ λ¬΄ν•œ λ°˜λ³΅μ„ λ§‰μŠ΅λ‹ˆλ‹€. // μ½” κ·œμΉ™ 확인 isKoViolation(x, y) { if (!this.gameState.lastMove) return false; // λ§ˆμ§€λ§‰ 이동이 μ°©μˆ˜μ˜€λ‹€λ©΄, 같은 μœ„μΉ˜μ— 놓을 수 μ—†μŒ if (this.gameState.lastMove.type === 'capture') { return x === this.gameState.lastMove.capturedX && y === this.gameState.lastMove.capturedY; } return false; } } ---- ==== 3. 착수 둜직 ==== 이 μ„Ήμ…˜μ—μ„œλŠ” μ‹€μ œλ‘œ λ°”λ‘‘νŒμ— λŒμ„ 놓고, 돌이 μž‘ν˜”μ„ λ•Œ 이λ₯Ό μ²˜λ¦¬ν•˜λŠ” ''BadukCaptureLogic'' ν΄λž˜μŠ€μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. === 1) μƒμ„±μž (Constructor) === ''BadukCaptureLogic'' ν΄λž˜μŠ€λŠ” κ²Œμž„ μƒνƒœ(''gameState'') 객체λ₯Ό λ°›μ•„ ν˜„μž¬ κ²Œμž„μ˜ λ°”λ‘‘νŒκ³Ό 기타 정보λ₯Ό μ‘°μž‘ν•©λ‹ˆλ‹€. export default class BadukCaptureLogic { constructor(gameState) { this.gameState = gameState; } === 2) 돌 놓기 및 착수 처리 (placeStone) === ''placeStone(x, y, player)'' λ©”μ„œλ“œλŠ” ν”Œλ ˆμ΄μ–΄κ°€ νŠΉμ • μœ„μΉ˜μ— λŒμ„ λ†“μœΌλ € ν•  λ•Œ ν˜ΈμΆœλ˜λŠ” 핡심 ν•¨μˆ˜μž…λ‹ˆλ‹€. * λ¨Όμ € ''BadukMoveValidator''λ₯Ό μ‚¬μš©ν•˜μ—¬ μ°©μˆ˜κ°€ μœ νš¨ν•œμ§€ ν™•μΈν•©λ‹ˆλ‹€. * μœ νš¨ν•˜λ©΄ λ°”λ‘‘νŒμ— λŒμ„ 놓고, κ·Έ 결과둜 μ£Όλ³€μ˜ μƒλŒ€λ°© 돌이 μž‘νžˆλŠ”μ§€ ''processCapture()''λ₯Ό 톡해 ν™•μΈν•©λ‹ˆλ‹€. * λͺ¨λ“  μ°©μˆ˜μ™€ 착수 κ²°κ³ΌλŠ” ''moveHistory''에 κΈ°λ‘λ©λ‹ˆλ‹€. * λ§ˆμ§€λ§‰μœΌλ‘œ ν˜„μž¬ ν”Œλ ˆμ΄μ–΄λ₯Ό λ‹€μŒ ν”Œλ ˆμ΄μ–΄(흑 -> λ°±, λ°± -> 흑)둜 λ³€κ²½ν•©λ‹ˆλ‹€. // 돌 놓기 및 착수 처리 placeStone(x, y, player) { const validator = new BadukMoveValidator(this.gameState); const validation = validator.isValidMove(x, y, player); if (!validation.valid) { return { success: false, reason: validation.reason }; } // 돌 놓기 this.gameState.board[y][x] = player; // 착수 확인 및 처리 const captured = this.processCapture(x, y, player); // 이동 기둝 this.gameState.moveHistory.push({ x: x, y: y, player: player, captured: captured, timestamp: Date.now() }); this.gameState.lastMove = { x: x, y: y, player: player, type: captured > 0 ? 'capture' : 'place' }; // ν”Œλ ˆμ΄μ–΄ λ³€κ²½ this.gameState.currentPlayer = player === 'black' ? 'white' : 'black'; return { success: true, captured: captured }; } === 3) 착수 μ‹€ν–‰ (processCapture) === λŒμ„ 놓은 ν›„, 이 λ©”μ„œλ“œλŠ” μƒˆλ‘œ 놓인 돌 μ£Όλ³€μ˜ 4λ°©ν–₯을 ν™•μΈν•©λ‹ˆλ‹€. λ§Œμ•½ μΈμ ‘ν•œ μƒλŒ€λ°© 돌 그룹이 μžμœ λ„λ₯Ό λͺ¨λ‘ μžƒμ—ˆλ‹€λ©΄, ''removeGroup()'' λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ ν•΄λ‹Ή 그룹의 λͺ¨λ“  λŒμ„ λ°”λ‘‘νŒμ—μ„œ μ œκ±°ν•˜κ³ , μž‘μ€ 돌의 수λ₯Ό μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€. // 착수 처리 processCapture(x, y, player) { const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; let totalCaptured = 0; const capturedPositions = []; for (const [dx, dy] of directions) { const nx = x + dx; const ny = y + dy; if (!this.isInBounds(nx, ny)) continue; const opponent = player === 'black' ? 'white' : 'black'; if (this.gameState.board[ny][nx] === opponent) { if (!this.hasLiberties(nx, ny)) { const captured = this.removeGroup(nx, ny); totalCaptured += captured; capturedPositions.push({ x: nx, y: ny, count: captured }); } } } // 착수 기둝 if (capturedPositions.length > 0) { this.gameState.lastMove.capturedPositions = capturedPositions; } this.gameState.capturedStones[player] += totalCaptured; return totalCaptured; } === 4) 돌 κ·Έλ£Ή 제거 (removeGroup) === 이 λ©”μ„œλ“œλŠ” 작힌 돌 그룹의 νŠΉμ • 돌 ''(x, y)''λ₯Ό μ‹œμž‘μ μœΌλ‘œ μ‚Όμ•„, ν•΄λ‹Ή 그룹에 μ†ν•œ λͺ¨λ“  λŒμ„ λ°”λ‘‘νŒμ—μ„œ μ œκ±°ν•©λ‹ˆλ‹€. μŠ€νƒ(Stack)을 μ΄μš©ν•œ 깊이 μš°μ„  탐색(DFS) λ°©μ‹μœΌλ‘œ κ·Έλ£Ή 전체λ₯Ό μˆœνšŒν•˜λ©° λŒμ„ ''null''둜 μ„€μ •ν•˜κ³ , 제거된 돌의 총 개수λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. // κ·Έλ£Ή 제거 removeGroup(x, y) { const color = this.gameState.board[y][x]; if (!color) return 0; let count = 0; const stack = [[x, y]]; while (stack.length > 0) { const [cx, cy] = stack.pop(); if (this.gameState.board[cy][cx] === color) { this.gameState.board[cy][cx] = null; count++; const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; for (const [dx, dy] of directions) { const nx = cx + dx; const ny = cy + dy; if (this.isInBounds(nx, ny) && this.gameState.board[ny][nx] === color) { stack.push([nx, ny]); } } } } return count; } === 5) μžμœ λ„ 확인 μœ ν‹Έλ¦¬ν‹° (hasLiberties, checkGroupLiberties, isInBounds) === 이 λ©”μ„œλ“œλ“€μ€ ''BadukMoveValidator'' ν΄λž˜μŠ€μ— μžˆλŠ” 것과 λ™μΌν•œ μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜μž…λ‹ˆλ‹€. 돌 그룹의 μžμœ λ„λ₯Ό ν™•μΈν•˜κ³  λ°”λ‘‘νŒ λ²”μœ„ 내에 μžˆλŠ”μ§€ ν™•μΈν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. μ½”λ“œ 쀑볡을 ν”Όν•˜κΈ° μœ„ν•΄ λ³„λ„μ˜ μœ ν‹Έλ¦¬ν‹° λͺ¨λ“ˆλ‘œ 뢄리할 μˆ˜λ„ μžˆμ§€λ§Œ, μ—¬κΈ°μ„œλŠ” ν•΄λ‹Ή 둜직 λ‚΄μ—μ„œ 직접 ν¬ν•¨λ˜μ–΄ μ‚¬μš©λ©λ‹ˆλ‹€. // μžμœ λ„ 확인 (BadukMoveValidator와 동일) hasLiberties(x, y) { const color = this.gameState.board[y][x]; if (!color) return false; const visited = new Set(); return this.checkGroupLiberties(x, y, color, visited); } checkGroupLiberties(x, y, color, visited) { const key = `${x},${y}`; if (visited.has(key)) return false; visited.add(key); const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; for (const [dx, dy] of directions) { const nx = x + dx; const ny = y + dy; if (!this.isInBounds(nx, ny)) continue; if (this.gameState.board[ny][nx] === null) { return true; } if (this.gameState.board[ny][nx] === color) { if (this.checkGroupLiberties(nx, ny, color, visited)) { return true; } } } return false; } isInBounds(x, y) { return x >= 0 && x < 19 && y >= 0 && y < 19; } } ---- ==== 4. 패슀 및 항볡 ==== 이 μ„Ήμ…˜μ—μ„œλŠ” ν”Œλ ˆμ΄μ–΄κ°€ λŒμ„ λ†“λŠ” λŒ€μ‹  '패슀'ν•˜κ±°λ‚˜ '항볡'ν•˜λŠ” 경우의 둜직, 그리고 κ²Œμž„μ΄ μ’…λ£Œλ  λ•Œ μ˜μ—­κ³Ό 점수λ₯Ό κ³„μ‚°ν•˜λŠ” ''BadukPassLogic'' ν΄λž˜μŠ€μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. === 1) μƒμ„±μž (Constructor) === ''BadukPassLogic'' ν΄λž˜μŠ€λŠ” ν˜„μž¬ κ²Œμž„ μƒνƒœ(''gameState'')λ₯Ό μ°Έμ‘°ν•˜λ©°, μ—°μ†μœΌλ‘œ νŒ¨μŠ€ν•œ 횟수λ₯Ό μΆ”μ ν•˜λŠ” ''consecutivePasses'' λ³€μˆ˜λ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€. export default class BadukPassLogic { constructor(gameState) { this.gameState = gameState; this.consecutivePasses = 0; } === 2) 패슀 처리 (pass) === ν”Œλ ˆμ΄μ–΄κ°€ '패슀' λ²„νŠΌμ„ λˆŒλ €μ„ λ•Œ ν˜ΈμΆœλ©λ‹ˆλ‹€. ''consecutivePasses'' 횟수λ₯Ό μ¦κ°€μ‹œν‚€κ³ , 패슀 기둝을 ''moveHistory''에 μΆ”κ°€ν•©λ‹ˆλ‹€. μ—°μ†μœΌλ‘œ 두 번 νŒ¨μŠ€ν•˜λ©΄ κ²Œμž„μ΄ μ’…λ£Œλ©λ‹ˆλ‹€. // 패슀 처리 pass(player) { this.consecutivePasses++; this.gameState.moveHistory.push({ type: 'pass', player: player, timestamp: Date.now() }); this.gameState.currentPlayer = player === 'black' ? 'white' : 'black'; // 연속 νŒ¨μŠ€κ°€ 2번이면 κ²Œμž„ μ’…λ£Œ if (this.consecutivePasses >= 2) { this.endGame(); } return { success: true, consecutivePasses: this.consecutivePasses }; } === 3) 항볡 처리 (surrender) === ν”Œλ ˆμ΄μ–΄κ°€ '항볡'ν–ˆμ„ λ•Œ ν˜ΈμΆœλ©λ‹ˆλ‹€. ν•­λ³΅ν•œ ν”Œλ ˆμ΄μ–΄μ˜ μƒλŒ€λ°©μ„ 승자둜 μ„ μ–Έν•˜κ³ , κ²Œμž„ 단계λ₯Ό 'finished'둜 λ³€κ²½ν•˜μ—¬ μ¦‰μ‹œ κ²Œμž„μ„ μ’…λ£Œν•©λ‹ˆλ‹€. 항볡 기둝 λ˜ν•œ ''moveHistory''에 μΆ”κ°€λ©λ‹ˆλ‹€. // 항볡 처리 surrender(player) { const winner = player === 'black' ? 'white' : 'black'; this.gameState.gamePhase = 'finished'; this.gameState.winner = winner; this.gameState.surrender = true; this.gameState.moveHistory.push({ type: 'surrender', player: player, timestamp: Date.now() }); return { success: true, winner: winner }; } --- === 4) κ²Œμž„ μ’…λ£Œ (endGame) === 두 ν”Œλ ˆμ΄μ–΄κ°€ μ—°μ†μœΌλ‘œ νŒ¨μŠ€ν–ˆκ±°λ‚˜ ν•œ ν”Œλ ˆμ΄μ–΄κ°€ ν•­λ³΅ν•˜μ—¬ κ²Œμž„μ΄ μ’…λ£Œλ  λ•Œ ν˜ΈμΆœλ©λ‹ˆλ‹€. κ²Œμž„ 단계λ₯Ό 'counting'(계가)으둜 λ³€κ²½ν•˜κ³ , ''calculateTerritory()'' 및 ''calculateFinalScore()'' λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ μ΅œμ’… 점수λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€. // κ²Œμž„ μ’…λ£Œ endGame() { this.gameState.gamePhase = 'counting'; this.calculateTerritory(); this.calculateFinalScore(); } ---- === 5) μ˜μ—­ 계산 (calculateTerritory & analyzeTerritory) === λ°”λ‘‘νŒμ˜ 빈 곡간(μ§‘)을 νƒμƒ‰ν•˜μ—¬ 각 μ˜μ—­μ΄ μ–΄λŠ ν”Œλ ˆμ΄μ–΄μ˜ μ†Œμœ μΈμ§€ κ³„μ‚°ν•©λ‹ˆλ‹€. * ''calculateTerritory()''λŠ” λ°”λ‘‘νŒμ˜ λͺ¨λ“  빈 곡간을 μˆœνšŒν•˜λ©° 아직 λ°©λ¬Έν•˜μ§€ μ•Šμ€ 빈 지점을 μ°Ύμ•„ ''analyzeTerritory()''λ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€. * ''analyzeTerritory()''λŠ” μ‹œμž‘ μ§€μ μœΌλ‘œλΆ€ν„° μ—°κ²°λœ 빈 곡간듀을 νƒμƒ‰ν•˜κ³ , ν•΄λ‹Ή μ˜μ—­μ„ λ‘˜λŸ¬μ‹Έκ³  μžˆλŠ” 돌이 ν‘λŒλ§Œ μžˆλŠ”μ§€ 백돌만 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. 이λ₯Ό 톡해 μ˜μ—­μ˜ μ†Œμœ μžλ₯Ό νŒλ‹¨ν•˜κ³ , ν•΄λ‹Ή μ˜μ—­μ˜ 크기λ₯Ό μ μˆ˜μ— λ°˜μ˜ν•©λ‹ˆλ‹€. // μ˜μ—­ 계산 calculateTerritory() { const visited = Array(19).fill().map(() => Array(19).fill(false)); for (let y = 0; y < 19; y++) { for (let x = 0; x < 19; x++) { if (!visited[y][x] && this.gameState.board[y][x] === null) { this.analyzeTerritory(x, y, visited); } } } } // μ˜μ—­ 뢄석 analyzeTerritory(x, y, visited) { const territory = []; const stack = [[x, y]]; let blackStones = 0; let whiteStones = 0; while (stack.length > 0) { const [cx, cy] = stack.pop(); if (visited[cy][cx]) continue; visited[cy][cx] = true; if (this.gameState.board[cy][cx] === null) { territory.push([cx, cy]); const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]]; for (const [dx, dy] of directions) { const nx = cx + dx; const ny = cy + dy; if (this.isInBounds(nx, ny) && !visited[ny][nx]) { stack.push([nx, ny]); } } } else if (this.gameState.board[cy][cx] === 'black') { blackStones++; } else if (this.gameState.board[cy][cx] === 'white') { whiteStones++; } } // μ˜μ—­ μ†Œμœ μž κ²°μ • if (territory.length > 0) { const owner = this.determineTerritoryOwner(blackStones, whiteStones); if (owner) { this.gameState.territory[owner] += territory.length; } } } ---- === 6) μ˜μ—­ μ†Œμœ μž κ²°μ • (determineTerritoryOwner) === 이 λ©”μ„œλ“œλŠ” νŠΉμ • 빈 μ˜μ—­μ„ λ‘˜λŸ¬μ‹Έκ³  μžˆλŠ” 돌의 색깔을 λΆ„μ„ν•˜μ—¬ ν•΄λ‹Ή μ˜μ—­μ˜ μ†Œμœ μžλ₯Ό κ²°μ •ν•©λ‹ˆλ‹€. μ˜μ—­ 주변에 ν‘λŒλ§Œ 있으면 ν‘μ˜ μ˜μ—­μœΌλ‘œ, 백돌만 있으면 백의 μ˜μ—­μœΌλ‘œ νŒλ‹¨ν•©λ‹ˆλ‹€. λ§Œμ•½ ν‘λŒκ³Ό 백돌이 λͺ¨λ‘ μ‘΄μž¬ν•˜κ±°λ‚˜ 돌이 μ „ν˜€ μ—†λŠ” 경우 (예: μ„Έν‚€), ν•΄λ‹Ή μ˜μ—­μ€ μ†Œμœ μžκ°€ μ—†λŠ” κ²ƒμœΌλ‘œ κ°„μ£Όλ©λ‹ˆλ‹€. // μ˜μ—­ μ†Œμœ μž κ²°μ • determineTerritoryOwner(blackStones, whiteStones) { if (blackStones > 0 && whiteStones === 0) { return 'black'; } else if (whiteStones > 0 && blackStones === 0) { return 'white'; } else { return null; // μ†Œμœ μž μ—†μŒ (예: μ„Έν‚€ λ˜λŠ” 곡톡 μ˜μ—­) } } ===== 🎲 λ°”λ‘‘ κ²Œμž„ 핡심 κΈ°λŠ₯ ===== ==== 1. κ²Œμž„ μƒνƒœ 및 점수 계산 ==== 이 μ„Ήμ…˜μ—μ„œλŠ” λ°”λ‘‘ κ²Œμž„μ˜ ν˜„μž¬ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³ , κ²Œμž„ μ’…λ£Œ μ‹œ μ΅œμ’… 점수λ₯Ό κ³„μ‚°ν•˜λ©°, λ°”λ‘‘νŒ λ‚΄ μœ νš¨ν•œ μœ„μΉ˜λ₯Ό ν™•μΈν•˜λŠ” 핡심 ν•¨μˆ˜μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. ---- === 1) μ΅œμ’… 점수 계산 === ''calculateFinalScore'' ν•¨μˆ˜λŠ” κ²Œμž„μ΄ μ’…λ£Œλœ ν›„ 흑과 백의 μ΅œμ’… 점수λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€. 이 ν•¨μˆ˜λŠ” 각 ν”Œλ ˆμ΄μ–΄κ°€ μ°¨μ§€ν•œ μ˜ν† μ™€ νšλ“ν•œ 포둜의 수λ₯Ό ν•©μ‚°ν•˜μ—¬ 총점을 κ²°μ •ν•©λ‹ˆλ‹€. λ°±μ—κ²ŒλŠ” μΆ”κ°€λ‘œ 6.5μ§‘μ˜ λ°˜μ§‘(덀, komi)이 λΆ€μ—¬λ©λ‹ˆλ‹€. calculateFinalScore() { const blackScore = this.gameState.territory.black + this.gameState.capturedStones.black; const whiteScore = this.gameState.territory.white + this.gameState.capturedStones.white + 6.5; // 백의 λ°˜μ§‘ this.gameState.finalScore = { black: blackScore, white: whiteScore, winner: blackScore > whiteScore ? 'black' : 'white' }; } ---- === 2) λ³΄λ“œ λ‚΄ μœ„μΉ˜ μœ νš¨μ„± 검사 === ''isInBounds'' ν•¨μˆ˜λŠ” μ£Όμ–΄μ§„ μ’Œν‘œ (x, y)κ°€ λ°”λ‘‘νŒμ˜ μœ νš¨ν•œ λ²”μœ„(0λΆ€ν„° 18κΉŒμ§€) 내에 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. μ΄λŠ” λŒμ„ λ†“κ±°λ‚˜ νŠΉμ • μœ„μΉ˜λ₯Ό μ°Έμ‘°ν•  λ•Œ ν•„μˆ˜μ μΈ κ²€μ‚¬μž…λ‹ˆλ‹€. isInBounds(x, y) { return x >= 0 && x < 19 && y >= 0 && y < 19; } ===== πŸ“Š κ²Œμž„ 톡계 ===== 이 μ„Ήμ…˜μ—μ„œλŠ” λ°”λ‘‘ κ²Œμž„μ˜ λ‹€μ–‘ν•œ 톡계λ₯Ό κΈ°λ‘ν•˜κ³  κ΄€λ¦¬ν•˜λŠ” 방법에 λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. ==== 1. κ²Œμž„ 톡계 관리 클래슀 (BadukStatistics) ==== ''BadukStatistics'' ν΄λž˜μŠ€λŠ” λ°”λ‘‘ κ²Œμž„μ˜ 전체적인 톡계λ₯Ό κ΄€λ¦¬ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. 이 ν΄λž˜μŠ€λŠ” 총 κ²Œμž„ 수, 승패 기둝, 평균 κ²Œμž„ 길이, 착수 및 포획된 돌의 수 λ“± λ‹€μ–‘ν•œ κ²Œμž„ κ΄€λ ¨ 데이터λ₯Ό μΆ”μ ν•˜κ³ , 톡계에 μ ‘κ·Όν•˜κ±°λ‚˜ μ΄ˆκΈ°ν™”ν•˜λŠ” κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. export default class BadukStatistics { constructor() { this.stats = { totalGames: 0, blackWins: 0, whiteWins: 0, averageGameLength: 0, totalMoves: 0, captures: { black: 0, white: 0 }, passes: 0, surrenders: 0 }; } // κ²Œμž„ κ²°κ³Ό 기둝 recordGameResult(gameState) { this.stats.totalGames++; if (gameState.surrender) { this.stats.surrenders++; } else { if (gameState.finalScore.winner === 'black') { this.stats.blackWins++; } else { this.stats.whiteWins++; } } // 평균 κ²Œμž„ 길이 μ—…λ°μ΄νŠΈ const gameLength = gameState.moveHistory.length; this.stats.averageGameLength = (this.stats.averageGameLength * (this.stats.totalGames - 1) + gameLength) / this.stats.totalGames; // 총 이동 수 μ—…λ°μ΄νŠΈ this.stats.totalMoves += gameLength; // 착수 톡계 μ—…λ°μ΄νŠΈ this.stats.captures.black += gameState.capturedStones.black; this.stats.captures.white += gameState.capturedStones.white; // 패슀 톡계 μ—…λ°μ΄νŠΈ const passCount = gameState.moveHistory.filter(move => move.type === 'pass').length; this.stats.passes += passCount; } // 톡계 κ°€μ Έμ˜€κΈ° getStats() { return { ...this.stats, winRate: { black: this.stats.totalGames > 0 ? (this.stats.blackWins / this.stats.totalGames * 100).toFixed(1) : 0, white: this.stats.totalGames > 0 ? (this.stats.whiteWins / this.stats.totalGames * 100).toFixed(1) : 0 }, averageCaptures: { black: this.stats.totalGames > 0 ? (this.stats.captures.black / this.stats.totalGames).toFixed(1) : 0, white: this.stats.totalGames > 0 ? (this.stats.captures.white / this.stats.totalGames).toFixed(1) : 0 } }; } // 톡계 μ΄ˆκΈ°ν™” resetStats() { this.stats = { totalGames: 0, blackWins: 0, whiteWins: 0, averageGameLength: 0, totalMoves: 0, captures: { black: 0, white: 0 }, passes: 0, surrenders: 0 }; } // 톡계 좜λ ₯ (μ˜ˆμ‹œ) displayStats() { const currentStats = this.getStats(); console.log("--- λ°”λ‘‘ κ²Œμž„ 톡계 ---"); console.log(`총 κ²Œμž„ 수: ${currentStats.totalGames}`); console.log(`흑 승리: ${currentStats.blackWins} (${currentStats.winRate.black}%)`); console.log(`λ°± 승리: ${currentStats.whiteWins} (${currentStats.winRate.white}%)`); console.log(`평균 κ²Œμž„ 길이 (수): ${currentStats.averageGameLength.toFixed(1)}`); console.log(`총 착수 수: ${currentStats.totalMoves}`); console.log(`평균 흑 포획 수: ${currentStats.averageCaptures.black}`); console.log(`평균 λ°± 포획 수: ${currentStats.averageCaptures.white}`); console.log(`패슀 수: ${currentStats.passes}`); console.log(`기ꢌ 수: ${currentStats.surrenders}`); } } ---- === 1) κ²Œμž„ κ²°κ³Ό 기둝 (recordGameResult) === ''recordGameResult'' λ©”μ„œλ“œλŠ” κ²Œμž„μ΄ μ’…λ£Œλ  λ•Œ ν˜ΈμΆœλ˜μ–΄ ν•΄λ‹Ή κ²Œμž„μ˜ κ²°κ³Όλ₯Ό 톡계에 λ°˜μ˜ν•©λ‹ˆλ‹€. μ΄λŠ” 총 κ²Œμž„ 수, 승패 μ—¬λΆ€, 평균 κ²Œμž„ 길이, 총 착수 수, 포획된 돌의 수, 패슀 및 기ꢌ 횟수 등을 μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€. ---- === 2) 톡계 κ°€μ Έμ˜€κΈ° (getStats) === ''getStats'' λ©”μ„œλ“œλŠ” ν˜„μž¬κΉŒμ§€ 기둝된 λͺ¨λ“  톡계 데이터λ₯Ό 객체 ν˜•νƒœλ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€. 이 κ°μ²΄μ—λŠ” 총 κ²Œμž„ 수, 승패 기둝, 평균 κ²Œμž„ 길이, 총 착수 수, 포획 수 λ“± κΈ°λ³Έ 톡계 외에도 ν‘λ°±μ˜ 승λ₯  및 평균 포획 μˆ˜μ™€ 같은 κ³„μ‚°λœ μ§€ν‘œκ°€ ν¬ν•¨λ©λ‹ˆλ‹€. ---- === 3) 톡계 μ΄ˆκΈ°ν™” (resetStats) === ''resetStats'' λ©”μ„œλ“œλŠ” λͺ¨λ“  κ²Œμž„ 톡계 데이터λ₯Ό 초기 μƒνƒœλ‘œ λ¦¬μ…‹ν•©λ‹ˆλ‹€. μ΄λŠ” μƒˆλ‘œμš΄ μ„Έμ…˜μ΄λ‚˜ ν…ŒμŠ€νŠΈλ₯Ό μ‹œμž‘ν•  λ•Œ μœ μš©ν•˜λ©°, λͺ¨λ“  톡계 수치λ₯Ό 0으둜 λ˜λŒλ¦½λ‹ˆλ‹€. ---- === 4) 톡계 좜λ ₯ (displayStats) === ''displayStats'' λ©”μ„œλ“œλŠ” ν˜„μž¬ 기둝된 톡계λ₯Ό μ½˜μ†”μ— 좜λ ₯ν•˜λŠ” μ˜ˆμ‹œ κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” ''getStats''λ₯Ό ν˜ΈμΆœν•˜μ—¬ μ΅œμ‹  톡계 데이터λ₯Ό κ°€μ Έμ˜¨ ν›„, 가독성 높은 ν˜•νƒœλ‘œ 각 μ§€ν‘œλ₯Ό λ‚˜μ—΄ν•©λ‹ˆλ‹€. μ‹€μ œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” 이 데이터λ₯Ό μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€μ— ν‘œμ‹œν•˜λŠ” 데 ν™œμš©λ  수 μžˆμŠ΅λ‹ˆλ‹€.