๋ฐ˜์‘ํ˜•

ํ‹ฑํƒํ†  ๊ฒŒ์ž„ ํ™”๋ฉด โ˜ ์ด๋ฏธ์ง€ ์ถœ์ฒ˜ - ๋ณธ์ธ ํ”„๋กœ์ ํŠธ

 

ํ‹ฑํƒํ†  ์†Œ๊ฐœ


ํ‹ฑํƒํ† ๋Š” 2๋ช…์˜ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ž์‹ ์˜ ๊ธฐํ˜ธ(X, O) 3๊ฐœ๋ฅผ ๊ฐ€๋กœ, ์„ธ๋กœ, ๋Œ€๊ฐ์„ ์œผ๋กœ ์—ฐ์†ํ•ด์„œ ๋†“์ด๋„๋ก ํ•˜๋Š” ๋ณด๋“œ ๊ฒŒ์ž„์ด๋‹ค. ๊ฒŒ์ž„์€ ์ฃผ๋กœ 3x3 ๊ฒฉ์ž ๋ณด๋“œ์—์„œ ์ง„ํ–‰๋œ๋‹ค. ํ”Œ๋ ˆ์ด์–ด๋Š” ๋ฒˆ๊ฐˆ์•„๊ฐ€๋ฉด์„œ ์ž์‹ ์˜ ๊ธฐํ˜ธ๋ฅผ ๋†“๊ณ , ํ•œ ์นธ์—” ํ•œ ๊ฐœ์˜ ๊ธฐํ˜ธ๋งŒ ๋†“์„ ์ˆ˜ ์žˆ๋‹ค. ๋ชจ๋“  ์นธ์— ๊ธฐํ˜ธ๋ฅผ ๋†“์•˜์ง€๋งŒ ์–ด๋Š ํ•œ์ชฝ๋„ ์—ฐ์†์ ์ธ ์„ธ ๊ฐœ์˜ ๊ธฐํ˜ธ๋ฅผ ๋ฐฐ์—ดํ•˜์ง€ ๋ชปํ•˜๋ฉด ๊ฒŒ์ž„์€ ๋ฌด์Šน๋ถ€๋กœ ๋๋‚œ๋‹ค(๋ฌด์Šน๋ถ€๊ฐ€ ๋งŽ์€ ๊ฒŒ์ž„).

 

 

์Šน๋ฆฌ ์กฐ๊ฑด ์ฒดํฌ


ํ‹ฑํƒํ† ๋Š” ํ–‰๋ ฌ๋กœ ์ด๋ค„์ง„ 2์ฐจ์› ๋ณด๋“œ์—์„œ ์ง„ํ–‰ํ•˜์ง€๋งŒ, 1์ฐจ์› ๋ฐฐ์—ด๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ๊ฐ ์นธ์„ ๋‹จ์ผ ์ธ๋ฑ์Šค๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋” ์ˆ˜์›”ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ฒŒ์ž„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๋•Œ๋„ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ์„ ๋‹จ์ˆœํ™”์‹œ์ผœ ์ฝ”๋“œ์˜ ๋ณต์žก์„ฑ์„ ์ค„์ด๋Š” ๋ฐ ๋„์›€์ด ๋œ๋‹ค. ๋˜ํ•œ, 1์ฐจ์› ๋ฐฐ์—ด์€ ๊ทธ๋ฆฌ๋“œ ์Šคํƒ€์ผ์„ ์ด์šฉํ•ด 2์ฐจ์› ๋ณด๋“œ๋กœ ๋ Œ๋”๋ง ํ•  ์ˆ˜ ์žˆ๋‹ค.

const board = [null, 'O', 'X', null, null, 'O', ...];

/*
  [null, 'O', 'X']
  [null, null, 'O']    
  [null, null, null]
*/

 

Basic

3x3 ๋ณด๋“œ ๊ฐ ๊ฒฉ์ž์— ์ธ๋ฑ์Šค๋ฅผ ํ‘œ๊ธฐํ•œ ์ด๋ฏธ์ง€

์Šน๋ฆฌ ์กฐ๊ฑด์„ ๊ฒ€์‚ฌํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ์˜ ์ˆ˜(์ธ๋ฑ์Šค)๋ฅผ 2์ฐจ์› ๋ฐฐ์—ด์— ์ €์žฅํ•ด๋‘๊ณ , ํ˜„์žฌ ๊ฒŒ์ž„ ๋ณด๋“œ์™€ ๋น„๊ตํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. 3x3 ๋ณด๋“œ์—์„  ๊ฐ€๋กœ, ์„ธ๋กœ, ๋Œ€๊ฐ์„  ์ด 8๊ฐ€์ง€ ๋ฐฉ๋ฒ•($size \times 2 +2$)์œผ๋กœ ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. React ๊ณต์‹ ๋ฌธ์„œ์— ์žˆ๋Š” Tic-Tac-Toe ํŠœํ† ๋ฆฌ์–ผ๋„ ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•œ๋‹ค.

// React ๊ณต์‹ ๋ฌธ์„œ ์ฝ”๋“œ ์‚ด์ง ๋ณ€ํ˜•
function calculateWinner(board: number[]) {
  const lines = [
    [0, 1, 2], // row 1
    [3, 4, 5], // row 2
    [6, 7, 8], // row 3
    [0, 3, 6], // column 1
    [1, 4, 7], // column 2
    [2, 5, 8], // column 3
    [0, 4, 8], // diagonal 1
    [2, 4, 6], // diagonal 2
  ];

  for (const [a, b, c] of lines) {
    const isLineMatch = board?.[a] === board?.[b] && board?.[a] === board?.[c];
    if (isLineMatch) return board[a]; // 'X' | 'O'
  }

  return null;
}

 

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

 

 

Advanced


๋”๋ณด๊ธฐ

์ฃผ์š” ๋กœ์ง (game-core.ts)

const checkWinIndexes = (
  board: TBoard,
  winCondition: number,
  linearIndex: number,
  player: BasePlayer,
) => {
  const size = getBoardSize(board);
  const { row: lastRow, col: lastCol } = getCoordinates(linearIndex, 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)
      return winningIndexes.map(({ row, col }) =>
        getLinearIndex(row, col, size),
      );
  }

  return null;
};

const checkDirection = (
  board: TBoard,
  winCondition: number,
  player: 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 currentRow = i * deltaRow + lastRow;
      const currentCol = i * deltaCol + lastCol;

      // ๋ณด๋“œ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์œผ๋ฉด ๊ฒ€์‚ฌ ์ค‘์ง€
      if (!isWithinBounds(size, currentRow, currentCol)) break;
      // ๊ธฐํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฒ€์‚ฌ ์ค‘์ง€
      if (getCellIdentifier(board, currentRow, currentCol) !== player) break;

      winningIndexes.push({ row: currentRow, col: currentCol });
    }

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

 

์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ (game-core.ts)

// row, col ์ขŒํ‘œ๊ฐ’์ด ๋ณด๋“œ ๋ฒ”์œ„ ์•ˆ์— ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌ
const isWithinBounds = (size: number, row: number, col: number) => {
  return row >= 0 && row < size && col >= 0 && col < size;
};

// 1์ฐจ์› ๋ณด๋“œ์—์„œ row, col ์ขŒํ‘œ๊ฐ’์— ํ•ด๋‹นํ•˜๋Š” ๊ธฐํ˜ธ ์กฐํšŒ
const getCellIdentifier = (board: TBoard, row: number, col: number) => {
  const size = getBoardSize(board);
  const idx = getLinearIndex(row, col, size);
  return board[idx].identifier;
};

// ๋ณด๋“œ ์‚ฌ์ด์ฆˆ ์กฐํšŒ(row.length)
export const getBoardSize = (board: TBoard) => {
  const size = Math.sqrt(board.length);
  if (!Number.isInteger(size))
    throw new Error('Board is not a perfect square.');

  return size;
};

 

๋งˆ์ง€๋ง‰ ๋†“์•˜๋˜ ๊ฒฉ์ž ์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฐ€๋กœ, ์„ธ๋กœ, ๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ์œผ๋กœ ์Šน๋ฆฌ ์กฐ๊ฑด ์ˆ˜ ๋งŒํผ ๊ฒฉ์ž๋ฅผ ๊ฒ€์‚ฌํ•˜๋ฉด ๋ณด๋“œ ํฌ๊ธฐ๋‚˜ ์Šน๋ฆฌ ์กฐ๊ฑด์ด ๋™์ ์ธ ๊ฒฝ์šฐ์—๋„ ์Šน๋ฆฌ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด 4×4 ๋ณด๋“œ์—์„œ 10๋ฒˆ ์ธ๋ฑ์Šค์— ๋†“์•˜๋‹ค๋ฉด ํ•ด๋‹น ์ธ๋ฑ์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฐ€๋กœ, ์„ธ๋กœ, ๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ์œผ๋กœ ์Šน๋ฆฌ ์กฐ๊ฑด์— ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•˜๋ฉด ๋œ๋‹ค.

 

 

๋งˆ์ง€๋ง‰์œผ๋กœ ๋†“์€ 10๋ฒˆ ์ธ๋ฑ์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฐ€๋กœ/์„ธ๋กœ/๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ์œผ๋กœ ์Šน๋ฆฌ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•ด๋ณธ๋‹ค

๐Ÿ’ก Delta(์†Œ๋ฌธ์ž $\delta$ / ๋Œ€๋ฌธ์ž $\Delta$)๋Š” ๋ณ€ํ™”, ์ฐจ์ด๋ฅผ ์˜๋ฏธํ•œ๋‹ค. ์ฃผ๋กœ ์ˆ˜ํ•™์ด๋‚˜ ๊ณผํ•™์—์„œ ์‚ฌ์šฉ๋˜๋ฉฐ, ํŠน์ • ๊ฐ’์ด๋‚˜ ์–‘์˜ ๋ณ€ํ™”๋Ÿ‰์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด $\delta{x}$๋Š” $x$์˜ ๋ณ€ํ™”๋Ÿ‰์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.

 

๋จผ์ € ๊ฒŒ์ž„ ๋ณด๋“œ์—์„œ ๊ฒ€์‚ฌํ•  4๊ฐ€์ง€ ๋ฐฉํ–ฅ์„ directions ๋ฐฐ์—ด์— ๋ฏธ๋ฆฌ ์ •์˜ํ•ด๋‘”๋‹ค. ๋ฐฐ์—ด์˜ ๊ฐ ์š”์†Œ๋Š” deltaRow, deltaCol์„ ํฌํ•จํ•˜์—ฌ, ๋ณด๋“œ์˜ ํŠน์ • ์…€์—์„œ ๋‹ค๋ฅธ ์…€๋กœ ์ด๋™ํ•  ๋•Œ ํ–‰๊ณผ ์—ด์ด ์–ด๋–ป๊ฒŒ ๋ณ€ํ™”ํ•˜๋Š”์ง€ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด { deltaRow: 0, deltaCol: 1 }์€ ๊ฐ€๋กœ ๋ฐฉํ–ฅ์„ ์˜๋ฏธํ•˜๋ฉฐ, ํ–‰์€ ๋ณ€ํ•˜์ง€ ์•Š๊ณ  ์—ด๋งŒ 1์”ฉ ์ฆ๊ฐ€ํ•œ๋‹ค. ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์—์„  ์—ด์ด 1์”ฉ ๊ฐ์†Œํ•œ๋‹ค.

const directions = [
  { deltaRow: 0, deltaCol: 1 }, // ๊ฐ€๋กœ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ -0, -1)
  { deltaRow: 1, deltaCol: 0 }, // ์„ธ๋กœ (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ -1, -0)
  { deltaRow: 1, deltaCol: 1 }, // ์šฐํ•˜ ๋Œ€๊ฐ์„  (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ -1, -1)
  { deltaRow: 1, deltaCol: -1 }, // ์ขŒํ•˜ ๋Œ€๊ฐ์„  (๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ -1, 1)
];

 

์ด ๋ฐฉํ–ฅ ๊ฐ’๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ์ง€๋ง‰ ๋†“์•˜๋˜ ์œ„์น˜๋ถ€ํ„ฐ ๊ฐ ๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ์†๋œ ์…€์„ ๊ฒ€์‚ฌํ•œ๋‹ค. winCondition์€ ์—ฐ์†์ ์œผ๋กœ ์ผ์น˜ํ•ด์•ผ ํ•˜๋Š” ์…€์˜ ๊ฐœ์ˆ˜๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด winCondition์ด 3์ผ ๊ฒฝ์šฐ, X ํ˜น์€ O ๊ธฐํ˜ธ๊ฐ€ ๊ฐ€๋กœ, ์„ธ๋กœ ๋˜๋Š” ๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ์†ํ•ด์„œ 3๋ฒˆ ์œ„์น˜ํ•ด์•ผ ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

checkWinIndexes ํ•จ์ˆ˜๊ฐ€ ์ธ์ž๋กœ ๋ฐ›์€ ์ธ๋ฑ์Šค๋Š” ๋งˆ์ง€๋ง‰ ์ˆ˜๋ฅผ ๋‘” ๊ณณ์œผ๋กœ ๊ฐ„์ฃผํ•œ๋‹ค

 

export const checkWinIndexes = (
  board: TBoard,
  winCondition: number,
  linearIndex: number,
  player: BasePlayer,
) => {
  // ...
  for (const { deltaRow, deltaCol } of directions) {
    const winningIndexes = checkDirection(/* ... */);
    // ...
  }
  return null;
};

const checkDirection = (/* ... */) => {
  const searchDirection = (deltaRow: number, deltaCol: number) => {
    const winningIndexes = [];

    // ๋งˆ์ง€๋ง‰ ๋†“์•˜๋˜ ์œ„์น˜๋Š” ์ œ์™ธํ•˜๊ธฐ ์œ„ํ•ด i = 1 ๋ถ€ํ„ฐ ์‹œ์ž‘
    for (let i = 1; i < winCondition; i++) {
      const currentRow = i * deltaRow + lastRow;
      const currentCol = i * deltaCol + lastCol;

      if (/* ... */) break; // ๋ณด๋“œ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ๊ฑฐ๋‚˜ ๊ธฐํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฒ€์‚ฌ ์ค‘์ง€
      winningIndexes.push({ row: currentRow, col: currentCol });
    }

    return winningIndexes;
  };

  // ์–‘์ชฝ ๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ์†๋œ ๋งˆํฌ ๊ฒ€์‚ฌ
  const forwardWinningIndexes = searchDirection(deltaRow, deltaCol);
  const backwardWinningIndexes = searchDirection(-deltaRow, -deltaCol);
  // ...
};

 

๋งŒ์•ฝ deltaRow ํ˜น์€ deltaCol ๊ฐ’์ด 0์ด๋ผ๋ฉด, i๋ฅผ ๊ณฑํ•ด๋„ ํ•ญ์ƒ 0์ด ๋˜๋ฏ€๋กœ ์œ„์น˜ ๋ณ€ํ™”์—†์ด ํ•ด๋‹น ์ขŒํ‘œ์— ๊ณ ์ •๋œ๋‹ค. i๋Š” 1์”ฉ ์ฆ๊ฐ€ํ•˜๋ฏ€๋กœ delta ๊ฐ’์ด 1 ํ˜น์€ -1์ผ ๋• ๊ฒฐ๊ณผ์ ์œผ๋กœ 1์”ฉ ์ฆ๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๊ฐ์†Œํ•˜๋Š” ํŒจํ„ด์ด ๋œ๋‹ค.

 

$$\begin{align*}\delta = 1: \quad & 1 \times 1, \quad 2 \times 1, \quad 3 \times 1, \quad \ldots \\ \delta = -1: \quad & 1 \times (-1), \quad 2 \times (-1), \quad 3 \times (-1), \quad \ldots\end{align*}$$

 

4×4 ๋ณด๋“œ์—์„œ ๋งˆ์ง€๋ง‰ ๋†“์•˜๋˜ ์ขŒํ‘œ๋ฅผ ํ–‰ 2, ์—ด 2๋กœ ๊ฐ€์ •ํ•˜๊ณ  ๊ณ„์‚ฐํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

[๊ฐ€๋กœ ๋ฐฉํ–ฅ]
------------------------------------------
i = 1
row = 1(i) * 0(deltaRow) + 2(lastRow) = 2
col = 1(i) * 1(deltaCol) + 2(lastCol) = 3
push({ row: 2, col: 3 })

i = 2
row = 2(i) * 0(deltaRow) + 2(lastRow) = 2
col = 2(i) * 1(deltaCol) + 2(lastCol) = 4
col ๋ณด๋“œ ๋ฒ”์œ„ ์ดˆ๊ณผ -> break

[๊ฐ€๋กœ ๋ฐฉํ–ฅ - ๋ฐ˜๋Œ€]
------------------------------------------
i = 1
row = 1(i) * -0(deltaRow) + 2(lastRow) = 2
col = 1(i) * -1(deltaCol) + 2(lastCol) = 1
push({ row: 2, col: 1 })

i = 2
row = 2(i) * -0(deltaRow) + 2(lastRow) = 2
col = 2(i) * -1(deltaCol) + 2(lastCol) = 0
push({ row: 2, col: 0 })

...

 

์ธ๋ฑ์Šค ๊ณ„์‚ฐ

1์ฐจ์› ๋ณด๋“œ ์ธ๋ฑ์Šค → ํ–‰๋ ฌ ์ขŒํ‘œ ๊ณ„์‚ฐ

1์ฐจ์› ๋ณด๋“œ์˜ ์ธ๋ฑ์Šค์™€ ๋ณด๋“œ ์‚ฌ์ด์ฆˆ๋ฅผ ์ด์šฉํ•ด 2์ฐจ์› ๋ณด๋“œ์˜ ํ–‰(row), ์—ด(col) ์ขŒํ‘œ ๊ฐ’์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด 4×4 ๋ณด๋“œ ์ธ๋ฑ์Šค 10 ์ขŒํ‘œ ๊ฐ’์€ ํ–‰ row = floor(10 / 4) = 2, ์—ด 10 mod 4 = 2 ๋กœ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

const getCoordinates = (i: number, size: number) => {
  const row = Math.floor(i / size);
  const col = i % size; // ํ•ญ์ƒ 0 ~ (size - 1) ๊ฐ’ ๋ฐ˜ํ™˜
  return { row, col };
};

 

์ฐธ๊ณ ๋กœ ๋ชจ๋“ˆ๋กœ ์—ฐ์‚ฐ(๋‚˜๋จธ์ง€ ์—ฐ์‚ฐ)์€ ํ•ญ์ƒ 0๋ถ€ํ„ฐ ์ œ์ˆ˜ - 1 ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค

0 mod 4 = 0 (ํ”ผ์ œ์ˆ˜ mod ์ œ์ˆ˜)
1 mod 4 = 1
2 mod 4 = 2
3 mod 4 = 3
4 mod 4 = 0
5 mod 4 = 1
...

 

ํ–‰๋ ฌ ์ขŒํ‘œ → 1์ฐจ์› ๋ณด๋“œ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ

ํ–‰ ์ขŒํ‘œ์— ๋ณด๋“œ ํฌ๊ธฐ๋ฅผ ๊ณฑํ•œ ํ›„ ์—ด ์ขŒํ‘œ๋ฅผ ๋”ํ•˜๋ฉด 1์ฐจ์› ๋ณด๋“œ์˜ ์ธ๋ฑ์Šค ๊ฐ’์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด 4×4 ํฌ๊ธฐ ๋ณด๋“œ์—์„œ ํ–‰ ์ขŒํ‘œ๊ฐ€ 2์ผ ๊ฒฝ์šฐ 4 * 2 = 8๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ํ•ด๋‹น ํ–‰์˜ ์ฒซ ๋ฒˆ์งธ ์—ด ์ธ๋ฑ์Šค ๊ฐ’์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์—ฌ๊ธฐ์— ์—ด ์ขŒํ‘œ๊ฐ’์„ ๋”ํ•˜๋ฉด ์ตœ์ข…์ ์ธ 1์ฐจ์› ๋ณด๋“œ์˜ ์ธ๋ฑ์Šค๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

const getLinearIndex = (row: number, col: number, size: number) => {
  return row * size + col;
};

 

๋ชจ์„œ๋ฆฌ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ

์ •์‚ฌ๊ฐํ˜• ๊ฒฉ์ž์—์„œ size(ํ•œ ๋ณ€์˜ ๊ธธ์ด)๋ฅผ ์ด์šฉํ•ด ๋„ค ๋ชจ์„œ๋ฆฌ์˜ ์ธ๋ฑ์Šค๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

const getCornerIndexes = (size: number) => {
  const leftTop = 0;
  const rightTop = size - 1;
  const leftBottom = size * (size - 1);
  const rightBottom = size * size - 1;

  return [leftTop, rightTop, leftBottom, rightBottom];
};

 

  • ์™ผ์ชฝ ์ƒ๋‹จ : ๊ฒฉ์ž์˜ ์‹œ์ž‘์ ์ด๋ฏ€๋กœ ์ธ๋ฑ์Šค๋Š” ํ•ญ์ƒ 0
  • ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ : ์ฒซ๋ฒˆ์งธ ํ–‰์˜ ๋งˆ์ง€๋ง‰ ์š”์†Œ์ด๋ฏ€๋กœ ํ–‰ ๊ธธ์ด size์—์„œ 1์„ ๋บ€๋‹ค
  • ์™ผ์ชฝ ํ•˜๋‹จ : ๋งˆ์ง€๋ง‰ ํ–‰์˜ ์ธ๋ฑ์Šค size - 1 ์—์„œ ์—ด ๊ธธ์ด size ๋ฅผ ๊ณฑํ•œ๋‹ค
  • ์˜ค๋ฅธ์ชฝ ํ•˜๋‹จ : ์ธ๋ฑ์Šค๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋ฏ€๋กœ ์ „์ฒด ๊ฒฉ์ž ํฌ๊ธฐ size * size ์—์„œ 1์„ ๋บ€๋‹ค

 

์ค‘์•™ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ

 

3×3 ๊ฐ™์€ ํ™€์ˆ˜ ํฌ๊ธฐ ๋ณด๋“œ๋Š” ์ •ํ™•ํ•œ ์ค‘์•™์ ์ด ์กด์žฌํ•œ๋‹ค. ๋ฐ˜๋ฉด, ์ง์ˆ˜ ํฌ๊ธฐ ๋ณด๋“œ๋Š” ๋‹จ์ผ ์ค‘์•™์ ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ค‘์‹ฌ์— ๊ฐ€๊นŒ์šด 4๊ฐœ ๊ฒฉ์ž๋ฅผ ์ค‘์•™์œผ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•œ๋‹ค. ์ด 4๊ฐœ ๊ฒฉ์ž๋Š” ๋ณด๋“œ ์ •์ค‘์•™์— 2×2 ๊ฒฉ์ž ๊ตฌ์—ญ์„ ํ˜•์„ฑํ•œ๋‹ค.

const getCenterIndexes = (size: number) => {
  // ํ™€์ˆ˜ size ๋ณด๋“œ
  if (size % 2 !== 0) return [Math.floor((size * size) / 2)];

  // ์ง์ˆ˜ size ๋ณด๋“œ
  const halfSize = size / 2;
  return [
    (halfSize - 1) * size + halfSize - 1, // ์ค‘์•™ ๊ฒฉ์ž ์™ผ์ชฝ ์ƒ๋‹จ
    (halfSize - 1) * size + halfSize, // ์ค‘์•™ ๊ฒฉ์ž ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ
    halfSize * size + halfSize - 1, // ์ค‘์•™ ๊ฒฉ์ž ์™ผ์ชฝ ํ•˜๋‹จ
    halfSize * size + halfSize, // ์ค‘์•™ ๊ฒฉ์ž ์˜ค๋ฅธ์ชฝ ํ•˜๋‹จ
  ];
};

 

  • (halfSize - 1) * size : ์ค‘์•™ ๊ฒฉ์ž ์œ„์ชฝ ํ–‰์˜ ์‹œ์ž‘์ 
  • halfSize * size : ์ค‘์•™ ๊ฒฉ์ž ์•„๋ž˜์ชฝ ํ–‰์˜ ์‹œ์ž‘์ 
  • ํ–‰ ์‹œ์ž‘ ์ธ๋ฑ์Šค + (halfSize - 1) : ์ค‘์•™ ์—ด์˜ ์™ผ์ชฝ ์ธ๋ฑ์Šค
  • ํ–‰ ์‹œ์ž‘ ์ธ๋ฑ์Šค + halfSize : ์ค‘์•™ ์—ด์˜ ์˜ค๋ฅธ์ชฝ ์ธ๋ฑ์Šค

 

 

์ตœ์ ์˜ ์ˆ˜ ์ฐพ๊ธฐ


๐Ÿ’ก ์•„๋ž˜ ์„ค๋ช…์—์„  ๋ฏธ๋‹ˆ๋งฅ์Šค, ์•ŒํŒŒ-๋ฒ ํƒ€ ๊ฐ€์ง€์น˜๊ธฐ ๊ฐœ๋…๋งŒ ์†Œ๊ฐœ. ๋” ์ž์„ธํ•œ ์„ค๋ช…/๊ตฌํ˜„ ๋‚ด์šฉ์€ ํฌ์ŠคํŒ… ๋งํฌ ์ฐธ๊ณ 

 

ํ‹ฑํƒํ†  ๊ฒŒ์ž„์—์„œ ์ตœ์ ์˜ ์ˆ˜๋Š” ๋ฏธ๋‹ˆ๋งฅ์Šค(Minimax), ์•ŒํŒŒ-๋ฒ ํƒ€ ๊ฐ€์ง€์น˜๊ธฐ(Alpha-Beta Pruning) ๋“ฑ์˜ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•œ๋‹ค. ๋ฏธ๋‹ˆ๋งฅ์Šค๋Š” ํ‹ฑํƒํ† ์ฒ˜๋Ÿผ 2๋ช…์ด ์ง„ํ–‰ํ•˜๋Š” ์ œ๋กœ์„ฌ ๊ฒŒ์ž„(zero-sum game)์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ, ๊ฐ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์ตœ์„ ์˜ ์ˆ˜๋ฅผ ๋‘”๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ์Šน๋ฆฌ๋Š” ์ตœ๋Œ€ํ™”ํ•˜๊ณ  ํŒจ๋ฐฐ๋Š” ์ตœ์†Œํ™”ํ•˜๋Š” ์ˆ˜๋ฅผ ์ฐพ๋Š”๋‹ค.

 

๋ฏธ๋‹ˆ๋งฅ์Šค๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์•ŒํŒŒ-๋ฒ ํƒ€ ๊ฐ€์ง€์น˜๊ธฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•œ๋‹ค. ์•ŒํŒŒ-๋ฒ ํƒ€ ๊ฐ€์ง€์น˜๊ธฐ๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํšจ์œจ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ฒ•์œผ๋กœ, ์ด๋ฏธ ํƒ์ƒ‰ํ•œ ๊ฒฝ๋กœ๋ณด๋‹ค ๋” ๋‚˜์œ ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋ฅผ ์กฐ๊ธฐ์— ์ œ๊ฑฐํ•˜์—ฌ ๊ฒ€์ƒ‰ ์‹œ๊ฐ„์„ ๋‹จ์ถ•์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค. ์•ŒํŒŒ-๋ฒ ํƒ€ ๊ฐ€์ง€์น˜๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฏธ๋‹ˆ๋งฅ์Šค ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์ˆ˜๋ฅผ ๊ณ ๋ คํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง€๋ฏ€๋กœ ๊ณ„์‚ฐ๋Ÿ‰์„ ์ƒ๋‹นํžˆ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

๋”๋ณด๊ธฐ

์ฃผ์š” ๋กœ์ง (game-core.ts)

const findBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  const opponent = getOpponent(player);
  const size = getBoardSize(board);

  // ์Šน๋ฆฌ ์œ„์น˜ ํƒ์ƒ‰
  const bestMove = getFirstBestMoveIdx(board, winCondition, player);
  if (bestMove !== null) return bestMove;

  // ๋ฐฉ์–ด ์œ„์น˜ ํƒ์ƒ‰
  const minDefenseCondition = winCondition - 2;
  // ์—ฐ์†๋œ ๊ธฐํ˜ธ๊ฐ€ ๋ฐฐ์—ด๋  ์ˆ˜ ์žˆ๋Š” ์ตœ์†Œ ๊ธธ์ด๋ถ€ํ„ฐ ์Šน๋ฆฌ ์กฐ๊ฑด๊นŒ์ง€์˜ ๋ฒ”์œ„. e.g. ์Šน๋ฆฌ ์กฐ๊ฑด 4, ์ตœ์†Œ ๋ฐฉ์–ด ์กฐ๊ฑด 2 -> ๋ฒ”์œ„๋Š” 3 (2, 3, 4)
  const defenseRange = winCondition - minDefenseCondition + 1;
  // ๊ณ„์‚ฐ๋œ ๋ฒ”์œ„์— ๋”ฐ๋ผ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ๋ฐฉ์–ด ์กฐ๊ฑด ๋ฐฐ์—ด ์ƒ์„ฑ. e.g. ์Šน๋ฆฌ ์กฐ๊ฑด 4 -> [4, 3, 2]
  const defenseConditions = Array.from(
    { length: defenseRange },
    (_, i) => winCondition - i,
  );

  for (const condition of defenseConditions) {
    const defenseMove = getFirstBestMoveIdx(board, condition, opponent);
    if (defenseMove !== null) return defenseMove;
    // ๋ณด๋“œ ํฌ๊ธฐ์™€ ์Šน๋ฆฌ ์กฐ๊ฑด์ด ๊ฐ™๋‹ค๋ฉด ๋ณด๋“œ ์ „์ฒด๋ฅผ ์ฑ„์›Œ์•ผ๋งŒ ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ด๋ณด๋‹ค ์ž‘์€ ์Šน๋ฆฌ์กฐ๊ฑด์€ ๊ฒ€์‚ฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค
    if (condition === size) break;
  }

  // ์ค‘์•™, ๋ชจ์„œ๋ฆฌ, ๋นˆ์นธ ์ค‘ ์ž„์˜ ์„ ํƒ. ๋ชจ๋“  ์นธ์ด ๋‹ค ์ฐผ์œผ๋ฉด null ๋ฐ˜ํ™˜
  return chooseStrategicPosition(board, size);
};

const getFirstBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  const idx = board.findIndex((cell, i) => {
    if (cell.identifier === null) {
      const winningIndexes = checkWinIndexes(board, winCondition, i, player);
      return winningIndexes !== null;
    }
    return false;
  });

  return idx !== -1 ? idx : null;
};

const chooseStrategicPosition = (board: TBoard, size: number) => {
  const availableMoves = getAvailableMoves(board);

  if (availableMoves.size === 0) return null;

  // 3x3 ๋ณด๋“œ๋Š” ์ค‘์•™, ๋ชจ์„œ๋ฆฌ๋ฅผ ๋จผ์ € ์„ ์ ํ•˜๋Š”๊ฒŒ ๊ฒŒ์ž„ ์Šน๋ฆฌ์— ์œ ๋ฆฌํ•จ
  if (size === 3) {
    const centerIdx = getCenterIndexes(size);
    const cornerIndexes = getCornerIndexes(size);
    const filtered = [...centerIdx, ...cornerIndexes].filter((idx) =>
      availableMoves.has(idx),
    );
    if (filtered.length > 0) return selectRandomElement(filtered);
  }

  return selectRandomElement([...availableMoves]);
};

 

์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜

// game-core.ts
const getAvailableMoves = (board: TBoard) => {
  return board.reduce((moves, cell, i) => {
    if (cell.identifier === null) moves.add(i);
    return moves;
  }, new Set<number>());
};

const getOpponent = (player: BasePlayer) => {
  return player === BasePlayer.X ? BasePlayer.O : BasePlayer.X;
};

// helpers.ts
const randomIntBetween = (from: number, to: number) => {
  return Math.floor(Math.random() * (to - from + 1) + from);
};

const selectRandomElement = <T,>(arr: T[]) => {
  return arr[randomIntBetween(0, arr.length - 1)];
};

 

์Šน๋ฆฌ ์œ„์น˜ ํƒ์ƒ‰

์Šน๋ฆฌ ์œ„์น˜๋ฅผ ํƒ์ƒ‰ํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ํ˜„์žฌ ๋ณด๋“œ์˜ ๋นˆ ์นธ์„ ํ”Œ๋ ˆ์ด์–ด ๊ธฐํ˜ธ๋กœ ์ฑ„์šฐ๊ณ , ํ•ด๋‹น ์œ„์น˜๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฐ€๋กœ, ์„ธ๋กœ, ๋Œ€๊ฐ์„  ๋ฐฉํ–ฅ์œผ๋กœ ์Šน๋ฆฌ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•ด๋ณด๋Š” ๊ฒƒ์ด๋‹ค. ์ด ์ ‘๊ทผ๋ฒ•์€ ๋‹จ์ˆœํ•˜์ง€๋งŒ ์œ„์—์„œ ์ž‘์„ฑํ•œ ์Šน๋ฆฌ ์กฐ๊ฑด ์ฒดํฌ ํ•จ์ˆ˜๋ฅผ ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค(checkWinIndexes ํ•จ์ˆ˜๊ฐ€ ์ธ์ž๋กœ ๋ฐ›์€ ์ธ๋ฑ์Šค๋Š” ๋งˆ์ง€๋ง‰ ์ˆ˜๋ฅผ ๋‘” ์œ„์น˜๋กœ ๊ฐ„์ฃผํ•˜๊ธฐ ๋•Œ๋ฌธ).

const getFirstBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  const idx = board.findIndex((cell, i) => {
    if (cell.identifier === null) {
      const winningIndexes = checkWinIndexes(board, winCondition, i, player);
      return winningIndexes !== null;
    }
    return false;
  });

  return idx !== -1 ? idx : null;
};

const findBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  // ...
  const bestMove = getFirstBestMoveIdx(board, winCondition, player);
  if (bestMove !== null) return bestMove;
  // ...
};

 

  1. ๋ณด๋“œ๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ๋นˆ ๊ฒฉ์ž์ธ์ง€ ํ™•์ธํ•œ๋‹ค
  2. ๋นˆ ๊ฒฉ์ž๋ผ๋ฉด ํ•ด๋‹น ์ธ๋ฑ์Šค์™€ ํ”Œ๋ ˆ์ด์–ด ์ •๋ณด๋กœ checkWinIndexes ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค
  3. ์Šน๋ฆฌ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•œ๋‹ค๋ฉด ํ˜„์žฌ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์•„๋‹ˆ๋ผ๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค

 

๋ฐฉ์–ด ์œ„์น˜ ํƒ์ƒ‰

Basic

ํ”Œ๋ ˆ์ด์–ด ์ •๋ณด๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด getFirstBestMoveIdx ๋“ฑ์˜ ๊ธฐ์กด ๋กœ์ง์„ ๊ทธ๋Œ€๋กœ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ๋ฐฉ์–ด ์œ„์น˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค. ์ƒ๋Œ€๋ฐฉ ๊ธฐํ˜ธ๋ฅผ ์ด์šฉํ•ด ๋ณด๋“œ๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ ์Šน๋ฆฌ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๊ณณ์„ ํƒ์ƒ‰ํ•˜๊ณ , ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ณณ์„ ์ฐพ์•˜๋‹ค๋ฉด ํ•ด๋‹น ์œ„์น˜๋ฅผ ๋ฐฉ์–ด ์œ„์น˜๋กœ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

const findBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  const opponent = getOpponent(player); // player = 'O' | 'X'
  // ...
  const defenseMove = getFirstBestMoveIdx(board, winCondition, opponent);
  if (defenseMove !== null) return defenseMove;
};

 

Advanced

์Šน๋ฆฌ ์กฐ๊ฑด์ด 4๋ผ๋ฉด ๋ฐฉ์–ด๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ

๋งŒ์•ฝ ์Šน๋ฆฌ ์กฐ๊ฑด์ด ๋ณด๋“œ ์‚ฌ์ด์ฆˆ(๋ณด๋“œ ํ•œ ๋ณ€์˜ ๊ธธ์ด) ๋ณด๋‹ค ์ž‘์€ ๊ฒฝ์šฐ, ์Šน๋ฆฌ ์กฐ๊ฑด - 2 ๋งŒํผ ๊ฒฉ์ž๊ฐ€ ์—ฐ์†์ ์œผ๋กœ ๋ฐฐ์—ด๋œ ์‹œ์ ์—, ์—ฐ์†๋œ ๋ฐฐ์—ด ์–‘ ๋ ์ค‘ ํ•œ๊ณณ์„ ๋ฐฉ์–ดํ•ด ์ค˜์•ผ ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋ณด๋“œ ์‚ฌ์ด์ฆˆ๊ฐ€ 6์ด๊ณ  ์Šน๋ฆฌ ์กฐ๊ฑด์ด 4์ธ ์ƒํ™ฉ์—์„œ ์œ„ ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ 20~21๋ฒˆ์— ์—ฐ์†์ ์œผ๋กœ ๊ธฐํ˜ธ๊ฐ€ ๋ฐฐ์น˜๋๋‹ค๋ฉด, 19๋ฒˆ ํ˜น์€ 22๋ฒˆ์„ ๋ฐฉ์–ดํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์ƒ๋Œ€๋ฐฉ์ด ๋‹ค์Œ ํ„ด์— ์ด๋“ค ์œ„์น˜ ์ค‘ ํ•˜๋‚˜์— ๊ธฐํ˜ธ๋ฅผ ๋‘ฌ์„œ ์Šน๋ฆฌ ์กฐ๊ฑด์„ ์ถฉ์กฑ์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

const findBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  // ...
  const minDefenseCondition = winCondition - 2;
  const defenseRange = winCondition - minDefenseCondition + 1;
  const defenseConditions = Array.from(
    { length: defenseRange },
    (_, i) => winCondition - i,
  );

  for (const condition of defenseConditions) {
    const defenseMove = getFirstBestMoveIdx(board, condition, opponent);
    if (defenseMove !== null) return defenseMove;
    if (condition === size) break; // ๋ณด๋“œ ํฌ๊ธฐ์™€ ์Šน๋ฆฌ ์กฐ๊ฑด์ด ๊ฐ™๋‹ค๋ฉด ํ•ด๋‹น ์Šน๋ฆฌ ์กฐ๊ฑด๋งŒ ๊ฒ€์‚ฌ
  }
  // ...
};

 

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

 

๋˜ํ•œ, ์—ฐ์†๋œ ๊ฒฉ์ž์˜ ์ˆ˜๊ฐ€ ๋งŽ์€ ๊ณณ๋ถ€ํ„ฐ ๋จผ์ € ๋ฐฉ์–ดํ•˜๋ ค๋ฉด ์กฐ๊ฑด ๋ฐฐ์—ด์„ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค. ๋ณด๋“œ ์‚ฌ์ด์ฆˆ๊ฐ€ 6, ์Šน๋ฆฌ ์กฐ๊ฑด์ด 4๋ผ๋ฉด defenseConditions ๋ฐฐ์—ด์€ [4, 3, 2]๊ฐ€ ๋œ๋‹ค.

 

๋ณด๋“œ ํฌ๊ธฐ์™€ ์Šน๋ฆฌ ์กฐ๊ฑด์ด ๋™์ผํ•  ๋• ๋ณด๋“œ ์ „์ฒด๋ฅผ ์ฑ„์›Œ์•ผ๋งŒ ์Šน๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋ณด๋“œ ํฌ๊ธฐ๋ณด๋‹ค ์ž‘์€ ์Šน๋ฆฌ ์กฐ๊ฑด์€ ๊ฒ€์‚ฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ๋”ฐ๋ผ์„œ if (condition === size) break; ์กฐ๊ฑด๋ฌธ์„ ํ†ตํ•ด ๋ฐ˜๋ณต์„ ์ค‘์ง€์‹œํ‚จ๋‹ค.

 

์ „๋žต์  ์œ„์น˜ ํƒ์ƒ‰

3×3 ๋ณด๋“œ์—์„  ์ผ๋ฐ˜์ ์œผ๋กœ ์ค‘์•™์ด๋‚˜ ๋ชจ์„œ๋ฆฌ๋ฅผ ์„ ์ ํ•˜๋Š” ๊ฒƒ์ด ์ „๋žต์ ์œผ๋กœ ์œ ๋ฆฌํ•˜๋‹ค. ์ค‘์•™ ์นธ์€ ๋ณด๋“œ์˜ ๋ชจ๋“  ๋ฐฉํ–ฅ์— ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ๋Š” ์ค‘์š”ํ•œ ์œ„์น˜๋กœ, ํ”Œ๋ ˆ์ด์–ด์—๊ฒŒ ๋‹ค์–‘ํ•œ ์ „๋žต์  ์„ ํƒ์ง€๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ๋ชจ์„œ๋ฆฌ ์นธ์€ ํ–‰๊ณผ ์—ด, ๋Œ€๊ฐ์„ ์„ ํ†ตํ•ด ์Šน๋ฆฌ ๊ธฐํšŒ๋ฅผ ๋†’์—ฌ์ค€๋‹ค.

const chooseStrategicPosition = (board: TBoard, size: number) => {
  const availableMoves = getAvailableMoves(board);

  if (availableMoves.size === 0) return null;

  if (size === 3) {
    const centerIdx = getCenterIndexes(size);
    const cornerIndexes = getCornerIndexes(size);
    const filtered = [...centerIdx, ...cornerIndexes].filter((idx) =>
      availableMoves.has(idx),
    );
    if (filtered.length > 0) return selectRandomElement(filtered);
  }

  return selectRandomElement([...availableMoves]);
};

const findBestMoveIdx = (
  board: TBoard,
  winCondition: number,
  player: BasePlayer,
) => {
  // ...
  const size = getBoardSize(board);
  return chooseStrategicPosition(board, size); // ์ค‘์•™, ๋ชจ์„œ๋ฆฌ, ๋นˆ์นธ ์ค‘ ๋žœ๋คํ•˜๊ฒŒ ๋ฐ˜ํ™˜. ๋ชจ๋“  ์นธ์ด ๋‹ค ์ฐผ์œผ๋ฉด null ๋ฐ˜ํ™˜
};

 

3×3 ๋ณด๋“œ์—์„  ์ค‘์•™๊ณผ ๋ชจ์„œ๋ฆฌ ์ค‘ ๋น„์–ด์žˆ๋Š” ๊ณณ์„ ๋ฌด์ž‘์œ„๋กœ ์„ ํƒํ•œ๋‹ค. ๋ด‡์ด ํ•ญ์ƒ ์ค‘์•™์„ ๋จผ์ € ์„ ์ ํ•˜๋ฉด ๊ฒŒ์ž„์˜ ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์ ธ ์žฌ๋ฏธ๊ฐ€ ์ค„์–ด๋“ค ์ˆ˜๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ. ๋‹ค๋ฅธ ํฌ๊ธฐ์˜ ๋ณด๋“œ์—์„  ๋น„์–ด ์žˆ๋Š” ์นธ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ž„์˜๋กœ ์„ ํƒํ•œ๋‹ค.

 

 

๋ ˆํผ๋Ÿฐ์Šค


 

 

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


 

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

 


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