728x90
반응형
오늘도 심심해서 만들어본 오목 게임입니다!
코드
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;
/**
* 게임을 시작하는 함수
* @param {string} userChoice - 사용자가 선택한 돌 색상 ('black' 또는 'white')
*/
function startGame(userChoice) {
// 사용자와 AI의 돌 색상을 설정
userStone = userChoice;
aiStone = userChoice === 'black' ? 'white' : 'black';
// 사용자가 흑돌이면 먼저 시작
isUserTurn = userStone === 'black';
// 돌 선택 화면을 숨김
document.getElementById('choice-container').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);
// 사용자가 백돌을 선택한 경우 AI가 먼저 한 수를 둠
if (userStone === 'white') {
setTimeout(aiMove, 500);
}
}
// 바둑판을 그리는 함수
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 findBestMove() {
// 1. 이길 수 있는 수가 있으면 그 위치에 둠
let winningMove = findWinningMove(aiStone);
if (winningMove) return winningMove;
// 2. 사용자가 이길 수 있는 수를 막음
let blockingMove = findWinningMove(userStone);
if (blockingMove) return blockingMove;
// 3. 연속된 돌의 수를 늘릴 수 있는 위치에 둠
let bestMove = findLongestSequenceMove(aiStone);
if (bestMove) return bestMove;
// 4. 사용자의 돌 근처에 둠
let nearUserMove = findNearUserMove();
if (nearUserMove) return nearUserMove;
// 5. 중앙에 둠
if (board[Math.floor(boardSize / 2)][Math.floor(boardSize / 2)] === null) {
return [Math.floor(boardSize / 2), Math.floor(boardSize / 2)];
}
// 6. 랜덤한 빈 위치에 둠
return getRandomEmptyCell();
}
/**
* 특정 돌로 승리할 수 있는 위치를 찾는 함수
* @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;
}
/**
* 특정 위치에서의 점수를 평가하는 함수
* @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 count = countStones(row, col, dr, dc, stone) + countStones(row, col, -dr, -dc, stone);
// 점수는 연속된 돌의 수의 제곱으로 계산
totalScore += Math.pow(count, 2);
}
return totalScore;
}
// 사용자의 돌 근처의 빈 위치를 찾는 함수
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) {
return (
countStones(row, col, 1, 0, stone) + countStones(row, col, -1, 0, stone) >= 4 || // 수직
countStones(row, col, 0, 1, stone) + countStones(row, col, 0, -1, stone) >= 4 || // 수평
countStones(row, col, 1, 1, stone) + countStones(row, col, -1, -1, stone) >= 4 || // 대각선 (\)
countStones(row, col, 1, -1, stone) + countStones(row, col, -1, 1, stone) >= 4 // 대각선 (/)
);
}
/**
* 특정 방향으로 연속된 같은 돌의 수를 세는 함수
* @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);
}
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>
<link rel="stylesheet" href="omok.css">
</head>
<body>
<div id="choice-container">
<h2>돌 색상을 선택하세요</h2>
<button onclick="startGame('black')">흑돌</button>
<button onclick="startGame('white')">백돌</button>
</div>
<div id="board-container">
<canvas id="board" width="600" height="600"></canvas>
</div>
<script src="omok.js"></script>
</body>
</html>
728x90
반응형
'프로그래밍(Web) > Javascript(TS,Node)' 카테고리의 다른 글
[바미] JS - 스도쿠 게임 (0) | 2024.10.29 |
---|---|
[바미] JS - 테트리스 (0) | 2024.09.21 |
[바미] Node-Express 간단한 REST API 예제 (0) | 2024.08.01 |
[바미] 심심해서 만들어 본 슬롯머신 (0) | 2024.06.07 |
[바미] 로또 추첨기(JS) (0) | 2024.06.02 |