728x90
반응형
728x170
오늘도 심심해서 만들어본 오목 게임입니다!


코드
omok.js
// 바둑판의 크기를 설정 (15 x 15)
const boardSize = 15;
// 각 셀의 크기를 설정 (픽셀 단위)
const cellSize = 40;
// 바둑판 배열을 초기화 (2차원 배열로, 모든 위치를 null로 설정)
let board = Array.from(Array(boardSize), () => Array(boardSize).fill(null));
// 사용자와 AI의 돌 색상을 저장할 변수
let userStone = null;
let aiStone = null;
// 현재 사용자의 차례인지 여부를 나타내는 변수
let isUserTurn = true;
// 캔버스 요소와 그리기 컨텍스트를 저장할 변수
let canvas, ctx;
// 마우스 커서에 따라 보여줄 미리보기 돌 정보를 저장할 변수
let previewStone = null;
// AI 난이도 (1-3: 쉬움, 4-7: 보통, 8-10: 어려움)
let aiDifficulty = 7;
/**
* 게임을 시작하는 함수
* @param {string} userChoice - 사용자가 선택한 돌 색상 ('black' 또는 'white')
*/
function startGame(userChoice) {
// 게임 상태 초기화
board = Array.from(Array(boardSize), () => Array(boardSize).fill(null));
// 사용자와 AI의 돌 색상을 설정
userStone = userChoice;
aiStone = userChoice === 'black' ? 'white' : 'black';
// 사용자가 흑돌이면 먼저 시작
isUserTurn = userStone === 'black';
// 돌 선택 화면을 숨김
document.getElementById('choice-container').style.display = 'none';
// 난이도 설정 UI 표시
document.getElementById('difficulty-container').style.display = 'block';
// 게임 영역 숨김
document.getElementById('game-area').style.display = 'none';
// 캔버스 요소와 2D 그리기 컨텍스트를 가져옴
canvas = document.getElementById('board');
ctx = canvas.getContext('2d');
// 바둑판을 그림
drawBoard();
// 이벤트 리스너 추가
canvas.addEventListener('click', handleCanvasClick);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseleave', handleMouseLeave);
// 난이도 설정 이벤트 리스너 추가
document.getElementById('easy-button').addEventListener('click', () => {
setAIDifficulty(3);
startGameWithDifficulty();
});
document.getElementById('medium-button').addEventListener('click', () => {
setAIDifficulty(7);
startGameWithDifficulty();
});
document.getElementById('hard-button').addEventListener('click', () => {
setAIDifficulty(10);
startGameWithDifficulty();
});
}
function startGameWithDifficulty() {
try {
// 난이도 설정 UI 숨김
document.getElementById('difficulty-container').style.display = 'none';
// 게임 영역 표시
document.getElementById('game-area').style.display = 'block';
// 바둑판 다시 그림 (게임 영역이 표시된 후에)
setTimeout(() => {
drawBoard();
// 사용자가 백돌을 선택한 경우 AI가 먼저 한 수를 둠
if (userStone === 'white') {
setTimeout(aiMove, 300);
}
}, 100);
} catch (error) {
console.error("게임 시작 오류:", error);
alert("게임 시작 중 오류가 발생했습니다.");
}
}
// 바둑판을 그리는 함수
function drawBoard() {
// 캔버스를 깨끗하게 지움
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 선 색상을 검정으로 설정
ctx.strokeStyle = '#000';
// 바둑판의 선을 그림
for (let i = 0; i < boardSize; i++) {
// 수직선 그리기
ctx.beginPath();
ctx.moveTo(cellSize / 2 + i * cellSize, cellSize / 2);
ctx.lineTo(cellSize / 2 + i * cellSize, canvas.height - cellSize / 2);
ctx.stroke();
// 수평선 그리기
ctx.beginPath();
ctx.moveTo(cellSize / 2, cellSize / 2 + i * cellSize);
ctx.lineTo(canvas.width - cellSize / 2, cellSize / 2 + i * cellSize);
ctx.stroke();
}
// 이미 놓인 돌들을 그림
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] !== null) {
drawStone(row, col, board[row][col]);
}
}
}
// 미리보기 돌을 그림
if (previewStone) {
drawStone(previewStone.row, previewStone.col, previewStone.stone, true);
}
}
/**
* 돌을 그리는 함수
* @param {number} row - 돌의 행 위치
* @param {number} col - 돌의 열 위치
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @param {boolean} isPreview - 미리보기 돌인지 여부 (기본값: false)
*/
function drawStone(row, col, stone, isPreview = false) {
// 돌의 위치 계산
const x = cellSize / 2 + col * cellSize;
const y = cellSize / 2 + row * cellSize;
// 돌 그리기 시작
ctx.beginPath();
ctx.arc(x, y, cellSize / 2 - 2, 0, Math.PI * 2);
// 돌의 색상 및 투명도 설정
if (stone === 'black') {
ctx.fillStyle = 'rgba(0, 0, 0,' + (isPreview ? '0.5' : '1') + ')';
} else {
ctx.fillStyle = 'rgba(255, 255, 255,' + (isPreview ? '0.5' : '1') + ')';
}
// 돌을 채움
ctx.fill();
// 돌의 테두리 그리기
ctx.strokeStyle = '#000';
ctx.stroke();
}
/**
* 캔버스에서 클릭 이벤트가 발생했을 때 호출되는 함수
* @param {MouseEvent} event - 마우스 이벤트 객체
*/
function handleCanvasClick(event) {
// 사용자의 차례가 아니면 함수 종료
if (!isUserTurn) return;
// 마우스 위치로부터 행과 열을 계산
const { row, col } = getMousePosition(event);
// 유효한 위치이고 빈 자리인 경우에만 진행
if (isValidPosition(row, col) && board[row][col] === null) {
// 돌을 놓음
placeStone(row, col, userStone);
// 승리 여부 확인
if (checkWin(row, col, userStone)) {
setTimeout(() => alert("사용자가 이겼습니다!"), 100);
resetGame();
return;
}
// 차례 변경
isUserTurn = false;
previewStone = null;
drawBoard();
// 일정 시간 후에 AI의 수를 둠
setTimeout(aiMove, 500);
}
}
/**
* 캔버스에서 마우스가 움직일 때 호출되는 함수
* @param {MouseEvent} event - 마우스 이벤트 객체
*/
function handleMouseMove(event) {
// 사용자의 차례가 아니면 함수 종료
if (!isUserTurn) return;
// 마우스 위치로부터 행과 열을 계산
const { row, col } = getMousePosition(event);
// 유효한 위치이고 빈 자리인 경우 미리보기 돌 설정
if (isValidPosition(row, col) && board[row][col] === null) {
previewStone = { row, col, stone: userStone };
} else {
previewStone = null;
}
// 바둑판 다시 그림
drawBoard();
}
// 캔버스에서 마우스가 벗어날 때 호출되는 함수
function handleMouseLeave() {
// 미리보기 돌 제거
previewStone = null;
drawBoard();
}
/**
* 마우스 이벤트로부터 보드의 행과 열을 계산하는 함수
* @param {MouseEvent} event - 마우스 이벤트 객체
* @returns {{row: number, col: number}} - 계산된 행과 열 인덱스
*/
function getMousePosition(event) {
// 캔버스의 위치 정보 가져오기
const rect = canvas.getBoundingClientRect();
// 마우스의 x, y 좌표 계산
const x = event.clientX - rect.left - cellSize / 2;
const y = event.clientY - rect.top - cellSize / 2;
// 보드의 열과 행 인덱스로 변환
const col = Math.round(x / cellSize);
const row = Math.round(y / cellSize);
return { row, col };
}
/**
* 주어진 위치가 보드 내의 유효한 위치인지 확인하는 함수
* @param {number} row - 행 인덱스
* @param {number} col - 열 인덱스
* @returns {boolean} - 유효한 위치이면 true, 아니면 false
*/
function isValidPosition(row, col) {
return row >= 0 && row < boardSize && col >= 0 && col < boardSize;
}
/**
* 보드에 돌을 놓는 함수
* @param {number} row - 돌을 놓을 행 인덱스
* @param {number} col - 돌을 놓을 열 인덱스
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
*/
function placeStone(row, col, stone) {
// 보드 배열에 돌을 배치
board[row][col] = stone;
drawBoard();
}
// AI의 수를 두는 함수
function aiMove() {
// 최적의 수를 찾음
let bestMove = findBestMove();
if (bestMove) {
const [aiRow, aiCol] = bestMove;
placeStone(aiRow, aiCol, aiStone);
// 승리 여부 확인
if (checkWin(aiRow, aiCol, aiStone)) {
setTimeout(() => alert("AI가 이겼습니다!"), 100);
resetGame();
return;
}
// 차례를 사용자에게 넘김
isUserTurn = true;
} else {
// 둘 수 있는 곳이 없으면 비김
setTimeout(() => alert("비겼습니다!"), 100);
resetGame();
}
}
// AI 난이도를 설정하는 함수
function setAIDifficulty(difficulty) {
aiDifficulty = Math.max(1, Math.min(10, difficulty));
}
// AI가 최적의 수를 찾는 함수
function findBestMove() {
// 랜덤 요소 (난이도에 따라 최선의 수를 선택하지 않을 확률)
const randomFactor = Math.max(0, (11 - aiDifficulty) / 10);
// 확률적으로 랜덤한 수를 두도록 설정
if (Math.random() < randomFactor) {
// 낮은 난이도에서는 완전 랜덤하게 둠
if (aiDifficulty <= 3) {
return getRandomEmptyCell();
}
// 중간 난이도에서는 사용자 근처에 둠
else if (aiDifficulty <= 7) {
const nearMove = findNearUserMove();
return nearMove || getRandomEmptyCell();
}
}
// 1. 이길 수 있는 수가 있으면 그 위치에 둠
let winningMove = findWinningMove(aiStone);
if (winningMove) return winningMove;
// 2. 사용자가 이길 수 있는 수를 막음
let blockingMove = findWinningMove(userStone);
if (blockingMove) return blockingMove;
// 3. 상대방이 열린 4개 만들기 직전인지 확인 (예비 승리 수)
let threatMove = findThreatMove(userStone);
if (threatMove && aiDifficulty >= 6) return threatMove;
// 4. 자신이 열린 4개를 만들 수 있는지 확인 (예비 승리 수)
let attackingMove = findThreatMove(aiStone);
if (attackingMove && aiDifficulty >= 7) return attackingMove;
// 5. 미니맥스 알고리즘 (어려움 난이도에서만)
if (aiDifficulty >= 8) {
let miniMaxMove = findMiniMaxMove(3); // 난이도가 높을수록 더 깊게 탐색
if (miniMaxMove) return miniMaxMove;
}
// 6. 연속된 돌의 수를 늘릴 수 있는 위치에 둠
let bestMove = findLongestSequenceMove(aiStone);
if (bestMove) return bestMove;
// 7. 사용자의 연속된 돌을 막는 위치에 둠
let defensiveMove = findLongestSequenceMove(userStone);
if (defensiveMove) return defensiveMove;
// 8. 사용자의 돌 근처에 둠
let nearUserMove = findNearUserMove();
if (nearUserMove) return nearUserMove;
// 9. 중앙에 둠
if (board[Math.floor(boardSize / 2)][Math.floor(boardSize / 2)] === null) {
return [Math.floor(boardSize / 2), Math.floor(boardSize / 2)];
}
// 10. 랜덤한 빈 위치에 둠
return getRandomEmptyCell();
}
/**
* 미니맥스 알고리즘을 사용하여 최적의 수를 찾는 함수
* @param {number} depth - 탐색 깊이
* @returns {number[] | null} - 최적의 위치 [row, col], 없으면 null
*/
function findMiniMaxMove(depth) {
let bestScore = -Infinity;
let bestMove = null;
// 어려움 난이도에서는 탐색 깊이를 조정
if (aiDifficulty === 10) {
depth = 3; // 최고 난이도에서도 적절한 깊이로 제한
} else if (aiDifficulty === 9) {
depth = 2;
} else {
depth = 1; // 낮은 난이도에서는 깊이를 줄임
}
// 15x15 보드 전체 탐색 대신 유망한 셀만 탐색
const promisingCells = findPromisingCells();
// 유망한 셀들에 대해서만 시도
for (const [row, col] of promisingCells) {
if (board[row][col] === null) {
// 시뮬레이션: 돌 놓기
board[row][col] = aiStone;
// 승리 시 즉시 선택
if (checkWin(row, col, aiStone)) {
board[row][col] = null;
return [row, col];
}
// 미니맥스 알고리즘으로 점수 계산 (제한된 시간 내에)
let score = miniMax(depth - 1, false, -Infinity, Infinity);
// 돌 제거 (시뮬레이션 종료)
board[row][col] = null;
// 더 좋은 점수가 나오면 업데이트
if (score > bestScore) {
bestScore = score;
bestMove = [row, col];
}
}
}
return bestMove;
}
/**
* 유망한 셀을 찾는 함수 (주변에 돌이 있는 빈 위치)
* @returns {Array} - 유망한 위치 목록 [row, col]
*/
function findPromisingCells() {
const cells = new Set();
const vicinity = 2; // 주변 범위 (2칸)
// 이미 놓은 돌 주변의 빈 위치를 찾음
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] !== null) {
// 주변 위치를 유망한 셀에 추가
for (let r = Math.max(0, row - vicinity); r <= Math.min(boardSize - 1, row + vicinity); r++) {
for (let c = Math.max(0, col - vicinity); c <= Math.min(boardSize - 1, col + vicinity); c++) {
if (board[r][c] === null) {
cells.add(`${r},${c}`);
}
}
}
}
}
}
// 셀이 없으면 (첫 수 등) 중앙 근처 위치를 추가
if (cells.size === 0) {
const center = Math.floor(boardSize / 2);
for (let r = center - 1; r <= center + 1; r++) {
for (let c = center - 1; c <= center + 1; c++) {
if (isValidPosition(r, c)) {
cells.add(`${r},${c}`);
}
}
}
}
// Set을 배열로 변환
return Array.from(cells).map(cell => {
const [r, c] = cell.split(',').map(Number);
return [r, c];
});
}
/**
* 미니맥스 알고리즘 구현 (최적화 버전)
* @param {number} depth - 남은 탐색 깊이
* @param {boolean} isMaximizing - 최대화 플레이어 차례인지 여부
* @param {number} alpha - 알파 값 (알파-베타 가지치기)
* @param {number} beta - 베타 값 (알파-베타 가지치기)
* @returns {number} - 계산된 점수
*/
function miniMax(depth, isMaximizing, alpha, beta) {
// 게임 종료 또는 최대 깊이 도달 시 평가
if (depth === 0) {
return evaluateBoardState();
}
// 유망한 셀만 탐색
const promisingCells = findPromisingCells();
if (isMaximizing) {
// AI 차례 (최대화)
let maxScore = -Infinity;
// 유망한 위치만 시도
for (const [row, col] of promisingCells) {
if (board[row][col] === null) {
board[row][col] = aiStone;
// 승리 시 높은 점수 반환
if (checkWin(row, col, aiStone)) {
board[row][col] = null;
return 1000 + depth; // 깊이를 더해 빠른 승리를 선호
}
// 다음 단계 탐색
let score = miniMax(depth - 1, false, alpha, beta);
board[row][col] = null;
maxScore = Math.max(maxScore, score);
alpha = Math.max(alpha, score);
// 알파-베타 가지치기
if (beta <= alpha) break;
}
}
return maxScore;
} else {
// 사용자 차례 (최소화)
let minScore = Infinity;
// 유망한 위치만 시도
for (const [row, col] of promisingCells) {
if (board[row][col] === null) {
board[row][col] = userStone;
// 사용자 승리 시 낮은 점수 반환
if (checkWin(row, col, userStone)) {
board[row][col] = null;
return -1000 - depth; // 깊이를 빼서 빠른 패배를 피함
}
// 다음 단계 탐색
let score = miniMax(depth - 1, true, alpha, beta);
board[row][col] = null;
minScore = Math.min(minScore, score);
beta = Math.min(beta, score);
// 알파-베타 가지치기
if (beta <= alpha) break;
}
}
return minScore;
}
}
/**
* 현재 보드 상태를 평가하는 함수
* @returns {number} - 평가 점수 (양수: AI 유리, 음수: 사용자 유리)
*/
function evaluateBoardState() {
let score = 0;
// 가로, 세로, 대각선 평가
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
// AI 돌에 대한 평가
if (board[row][col] === aiStone) {
score += evaluateStonePosition(row, col, aiStone);
}
// 사용자 돌에 대한 평가
else if (board[row][col] === userStone) {
score -= evaluateStonePosition(row, col, userStone);
}
}
}
return score;
}
/**
* 특정 위치의 돌에 대한 점수 평가
* @param {number} row - 행 인덱스
* @param {number} col - 열 인덱스
* @param {string} stone - 돌 색상
* @returns {number} - 평가 점수
*/
function evaluateStonePosition(row, col, stone) {
let score = 0;
const directions = [
[1, 0], // 수직 방향
[0, 1], // 수평 방향
[1, 1], // 대각선 (\) 방향
[1, -1] // 대각선 (/) 방향
];
for (let [dr, dc] of directions) {
// 양쪽 방향으로의 연속된 돌과 빈 공간 분석
let sequence = analyzeSequence(row, col, dr, dc, stone);
// 연속된 돌의 개수에 따른 점수 부여
if (sequence.stones === 5) score += 100000; // 승리
else if (sequence.stones === 4) {
// 4개 연속 + 한쪽이라도 뚫려 있으면 매우 위험/유리
if (sequence.openEnds >= 1) score += 10000;
else score += 500; // 막힌 4개는 덜 위험/유리
}
else if (sequence.stones === 3) {
// 3개 연속 + 양쪽이 뚫려 있으면 매우 가치 있음
if (sequence.openEnds === 2) score += 5000;
// 3개 연속 + 한쪽만 뚫려 있으면 덜 가치 있음
else if (sequence.openEnds === 1) score += 500;
}
else if (sequence.stones === 2) {
// 2개 연속 + 양쪽이 뚫려 있으면 가치 있음
if (sequence.openEnds === 2) score += 100;
// 2개 연속 + 한쪽만 뚫려 있으면 덜 가치 있음
else if (sequence.openEnds === 1) score += 10;
}
else if (sequence.stones === 1) {
// 주변에 빈 공간이 많을수록 약간의 가치
score += sequence.openEnds * 2;
}
}
// 중앙 위치는 더 가치 있음
const centerDistance = Math.sqrt(
Math.pow(row - Math.floor(boardSize / 2), 2) +
Math.pow(col - Math.floor(boardSize / 2), 2)
);
score += Math.max(0, (boardSize / 2 - centerDistance)) * 5;
return score;
}
/**
* 특정 방향의 돌 시퀀스를 분석하는 함수
* @param {number} row - 시작 행 인덱스
* @param {number} col - 시작 열 인덱스
* @param {number} dr - 행 방향
* @param {number} dc - 열 방향
* @param {string} stone - 돌 색상
* @returns {{stones: number, openEnds: number}} - 연속된 돌의 수와 열린 끝의 수
*/
function analyzeSequence(row, col, dr, dc, stone) {
let stones = 1; // 현재 위치의 돌 포함
let openEnds = 0;
// 정방향 검사
let r = row + dr;
let c = col + dc;
// 연속된 같은 돌 계산
while (isValidPosition(r, c) && board[r][c] === stone) {
stones++;
r += dr;
c += dc;
}
// 정방향 끝이 빈 공간인지 확인
if (isValidPosition(r, c) && board[r][c] === null) {
openEnds++;
}
// 역방향 검사
r = row - dr;
c = col - dc;
// 연속된 같은 돌 계산
while (isValidPosition(r, c) && board[r][c] === stone) {
stones++;
r -= dr;
c -= dc;
}
// 역방향 끝이 빈 공간인지 확인
if (isValidPosition(r, c) && board[r][c] === null) {
openEnds++;
}
return { stones, openEnds };
}
/**
* 특정 위치에서의 점수를 평가하는 함수
* @param {number} row - 평가할 행 인덱스
* @param {number} col - 평가할 열 인덱스
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @returns {number} - 해당 위치의 점수
*/
function evaluatePosition(row, col, stone) {
let totalScore = 0;
const directions = [
[1, 0], // 수직 방향
[0, 1], // 수평 방향
[1, 1], // 대각선 (\) 방향
[1, -1] // 대각선 (/) 방향
];
for (let [dr, dc] of directions) {
// 양쪽 방향의 연속된 돌과 빈 공간 분석
let sequence = analyzeDirectionalSequence(row, col, dr, dc, stone);
// 연속성과 열린 공간에 따라 점수 부여
if (sequence.consecutive === 4) {
// 승리 가능한 위치에 매우 높은 점수
totalScore += 10000;
} else if (sequence.consecutive === 3) {
// 양쪽이 열려있는 3개 연속은 가치가 매우 높음
if (sequence.openEnds === 2) totalScore += 5000;
// 한쪽만 열려 있는 3개 연속
else if (sequence.openEnds === 1) totalScore += 1000;
} else if (sequence.consecutive === 2) {
// 양쪽이 열려있는 2개 연속
if (sequence.openEnds === 2) totalScore += 500;
// 한쪽만 열려 있는 2개 연속
else if (sequence.openEnds === 1) totalScore += 100;
} else if (sequence.consecutive === 1) {
// 주변에 빈 공간이 많을수록 약간의 가치
totalScore += sequence.openEnds * 10;
}
// 중앙 위치에 가까울수록 추가 점수
totalScore += (boardSize / 2 - Math.abs(row - boardSize / 2)) * 2;
totalScore += (boardSize / 2 - Math.abs(col - boardSize / 2)) * 2;
}
return totalScore;
}
/**
* 특정 방향의 돌 배치를 분석하는 함수
* @param {number} row - 평가할 행 인덱스
* @param {number} col - 평가할 열 인덱스
* @param {number} dr - 행 방향
* @param {number} dc - 열 방향
* @param {string} stone - 돌의 색상
* @returns {{consecutive: number, openEnds: number}} - 연속성과 열린 끝 정보
*/
function analyzeDirectionalSequence(row, col, dr, dc, stone) {
// 가상으로 돌을 놓는다고 가정
let consecutive = 1;
let openEnds = 0;
// 양의 방향 탐색
let r = row + dr;
let c = col + dc;
// 첫 번째 빈 공간, 상대 돌, 또는 보드 경계까지 확인
let blocked = false;
while (isValidPosition(r, c)) {
if (board[r][c] === stone) {
consecutive++;
} else if (board[r][c] === null) {
openEnds++;
break;
} else {
// 상대 돌에 의해 차단됨
blocked = true;
break;
}
r += dr;
c += dc;
}
// 보드 경계에 의해 차단됨
if (!isValidPosition(r, c) && !blocked) {
blocked = true;
}
// 음의 방향 탐색
r = row - dr;
c = col - dc;
// 첫 번째 빈 공간, 상대 돌, 또는 보드 경계까지 확인
blocked = false;
while (isValidPosition(r, c)) {
if (board[r][c] === stone) {
consecutive++;
} else if (board[r][c] === null) {
openEnds++;
break;
} else {
// 상대 돌에 의해 차단됨
blocked = true;
break;
}
r -= dr;
c -= dc;
}
// 보드 경계에 의해 차단됨
if (!isValidPosition(r, c) && !blocked) {
blocked = true;
}
return { consecutive, openEnds };
}
// 사용자의 돌 근처의 빈 위치를 찾는 함수
function findNearUserMove() {
let emptyCells = [];
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] === null && isNearUserStone(row, col)) {
emptyCells.push([row, col]);
}
}
}
if (emptyCells.length > 0) {
// 근처의 빈 위치들 중 하나를 랜덤하게 선택
return emptyCells[Math.floor(Math.random() * emptyCells.length)];
}
return null;
}
// 랜덤한 빈 위치를 반환하는 함수
function getRandomEmptyCell() {
let emptyCells = [];
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] === null) {
emptyCells.push([row, col]);
}
}
}
if (emptyCells.length > 0) {
// 빈 위치들 중 하나를 랜덤하게 선택
return emptyCells[Math.floor(Math.random() * emptyCells.length)];
}
return null;
}
/**
* 해당 위치가 사용자의 돌 근처인지 확인하는 함수
* @param {number} row - 확인할 행 인덱스
* @param {number} col - 확인할 열 인덱스
* @returns {boolean} - 근처에 사용자의 돌이 있으면 true, 아니면 false
*/
function isNearUserStone(row, col) {
const directions = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[1, 1], [-1, -1], [1, -1], [-1, 1]
];
return directions.some(([dr, dc]) => {
const nr = row + dr;
const nc = col + dc;
return isValidPosition(nr, nc) && board[nr][nc] === userStone;
});
}
/**
* 현재 놓은 돌로 승리했는지 확인하는 함수
* @param {number} row - 최근에 놓은 돌의 행 인덱스
* @param {number} col - 최근에 놓은 돌의 열 인덱스
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @returns {boolean} - 승리하면 true, 아니면 false
*/
function checkWin(row, col, stone) {
const directions = [
[1, 0], // 수직
[0, 1], // 수평
[1, 1], // 대각선 (\)
[1, -1] // 대각선 (/)
];
for (const [dr, dc] of directions) {
// 양쪽 방향으로 연속된 돌의 수를 합산 (현재 돌 포함)
const count = 1 +
countStones(row, col, dr, dc, stone) +
countStones(row, col, -dr, -dc, stone);
// 정확히 5개면 승리
if (count === 5) {
return true;
}
}
return false;
}
/**
* 특정 방향으로 연속된 같은 돌의 수를 세는 함수
* @param {number} row - 시작 행 인덱스
* @param {number} col - 시작 열 인덱스
* @param {number} rowDir - 행 방향 (1, 0, -1)
* @param {number} colDir - 열 방향 (1, 0, -1)
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @returns {number} - 연속된 돌의 수
*/
function countStones(row, col, rowDir, colDir, stone) {
let count = 0;
let r = row + rowDir;
let c = col + colDir;
while (isValidPosition(r, c) && board[r][c] === stone) {
count++;
r += rowDir;
c += colDir;
}
return count;
}
// 게임을 초기화하는 함수
function resetGame() {
// 보드 배열 초기화
board = Array.from(Array(boardSize), () => Array(boardSize).fill(null));
// 돌 선택 화면을 다시 표시
document.getElementById('choice-container').style.display = 'block';
// 바둑판을 다시 그림
drawBoard();
// 이벤트 리스너 제거
canvas.removeEventListener('click', handleCanvasClick);
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('mouseleave', handleMouseLeave);
}
/**
* 특정 돌로 승리할 수 있는 위치를 찾는 함수
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @returns {number[] | null} - 승리할 수 있는 위치 [row, col], 없으면 null
*/
function findWinningMove(stone) {
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] === null) {
// 임시로 돌을 놓아봄
board[row][col] = stone;
// 승리 여부 확인
if (checkWin(row, col, stone)) {
board[row][col] = null; // 원상복구
return [row, col];
}
board[row][col] = null; // 원상복구
}
}
}
return null;
}
/**
* 가장 긴 연속된 돌의 수를 만들 수 있는 위치를 찾는 함수
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @returns {number[] | null} - 최적의 위치 [row, col], 없으면 null
*/
function findLongestSequenceMove(stone) {
let maxScore = 0;
let bestMoves = [];
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] === null) {
// 해당 위치에서의 점수 계산
let score = evaluatePosition(row, col, stone);
if (score > maxScore) {
maxScore = score;
bestMoves = [[row, col]];
} else if (score === maxScore) {
bestMoves.push([row, col]);
}
}
}
}
if (bestMoves.length > 0) {
// 가장 좋은 위치들 중 하나를 랜덤하게 선택
return bestMoves[Math.floor(Math.random() * bestMoves.length)];
}
return null;
}
/**
* 잠재적 위협(열린 3, 4)을 찾는 함수
* @param {string} stone - 돌의 색상 ('black' 또는 'white')
* @returns {number[] | null} - 위협을 막거나 만들 수 있는 위치
*/
function findThreatMove(stone) {
let bestScore = 0;
let bestMoves = [];
// 모든 빈 위치를 평가
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] === null) {
// 해당 위치에 돌을 놓았을 때의 위협 정도 평가
let threatScore = evaluateThreat(row, col, stone);
if (threatScore > bestScore) {
bestScore = threatScore;
bestMoves = [[row, col]];
} else if (threatScore === bestScore && threatScore > 0) {
bestMoves.push([row, col]);
}
}
}
}
// 위협 점수가 높은 수가 있다면 선택
if (bestMoves.length > 0 && bestScore >= 1000) {
return bestMoves[Math.floor(Math.random() * bestMoves.length)];
}
return null;
}
/**
* 특정 위치에 돌을 놓았을 때의 위협 정도를 평가하는 함수
* @param {number} row - 평가할 행 인덱스
* @param {number} col - 평가할 열 인덱스
* @param {string} stone - 돌의 색상
* @returns {number} - 위협 점수
*/
function evaluateThreat(row, col, stone) {
// 임시로 돌 배치
board[row][col] = stone;
let threatScore = 0;
const directions = [
[1, 0], // 수직 방향
[0, 1], // 수평 방향
[1, 1], // 대각선 (\) 방향
[1, -1] // 대각선 (/) 방향
];
// 각 방향으로 분석
for (let [dr, dc] of directions) {
let sequence = analyzeDirectionalSequence(row, col, dr, dc, stone);
// 열린 4는 매우 위험/유리한 상황
if (sequence.consecutive === 4 && sequence.openEnds >= 1) {
threatScore = Math.max(threatScore, 10000);
}
// 열린 3은 잠재적 위험/기회
else if (sequence.consecutive === 3 && sequence.openEnds === 2) {
threatScore = Math.max(threatScore, 5000);
}
// 한쪽이 막힌 3은 약간의 위험/기회
else if (sequence.consecutive === 3 && sequence.openEnds === 1) {
threatScore = Math.max(threatScore, 1000);
}
}
// 돌 제거 (원래 상태로 되돌림)
board[row][col] = null;
return threatScore;
}
/**
* 패턴 분석: 연속 돌의 구조를 분석하는 함수
* @param {number} row - 시작 행 인덱스
* @param {number} col - 시작 열 인덱스
* @param {string} stone - 돌의 색상
* @returns {Object} - 다양한 패턴의 개수 정보
*/
function analyzePatterns(row, col, stone) {
// 임시로 돌 배치
const originalValue = board[row][col];
board[row][col] = stone;
const patterns = {
openFour: 0, // 양쪽이 열린 4
halfOpenFour: 0, // 한쪽만 열린 4
openThree: 0, // 양쪽이 열린 3
halfOpenThree: 0, // 한쪽만 열린 3
openTwo: 0, // 양쪽이 열린 2
};
const directions = [
[1, 0], // 수직 방향
[0, 1], // 수평 방향
[1, 1], // 대각선 (\) 방향
[1, -1] // 대각선 (/) 방향
];
// 각 방향으로 분석
for (let [dr, dc] of directions) {
let sequence = analyzeDirectionalSequence(row, col, dr, dc, stone);
if (sequence.consecutive === 4) {
if (sequence.openEnds === 2) patterns.openFour++;
else if (sequence.openEnds === 1) patterns.halfOpenFour++;
}
else if (sequence.consecutive === 3) {
if (sequence.openEnds === 2) patterns.openThree++;
else if (sequence.openEnds === 1) patterns.halfOpenThree++;
}
else if (sequence.consecutive === 2 && sequence.openEnds === 2) {
patterns.openTwo++;
}
}
// 원래 상태로 되돌림
board[row][col] = originalValue;
return patterns;
}
// AI 공격 패턴을 구현한 함수
function findAttackPattern() {
let bestMove = null;
let bestScore = -1;
// 모든 빈 위치를 평가
for (let row = 0; row < boardSize; row++) {
for (let col = 0; col < boardSize; col++) {
if (board[row][col] === null) {
// 이 위치에 돌을 놓았을 때 생성되는 패턴 분석
const patterns = analyzePatterns(row, col, aiStone);
// 패턴에 따른 점수 계산
let score =
patterns.openFour * 10000 +
patterns.halfOpenFour * 1000 +
patterns.openThree * 500 +
patterns.halfOpenThree * 100 +
patterns.openTwo * 10;
// 더 좋은 점수가 나오면 업데이트
if (score > bestScore) {
bestScore = score;
bestMove = [row, col];
}
}
}
}
return bestMove;
}
omok.css
/* CSS 스타일 */
/* 전체 페이지의 기본 글꼴, 중앙 정렬, 배경색 설정 */
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
}
/* 돌 선택 컨테이너의 여백 설정 */
#choice-container {
margin-top: 20px;
}
/* 바둑판 컨테이너의 중앙 정렬, 크기, 위치 설정 */
#board-container {
margin: 20px auto;
width: 600px;
height: 600px;
position: relative;
}
/* 바둑판의 테두리와 배경색 설정, 커서 모양 변경 */
#board {
border: 2px solid #000;
background-color: #deb887; /* 바둑판 배경색 (밝은 갈색) */
cursor: pointer; /* 바둑판 위에서 커서 모양을 포인터로 변경 */
}
/* 버튼의 크기, 여백, 글꼴 크기, 커서 모양 설정 */
button {
padding: 10px 20px;
margin: 0 10px;
font-size: 16px;
cursor: pointer; /* 버튼 위에 커서가 있을 때 포인터 표시 */
}
/* 각 바둑판 셀의 크기, 테두리, 위치 설정 */
.cell {
width: 40px;
height: 40px;
box-sizing: border-box;
border: 1px solid #000; /* 셀의 테두리 */
position: relative;
}
/* 바둑돌 크기와 둥근 모양 설정, 위치 설정 */
.dot {
width: 36px; /* 바둑돌의 가로 크기 */
height: 36px; /* 바둑돌의 세로 크기 */
border-radius: 50%; /* 원형으로 표시 */
position: absolute;
top: 2px; /* 바둑판 셀의 상단에서 약간 내려오도록 위치 */
left: 2px; /* 바둑판 셀의 좌측에서 약간 오른쪽으로 위치 */
}
/* 검정 바둑돌의 배경색 설정 */
.dot.black {
background-color: black;
}
/* 흰색 바둑돌의 배경색 설정 */
.dot.white {
background-color: white;
}
/* 미리보기 돌의 투명도 설정 */
.preview-dot {
opacity: 0.5; /* 미리보기 돌의 투명도를 50%로 설정 */
}
omok.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>오목 게임</title>
<style>
body {
font-family: 'Malgun Gothic', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
h1 {
color: #333;
margin-bottom: 20px;
}
#game-container {
text-align: center;
}
canvas {
background-color: #e9c170;
border: 2px solid #333;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
margin-bottom: 20px;
}
#choice-container, #difficulty-container {
margin-bottom: 30px;
}
button {
padding: 10px 20px;
margin: 0 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s;
background-color: #3498db;
color: white;
}
button:hover {
background-color: #2980b9;
transform: translateY(-2px);
}
#black-button {
background-color: #333;
}
#black-button:hover {
background-color: #111;
}
#white-button {
background-color: #f5f5f5;
color: #333;
border: 1px solid #ddd;
}
#white-button:hover {
background-color: #e5e5e5;
}
#easy-button {
background-color: #2ecc71;
}
#easy-button:hover {
background-color: #27ae60;
}
#medium-button {
background-color: #f39c12;
}
#medium-button:hover {
background-color: #d35400;
}
#hard-button {
background-color: #e74c3c;
}
#hard-button:hover {
background-color: #c0392b;
}
#game-area, #difficulty-container {
display: none;
}
.info-text {
margin-bottom: 15px;
color: #555;
}
</style>
</head>
<body>
<div id="game-container">
<h1>오목 게임</h1>
<div id="choice-container">
<p class="info-text">돌 색상을 선택하세요. 흑돌이 먼저 시작합니다.</p>
<button id="black-button" onclick="startGame('black')">흑돌</button>
<button id="white-button" onclick="startGame('white')">백돌</button>
</div>
<div id="difficulty-container">
<p class="info-text">AI 난이도를 선택하세요.</p>
<button id="easy-button">쉬움</button>
<button id="medium-button">보통</button>
<button id="hard-button">어려움</button>
</div>
<div id="game-area">
<canvas id="board" width="610" height="610"></canvas>
<div>
<button onclick="resetGame()">다시 시작</button>
</div>
</div>
</div>
<script src="index.js"></script>
</body>
</html>
데모
오목 게임
돌 색상을 선택하세요. 흑돌이 먼저 시작합니다. 흑돌 백돌
gomoku-chi.vercel.app
728x90
반응형
그리드형
'프로그래밍(Web) > Javascript(TS,Node)' 카테고리의 다른 글
| [바미] 공공 API 대량 호출을 어떻게 최적화했을까? (p-limit 적용기) (1) | 2025.05.02 |
|---|---|
| [바미] JS - 스도쿠 게임 (0) | 2024.10.29 |
| [바미] JS - 테트리스 (0) | 2024.09.21 |
| [바미] Node-Express 간단한 REST API 예제 (0) | 2024.08.01 |
| [바미] 심심해서 만들어 본 슬롯머신 (0) | 2024.06.07 |