====== π§ λ°λ κ²μ λ‘μ§ ======
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''λ₯Ό νΈμΆνμ¬ μ΅μ ν΅κ³ λ°μ΄ν°λ₯Ό κ°μ Έμ¨ ν, κ°λ
μ± λμ ννλ‘ κ° μ§νλ₯Ό λμ΄ν©λλ€. μ€μ μ ν리μΌμ΄μ
μμλ μ΄ λ°μ΄ν°λ₯Ό μ¬μ©μ μΈν°νμ΄μ€μ νμνλ λ° νμ©λ μ μμ΅λλ€.