μ‚¬μš©μž 도ꡬ

μ‚¬μ΄νŠΈ 도ꡬ


wiki:it:dream_of_enc:metaverse:game_logic

λ¬Έμ„œμ˜ 이전 νŒμž…λ‹ˆλ‹€!


🧠 λ°”λ‘‘ κ²Œμž„ 둜직

Phaser Baduk Metaverse ν”„λ‘œμ νŠΈμ˜ λ°”λ‘‘ κ²Œμž„ 둜직과 κ·œμΉ™ κ΅¬ν˜„μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€.

πŸ“‹ κ°œμš”

λ°”λ‘‘ κ²Œμž„μ˜ 핡심 κ·œμΉ™κ³Ό λ‘œμ§μ„ JavaScript둜 κ΅¬ν˜„ν•˜μ—¬ μ •ν™•ν•œ λ°”λ‘‘ κ²Œμž„μ„ μ œκ³΅ν•©λ‹ˆλ‹€.

🎯 κΈ°λ³Έ κ·œμΉ™

λ°”λ‘‘μ˜ κΈ°λ³Έ κ·œμΉ™:

  • 착수: ν‘λŒλΆ€ν„° μ‹œμž‘ν•˜μ—¬ λ²ˆκ°ˆμ•„κ°€λ©° λŒμ„ λ†“μŠ΅λ‹ˆλ‹€.
  • μžμœ λ„: λŒμ€ μ΅œμ†Œ ν•˜λ‚˜μ˜ μžμœ λ„λ₯Ό κ°€μ Έμ•Ό ν•©λ‹ˆλ‹€.
  • 착수: μƒλŒ€λ°© 돌의 μžμœ λ„λ₯Ό λͺ¨λ‘ μ—†μ• λ©΄ κ·Έ λŒμ„ μ œκ±°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • 자좩: μžμ‹ μ˜ λŒμ„ μžμœ λ„ 없이 놓을 수 μ—†μŠ΅λ‹ˆλ‹€.
  • 계가: κ²Œμž„ μ’…λ£Œ ν›„ 점수λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.

1. κ²Œμž„ μƒνƒœ 관리

이 μ„Ήμ…˜μ—μ„œλŠ” λ°”λ‘‘ κ²Œμž„μ˜ ν˜„μž¬ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³  μ €μž₯ν•˜λŠ” BadukGameState ν΄λž˜μŠ€μ— λŒ€ν•΄ μ„€λͺ…ν•©λ‹ˆλ‹€. 이 ν΄λž˜μŠ€λŠ” λ°”λ‘‘νŒμ˜ 돌 배치, ν˜„μž¬ ν”Œλ ˆμ΄μ–΄, μž‘μ€ 돌의 수, κ²Œμž„ 단계 λ“± κ²Œμž„μ˜ λͺ¨λ“  μ€‘μš”ν•œ 정보λ₯Ό ν¬ν•¨ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

1) μƒμ„±μž (Constructor) - κ²Œμž„ μ΄ˆκΈ°ν™”

이 μ½”λ“œλŠ” BadukGameState 객체가 생성될 λ•Œ 호좜되며, κ²Œμž„μ˜ 초기 μƒνƒœλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€. λ°”λ‘‘νŒμ„ 19×19 격자둜 μ΄ˆκΈ°ν™”ν•˜κ³ , ν˜„μž¬ ν”Œλ ˆμ΄μ–΄λ₯Ό ν‘λŒλ‘œ μ„€μ •ν•˜λ©°, μž‘μ€ 돌의 μˆ˜μ™€ μ˜μ—­ 점수λ₯Ό 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)κ°€ 19×19 λ°”λ‘‘νŒμ˜ μœ νš¨ν•œ λ²”μœ„ 내에 μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. λͺ¨λ“  착수 및 μ£Όλ³€ 탐색 전에 ν•„μˆ˜μ μœΌλ‘œ μˆ˜ν–‰λ©λ‹ˆλ‹€.

    // λ°”λ‘‘νŒ λ²”μœ„ 확인
    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λ₯Ό ν˜ΈμΆœν•˜μ—¬ μ΅œμ‹  톡계 데이터λ₯Ό κ°€μ Έμ˜¨ ν›„, 가독성 높은 ν˜•νƒœλ‘œ 각 μ§€ν‘œλ₯Ό λ‚˜μ—΄ν•©λ‹ˆλ‹€. μ‹€μ œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” 이 데이터λ₯Ό μ‚¬μš©μž μΈν„°νŽ˜μ΄μŠ€μ— ν‘œμ‹œν•˜λŠ” 데 ν™œμš©λ  수 μžˆμŠ΅λ‹ˆλ‹€.

wiki/it/dream_of_enc/metaverse/game_logic.1753770587.txt.gz Β· λ§ˆμ§€λ§‰μœΌλ‘œ μˆ˜μ •λ¨: μ €μž syjang0803

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki