๋ฐ˜์‘ํ˜•

๋ฏธ๋‹ˆ๋งฅ์Šค ์•Œ๊ณ ๋ฆฌ์ฆ˜


๊ฐœ๋…

๐Ÿ’ก ์ œ๋กœ์„ฌ ๊ฒŒ์ž„์€ ํ•œ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ด๋“์„ ์–ป์œผ๋ฉด ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด๋Š” ๊ทธ๋งŒํผ ์†ํ•ด๋ฅผ ๋ณด๋Š” ๊ฒŒ์ž„์„ ๊ฐ€๋ฆฌํ‚จ๋‹ค.

 

๋ฏธ๋‹ˆ๋งฅ์Šค ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ ํ‹ฑํƒํ† , ์ฒด์Šค์ฒ˜๋Ÿผ 2๋ช…์ด ์ฐธ์—ฌํ•˜๋Š” ์ œ๋กœ์„ฌ ๊ฒŒ์ž„์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ, ๋ชจ๋“  ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ตœ์„ ์˜ ์ˆ˜๋ฅผ ๋‘”๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์ˆ˜๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ „๋žต์„ ๋„์ถœํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. X ํ”Œ๋ ˆ์ด์–ด๋Š” ์Šน๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ตœ๋Œ€ ์ ์ˆ˜๋ฅผ ์–ป์œผ๋ ค ํ•˜๊ณ , O ํ”Œ๋ ˆ์ด์–ด๋Š” ํŒจ๋ฐฐํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์ตœ์†Œ ์ ์ˆ˜๋ฅผ ์–ป์œผ๋ ค ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์ตœ์ ์˜ ํ•ด๋ฅผ ์ฐพ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

 

ํ˜„์žฌ X ํ”Œ๋ ˆ์ด์–ด ์ฐจ๋ก€์ด๊ณ , X ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์€ 1, 4, 5๋ฒˆ ์ธ๋ฑ์Šค(Zero-Based)๋ผ๊ณ  ๊ฐ€์ •ํ•ด ๋ณธ๋‹ค. X๊ฐ€ ์ด๊ธฐ๋ฉด +100์ , O๊ฐ€ ์ด๊ธฐ๋ฉด -100์ ์„ ํš๋“ํ•œ๋‹ค. ๋ณด๋“œ์˜ ๋ชจ๋“  ์ˆ˜๋ฅผ ๋’€์ง€๋งŒ ๋ฌด์Šน๋ถ€์ผ ๊ฒฝ์šฐ์—” 0์ ์ด ๋œ๋‹ค.

 

  1. X ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 1๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•  ๊ฒฝ์šฐ → -100์ 
    • ๋‹ค์Œ ํ„ด์—์„œ O ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์€ 4, 5๋ฒˆ ์ธ๋ฑ์Šค
      • O ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 4๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•˜๋ฉด O๊ฐ€ ์Šน๋ฆฌํ•ด์„œ -100์ 
      • O ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 5๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•˜๋ฉด X๊ฐ€ ์Šน๋ฆฌํ•ด์„œ +100์ 
    • O ํ”Œ๋ ˆ์ด์–ด๋Š” ์ตœ์†Œ ์ ์ˆ˜๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š” 4๋ฒˆ ์ธ๋ฑ์Šค ์„ ํƒ
  2. X ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 4๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•  ๊ฒฝ์šฐ → +100์ 
  3. X ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 5๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•  ๊ฒฝ์šฐ → -100์ 
    • ๋‹ค์Œ ํ„ด์—์„œ O ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์€ 1, 4๋ฒˆ ์ธ๋ฑ์Šค
      • O ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 1๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•˜๋ฉด X๊ฐ€ ์Šน๋ฆฌํ•ด์„œ +100์ 
      • O ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 4๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ์„ ํƒํ•˜๋ฉด O๊ฐ€ ์Šน๋ฆฌํ•ด์„œ -100์ 
    • O ํ”Œ๋ ˆ์ด์–ด๋Š” ์ตœ์†Œ ์ ์ˆ˜๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š” 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๊ฐ€์ง€ ์ƒํ™ฉ์„ ์˜ˆ์ƒํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

์œ„์—์„œ ์‚ดํŽด๋ณธ ๋ฏธ๋‹ˆ๋งฅ์Šค ๋น ๋ฅธ ์Šน๋ฆฌ ์˜ˆ์‹œ ๋ณด๋“œ๋ฅผ ๋„์‹ํ™”ํ•œ ์ด๋ฏธ์ง€

  1. C2 ๋…ธ๋“œ๊ฐ€ C1(-98) ๋ณด๋‹ค ์ž‘์€ ์ ์ˆ˜๊ฐ€(-100) ๋‚˜์™”์„ ๋•Œ
    • ์ตœ์†Œํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” C ๋…ธ๋“œ๋Š” C2(-100) ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜๊ณ , C ๋…ธ๋“œ์˜ ์ ์ˆ˜๋Š” -100์ ์ด ๋œ๋‹ค
    • ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ์ž์‹ ๋…ธ๋“œ ์ค‘ ๊ฐ€์žฅ ๋†’์€ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ B(99)๋ฅผ ์„ ํƒํ•œ๋‹ค
  2. C2 ๋…ธ๋“œ๊ฐ€ C1(-98) ๋ณด๋‹ค ํฐ ์ ์ˆ˜๊ฐ€(100) ๋‚˜์™”์„ ๋•Œ
    • ์ตœ์†Œํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” C ๋…ธ๋“œ๋Š” C1(-98) ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜๊ณ , C ๋…ธ๋“œ์˜ ์ ์ˆ˜๋Š” -98์ ์ด ๋œ๋‹ค
    • ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ์ž์‹ ๋…ธ๋“œ ์ค‘ ๊ฐ€์žฅ ๋†’์€ ์ ์ˆ˜๋ฅผ ๊ฐ€์ง„ B(99)๋ฅผ ์„ ํƒํ•œ๋‹ค

 

C2 ๋…ธ๋“œ๊ฐ€ ์–ด๋–ค ๊ฐ’์ด ๋‚˜์˜ค๋“  ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ํ•ญ์ƒ B ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•œ๋‹ค

๊ฒฐ๊ณผ์ ์œผ๋กœ C2 ๋…ธ๋“œ๊ฐ€ ์–ด๋–ค ๊ฐ’์ด ๋‚˜์˜ค๋“  ๋ฃจํŠธ ๋…ธ๋“œ์—์„  ํ•ญ์ƒ B ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜๋Š” ์ƒํ™ฉ์ด ๋œ๋‹ค. ์ฆ‰, C2 ๋…ธ๋“œ์˜ ํ‰๊ฐ€ ๊ฒฐ๊ณผ๊ฐ€ B ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜๋Š”๋ฐ ์ „ํ˜€ ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š๊ณ  ์žˆ๋‹ค. ์ด๋Ÿฐ ์ƒํ™ฉ์—์„  C2 ๋…ธ๋“œ๋ฅผ ํƒ์ƒ‰ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค.

 

์•ŒํŒŒ-๋ฒ ํƒ€ ๊ฐ€์ง€์น˜๊ธฐ๋Š” ์œ„ ๊ฐ™์€ ์›๋ฆฌ๋ฅผ ์ด์šฉํ•ด ๋ฏธ๋‹ˆ๋งฅ์Šค ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ๋ถˆํ•„์š”ํ•œ ํƒ์ƒ‰์„ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ๋งŒ๋“œ๋Š” ๊ธฐ๋ฒ•์œผ๋กœ, ์•ŒํŒŒ($\alpha$) ๊ฐ’๊ณผ ๋ฒ ํƒ€($\beta$) ๊ฐ’์„ ์ด์šฉํ•ด ๋‚จ์€ ์ž์‹ ๋…ธ๋“œ์˜ ํƒ์ƒ‰ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค.

 

  • $\alpha$ ์•ŒํŒŒ : ํ˜„์žฌ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋ฐœ๊ฒฌํ•œ ์ตœ๋Œ€ ์ ์ˆ˜
    • ๊ฐ€์žฅ ์ž‘์€ ์ˆ˜ $-\infin$ ๋ฅผ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ง€์ •
    • ์ตœ๋Œ€ํ™” ๋…ธ๋“œ์—์„œ๋งŒ ์—…๋ฐ์ดํŠธ
  • $\beta$ ๋ฒ ํƒ€ : ์ƒ๋Œ€ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋ฐœ๊ฒฌํ•œ ์ตœ์†Œ ์ ์ˆ˜
    • ๊ฐ€์žฅ ํฐ ์ˆ˜ $\infin$๋ฅผ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ์ง€์ •
    • ์ตœ์†Œํ™” ๋…ธ๋“œ์—์„œ๋งŒ ์—…๋ฐ์ดํŠธ

 

C1 ๋…ธ๋“œ๋ฅผ ํ‰๊ฐ€ํ•œ ํ›„ ๋ถ€๋ชจ ๋…ธ๋“œ C์˜ ์ตœ์†Œ ์ ์ˆ˜(๋ฒ ํƒ€)๋Š” -98 ์ ์ด ๋˜๊ณ , ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ํ˜„์žฌ๊นŒ์ง€ ํ™•์ธ๋œ ์ตœ๋Œ€ ์ ์ˆ˜(์•ŒํŒŒ)์ธ 99์  ๋ณด๋‹ค ๋†’์€ ์ ์ˆ˜๋ฅผ ๋ฐœ๊ฒฌํ•˜์ง€ ๋ชปํ•˜๋ฉด ํ•ญ์ƒ B ๋…ธ๋“œ(99)๋ฅผ ์„ ํƒํ•œ๋‹ค. ์ฆ‰, C ๋…ธ๋“œ์—์„œ ๊ณ„์‚ฐ๋œ ๋ฒ ํƒ€ ๊ฐ’(-98)์ด ์•ŒํŒŒ ๊ฐ’(99) ์ดํ•˜์ธ ๊ฒฝ์šฐ, ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ํ•ญ์ƒ B ๋…ธ๋“œ๋ฅผ ์„ ํƒํ•˜๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

์–ด๋–ค ๋…ธ๋“œ์—์„œ ์•ŒํŒŒ ๊ฐ’์ด ๋ฒ ํƒ€ ๊ฐ’๋ณด๋‹ค ํฌ๊ฑฐ๋‚˜ ๊ฐ™์€ ์ƒํ™ฉ($\alpha \geq \beta$)์ด๋ผ๋ฉด, ํ˜„์žฌ ๋…ธ๋“œ์˜ ์ตœ์  ํ•ด๋ฅผ ์ด๋ฏธ ๋ฐœ๊ฒฌํ•œ ์ƒํƒœ๋กœ ๊ฐ„์ฃผํ•  ์ˆ˜ ์žˆ๊ณ , ์ด๋Š” ์ž์‹ ๋…ธ๋“œ๋ฅผ ๋” ์ด์ƒ ํƒ์ƒ‰ํ•  ํ•„์š”๊ฐ€ ์—†์Œ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.

 

์•ŒํŒŒ, ๋ฒ ํƒ€ ๊ฐ’์€ ํ•˜์œ„ ๋…ธ๋“œ๋ฅผ ํƒ์ƒ‰ํ•˜๋ฉด์„œ ์•„๋ž˜ ๊ณผ์ •์„ ํ†ตํ•ด ์—…๋ฐ์ดํŠธ ๋œ๋‹ค.

 

์ž์‹ ๋…ธ๋“œ๋ฅผ ํƒ์ƒ‰ํ•˜๋ฉด์„œ ์•ŒํŒŒ, ๋ฒ ํƒ€ ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ณผ์ •

  1. ๋ฃจํŠธ ๋…ธ๋“œ : ์•ŒํŒŒ, ๋ฒ ํƒ€ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • → $[-\infty, \infty]$
  2. A ๋…ธ๋“œ ํƒ์ƒ‰
    • ํ•˜์œ„ ๋…ธ๋“œ ํƒ์ƒ‰ ํ›„(์ด๋ฏธ์ง€์—์„  ์ƒ๋žต) ํ‰๊ฐ€ ์ ์ˆ˜ 97 ๋ฐ˜ํ™˜(๋ฃจํŠธ ๋…ธ๋“œ ๋ณต๊ท€)
    • ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„์ด๋ฏ€๋กœ ์•ŒํŒŒ ๊ฐ’ ์—…๋ฐ์ดํŠธ $[-\infty, \infty]$ → $[97, \infty]$
  3. B ๋…ธ๋“œ ํƒ์ƒ‰
    • ํ•˜์œ„ ๋…ธ๋“œ ํƒ์ƒ‰ ํ›„(์ด๋ฏธ์ง€์—์„  ์ƒ๋žต) ํ‰๊ฐ€ ์ ์ˆ˜ 99 ๋ฐ˜ํ™˜(๋ฃจํŠธ ๋…ธ๋“œ ๋ณต๊ท€)
    • ๋ฃจํŠธ ๋…ธ๋“œ๋Š” ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„์ด๋ฏ€๋กœ ์•ŒํŒŒ ๊ฐ’ ์—…๋ฐ์ดํŠธ $[97, \infty]$ → $[99, \infty]$
  4. C ๋…ธ๋“œ ํƒ์ƒ‰ : ๋ถ€๋ชจ ๋…ธ๋“œ๋กœ๋ถ€ํ„ฐ ์•ŒํŒŒ, ๋ฒ ํƒ€ ๊ฐ’ $[99, \infty]$ ์ „๋‹ฌ๋ฐ›์Œ
    • C1 ๋…ธ๋“œ ํƒ์ƒ‰ ํ›„ ํ‰๊ฐ€ ์ ์ˆ˜ -98 ๋ฐ˜ํ™˜(C ๋…ธ๋“œ ๋ณต๊ท€)
    • C ๋…ธ๋“œ๋Š” ์ตœ์†Œํ™” ๋‹จ๊ณ„์ด๋ฏ€๋กœ ๋ฒ ํƒ€ ๊ฐ’ ์—…๋ฐ์ดํŠธ $[99, \infty]$ → $[99, -98]$
    • ์•ŒํŒŒ ๊ฐ’ 99๊ฐ€ ๋ฒ ํƒ€ ๊ฐ’ -98๋ณด๋‹ค ๋” ํฌ๋ฏ€๋กœ ๋‹ค์Œ ๋…ธ๋“œ(C2) ํƒ์ƒ‰ ์•ˆ ํ•จ — โœ‚๏ธ Pruning

 

์˜ˆ์ œ

๋จผ์ € ๋ฃจํŠธ ๋…ธ๋“œ์—์„œ ์•ŒํŒŒ, ๋ฒ ํƒ€ ๊ธฐ๋ณธ๊ฐ’์„ $[-\infty, \infty]$ ์„ค์ •ํ•œ ํ›„ ์ž์‹ ๋…ธ๋“œ๋กœ ์ „๋‹ฌํ•œ๋‹ค. ๊ฐ ์ž์‹ ๋…ธ๋“œ์—์„  ์ „๋‹ฌ๋ฐ›์€ ๊ฐ’์„ ๋กœ์ปฌ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ํ•˜์œ„ ๋…ธ๋“œ ํ‰๊ฐ€ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค. ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” ๋…ธ๋“œ๋Š” ์•ŒํŒŒ ๊ฐ’์„, ์ตœ์†Œํ™” ๋‹จ๊ณ„์— ์žˆ๋Š” ๋…ธ๋“œ๋Š” ๋ฒ ํƒ€ ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ•ด์„œ ๋‚จ์€ ์ž์‹ ๋…ธ๋“œ์˜ ํƒ์ƒ‰ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค.

 

A1 ๋…ธ๋“œ ์•ŒํŒŒ, A ๋…ธ๋“œ ๋ฒ ํƒ€ ์—…๋ฐ์ดํŠธ ๊ณผ์ •

A - A1 ๋…ธ๋“œ ํƒ์ƒ‰

  • Max | A1 : ๊ฐ€์žฅ ํฐ ์ž์‹ ๋…ธ๋“œ(A4) ๊ฐ’(3)์œผ๋กœ ์•ŒํŒŒ ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ $[-\infin, \infin]$ → $[3, \infin]$
  • Min | A : A1 ๋…ธ๋“œ ๊ฐ’(3)์œผ๋กœ ๋ฒ ํƒ€ ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ $[-\infin, \infin]$ → $[-\infin, 3]$

 

A2 ๋…ธ๋“œ ์•ŒํŒŒ, A ๋…ธ๋“œ ๋ฒ ํƒ€ ์—…๋ฐ์ดํŠธ ๊ณผ์ •

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]$

 

B1 ๋…ธ๋“œ ์•ŒํŒŒ, B ๋…ธ๋“œ ๋ฒ ํƒ€ ์—…๋ฐ์ดํŠธ ๊ณผ์ •

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;
};

 

  1. ๋ฃจํŠธ ๋…ธ๋“œ์—์„œ ์•ŒํŒŒ/๋ฒ ํƒ€ ์ดˆ๊ธฐ๊ฐ’($-\infin,\infin$)์„ ์„ค์ •ํ•œ๋‹ค (cutBounds ๊ฐ์ฒด)
  2. ์ดˆ๊ธฐ๊ฐ’ ํ˜น์€ 3๋ฒˆ์—์„œ ์—…๋ฐ์ดํŠธํ•œ ์•ŒํŒŒ/๋ฒ ํƒ€ ๊ฐ’์„ ์ž์‹ ๋…ธ๋“œ๋กœ ์ „๋‹ฌํ•œ๋‹ค
    • ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์•ŒํŒŒ/๋ฒ ํƒ€ ๊ฐ’์ด ๋‹ด๊ธด cutBounds ๊ฐ์ฒด๋ฅผ ๋ณต์‚ฌํ•ด์„œ ์ „๋‹ฌํ•œ๋‹ค
    • cutBounds ๊ฐ์ฒด์˜ ๋ถˆ๋ณ€์„ฑ ์œ ์ง€๋Š” ๊ฐ ๋…ธ๋“œ์—์„œ ์•ŒํŒŒ/๋ฒ ํƒ€ ๊ฐ’์„ ๋กœ์ปฌ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•จ์„ ์˜๋ฏธํ•œ๋‹ค
  3. ์ž์‹ ๋…ธ๋“œ ํ‰๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ์•ŒํŒŒ/๋ฒ ํƒ€ ๊ฐ’์— ๋ฐ˜์˜ํ•œ๋‹ค
    • ์ตœ๋Œ€ํ™” ๋‹จ๊ณ„ : ํ˜„์žฌ ๋…ธ๋“œ์—์„œ ๋ฐœ๊ฒฌ๋œ ์ตœ๋Œ€ ๊ฐ’(bestScore)์„ ์•ŒํŒŒ ๊ฐ’์œผ๋กœ ์„ค์ •
    • ์ตœ์†Œํ™” ๋‹จ๊ณ„ : ํ˜„์žฌ ๋…ธ๋“œ์—์„œ ๋ฐœ๊ฒฌ๋œ ์ตœ์†Œ ๊ฐ’(bestScore)์„ ๋ฒ ํƒ€ ๊ฐ’์œผ๋กœ ์„ค์ •
  4. ํ˜„์žฌ ๋…ธ๋“œ์˜ ์ตœ์ ํ•ด๋ฅผ ์ฐพ์•˜๋Š”์ง€ ๊ฒ€์‚ฌ
    • $\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์ดˆ)

 

 

๋ ˆํผ๋Ÿฐ์Šค


 

 

์™„์„ฑ ๋ ˆํฌ์ง€ํ† ๋ฆฌ


 

GitHub - romantech/tic-tac-toe: Customizable Tic-Tac-Toe game with React

Customizable Tic-Tac-Toe game with React. Contribute to romantech/tic-tac-toe development by creating an account on GitHub.

github.com

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•