[React] ํฑํํ (Tic-Tac-Toe) ๊ฒ์ ์ฃผ์ ๋ก์ง ํบ์๋ณด๊ธฐ
ํฑํํ ์๊ฐ
ํฑํํ ๋ 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
์น๋ฆฌ ์กฐ๊ฑด์ ๊ฒ์ฌํ๋ ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ ์น๋ฆฌํ ์ ์๋ ๊ฒฝ์ฐ์ ์(์ธ๋ฑ์ค)๋ฅผ 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๋ฒ ์ธ๋ฑ์ค์ ๋์๋ค๋ฉด ํด๋น ์ธ๋ฑ์ค๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ฐ๋ก, ์ธ๋ก, ๋๊ฐ์ ๋ฐฉํฅ์ผ๋ก ์น๋ฆฌ ์กฐ๊ฑด์ ๋ง์กฑํ๋์ง ๊ฒ์ฌํ๋ฉด ๋๋ค.
๐ก 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;
// ...
};
- ๋ณด๋๋ฅผ ์ํํ๋ฉด์ ๋น ๊ฒฉ์์ธ์ง ํ์ธํ๋ค
- ๋น ๊ฒฉ์๋ผ๋ฉด ํด๋น ์ธ๋ฑ์ค์ ํ๋ ์ด์ด ์ ๋ณด๋ก
checkWinIndexes
ํจ์๋ฅผ ํธ์ถํ๋ค - ์น๋ฆฌ ์กฐ๊ฑด์ ๋ง์กฑํ๋ค๋ฉด ํ์ฌ ์ธ๋ฑ์ค๋ฅผ ๋ฐํํ๊ณ ์๋๋ผ๋ฉด
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
๋ง์ฝ ์น๋ฆฌ ์กฐ๊ฑด์ด ๋ณด๋ ์ฌ์ด์ฆ(๋ณด๋ ํ ๋ณ์ ๊ธธ์ด) ๋ณด๋ค ์์ ๊ฒฝ์ฐ, ์น๋ฆฌ ์กฐ๊ฑด - 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 ๋ณด๋์์ ์ค์๊ณผ ๋ชจ์๋ฆฌ ์ค ๋น์ด์๋ ๊ณณ์ ๋ฌด์์๋ก ์ ํํ๋ค. ๋ด์ด ํญ์ ์ค์์ ๋จผ์ ์ ์ ํ๋ฉด ๊ฒ์์ ์์ธก ๊ฐ๋ฅ์ฑ์ด ๋์์ ธ ์ฌ๋ฏธ๊ฐ ์ค์ด๋ค ์๋ ์๊ธฐ ๋๋ฌธ. ๋ค๋ฅธ ํฌ๊ธฐ์ ๋ณด๋์์ ๋น์ด ์๋ ์นธ ์ค ํ๋๋ฅผ ์์๋ก ์ ํํ๋ค.
๋ ํผ๋ฐ์ค
์์ฑ ๋ ํฌ์งํ ๋ฆฌ
๊ธ ์์ ์ฌํญ์ ๋ ธ์ ํ์ด์ง์ ๊ฐ์ฅ ๋น ๋ฅด๊ฒ ๋ฐ์๋ฉ๋๋ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์
'๐ช Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Next.js] App Router / ์๋ฒ ์ปดํฌ๋ํธ ์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ (0) | 2024.06.01 |
---|---|
[Algorithm] ๋ฏธ๋๋งฅ์ค / ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ (0) | 2024.06.01 |
[JS] Array.fill() ๋ฉ์๋ ์ฌ์ฉ ์ ์ฃผ์ํ ์ (0) | 2024.05.30 |
[JS] Bitwise ๋นํธ ์ฐ์ฐ์ ํบ์๋ณด๊ธฐ (feat. ๋นํธ๋ง์คํฌ) (0) | 2024.05.29 |
[JS] ์ ๋์ฝ๋์ ์ ๋์ฝ๋ ํ๋กํผํฐ (1) | 2024.05.29 |
๋๊ธ
์ด ๊ธ ๊ณต์ ํ๊ธฐ
-
๊ตฌ๋
ํ๊ธฐ
๊ตฌ๋ ํ๊ธฐ
-
์นด์นด์คํก
์นด์นด์คํก
-
๋ผ์ธ
๋ผ์ธ
-
ํธ์ํฐ
ํธ์ํฐ
-
Facebook
Facebook
-
์นด์นด์ค์คํ ๋ฆฌ
์นด์นด์ค์คํ ๋ฆฌ
-
๋ฐด๋
๋ฐด๋
-
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
๋ค์ด๋ฒ ๋ธ๋ก๊ทธ
-
Pocket
Pocket
-
Evernote
Evernote
๋ค๋ฅธ ๊ธ
-
[Next.js] App Router / ์๋ฒ ์ปดํฌ๋ํธ ์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ
[Next.js] App Router / ์๋ฒ ์ปดํฌ๋ํธ ์ฃผ์ ๋ด์ฉ ์ ๋ฆฌ
2024.06.01 -
[Algorithm] ๋ฏธ๋๋งฅ์ค / ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ
[Algorithm] ๋ฏธ๋๋งฅ์ค / ์ํ-๋ฒ ํ ๊ฐ์ง์น๊ธฐ ์๊ณ ๋ฆฌ์ฆ ํบ์๋ณด๊ธฐ
2024.06.01 -
[JS] Array.fill() ๋ฉ์๋ ์ฌ์ฉ ์ ์ฃผ์ํ ์
[JS] Array.fill() ๋ฉ์๋ ์ฌ์ฉ ์ ์ฃผ์ํ ์
2024.05.30 -
[JS] Bitwise ๋นํธ ์ฐ์ฐ์ ํบ์๋ณด๊ธฐ (feat. ๋นํธ๋ง์คํฌ)
[JS] Bitwise ๋นํธ ์ฐ์ฐ์ ํบ์๋ณด๊ธฐ (feat. ๋นํธ๋ง์คํฌ)
2024.05.29