[Algorithm] ๋ฏธ๋๋งฅ์ค / ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ
๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ
๊ฐ๋
๐ก ์ ๋ก์ฌ ๊ฒ์์ ํ ํ๋ ์ด์ด๊ฐ ์ด๋์ ์ป์ผ๋ฉด ๋ค๋ฅธ ํ๋ ์ด์ด๋ ๊ทธ๋งํผ ์ํด๋ฅผ ๋ณด๋ ๊ฒ์์ ๊ฐ๋ฆฌํจ๋ค.
๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ํฑํํ , ์ฒด์ค์ฒ๋ผ 2๋ช ์ด ์ฐธ์ฌํ๋ ์ ๋ก์ฌ ๊ฒ์์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉํ๋ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก, ๋ชจ๋ ํ๋ ์ด์ด๊ฐ ์ต์ ์ ์๋ฅผ ๋๋ค๊ณ ๊ฐ์ ํ๊ณ ๊ฐ๋ฅํ ๋ชจ๋ ์๋ฅผ ๊ณ ๋ คํ์ฌ ์น๋ฆฌํ ์ ์๋ ์ ๋ต์ ๋์ถํ ๋ ์ฌ์ฉํ๋ค. X ํ๋ ์ด์ด๋ ์น๋ฆฌํ๊ธฐ ์ํด ์ต๋ ์ ์๋ฅผ ์ป์ผ๋ ค ํ๊ณ , O ํ๋ ์ด์ด๋ ํจ๋ฐฐํ์ง ์๊ธฐ ์ํด ์ต์ ์ ์๋ฅผ ์ป์ผ๋ ค ํ๋ ์ํฉ์์ ์ต์ ์ ํด๋ฅผ ์ฐพ๋ ๋ฐฉ๋ฒ์ด๋ค.
ํ์ฌ X ํ๋ ์ด์ด ์ฐจ๋ก์ด๊ณ , X ํ๋ ์ด์ด๊ฐ ์ ํํ ์ ์๋ ๊ณณ์ 1, 4, 5๋ฒ ์ธ๋ฑ์ค(Zero-Based)๋ผ๊ณ ๊ฐ์ ํด ๋ณธ๋ค. X๊ฐ ์ด๊ธฐ๋ฉด +100์ , O๊ฐ ์ด๊ธฐ๋ฉด -100์ ์ ํ๋ํ๋ค. ๋ณด๋์ ๋ชจ๋ ์๋ฅผ ๋์ง๋ง ๋ฌด์น๋ถ์ผ ๊ฒฝ์ฐ์ 0์ ์ด ๋๋ค.
- X ํ๋ ์ด์ด๊ฐ 1๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ ๊ฒฝ์ฐ → -100์
- ๋ค์ ํด์์ O ํ๋ ์ด์ด๊ฐ ์ ํํ ์ ์๋ ๊ณณ์ 4, 5๋ฒ ์ธ๋ฑ์ค
- O ํ๋ ์ด์ด๊ฐ 4๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๋ฉด O๊ฐ ์น๋ฆฌํด์ -100์
- O ํ๋ ์ด์ด๊ฐ 5๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๋ฉด X๊ฐ ์น๋ฆฌํด์ +100์
- O ํ๋ ์ด์ด๋ ์ต์ ์ ์๋ฅผ ํ๋ํ ์ ์๋ 4๋ฒ ์ธ๋ฑ์ค ์ ํ
- ๋ค์ ํด์์ O ํ๋ ์ด์ด๊ฐ ์ ํํ ์ ์๋ ๊ณณ์ 4, 5๋ฒ ์ธ๋ฑ์ค
- X ํ๋ ์ด์ด๊ฐ 4๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ ๊ฒฝ์ฐ → +100์
- X ํ๋ ์ด์ด๊ฐ 5๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ ๊ฒฝ์ฐ → -100์
- ๋ค์ ํด์์ O ํ๋ ์ด์ด๊ฐ ์ ํํ ์ ์๋ ๊ณณ์ 1, 4๋ฒ ์ธ๋ฑ์ค
- O ํ๋ ์ด์ด๊ฐ 1๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๋ฉด X๊ฐ ์น๋ฆฌํด์ +100์
- O ํ๋ ์ด์ด๊ฐ 4๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๋ฉด O๊ฐ ์น๋ฆฌํด์ -100์
- O ํ๋ ์ด์ด๋ ์ต์ ์ ์๋ฅผ ํ๋ํ ์ ์๋ 4๋ฒ ์ธ๋ฑ์ค ์ ํ
- ๋ค์ ํด์์ O ํ๋ ์ด์ด๊ฐ ์ ํํ ์ ์๋ ๊ณณ์ 1, 4๋ฒ ์ธ๋ฑ์ค
์ ํ๊ฐ ๊ณผ์ ์ ํตํด X ํ๋ ์ด์ด๋ 4๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํจ์ผ๋ก์จ ์ต๋ ์ ์๋ฅผ ํ๋ํ๊ณ ์น๋ฆฌํ ์ ์๋ค๋ ๊ฒฐ๋ก ์ ๋๋ฌํ๋ค. ์ด์ฒ๋ผ ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ์์ ์ ์ ์๋ฅผ ์ต๋ํํ๋ ๋จ๊ณ์ ์๋๋ฐฉ์ ์ ์๋ฅผ ์ต์ํํ๋ ๋จ๊ณ๋ฅผ ์ฌ๊ท์ ์ผ๋ก ๋ฐ๋ณตํ์ฌ ๊ฐ๋ฅํ ๋ชจ๋ ์์ง์์ ํ๊ฐํ ํ ์ต์ ์ ์์น๋ฅผ ์ ํํ๋ค.
๊ตฌํ
๐ก ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ์ผ๋ฐ์ ์ผ๋ก board
, isMaximizing
, depth
3๊ฐ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ค. ์๋ ์ฝ๋์์ , ํ๋ ์ด์ด์ ์์๋ฅผ ๋ฐ๊พธ๊ฑฐ๋ ์น๋ฆฌ ์กฐ๊ฑด์ ๋ณ๊ฒฝํ๋ ๋ฑ์ ์ต์
๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ํ๋ผ๋ฏธํฐ๊ฐ ํ์ํ ์ ์ฐธ๊ณ . ์น๋ฆฌ ์ฌ๋ถ๋ฅผ ํ๊ฐํ๋ evaluateWinning
ํจ์(์ฐธ๊ณ ํฌ์คํ
)๋ ๋ง์ง๋ง ์๋ฅผ ๋ ๊ณณ์ ๊ธฐ์ค์ผ๋ก ๊ฐ๋ก/์ธ๋ก/๋๊ฐ์ ์ผ๋ก ๊ฒ์ฌํ๊ธฐ ๋๋ฌธ์ lastIndex
ํ๋ผ๋ฏธํฐ๊ฐ ํ์ํ๋ค.
export const evaluateWinning = (
board: TBoard,
winCondition: number,
lastIndex: number,
identifier: BasePlayer | null = null,
resultType: 'position' | 'winner' = 'position',
) => {
const player = identifier ?? board[lastIndex].identifier;
if (!player) return null;
const size = getBoardSize(board);
const { row: lastRow, col: lastCol } = getCoordinates(lastIndex, size);
const directions = [
{ deltaRow: 0, deltaCol: 1 }, // ๊ฐ๋ก
{ deltaRow: 1, deltaCol: 0 }, // ์ธ๋ก
{ deltaRow: 1, deltaCol: 1 }, // ์ฐํ ๋๊ฐ์
{ deltaRow: 1, deltaCol: -1 }, // ์ขํ ๋๊ฐ์
];
for (const { deltaRow, deltaCol } of directions) {
const winningIndexes = checkDirection(
board,
winCondition,
player,
size,
{ lastRow, lastCol },
{ deltaRow, deltaCol },
);
// ์น๋ฆฌํ ์์น์ ์ธ๋ฑ์ค ๋ฐฐ์ด ๋ฐํ
if (winningIndexes) {
if (resultType === 'winner') return player;
else
return winningIndexes.map(({ row, col }) =>
getLinearIndex(row, col, size),
);
}
}
return null;
};
const checkDirection = (
board: TBoard,
winCondition: number,
identifier: BasePlayer,
size: number,
{ lastRow, lastCol }: RowColPair<'lastRow' | 'lastCol'>,
{ deltaRow, deltaCol }: RowColPair<'deltaRow' | 'deltaCol'>,
) => {
const searchDirection = (deltaRow: number, deltaCol: number) => {
const winningIndexes = [];
// ๋ง์ง๋ง ๋์๋ ์์น๋ ์ ์ธํ๊ธฐ ์ํด i = 1 ๋ถํฐ ์์
for (let i = 1; i < winCondition; i++) {
const curRow = i * deltaRow + lastRow;
const curCol = i * deltaCol + lastCol;
// ๋ณด๋ ๋ฒ์๋ฅผ ๋ฒ์ด๋ฌ์ผ๋ฉด ๊ฒ์ฌ ์ค์ง
if (!isWithinBounds(size, curRow, curCol)) break;
// ๊ธฐํธ๊ฐ ์ผ์นํ์ง ์์ผ๋ฉด ๊ฒ์ฌ ์ค์ง
if (getIdentifierFromRowCol(board, size, curRow, curCol) !== identifier)
break;
winningIndexes.push({ row: curRow, col: curCol });
}
return winningIndexes;
};
// ์์ชฝ ๋ฐฉํฅ์ผ๋ก ์ฐ์๋ ๋งํฌ ๊ฒ์ฌ
const forwardWinningIndexes = searchDirection(deltaRow, deltaCol);
const backwardWinningIndexes = searchDirection(-deltaRow, -deltaCol);
const combinedIndexes = [{ row: lastRow, col: lastCol }]; // ๋ง์ง๋ง ๋์๋ ์์น ์ ์ฅ
combinedIndexes.push(...forwardWinningIndexes, ...backwardWinningIndexes);
return combinedIndexes.length >= winCondition ? combinedIndexes : null;
};
๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ๊ธฐ๋ณธ์ ์ผ๋ก DFS(๊น์ด ์ฐ์ ํ์) ๋ฐฉ์์ผ๋ก ์งํ๋๋ค. findBestMove
ํจ์๋ ์ต์ ์ ์์น๋ฅผ ์ฐพ๊ธฐ ์ํด ๋ณด๋, ์น๋ฆฌ ์กฐ๊ฑด, ํ๋ ์ด์ด ๊ธฐํธ๋ฅผ ๋ฐ์ ์์ง ์๋ฅผ ๋์ง ์์ ๊ณณ๋ถํฐ ๋ฏธ๋๋งฅ์ค ํจ์๋ฅผ ํธ์ถํ๋ค.
๋ฏธ๋๋งฅ์ค ํจ์ ๋ด๋ถ์์ , ์ต๋ํ ๋จ๊ณ์ผ ๋ ํ์ ๋
ธ๋ ์ค ๊ฐ์ฅ ํฐ ๊ฐ์ bestScore
๋ก ์ค์ ํ๊ณ , ์ต์ํ ๋จ๊ณ์ผ ๋ ๊ฐ์ฅ ์์ ๊ฐ์ bestScore
๋ก ์ค์ ํ๋ค. ์ด ๊ณผ์ ์ ์น๋ฆฌํ ํ๋ ์ด์ด๊ฐ ๋์ค๊ฑฐ๋ ๋ฌด์น๋ถ(๋ณด๋์ ๋ชจ๋ ์นธ์ ์ฑ์)๊ฐ ๋ ๋๊น์ง ํ์ ๋
ธ๋๋ฅผ ์ฌ๊ท์ ์ผ๋ก ํ์ํ๋ค.
const minimax = (
board: TBoard, // ๊ฒ์ ๋ณด๋
winCondition: number, // ์น๋ฆฌํ๊ธฐ ์ํด ํ์ํ ์ฐ์๋ ๊ธฐํธ์ ๊ฐ์ (3x3 ๋ณด๋์์ 3)
isMaximizing: boolean, // ์ต๋ํ ๋จ๊ณ ์ฌ๋ถ
lastIndex: number, // ๋ง์ง๋ง ์๋ฅผ ๋ ์ธ๋ฑ์ค
player: BasePlayer, // ์ต๋ ์ ์๋ฅผ ์ป์ผ๋ ค๋ ํ๋ ์ด์ด์ ์๋ณ์ e.g. 'X'
opponent: BasePlayer, // ์ต์ ์ ์๋ฅผ ์ป์ผ๋ ค๋ ํ๋ ์ด์ด์ ์๋ณ์ e.g. 'O'
): number => {
// evaluateWinning ํจ์๋ ๋ณด๋๋ฅผ ํ๊ฐํ์ฌ ์น๋ฆฌํ ํ๋ ์ด์ด์ ๊ธฐํธ "O"|"X" ํน์ null์ ๋ฐํํ๋ค
const winner = evaluateWinning(
board,
winCondition,
lastIndex,
null,
'winner',
);
// ์น, ํจ ๋๋ ๋ฌด์น๋ถ์ ๋ํ ์ ์ ๋ฐํ
if (winner) return winner === player ? Score.Win : Score.Lose; // 100 | -100
if (!hasAvailableMove(board)) return Score.Draw; // ๋ฌด์น๋ถ 0
// bestScore ๋ฐ compare ํจ์ ์ด๊ธฐํ
let bestScore = isMaximizing ? -Infinity : Infinity;
const compareFn = isMaximizing ? Math.max : Math.min;
const nextPlayer = isMaximizing ? player : opponent;
// ๊ฐ๋ฅํ ๋ชจ๋ ์๋ฅผ ํ์ํ๊ณ ์ ์ ๊ณ์ฐ
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
board[i].identifier = nextPlayer; // ํ๋ ์ด์ด ๊ธฐํธ๋ก ์ฑ์
const score = minimax(
board,
winCondition,
!isMaximizing,
i,
player,
opponent,
);
board[i].identifier = null; // ์์๋ณต๊ท
bestScore = compareFn(score, bestScore);
}
}
return bestScore;
};
const findBestMove = (
board: TBoard,
winCondition: number,
player: BasePlayer, // 'X' | 'O'
): number | null => {
// ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ์ผ๋ฐ์ ์ผ๋ก ์ต๋ํ ๋จ๊ณ๋ถํฐ ์์ํ๋ค. ํจ์๋ฅผ ํธ์ถํ๋ฉด ์ต๋ํ ๋จ๊ณ๊ฐ ์์๋๋ค
// ์ต๋ํ ๋จ๊ณ์์ ๊ฐ์ฅ ํฐ ๊ฐ์ ์ฐพ๊ธฐ ์ํด ๊ฐ์ฅ ์์ ์(-Infinity)๋ฅผ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ค์ ํ๋ค
let bestScore = -Infinity;
let bestMove = null;
const opponent = getOpponent(player);
for (let i = 0; i < board.length; i++) {
// ๋น ์นธ์ ํ์ฌ ํ๋ ์ด์ด ๊ธฐํธ๋ก ์ฑ์ด ํ ๊ณ์ฐ๋ ์ ์ ์ค ๊ฐ์ฅ ํฐ ๊ณณ์ ์์น๋ฅผ bestMove๋ก ์ค์ ํ๋ค
if (board[i].identifier === null) {
board[i].identifier = player;
// ๋ค์ ํธ์ถ์ ์ต์ํ ๋จ๊ณ์ด๋ฏ๋ก isMaximizing ํ๋ผ๋ฏธํฐ๋ false๋ก ๋๊ธด๋ค
const score = minimax(board, winCondition, false, i, player, opponent);
board[i].identifier = null;
if (score > bestScore) {
bestScore = score;
bestMove = i;
}
}
}
return bestMove;
};
๋น ๋ฅธ ์น๋ฆฌ, ๋๋ฆฐ ํจ๋ฐฐ
๋น ๋ฅธ ์น๋ฆฌ
ํ์ฌ ํ๋ ์ด์ด X๊ฐ 0, 2๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๋ฉด ์น๋ฆฌํ ์ ์๋ค๊ณ ๊ฐ์ ํด ๋ณด์. findBestMove
ํจ์ ๋ด๋ถ์์ score > bestScore
์ํฉ์ผ ๋๋ง bestMove
์ธ๋ฑ์ค๋ฅผ ๊ฐฑ์ ํ๋ค. 0, 2๋ฒ ์ธ๋ฑ์ค์ ๋ํ ์ ์๋ ๋ชจ๋ 100์ ์ด๋ฏ๋ก ๊ฒฐ๊ณผ์ ์ผ๋ก ๋จผ์ ๊ณ์ฐํ๋ 0๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๊ฒ ๋๋ค. ํ์ง๋ง 2๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๋ค๋ฉด ๋ ์ ์ ํด์ผ๋ก ์น๋ฆฌํ ์ ์๊ธฐ ๋๋ฌธ์ 0๋ฒ ์ธ๋ฑ์ค๋ณด๋ค ๋ ์ข์ ์ ํ์ผ๋ก ๋ณผ ์ ์๋ค.
๋ง์ฝ ํด ์๋ฅผ ๊ฒ์ ์ ์์ ๋ฐ์ํ๋ค๋ฉด, ๋ ์ ์ ํด์ ์ ํํด์ ๋น ๋ฅธ ์น๋ฆฌ๋ฅผ ์ ํธํ๋๋ก ๋ง๋ค ์ ์๋ค. ํด ์๋ ํ์ ๊น์ด๋ฅผ ์๋ฏธํ๋ฏ๋ก ์ต๋ํ ๋จ๊ณ์ผ ๋ 100 - ๊น์ด
, ์ต์ํ ๋จ๊ณ์ผ ๋ ๊น์ด - 100
๋ฐฉ์์ผ๋ก ๊ณ์ฐํ ์ ์๋ค. ํด ์๋ฅผ ๊ฒ์ ์ ์์ ๋ฐ์ํ์ ๋ 0๋ฒ ์ธ๋ฑ์ค๋ 97์ , 2๋ฒ ์ธ๋ฑ์ค๋ 99์ ์ด ๋๊ณ , ๋ถ๋ชจ ๋
ธ๋์์ ๊ฐ์ฅ ํฐ ์ ์๋ฅผ ๋ธ 2๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๊ฒ ๋๋ค.
๋๋ฆฐ ํจ๋ฐฐ
ํด ์๋ฅผ ๊ฒ์ ์ ์์ ๋ฐ์ํ๋ฉด ๋ ๋๋ฆฐ ํจ๋ฐฐ๋ฅผ ์ ํธํ๊ฒ ๋๋ค. ์๋ ์ด๋ฏธ์ง๋ X ํ๋ ์ด์ด๊ฐ ์ด๋ค ์ด๋์ ์ ํํ๋ ํจ๋ฐฐํ๋ ์ํฉ์ด๋ค. ํด ์๋ฅผ ์ ์์ ๋ฐ์ํ์ง ์์๋ค๋ฉด 0, 2, 3, 4 ์ธ๋ฑ์ค์ ๋ํ ์ ์๋ ๋ชจ๋ -100์ ์ด ๋ผ์ ๊ฐ์ฅ ์ฒ์์ ๊ณ์ฐํ๋ 0๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํ๊ฒ ๋๋ค. ๋ฐ๋ฉด, ์ ์์ ํด ์๋ฅผ ๋ฐ์ํ๋ฉด ๊ฒ์์ ์งํํ ์๋ก ์๋๋ฐฉ ์ ์๋ ๊ทธ๋งํผ ๊ฐ์ํ๊ธฐ ๋๋ฌธ์ ๊ฒฐ๊ณผ์ ์ผ๋ก ์ต๋ํ ๋ง์ ํด์ ๋๊ณ ๊ฐ ์ ์๋ ์ธ๋ฑ์ค๋ฅผ ์ ํํด์ ํจ๋ฐฐ๊ฐ ์ง์ฐ๋๋ค.
์ ์ด๋ฏธ์ง๋ฅผ ๋ณด๋ฉด ์๋ํธ ๊ธฐํธ O๊ฐ 5, 8๋ฒ ์ธ๋ฑ์ค์ ์ฐ์ํด์ 2๊ฐ ๋ฐฐ์น๋ผ ์๋ค. ์ค์ ์ฌ๋ ํ๋ ์ด์ด๋ผ๋ฉด ์ฆ์ ํจ๋ฐฐํ๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํด 2๋ฒ ์ธ๋ฑ์ค๋ฅผ ์ ํํด์ ๋ฐฉ์ดํ ๊ฒ์ด๋ค. ํด ์๋ฅผ ์ ์์ ๋ฐ์ํ๋ฉด ์๋ํธ ์น๋ฆฌ๋ฅผ ์ต๋ํ ๋ฆ์ถ๊ธฐ ๋๋ฌธ์ ์ค์ ์ฌ๋์ด ์๊ฐํ๊ณ ํ๋ ์ดํ๋ ๊ฒ์ฒ๋ผ ๋ง๋ค ์ ์๋ค.
์ฝ๋ ์์
minimax()
ํจ์ ํ๋ผ๋ฏธํฐ์ ํด ์๋ฅผ ๋ํ๋ด๋ depth
๋ฅผ ์ถ๊ฐํ๊ณ ์ ์ ๊ณ์ฐ์ depth
๋ฅผ ๋ฐ์ํ๋ค(์ต๋ํ ๋จ๊ณ: ์ ์ - ํด ์,
์ต์ํ ๋จ๊ณ: -์ ์ + ํด ์
). minimax()
ํจ์๋ฅผ ํธ์ถํ ๋๋ง๋ค ๋ค์ ํด์ผ๋ก ๋์ด๊ฐ๊ธฐ ๋๋ฌธ์ ํจ์ ํธ์ถ ์์ ํ์ฌ depth + 1
์ ์ธ์๋ก ๋๊ธด๋ค.
const minimax = (
board: TBoard,
winCondition: number,
depth: number,
isMaximizing: boolean,
lastIndex: number,
player: BasePlayer,
opponent: BasePlayer,
): number => {
const winner = evaluateWinning(
board,
winCondition,
lastIndex,
null,
'winner',
);
// ํ์ฌ ํด ์(depth)๋ฅผ ์ ์์ ๋ฐ์ํ๋ค
if (winner) return winner === player ? Score.Win - depth : Score.Lose + depth;
if (!hasAvailableMove(board)) return Score.Draw;
let bestScore = isMaximizing ? -Infinity : Infinity;
const compareFn = isMaximizing ? Math.max : Math.min;
const nextPlayer = isMaximizing ? player : opponent;
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
board[i].identifier = nextPlayer;
// minimax ํจ์๋ฅผ ํธ์ถํ ๋๋ง๋ค ๋ค์ ํด์ผ๋ก ๋์ด๊ฐ๋ฏ๋ก ํ์ฌ depth์ 1์ ๋ํ๋ค
const score = minimax(
board,
winCondition,
depth + 1,
!isMaximizing,
i,
player,
opponent,
);
board[i].identifier = null;
bestScore = compareFn(score, bestScore);
}
}
return bestScore;
};
์๊ฐ ๋ณต์ก๋
๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ๊ฒ์ ํธ๋ฆฌ์ ๋ชจ๋ ๊ฐ๋ฅํ ์๋ฅผ ํ์ํ์ฌ ์ต์ ์ ์๋ฅผ ์ฐพ๋ ์ ๋ต์ด๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก $O(b^d)$ ์ง์ ์๊ฐ ๋ณต์ก๋๋ฅผ ๊ฐ๋๋ค. $b(branch)$๋ ํ ์ํ์์ ์ ํํ ์ ์๋ ์ต๋ ๊ฐ์ง์, $d(depth)$๋ ๋ฆฌํ ๋ ธ๋๊น์ง์ ์ต๋ ๊น์ด๋ฅผ ์๋ฏธํ๋ค. ๊ฐ ๋จ๊ณ๋ง๋ค $b$๊ฐ์ ์ ํ์ง๊ฐ ์๊ณ , ์ด๋ฌํ ์ ํ์ง๋ค์ด ์ต๋ $d$๋จ๊ณ๊น์ง ๊ณ์๋ ๊ฒฝ์ฐ, ํ์ํด์ผ ํ ์ด๋ ธ๋์ ์๋ ๋๋ต $b^d$๊ฐ ๋๋ค.
์๋ฅผ ๋ค์ด ๊ฒ์ ์ฒซ ๋จ๊ณ์์ ํ๋ ์ด์ด๊ฐ ์ ํํ ์ ์๋ ๊ฐ์ง ์๊ฐ A, B, C 3๊ฐ์ง์ด๊ณ , ๊ฐ ์ ํ์ง์์ 3๊ฐ์ง ๋ค๋ฅธ ์ ํ์ ํ ์ ์๋ค๋ฉด ์๊ฐ ๋ณต์ก๋๋ $b^d = 3^2 = 9$๊ฐ ๋๋ค.
๋ฆฌํฉํ ๋ง
const minimax = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
) => {
// ...์๋ต
// #3,5 ๋ก์ปฌ ์ปจํ
์คํธ๋ฅผ ๋ฐํํ๋ ํจ์ ๋ถ๋ฆฌ
let { isMaximizing, bestScore, compareFn } = getMinimaxContext(depth);
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
// #4 ๋ณด๋ ํ๊ฐ ํจ์ ๋ถ๋ฆฌ
const score = evaluateMove(board, winCondition, depth, i, roles);
bestScore = compareFn(score, bestScore);
}
}
return bestScore;
};
const findBestMove = (
board: TBoard,
winCondition: number,
player: BasePlayer,
) => {
// ...์๋ต
// #1 ํ๋ ์ด์ด ํ๋ผ๋ฏธํฐ ๊ฐ์ํ
const roles = { player, opponent: getOpponent(player) };
// #2 bestScore, bestMove ๊ฐ ๋ณ์๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ๊ด๋ฆฌ
const bestOutcome: BestOutcome = { score: -Infinity, move: null };
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
// #4 ๋ณด๋ ํ๊ฐ ํจ์ ๋ถ๋ฆฌ
const score = evaluateMove(board, winCondition, depth, i, roles);
if (score > bestOutcome.score) {
bestOutcome.score = score;
bestOutcome.move = i;
}
}
}
return bestOutcome.move;
};
โถ ํ๋ ์ด์ด ํ๋ผ๋ฏธํฐ ๊ฐ์ํ
ํ์ฌ ํ๋ ์ด์ด(player
), ์๋ ํ๋ ์ด์ด(opponent
)๋ฅผ ๊ฐ๊ฐ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ๋ ๋์ , ๋ ์ญํ ์ ํฌํจํ๋ ๋จ์ผ ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ ํจ์ ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฐ์ํํ ์ ์๋ค.
const roles = { player, opponent: getOpponent(player) };
โท bestScore
, bestMove
๊ฐ ๋ณ์๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ๊ด๋ฆฌ
const bestOutcome: BestOutcome = { score: -Infinity, move: null };
โธ isMaximizing
ํ๋ผ๋ฏธํฐ ์ ๊ฑฐ
ํ์ฌ ํด ์(depth
)๊ฐ ํ์๋ผ๋ฉด ํญ์ ์ต์ํ ๋จ๊ณ์ด๋ฏ๋ก, ๊ตณ์ด ํ๋ผ๋ฏธํฐ๋ก ๋๊ธธ ํ์ ์์ด depth
๋ฅผ ์ธ์๋ก ๋ฐ์ isMaximizing
๋ถ๋ฆฌ์ธ์ ๋ฐํํ๋ ํจ์๋ก ๋ถ๋ฆฌํด์ ์ฌ์ฉํ ์ ์๋ค.
const isMaximizing = (depth: number) => depth % 2 === 0;
โน ๋ณด๋ ํ๊ฐ ํจ์ evaluateMove
๋ถ๋ฆฌ
ํ์ ๋
ธ๋๋ฅผ ํ์ํ ํ ํ๊ฐํ ์ ์๋ฅผ ๋ฐํํ๋ ๋ก์ง์ minimax
findBestMove
๋ ํจ์์์ ์ค๋ณตํด์ ์ฌ์ฉํ๊ณ ์์ผ๋ฏ๋ก ๋ณ๋ ํจ์๋ก ๋ถ๋ฆฌํด์ ์ฌ์ฌ์ฉํ ์ ์๋ค.
const evaluateMove = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
) => {
board[lastIndex].identifier = isMaximizing(depth)
? roles.player
: roles.opponent;
const score = minimax(board, winCondition, depth + 1, lastIndex, roles); // Evaluate the board
board[lastIndex].identifier = null;
return score;
};
โบ ๋ก์ปฌ ์ปจํ ์คํธ๋ฅผ ๋ฐํํ๋ ํจ์ ๋ถ๋ฆฌ
๊ฐ ๋จ๊ณ๋ง๋ค ์ต๋ํ ์ฌ๋ถ์ธ์ง ํ์ธํ๊ณ , ์ด ๊ฐ์ ์์กดํ๋ bestScore
, compareFn
๋ก์ปฌ ๋ณ์๊ฐ ํ์ํ๋ฏ๋ก depth
๋ฅผ ์ธ์๋ก ๋ฐ์ ์ด๋ค์ ๋ฐํํ๋ ํจ์๋ก ๋ถ๋ฆฌํ๋ฉด ์ฝ๋๋ฅผ ๋ ๊น๋ํ๊ฒ ๋ง๋ค ์ ์๋ค.
const getMinimaxContext = (depth: number) => {
const isMax = isMaximizing(depth);
return {
isMaximizing: isMax,
bestScore: isMax ? -Infinity : Infinity,
compareFn: isMax ? Math.max : Math.min,
};
};
์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ
๊ฐ๋
๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ๊ฒ์ ํธ๋ฆฌ์ ๋ชจ๋ ๋ ธ๋๋ฅผ ํ์ํด์ ๊ฒฐ๊ณผ๋ฅผ ์์ธกํ์ง๋ง, ๊ผญ ๋ชจ๋ ๋ ธ๋๋ฅผ ํ์ํ ํ์๋ ์๋ค. ์๋ ์ด๋ฏธ์ง๋ฅผ ๊ธฐ์ค์ผ๋ก A → B → C → C1 ๋ ธ๋๊น์ง ํ์ํ๋ค๊ณ ๊ฐ์ ํด ๋ณด์. ํ์ฌ C1 ๋ ธ๋๋ -98์ ์ผ๋ก ํ๊ฐ๋ ์ํ๋ค. ์ด์ C2 ๋ ธ๋ ์ ์์ ๋ฐ๋ผ ์๋ 2๊ฐ์ง ์ํฉ์ ์์ํด ๋ณผ ์ ์๋ค.
- C2 ๋
ธ๋๊ฐ C1(-98) ๋ณด๋ค ์์ ์ ์๊ฐ(-100) ๋์์ ๋
- ์ต์ํ ๋จ๊ณ์ ์๋ C ๋ ธ๋๋ C2(-100) ๋ ธ๋๋ฅผ ์ ํํ๊ณ , C ๋ ธ๋์ ์ ์๋ -100์ ์ด ๋๋ค
- ์ต๋ํ ๋จ๊ณ์ ์๋ ๋ฃจํธ ๋ ธ๋๋ ์์ ๋ ธ๋ ์ค ๊ฐ์ฅ ๋์ ์ ์๋ฅผ ๊ฐ์ง B(99)๋ฅผ ์ ํํ๋ค
- C2 ๋
ธ๋๊ฐ C1(-98) ๋ณด๋ค ํฐ ์ ์๊ฐ(100) ๋์์ ๋
- ์ต์ํ ๋จ๊ณ์ ์๋ C ๋ ธ๋๋ C1(-98) ๋ ธ๋๋ฅผ ์ ํํ๊ณ , C ๋ ธ๋์ ์ ์๋ -98์ ์ด ๋๋ค
- ์ต๋ํ ๋จ๊ณ์ ์๋ ๋ฃจํธ ๋ ธ๋๋ ์์ ๋ ธ๋ ์ค ๊ฐ์ฅ ๋์ ์ ์๋ฅผ ๊ฐ์ง B(99)๋ฅผ ์ ํํ๋ค
๊ฒฐ๊ณผ์ ์ผ๋ก C2 ๋ ธ๋๊ฐ ์ด๋ค ๊ฐ์ด ๋์ค๋ ๋ฃจํธ ๋ ธ๋์์ ํญ์ B ๋ ธ๋๋ฅผ ์ ํํ๋ ์ํฉ์ด ๋๋ค. ์ฆ, C2 ๋ ธ๋์ ํ๊ฐ ๊ฒฐ๊ณผ๊ฐ B ๋ ธ๋๋ฅผ ์ ํํ๋๋ฐ ์ ํ ์ํฅ์ ๋ฏธ์น์ง ์๊ณ ์๋ค. ์ด๋ฐ ์ํฉ์์ C2 ๋ ธ๋๋ฅผ ํ์ํ ํ์๊ฐ ์์ด์ง๋ค.
์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ๋ ์ ๊ฐ์ ์๋ฆฌ๋ฅผ ์ด์ฉํด ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ด ๋ถํ์ํ ํ์์ ๊ฑด๋๋ฐ๋๋ก ๋ง๋๋ ๊ธฐ๋ฒ์ผ๋ก, ์ํ($\alpha$) ๊ฐ๊ณผ ๋ฒ ํ($\beta$) ๊ฐ์ ์ด์ฉํด ๋จ์ ์์ ๋ ธ๋์ ํ์ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ค.
- $\alpha$ ์ํ : ํ์ฌ ํ๋ ์ด์ด๊ฐ ๋ฐ๊ฒฌํ ์ต๋ ์ ์
- ๊ฐ์ฅ ์์ ์ $-\infin$ ๋ฅผ ์ด๊ธฐ๊ฐ์ผ๋ก ์ง์
- ์ต๋ํ ๋ ธ๋์์๋ง ์ ๋ฐ์ดํธ
- $\beta$ ๋ฒ ํ : ์๋ ํ๋ ์ด์ด๊ฐ ๋ฐ๊ฒฌํ ์ต์ ์ ์
- ๊ฐ์ฅ ํฐ ์ $\infin$๋ฅผ ์ด๊ธฐ๊ฐ์ผ๋ก ์ง์
- ์ต์ํ ๋ ธ๋์์๋ง ์ ๋ฐ์ดํธ
C1 ๋ ธ๋๋ฅผ ํ๊ฐํ ํ ๋ถ๋ชจ ๋ ธ๋ C์ ์ต์ ์ ์(๋ฒ ํ)๋ -98 ์ ์ด ๋๊ณ , ์ต๋ํ ๋จ๊ณ์ ์๋ ๋ฃจํธ ๋ ธ๋๋ ํ์ฌ๊น์ง ํ์ธ๋ ์ต๋ ์ ์(์ํ)์ธ 99์ ๋ณด๋ค ๋์ ์ ์๋ฅผ ๋ฐ๊ฒฌํ์ง ๋ชปํ๋ฉด ํญ์ B ๋ ธ๋(99)๋ฅผ ์ ํํ๋ค. ์ฆ, C ๋ ธ๋์์ ๊ณ์ฐ๋ ๋ฒ ํ ๊ฐ(-98)์ด ์ํ ๊ฐ(99) ์ดํ์ธ ๊ฒฝ์ฐ, ๋ฃจํธ ๋ ธ๋๋ ํญ์ B ๋ ธ๋๋ฅผ ์ ํํ๊ฒ ๋๋ ๊ฒ์ด๋ค.
์ด๋ค ๋ ธ๋์์ ์ํ ๊ฐ์ด ๋ฒ ํ ๊ฐ๋ณด๋ค ํฌ๊ฑฐ๋ ๊ฐ์ ์ํฉ($\alpha \geq \beta$)์ด๋ผ๋ฉด, ํ์ฌ ๋ ธ๋์ ์ต์ ํด๋ฅผ ์ด๋ฏธ ๋ฐ๊ฒฌํ ์ํ๋ก ๊ฐ์ฃผํ ์ ์๊ณ , ์ด๋ ์์ ๋ ธ๋๋ฅผ ๋ ์ด์ ํ์ํ ํ์๊ฐ ์์์ ๋ํ๋ธ๋ค.
์ํ, ๋ฒ ํ ๊ฐ์ ํ์ ๋ ธ๋๋ฅผ ํ์ํ๋ฉด์ ์๋ ๊ณผ์ ์ ํตํด ์ ๋ฐ์ดํธ ๋๋ค.
- ๋ฃจํธ ๋ ธ๋ : ์ํ, ๋ฒ ํ ๊ธฐ๋ณธ๊ฐ ์ค์ → $[-\infty, \infty]$
- A ๋
ธ๋ ํ์
- ํ์ ๋ ธ๋ ํ์ ํ(์ด๋ฏธ์ง์์ ์๋ต) ํ๊ฐ ์ ์ 97 ๋ฐํ(๋ฃจํธ ๋ ธ๋ ๋ณต๊ท)
- ๋ฃจํธ ๋ ธ๋๋ ์ต๋ํ ๋จ๊ณ์ด๋ฏ๋ก ์ํ ๊ฐ ์ ๋ฐ์ดํธ $[-\infty, \infty]$ → $[97, \infty]$
- B ๋
ธ๋ ํ์
- ํ์ ๋ ธ๋ ํ์ ํ(์ด๋ฏธ์ง์์ ์๋ต) ํ๊ฐ ์ ์ 99 ๋ฐํ(๋ฃจํธ ๋ ธ๋ ๋ณต๊ท)
- ๋ฃจํธ ๋ ธ๋๋ ์ต๋ํ ๋จ๊ณ์ด๋ฏ๋ก ์ํ ๊ฐ ์ ๋ฐ์ดํธ $[97, \infty]$ → $[99, \infty]$
- C ๋
ธ๋ ํ์ : ๋ถ๋ชจ ๋
ธ๋๋ก๋ถํฐ ์ํ, ๋ฒ ํ ๊ฐ $[99, \infty]$ ์ ๋ฌ๋ฐ์
- C1 ๋ ธ๋ ํ์ ํ ํ๊ฐ ์ ์ -98 ๋ฐํ(C ๋ ธ๋ ๋ณต๊ท)
- C ๋ ธ๋๋ ์ต์ํ ๋จ๊ณ์ด๋ฏ๋ก ๋ฒ ํ ๊ฐ ์ ๋ฐ์ดํธ $[99, \infty]$ → $[99, -98]$
- ์ํ ๊ฐ 99๊ฐ ๋ฒ ํ ๊ฐ -98๋ณด๋ค ๋ ํฌ๋ฏ๋ก ๋ค์ ๋ ธ๋(C2) ํ์ ์ ํจ — โ๏ธ Pruning
์์
๋จผ์ ๋ฃจํธ ๋ ธ๋์์ ์ํ, ๋ฒ ํ ๊ธฐ๋ณธ๊ฐ์ $[-\infty, \infty]$ ์ค์ ํ ํ ์์ ๋ ธ๋๋ก ์ ๋ฌํ๋ค. ๊ฐ ์์ ๋ ธ๋์์ ์ ๋ฌ๋ฐ์ ๊ฐ์ ๋ก์ปฌ ๋ณ์๋ก ๊ด๋ฆฌํ๊ณ , ํ์ ๋ ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ ๋ฐ์ดํธํ๋ค. ์ต๋ํ ๋จ๊ณ์ ์๋ ๋ ธ๋๋ ์ํ ๊ฐ์, ์ต์ํ ๋จ๊ณ์ ์๋ ๋ ธ๋๋ ๋ฒ ํ ๊ฐ์ ์ ๋ฐ์ดํธํด์ ๋จ์ ์์ ๋ ธ๋์ ํ์ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ค.
A - A1 ๋ ธ๋ ํ์
- Max | A1 : ๊ฐ์ฅ ํฐ ์์ ๋ ธ๋(A4) ๊ฐ(3)์ผ๋ก ์ํ ๋ณ์ ์ ๋ฐ์ดํธ $[-\infin, \infin]$ → $[3, \infin]$
- Min | A : A1 ๋ ธ๋ ๊ฐ(3)์ผ๋ก ๋ฒ ํ ๋ณ์ ์ ๋ฐ์ดํธ $[-\infin, \infin]$ → $[-\infin, 3]$
A - A2 ๋ ธ๋ ํ์
- Max | A2 : A5 ๋
ธ๋ ๊ฐ(5)์ผ๋ก ์ํ ๋ณ์ ์
๋ฐ์ดํธ $[-\infin, 3]$ → $[5, 3]$
โ๏ธ ์ ๋ฐ์ดํธํ ์ํ ๊ฐ(5)์ด ๋ฒ ํ ๊ฐ(3) ๋ณด๋ค ํฌ๋ฏ๋ก ๋ ์ด์ ์์ ๋ ธ๋ ํ์ ์ ํจ - Min | A : A1 ๋ ธ๋(3) ๋ณด๋ค ๋ ์์ ๊ฐ์ ๋ฐ๊ฒฌํ์ง ๋ชปํ์ผ๋ฏ๋ก ๋ฒ ํ ๋ณ์ ์ ์ง $[-\infin, 3]$
- Max | Root : A ๋ ธ๋ ๊ฐ(3)์ผ๋ก ์ํ ๋ณ์ ์ ๋ฐ์ดํธ $[-\infin, \infin]$ → $[3, \infin]$
B - B1 ๋ ธ๋ ํ์
- Max | B1 : ๊ฐ์ฅ ํฐ ์์ ๋ ธ๋(B4) ๊ฐ(1)์ด ํ์ฌ ์ํ ๊ฐ(3) ๋ณด๋ค ์์ผ๋ฏ๋ก ์ํ ๋ณ์ ์ ์ง $[3,\infin]$
- Min | B : B1 ๋ ธ๋ ๊ฐ(1)์ผ๋ก ๋ฒ ํ ๋ณ์ ์ ๋ฐ์ดํธ $[3, \infin]$ → $[3, 1]$
- โ๏ธ ์ํ ๊ฐ(3)์ด ์ ๋ฐ์ดํธํ ๋ฒ ํ ๊ฐ(1) ๋ณด๋ค ํฌ๋ฏ๋ก ๋ ์ด์ ์์ ๋ ธ๋ ํ์ ์ ํจ
๊ตฌํ
const minimax = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
cutBounds: CutBounds,
): number => {
// Check if there is a winner
const winner = evaluateWinning(
board,
winCondition,
lastIndex,
null,
'winner',
);
// Return score for winning, losing, or draw (Prefer early wins or late losses)
if (winner)
return winner === roles.player ? Score.Win - depth : Score.Lose + depth;
if (!hasAvailableMove(board)) return Score.Draw;
// Initialize bestScore and compare function
// eslint-disable-next-line prefer-const
let { isMaximizing, bestScore, compareFn } = getMinimaxContext(depth);
// Iterate through available moves and calculate scores
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
const score = evaluateMove(
board,
winCondition,
depth,
i,
roles,
cutBounds,
);
bestScore = compareFn(score, bestScore);
// ์์ ๋
ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ํ/๋ฒ ํ ๊ฐ์ ๋ฐ์
updateCutBounds(cutBounds, score, isMaximizing);
// ํ์ฌ ๋
ธ๋์ ์ต์ ํด๋ฅผ ์ฐพ์๋์ง ๊ฒ์ฌํ๊ณ , ์ฐพ์๋ค๋ฉด ์์ ๋
ธ๋ ํ์ ์ค์ง
if (cutBounds.alpha >= cutBounds.beta) break;
}
}
return bestScore;
};
const findBestMove = (
board: TBoard,
winCondition: number,
player: BasePlayer,
): number | null => {
// ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ์ผ๋ฐ์ ์ผ๋ก ์ต๋ํ ๋จ๊ณ๋ถํฐ ์์ํ๋ค. ํจ์๋ฅผ ํธ์ถํ๋ฉด ์ต๋ํ ๋จ๊ณ๊ฐ ์์๋๋ค
// ์ต๋ํ ๋จ๊ณ์์ ๊ฐ์ฅ ํฐ ๊ฐ์ ์ฐพ๊ธฐ ์ํด ๊ฐ์ฅ ์์ ์(-Infinity)๋ฅผ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ค์ ํ๋ค
const bestOutcome: BestOutcome = { score: -Infinity, move: null };
const cutBounds = { alpha: -Infinity, beta: Infinity }; // ์ํ/๋ฒ ํ ์ด๊ธฐ๊ฐ ์ค์
const roles = { player, opponent: getOpponent(player) };
const depth = 0; // ์ต๋ํ ๋จ๊ณ๋ถํฐ ์์
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
// ๋น ์นธ์ ํ์ฌ ํ๋ ์ด์ด ๊ธฐํธ๋ก ์ฑ์ด ํ ๊ณ์ฐ๋ ์ ์ ์ค ๊ฐ์ฅ ํฐ ๊ณณ์ ์ธ๋ฑ์ค๋ฅผ bestMove ๊ฐ์ผ๋ก ์ค์ ํ๋ค
const score = evaluateMove(
board,
winCondition,
depth,
i,
roles,
cutBounds,
);
if (score > bestOutcome.score) {
bestOutcome.score = score;
bestOutcome.move = i;
// ์์ ๋
ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ํ/๋ฒ ํ ๊ฐ์ ๋ฐ์ (์
๋ฐ์ดํธ๋ ๊ฐ์ ์์ ๋
ธ๋๋ก ์ ๋ฌํด์ผ ํ๋ฏ๋ก)
updateCutBounds(cutBounds, score, true);
}
}
}
return bestOutcome.move;
};
- ๋ฃจํธ ๋
ธ๋์์ ์ํ/๋ฒ ํ ์ด๊ธฐ๊ฐ($-\infin,\infin$)์ ์ค์ ํ๋ค (
cutBounds
๊ฐ์ฒด) - ์ด๊ธฐ๊ฐ ํน์ 3๋ฒ์์ ์
๋ฐ์ดํธํ ์ํ/๋ฒ ํ ๊ฐ์ ์์ ๋
ธ๋๋ก ์ ๋ฌํ๋ค
- ์ฌ์ด๋ ์ดํํธ ๋ฐฉ์ง๋ฅผ ์ํด ์ํ/๋ฒ ํ ๊ฐ์ด ๋ด๊ธด
cutBounds
๊ฐ์ฒด๋ฅผ ๋ณต์ฌํด์ ์ ๋ฌํ๋ค cutBounds
๊ฐ์ฒด์ ๋ถ๋ณ์ฑ ์ ์ง๋ ๊ฐ ๋ ธ๋์์ ์ํ/๋ฒ ํ ๊ฐ์ ๋ก์ปฌ ๋ณ์๋ก ๊ด๋ฆฌํจ์ ์๋ฏธํ๋ค
- ์ฌ์ด๋ ์ดํํธ ๋ฐฉ์ง๋ฅผ ์ํด ์ํ/๋ฒ ํ ๊ฐ์ด ๋ด๊ธด
- ์์ ๋
ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ํ/๋ฒ ํ ๊ฐ์ ๋ฐ์ํ๋ค
- ์ต๋ํ ๋จ๊ณ : ํ์ฌ ๋
ธ๋์์ ๋ฐ๊ฒฌ๋ ์ต๋ ๊ฐ(
bestScore
)์ ์ํ ๊ฐ์ผ๋ก ์ค์ - ์ต์ํ ๋จ๊ณ : ํ์ฌ ๋
ธ๋์์ ๋ฐ๊ฒฌ๋ ์ต์ ๊ฐ(
bestScore
)์ ๋ฒ ํ ๊ฐ์ผ๋ก ์ค์
- ์ต๋ํ ๋จ๊ณ : ํ์ฌ ๋
ธ๋์์ ๋ฐ๊ฒฌ๋ ์ต๋ ๊ฐ(
- ํ์ฌ ๋
ธ๋์ ์ต์ ํด๋ฅผ ์ฐพ์๋์ง ๊ฒ์ฌ
- $\alpha >= \beta$ ์ต์ ํด๋ฅผ ์ฐพ์ ์ํ : ์์ ๋ ธ๋ ํ์์ ์ค์งํ๋ค — ๊ฐ์ง์น๊ธฐ โ๏ธ
- $\alpha < \beta$ ์ต์ ํด๋ฅผ ์ฐพ์ง ๋ชปํ ์ํ : 2๋ฒ์ผ๋ก ๋์๊ฐ์ ํ์ ๋ฐ๋ณต
ํฌํผ ํจ์ ์ ๋ฐ์ดํธ โผ
// #3 ์์ ๋
ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ์ธ score๋ฅผ ์ธ์๋ก ๋ฐ์ ์ํ/๋ฒ ํ ๊ฐ ์
๋ฐ์ดํธ
const updateCutBounds = (
cutBounds: CutBounds,
score: number,
isMaximizing: boolean,
) => {
if (isMaximizing) cutBounds.alpha = Math.max(cutBounds.alpha, score);
else cutBounds.beta = Math.min(cutBounds.beta, score);
};
const evaluateMove = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
cutBounds: CutBounds, // ์ํ/๋ฒ ํ ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
): number => {
board[lastIndex].identifier = isMaximizing(depth)
? roles.player
: roles.opponent;
// #2 ์ฌ์ด๋ ์ดํํธ ๋ฐฉ์ง๋ฅผ ์ํด ์ํ/๋ฒ ํ ๊ฐ์ด ๋ด๊ธด cutBounds ๊ฐ์ฒด์ ๋ถ๋ณ์ฑ์ ์ ์งํ๋ค
const score = minimax(board, winCondition, depth + 1, lastIndex, roles, {
...cutBounds,
});
board[lastIndex].identifier = null;
return score;
};
๋ฉ์ธ ํจ์ ์ ๋ฐ์ดํธ โผ
const minimax = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
cutBounds: CutBounds, // ์ํ/๋ฒ ํ ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
): number => {
// ...์๋ต
let { isMaximizing, bestScore, compareFn } = getMinimaxContext(depth);
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
// #2 ์ํ/๋ฒ ํ ๊ฐ ์์ ๋
ธ๋๋ก ์ ๋ฌ
const score = evaluateMove(
board,
winCondition,
depth,
i,
roles,
cutBounds,
);
bestScore = compareFn(score, bestScore);
// #3 ์์ ๋
ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ํ/๋ฒ ํ ๊ฐ์ ๋ฐ์
updateCutBounds(cutBounds, score, isMaximizing);
// #4 ํ์ฌ ๋
ธ๋์ ์ต์ ํด๋ฅผ ์ฐพ์๋์ง ๊ฒ์ฌํ๊ณ , ์ฐพ์๋ค๋ฉด ์์ ๋
ธ๋ ํ์ ์ค์ง
if (cutBounds.alpha >= cutBounds.beta) break;
}
}
return bestScore;
};
const findBestMove = (
board: TBoard,
winCondition: number,
player: BasePlayer,
) => {
// ...์๋ต
// #1 ์ํ/๋ฒ ํ ์ด๊ธฐ๊ฐ ์ค์
const cutBounds = { alpha: -Infinity, beta: Infinity };
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
// #2 ์ด๊ธฐ๊ฐ ํน์ 3๋ฒ์์ ์
๋ฐ์ดํธํ ์ํ/๋ฒ ํ ๊ฐ์ ์์ ๋
ธ๋๋ก ์ ๋ฌ
const score = evaluateMove(
board,
winCondition,
depth,
i,
roles,
cutBounds,
);
if (score > bestOutcome.score) {
bestOutcome.score = score;
bestOutcome.move = i;
// #3 ์์ ๋
ธ๋ ํ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์ํ/๋ฒ ํ ๊ฐ์ ๋ฐ์ (์
๋ฐ์ดํธ๋ ๊ฐ์ ์์ ๋
ธ๋๋ก ์ ๋ฌํด์ผ ํ๋ฏ๋ก)
updateCutBounds(cutBounds, score, true);
}
}
}
return bestOutcome.move;
};
์๊ฐ๋ณต์ก๋
๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ ๊ธฐ๋ฒ์ ์ ์ฉํ๋ฉด, ๋ ์ด์ ํ์ํ ํ์๊ฐ ์๋ ๋ ธ๋๋ฅผ ์กฐ๊ธฐ์ ์ ์ธํจ์ผ๋ก์จ ์๊ฐ ๋ณต์ก๋๋ฅผ $O(b^{d/2})$๊น์ง ์ค์ผ ์ ์๋ค(์ต์ ์ ๊ฒฝ์ฐ). ํนํ ํธ๋ฆฌ ๊น์ด๊ฐ ๊น์ด์ง์๋ก ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ ํจ๊ณผ๊ฐ ๋ฐฐ๊ฐ๋ผ์ ํ์ ์๊ฐ์ ํฌ๊ฒ ๋จ์ถ์ํฌ ์ ์๋ค.
ํ์ง๋ง ์ฒด์ค๋ ๋ฐ๋ ๊ฐ์ ์ํ ๊ณต๊ฐ์ด ๋งค์ฐ ํฐ ๊ฒ์์์ ๊ฐ๋ฅํ ๋ชจ๋ ์ํ๋ฅผ ํ์ํ๋ ๊ฒ์ ์ค์ง์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ๋ค. ์ด๋ฐ ๋ณต์กํ ๊ฒ์์์ ํน์ ๊น์ด์ ๋๋ฌํ์ ๋ ํ์์ ์ค๋จํ๊ณ , ํด๋ฆฌ์คํฑ ํ๊ฐ ๊ธฐ๋ฒ์ ํ์ฉํ์ฌ ํ์ฌ ๊ฒ์ ์ํ์ ์ถ์ ์น๋ฅผ ๊ณ์ฐํ๋ ๋ฐฉ์์ผ๋ก ์ต์ ํํด์ผ ํ๋ค. ํฑํํ ๋ฅผ ์๋ก ๋ค๋ฉด…
- ์ค์, ๋ชจ์๋ฆฌ ๋ฑ์ ๋์ ์ ์ ๋ถ์ฌ
- ์ฐ์๋ ๊ธฐํธ๊ฐ ์๋ ๊ณณ์ ๋์ ์ ์ ๋ถ์ฌ
- ์๋๋ฐฉ์ ์น๋ฆฌ๋ฅผ ๋ฐฉ์ดํ๋ ๊ณณ์ ๋์ ์ ์ ๋ถ์ฌ
- ๋ฑ…
๋ฉ๋ชจ์ด์ ์ด์
ํฑํํ ๊ฒ์์ ํ๋ค ๋ณด๋ฉด ๊ฐ ํ๋ ์ด์ด๊ฐ ์๋ก ๋ค๋ฅธ ์์๋ก ๊ธฐํธ๋ฅผ ๋์์๋ ๋ถ๊ตฌํ๊ณ ์ต์ข ์ ์ผ๋ก ๊ฐ์ ๋ณด๋ ์ํ์ ๋๋ฌํ๋ ๊ฒฝ์ฐ๊ฐ ์์ฃผ ์๋ค(์๋ ์ด๋ฏธ์ง ์ฐธ๊ณ ). ํนํ ๋ฏธ๋๋งฅ์ค ์๊ณ ๋ฆฌ์ฆ์ ๊ฐ๋ฅํ ๋ชจ๋ ์๋ฅผ ํ์ํ๊ธฐ ๋๋ฌธ์ ์ด๋ฏธ ํ๊ฐํ๋ ๋ณด๋ ์ํ๋ฅผ ๋ค์ ํ๊ฐํ๋ ๊ฒฝ์ฐ๊ฐ ์์ฃผ ๋ฐ์ํ๋ค. ๋ง์ฝ ์ด๋ฏธ ๊ณ์ฐํ๋ ๋ณด๋ ์ํ๋ฅผ ์ ์ฅํด ๋๊ณ ์ฌ์ฌ์ฉํ๋ฉด ์ค๋ณต ํ๊ฐ๋ฅผ ์ค์ผ ์ ์๊ธฐ ๋๋ฌธ์ ์๊ณ ๋ฆฌ์ฆ ํจ์จ์ ํฌ๊ฒ ํฅ์ํ ์ ์๋ค.
- ๊ฒฝ๋ก A : X ์ค์ → O ์ข์ธก ์๋จ → X ์ฐ์ธก ํ๋จ
- ๊ฒฝ๋ก B : X ์ฐ์ธก ํ๋จ → O ์ข์ธก ์๋จ → X ์ค์
ํ์ฌ ๋ณด๋ ์ํ๋ฅผ ๋ฌธ์์ด๋ก ๋ณํํ๋ ์ ํธ ํจ์ โผ
const generateBoardKey = (board: TBoard, depth: number): string => {
return `${board.map(({ identifier }) => identifier ?? '-').join('')}:${depth}`;
};
๊ธฐ์กด ํจ์ ์์ โผ
const findBestMove = (
board: TBoard,
winCondition: number,
player: BasePlayer,
) => {
// ...์๋ต
const memo: Memo = new Map(); // ๋ฉ๋ชจ์ด์ ์ด์
๊ฐ์ฒด ์ด๊ธฐํ Map<string, number>
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
const score = evaluateMove(
board,
winCondition,
depth,
i,
roles,
cutBounds,
memo,
);
// ... ์๋ต
}
}
return bestOutcome.move;
};
const evaluateMove = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
cutBounds: CutBounds,
memo: Memo, // ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
): number => {
// ...์๋ต
const score = minimax(
board,
winCondition,
depth + 1,
lastIndex,
roles,
{ ...cutBounds },
memo,
);
// ...์๋ต
};
const minimax = (
board: TBoard,
winCondition: number,
depth: number,
lastIndex: number,
roles: Roles,
cutBounds: CutBounds,
memo: Memo, // ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
): number => {
const key = generateBoardKey(board, depth); // ๋ณด๋ ์ํ์ depth๋ฅผ ๊ธฐ์ค์ผ๋ก ์ ๋ํฌ key ์์ฑ
if (memo.has(key)) return memo.get(key)!; // ์ด๋ฏธ ํ๊ฐํ ๋ณด๋ ์ํ๋ผ๋ฉด ํด๋น ์ ์ ๋ฐํ
// ... ์๋ต
for (let i = 0; i < board.length; i++) {
if (board[i].identifier === null) {
/* ...์๋ต */
}
}
memo.set(key, bestScore); // ํ์ฌ ๋ณด๋ ์ํ์ bestScore ์ ์ฅ
return bestScore;
};
4x4 ๋น ๋ณด๋์ Bot์ด ๋จผ์ ์ฒซ ๋ฒ์งธ ์๋ฅผ ๋๋ค๊ณ ๊ฐ์ ํ๊ณ , ๋ฉ๋ชจ์ด์ ์ด์ ์ ์ฉ ์ /ํ ์๊ณ ๋ฆฌ์ฆ ์คํ ์๊ฐ์ ์๋์ ๊ฐ๋ค. ๋ฉ๋ชจ์ด์ ์ด์ ์ ์ ์ฉํ๋ฉด ์คํ ์๊ฐ์ ์ฝ 72๋ฐฐ๊ฐ๋ ์ค์ผ ์ ์๋ค. ์๊ฐ๋ณด๋ค ์์ฒญ๋ ์ฐจ์ด.
- ๋ฉ๋ชจ์ด์ ์ด์ ์ : 159,853 milliseconds (์ฝ 159.8์ด)
- ๋ฉ๋ชจ์ด์ ์ด์ ํ : 2,209 milliseconds (์ฝ 2.2์ด)
๋ ํผ๋ฐ์ค
- Tic Tac Toe: Understanding the Minimax Algorithm | Never Stop Building
- Tic-Tac-Toe with JavaScript: AI Player with Minimax Algorithm | Ali Alaa
- Artificial Intelligence | Alpha-Beta Pruning - Javatpoint
- Adversarial Search - Minimax Search์ Alpha-beta Pruning | ์ง๊ทธ์
์์ฑ ๋ ํฌ์งํ ๋ฆฌ
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[HTML/CSS] ์ํ๋ ์์น์ ๋ ธ๋ ์ฝ์ ํ๊ธฐ — insertAdjacentHTML
[HTML/CSS] ์ํ๋ ์์น์ ๋ ธ๋ ์ฝ์ ํ๊ธฐ — insertAdjacentHTML
2024.06.02 -
[Next.js] App Router / ์๋ฒ ์ปดํฌ๋ํธ ์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ
[Next.js] App Router / ์๋ฒ ์ปดํฌ๋ํธ ์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ
2024.06.01 -
[React] ํฑํํ (Tic-Tac-Toe) ๊ฒ์ ์ฃผ์ ๋ก์ง ํบ์๋ณด๊ธฐ
[React] ํฑํํ (Tic-Tac-Toe) ๊ฒ์ ์ฃผ์ ๋ก์ง ํบ์๋ณด๊ธฐ
2024.05.30 -
[JS] Array.fill() ๋ฉ์๋ ์ฌ์ฉ ์ ์ฃผ์ํ ์
[JS] Array.fill() ๋ฉ์๋ ์ฌ์ฉ ์ ์ฃผ์ํ ์
2024.05.30