개인 프로젝트/오델로(Othello)

간단하게 하나의 파일로 만들어본 오델로 Othello

cotnmin 2024. 7. 10. 01:02

어느 날 시간이 좀 나서 큰 프로젝트를 만들기는 애매하고 간단하게 뭘 만들어볼까? 하다 만든 오델로입니다!

 

오델로는 간단한 게임으로 내 돌로 상대 돌을 감싸면 상대 돌이 뒤집는 방식으로 간단하게 플레이할 수 있습니다.

좀 더 자세한 룰은 아래와 같습니다.

  • 상대 돌을 자신의 돌로 감싸면 뒤집을 수 있습니다.
  • 상대 돌을 감싸 뒤집을 수 있는 곳에만 둘 수 있습니다.
  • 만약 상대 돌을 뒤집을 수 있는 위치가 없다면 패스할 수 있습니다.
  • 둘 다 둘 곳이 없어 연속으로 패스를 하면 게임은 종료됩니다.
  • 게임 당 패스는 두번까지만 가능하고 세 번째 패스와 동시에 게임은 종료됩니다.
  • 보드를 모두 채우면 게임이 끝납니다.
  • 게임 종료 후 더 많은 돌을 가지고 있는 사람이 승리합니다.

오델로 플레이 화면

화면은 간단하면서 직관적으로 만들었습니다.

제일 중요한 보드를 왼쪽에 크게 배치하고 우측에 콘솔과 턴 표시, 현재 상태 등을 표시해 주었습니다.

그리고 하단에 룰을 배치해 잘 모르는 사람도 쉽게 할 수 있도록 했습니다.

 

나름 컴퓨터와 승부를 겨룰 수 있도록 해두었습니다... AI로 써두었지만 인공지능이라고 부르기는 허접한 간단한 수 예측 알고리즘입니다.

간단하게 점수를 두고

내가 둘 수 있는 수를 계산 -> 뒤집을 수 있는 수를 계산해서 점수를 +,

상대가 둘 수 있는 수 계산 -> 뒤집을 수 있는 수를 계산해서 점수를 -해서

level 1은 내가 둘 수 있는 수만 계산, level 5는 내(1)가 상대(2)가 내(3)가 상대(4)가 내(5)가 두는 수를 계산해서 점수가 제일 높을 수에 착수합니다.

너무 많은 수를 계산하기는 어려워 첫 수를 제외하고 둘 수 있는 수 중 가장 많이 뒤집을 수 있는 수로만 계산하도록 만들었으나 지금 보니 좀 엉성한 부분이 많네요... (최대로 둘 수 있는 수만 계산하면서 최대를 만나기 전까지 넣어둔 걸 그대로 계산하는 등....)

성능은 나쁘지만 궁금하시면 아래에서 직접 코드를 확인해 보시죠 ㅎㅎ

 

처음엔 대충 간단하게 만드는 게 목적이라 하나의 HTML파일로 만들었습니다. 나름 좋네요 ㅎㅎ

바로 전 포스트인 복귀글에 적은 대로 일단 Next로 띄우면서 UI도 좀 더 고급스럽게 + vs 컴퓨터의 성능 업데이트를 진행해보려고 합니다.

 

레포지토리는 https://github.com/Chae-Sumin/Othello

 

GitHub - Chae-Sumin/Othello: Othello Game (Reversi)

Othello Game (Reversi). Contribute to Chae-Sumin/Othello development by creating an account on GitHub.

github.com

데모 깃헙페이지는 https://chae-sumin.github.io/Othello/

 

Othello

 

chae-sumin.github.io

들어가기 귀찮으시다면... 아래 코드를 보실 수 있으나 한 번씩 코드를 보시고 응원의 Star를 남겨주시면 감사하겠습니다~!

 

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Othello</title>
</head>
<body>
	<div class="game_wrap">
		<!-- 게임 보드 -->
		<div id="othello"></div>
		<!-- 게임 콘솔 -->
		<div class="console">
			<!-- 텍스트 안내 -->
			<div class="console_text" id="console"></div>
			<!-- 턴 표시 -->
			<div class="turn">
				<span>TURN:&emsp;</span><span id="setturn" class="coin"></span>
			</div>
			<!-- 돌 갯수 표시 -->
			<div class="count_wrap">
				<div class="count">Count</div>
				<div class="turn">
					<div class="coin"></div><div>&nbsp;:&emsp;<span id="black">2</span></div>
				</div>
				<div class="turn">
					<div class="coin reverse"></div><div>&nbsp;:&emsp;<span id="white">2</span></div>
				</div>
			</div>
			<!-- 초기화 버튼 -->
			<button id="reset">reset</button>
			<!-- 패스 버튼 -->
			<button id="pass">pass</button>
			<!-- 종료 버튼 -->
			<button id="end">end</button>
			<!-- 추천 표시 버튼 -->
			<button id="recommend">recommend</button>
			<form>
				<!-- 게임 모드 설정 -->
				<div class="mode_wrap">
					<div>
						<input type="radio" name="mode" id="mode1" value="1" checked>
						<label for="mode1">1P vs 2P</label>
					</div>
					<div>
						<input type="radio" name="mode" id="mode2" value="2">
						<label for="mode2">1P vs AI</label>
					</div>
					<div>
						<input type="radio" name="mode" id="mode3" value="3">
						<label for="mode3">AI vs AI</label>
					</div>
				</div>
				<!-- AI 난이도 설정 -->
				<div class="level_wrap">
					<div>
						<input type="radio" name="level" id="level1" value="1" checked>
						<label for="level1">level 1</label>
					</div>
					<div>
						<input type="radio" name="level" id="level2" value="2">
						<label for="level2">level 2</label>
					</div>
					<div>
						<input type="radio" name="level" id="level3" value="3">
						<label for="level3">level 3</label>
					</div>
					<div>
						<input type="radio" name="level" id="level4" value="4">
						<label for="level4">level 4</label>
					</div>
					<div>
						<input type="radio" name="level" id="level5" value="5">
						<label for="level5">level 5</label>
					</div>

				</div>
			</form>
		</div>
	</div>
	<ul class="game_rule">
		<li>상대 돌을 자신의 돌로 감싸면 뒤집을 수 있습니다.</li>
		<li>상대 돌을 감싸 뒤집을 수 있는곳에만 둘 수 있습니다.</li>
		<li>만약 상대 돌을 뒤집을 수 있는 위치가 없다면 패스할 수 있습니다.</li>
		<li>둘 다 둘 곳이 없어 연속으로 패스를 하면 게임은 종료됩니다.</li>
		<li>게임 당 패스는 두번까지만 가능하고 세번째 패스와 동시에 게임은 종료됩니다.</li>
		<li>보드를 모두 채우면 게임이 끝납니다.</li>
		<li>게임 종료 후 더 많은 돌을 가지고 있는 사람이 승리합니다.</li>
	</ul>
	<style>
		* {
			box-sizing: border-box;
		}
		body {
			padding: 20px;
			background-color: #ddd;
		}
		.game_wrap {
			display: grid;
			grid-template-columns: 640px 1fr;
			gap: 20px;
			width: 910px;
			margin: 0 auto 20px;
		}
		.game_rule {
			display: grid;
			grid-template-columns: 1fr;
			gap: 8px;
			width: 910px;
			margin: 0 auto;
			padding: 20px 30px;
			border: 1px solid #000;
			border-radius: 10px;
			background-color: #fff;
			font-size: 12px;
		}

		/* 보드 */
		#othello {
			position: relative;
			display: grid;
			grid-template-columns: repeat(8, 1fr);
			grid-template-rows: repeat(8, 1fr);
			width: 640px;
			height: 640px;
			/* 커스텀 커서 예정 */
			cursor: none;
		}
		.cell {
			position: relative;
			box-sizing: border-box;
			display: flex;
			justify-content: center;
			align-items: center;
			width: 80px;
			height: 80px;
			background-color: beige;
			-webkit-perspective: 1000px;
			perspective: 1000px;
		}
		.cell.darker {
			background-color: burlywood;
		}
		/* 커서 표시 */
		.cell:hover {
			border: 4px solid brown;
		}
		/* 추천 칸 표시 */
		.cell.recommend::after {
			content: '';
			position: absolute;
			left: 0;
			right: 0;
			top: 0;
			bottom: 0;
			background-color: rgba(0, 0, 0, 0.5);
		}

		/* 돌 */
		.coin {
			position: relative;
			width: 50px;
			height: 50px;
			border-radius: 25px;
			-webkit-transform-style: preserve-3d;
			transform-style: preserve-3d;
			transition: transform 1s ease;
			transform: rotateY(0) rotateZ(0);
			/* 마우스 이벤트 방지 */
			pointer-events: none;
		}
		.coin::after,
		.coin::before {
			box-sizing: border-box;
			content: '';
			position: absolute;
			left: 0;
			top: 0;
			display: block;
			width: 100%;
			height: 100%;
			border-radius: 50%;
			-webkit-backface-visibility: hidden;
			backface-visibility: hidden;
		}
		/* 앞면(흑) */
		.coin::after {
			background-color: #454545;
		}
		/* 뒷면(백) */
		.coin::before {
			background-color: #fefefe;
			transform: rotateY(180deg);
		}
		/* 마지막 돌 */
		.coin.curr::after,
		.coin.curr::before {
			border: 2px solid red;
		}
		/* 숨기기 */
		.coin.none {
			display: none;
		}
		/* 뒷면 노출 */
		.coin.reverse {
			transform: rotateY(180deg) rotateZ(45deg);
		}
		/* 마우스커서는 반 사이즈 */
		.mouse {
			position: absolute;
			width: 25px !important;
			height: 25px !important;
		}

		/* 콘솔 */
		.console_text {
			height: 80px;
			padding: 10px;
			border: 2px solid #000;
			border-radius: 4px;
			background-color: #fff;
			line-height: 20px;
			font-size: 16px;
			color: #222;
			margin-bottom: 20px;
		}
		/* 턴 표시 */
		.turn {
			display: flex;
			align-items: center;
			margin-bottom: 20px;
			font-weight: bold;
			font-size: 24px;
		}
		/* 점수 표시 */
		.count_wrap {
			margin-bottom: 20px;
		}
		.count {
			font-weight: bold;
			font-size: 24px;
			margin-bottom: 10px;
		}
		/* 버튼 디자인 */
		.console > button {
			margin: 8px 4px 0;
			padding: 10px 12px;
			font-size: 18px;
			font-weight: bold;
			border: none;
			border-radius: 5px;
			background-color: #454545;
			color: #fefefe;
			cursor: pointer;
		}
		/* 모드 / 난이도 표시 */
		.mode_wrap,
		.level_wrap {
			margin-top: 20px;
		}
	</style>
	<script>
		function game() {
			// 오델로 HTML 객체
			const othello = document.querySelector('#othello');
			// 메인 보드
			let board = new Array(8).fill(0).map(() => new Array(8).fill(0));
			// 1: 흑돌, -1: 백돌
			let turn = 1;
			// 게임 종료 여부
			let isEnd = true;
			// 직전 패스 여부
			let isPass = false;
			// 패스 횟수
			let passCount = 0;
			// setTimeout timer
			let AITimer = null;
			// 추천 사용 횟수
			let recommendCount = {
				'1': 0,
				'-1': 0,
			};
			// 패스 버튼
			const pass = document.querySelector('#pass');
			// 게임 종료 버튼
			const end = document.querySelector('#end');
			// 게임 초기화 버튼
			const reset = document.querySelector('#reset');
			// 추천 버튼
			const recommend = document.querySelector('#recommend');
			// 모드 버튼 (player vs AI)
			const mode2 = document.querySelector('#mode2');
			// 모드 버튼 (AI vs AI)
			const mode3 = document.querySelector('#mode3');

			// 이벤트 리스너
			reset.addEventListener('click', () => {
				if (!isEnd) return setConsole('게임이 끝나지 않았습니다.');
				resetFn();
			});
			pass.addEventListener('click', passFn);
			end.addEventListener('click', endGame);
			recommend.addEventListener('click', () => {
				if (isEnd) return setConsole('게임이 종료되었습니다. 새로 시작하려면 reset 버튼을 눌러주세요.');

				const beforeRecommendPos = document.querySelector('.recommend');
				if (beforeRecommendPos) return setConsole('이미 추천된 위치가 있습니다.');

				const recommendPos = getRecommendPos(turn);
				if (recommendPos.length === 0) {
					setConsole('돌을 놓을 수 있는 칸이 존재하지 않습니다. 2초 뒤 턴을 패스합니다.');
					setTimeout(passFn, 2000);
					return;
				}
				const maxRecommends = recommendPos.recommend.filter((el) => el.count === recommendPos.max);
				setRecommendPos(maxRecommends);
			});
			mode2.addEventListener('click', () => {
				if (isEnd) return setConsole('게임이 종료되었습니다. 새로 시작하려면 reset 버튼을 눌러주세요.');
				if (turn == -1) {
					clearTimeout(AITimer);
					AITimer = setTimeout(setAI, 1000);
				}
			});
			mode3.addEventListener('click', () => {
				if (isEnd) return setConsole('게임이 종료되었습니다. 새로 시작하려면 reset 버튼을 눌러주세요.');
				clearTimeout(AITimer);
				AITimer = setTimeout(setAI, 1000);
			});
			if (document.forms[0].mode.value === '3') {
				clearTimeout(AITimer);
				AITimer = setTimeout(setAI, 1000);
			}
			othello.addEventListener('click', (e) => {
				const mode = document.forms[0].mode.value;
				if (isEnd) return;
				if (mode === '3') return setConsole('AI 대결 모드에서는 플레이어가 돌을 놓을 수 없습니다.');
				if (mode === '2' && turn === -1) return setConsole('AI의 턴입니다. 잠시만 기다려주세요.');
				if (e.target.classList.contains('cell')) {
					const x = Number(e.target.dataset.x);
					const y = Number(e.target.dataset.y);
					const res = setPos(x, y, turn);
					if (res) {
						isPass = false;
						setConsole('');
						setRecommendPos();
						getNextTurn();
					}
				}
			});

			// 메소드
			// 보드 돔 생성
			function mkBoardDOM() {
				// 기존 돔 제거
				if (othello.children.length) othello.innerHTML = '';
				// 돔 생성
				for (let i = 0; i < 64; i++) {
					const x = i % 8;
					const y = Math.floor(i / 8);
					const cell = document.createElement('div');
					const coin = document.createElement('div');
					cell.dataset.x = x;
					cell.dataset.y = y;
					cell.classList.add('cell');
					coin.classList.add('coin');
					coin.classList.add('none');
					if (x % 2 && !(y % 2) || !(x % 2) && y % 2) {
						cell.classList.add('darker');
					}
					cell.appendChild(coin);
					othello.appendChild(cell);
				}
				// 마우스 커서
				const mouse = document.createElement('div');
				mouse.classList.add('mouse');
				mouse.classList.add('coin');
				othello.addEventListener('mousemove', (e) => {
					mouse.style.left = (Number(e.clientX - othello.offsetLeft) - 12.5) + 'px';
					mouse.style.top = (Number(e.clientY - othello.offsetTop) - 12.5) + 'px';
				});
				othello.appendChild(mouse);
			}
			// 리셋
			function resetFn() {
				// 파라미터 초기화
				turn = 1;
				isEnd = false;
				isPass = false;
				document.forms[0].mode.value = '1';
				document.forms[0].mode.level = '1';
				passCount = 0;
				recommendCount = {
					'1': 0,
					'-1': 0,
				};
				clearTimeout(AITimer);

				// 콘솔 초기화
				setConsole('');
				// 보드 초기화
				board = new Array(8).fill(0).map(() => new Array(8).fill(0));
				board[3][3] = 1;
				board[3][4] = -1;
				board[4][3] = -1;
				board[4][4] = 1;
				// 돔 초기화
				for (let i = 0; i < 64; i++) {
					const x = i % 8;
					const y = Math.floor(i / 8);
					const coin = document.querySelector(`.cell[data-x="${x}"][data-y="${y}"] .coin`);
					switch (board[y][x]) {
						case 0:
							coin.classList.add('none');
							coin.classList.remove('reverse');
							break;
						case -1:
							coin.classList.remove('none');
							coin.classList.add('reverse');
							break;
						default:
							coin.classList.remove('none');
							coin.classList.remove('reverse');
							break;
					}
				}
			}

			// 돌 놓기
			/**
			 * @param {number} x - x좌표
			 * @param {number} y - y좌표
			 * @param {number} turn - 턴
			 * @param {Array} [forReverse] - 뒤집을 돌
			 * @returns {boolean} - 놓을 수 없는 곳이면 false, 놓을 수 있으면 true
			 */
			function setPos(x, y, turn, forReverse) {
				// 놓을 수 없는 곳이면 1을 반환
				if (board[y][x] !== 0 || !turn) return false;

				// 뒤집을 돌이 있는지 확인
				if (!forReverse) {
					forReverse = checkReverse(x, y, turn);
				}
				// 뒤집을 돌이 없으면 1을 반환
				if (forReverse.length == 0) {
					setConsole(`${turn > 0 ? '흑돌' : '백돌'}을 그곳에 놓을 수 없습니다.`);
					return false;
				}
				// 마지막 수 초기화
				const curr = document.querySelector('.curr');
				if (curr) curr.classList.remove('curr');
				// 돌 놓기
				const cell = document.querySelector(`.cell[data-x="${x}"][data-y="${y}"]`);
				const coin = cell.querySelector('.coin');
				coin.classList.remove('none');
				coin.classList.add('curr');
				if (turn === -1) {
					coin.classList.add('reverse');
				}
				// 보드 배열 수정
				board[y][x] = turn;
				// 뒤집기
				forReverse.forEach(reverse);
				return true;
			}
			// 돌 놓기
			/**
			 * @param {number} x - x좌표
			 * @param {number} y - y좌표
			 * @param {number} turn - 턴
			 * @param {Array} board - 보드 배열
			 * @param {Array} [forReverse] - 뒤집을 돌
			 * @returns {Array} - 변경된 보드 배열
			 */
			function setSudoPos(x, y, turn, board, forReverse) {
				if (!board) return {res: false, board};
				// 놓을 수 없는 곳이면 1을 반환
				if (board[y][x] !== 0 || !turn) return {res: false, board};
				board = JSON.parse(JSON.stringify(board));

				if (!forReverse) {
					forReverse = checkReverse(x, y, turn, board);
				}
				// 뒤집을 돌이 없으면 1을 반환
				if (forReverse.length == 0) {
					return {res: false, board};
				}
				// 보드 배열 수정
				board[y][x] = turn;
				// 뒤집기
				forReverse.forEach(({x, y, turn}) => {board[y][x] = turn;});
				return {res: true, board};
			}
			// 뒤집을 수 있는 돌 체크
			/**
			 * @param {number} x - x좌표
			 * @param {number} y - y좌표
			 * @param {number} turn - 턴
			 * @param {Array} [b] - 보드 배열
			 * @return {Array} 뒤집을 돌
			 */
			function checkReverse(x, y, turn, b) {
				if (!b) b = board;
				const forReverse = [];
				// check left
				if (x > 1) {
					if (board[y][x - 1] === -turn) {
						for (let i = x - 2; i >= 0; i--) {
							if (b[y][i] === 0) break;
							if (b[y][i] === turn) {
								for (let j = x - 1; j > i; j--) {
									forReverse.push({x: j, y, turn});
								}
								break;
							}
						}
					}
				}
				// check right
				if (x < 6) {
					if (b[y][x + 1] === -turn) {
						for (let i = x + 2; i < 8; i++) {
							if (b[y][i] === 0) break;
							if (b[y][i] === turn) {
								for (let j = x + 1; j < i; j++) {
									forReverse.push({x: j, y, turn});
								}
								break;
							}
						}
					}
				}
				// check top
				if (y > 1) {
					if (b[y - 1][x] === -turn) {
						for (let i = y - 2; i >= 0; i--) {
							if (b[i][x] === 0) break;
							if (b[i][x] === turn) {
								for (let j = y - 1; j > i; j--) {
									forReverse.push({x, y: j, turn});
								}
								break;
							}
						}
					}
				}
				// check bottom
				if (y < 6) {
					if (b[y + 1][x] === -turn) {
						for (let i = y + 2; i < 8; i++) {
							if (b[i][x] === 0) break;
							if (b[i][x] === turn) {
								for (let j = y + 1; j < i; j++) {
									forReverse.push({x, y: j, turn});
								}
								break;
							}
						}
					}
				}
				// check top left
				if (x > 1 && y > 1) {
					if (b[y - 1][x - 1] === -turn) {
						for (let i = 2; i < 8; i++) {
							if (x - i < 0 || y - i < 0) break;
							if (b[y - i][x - i] === 0) break;
							if (b[y - i][x - i] === turn) {
								for (let j = 1; j < i; j++) {
									forReverse.push({x: x - j, y: y - j, turn});
								}
								break;
							}
						}
					}
				}
				// check top right
				if (x < 6 && y > 1) {
					if (b[y - 1][x + 1] === -turn) {
						for (let i = 2; i < 8; i++) {
							if (x + i > 7 || y - i < 0) break;
							if (b[y - i][x + i] === 0) break;
							if (b[y - i][x + i] === turn) {
								for (let j = 1; j < i; j++) {
									forReverse.push({x: x + j, y: y - j, turn});
								}
								break;
							}
						}
					}
				}
				// check bottom left
				if (x > 1 && y < 6) {
					if (b[y + 1][x - 1] === -turn) {
						for (let i = 2; i < 8; i++) {
							if (x - i < 0 || y + i > 7) break;
							if (b[y + i][x - i] === 0) break;
							if (b[y + i][x - i] === turn) {
								for (let j = 1; j < i; j++) {
									forReverse.push({x: x - j, y: y + j, turn});
								}
								break;
							}
						}
					}
				}
				// check bottom right
				if (x < 6 && y < 6) {
					if (b[y + 1][x + 1] === -turn) {
						for (let i = 2; i < 8; i++) {
							if (x + i > 7 || y + i > 7) break;
							if (b[y + i][x + i] === 0) break;
							if (b[y + i][x + i] === turn) {
								for (let j = 1; j < i; j++) {
									forReverse.push({x: x + j, y: y + j, turn});
								}
								break;
							}
						}
					}
				}
				return forReverse;
			}
			/**
			 * 돌 뒤집기
			 * @param {object} param - {x, y, turn}
			 * @param {number} param.x - x 좌표
			 * @param {number} param.y - y 좌표
			 * @param {number} param.turn - 1 or -1
			 * @return {void}
			 */
			function reverse({x, y, turn}) {
				const cell = document.querySelector(`.cell[data-x="${x}"][data-y="${y}"]`);
				const coin = cell.querySelector('.coin');
				const cmd = turn === 1 ? 'remove' : 'add';
				coin.classList[cmd]('reverse');
				board[y][x] = turn;
			}
			/**
			 * 패스 가능 여부 확인
			 * @param {number} turn - 1 or -1
			 * @return {boolean} - 패스 가능 여부
			 */
			function checkPass(turn) {
				for (let y = 0; y < 8; y++) {
					for (let x = 0; x < 8; x++) {
						if (board[y][x] == 0) {
							if (checkReverse(x, y, turn).length > 0) return false;
						}
					}
				}
				return true;
			}
			// 텍스트 콘솔 내용 변경
			function setConsole(str) {
				const console = document.querySelector('#console');
				console.textContent = str;
			}
			/**
			 * 콘솔 UI 변경
			 * @param {number} turn - 1 or -1
			 * @return {boolean} - 게임 종료 여부
			 */
			function setCousoleUI(turn) {
				const mouse = document.querySelector('.mouse');
				const setturn = document.querySelector('#setturn');
				const cmd = turn === 1 ? 'remove' : 'add';
				mouse.classList[cmd]('reverse');
				setturn.classList[cmd]('reverse');
				const blackCount = document.querySelector('#black');
				const whiteCount = document.querySelector('#white');
				const {black, white} = getCount();
				blackCount.textContent = black;
				whiteCount.textContent = white;
				return (black + white == 64);
			}
			/**
			 * 돌 개수 세기
			 * @param {Array} [b] - 보드
			 * @return {Object} - {black, white}
			 */
			function getCount(b) {
				if (!b) b = board;
				let black = 0;
				let white = 0;
				for (let y = 0; y < 8; y++) {
					for (let x = 0; x < 8; x++) {
						if (b[y][x] === 1) black++;
						if (b[y][x] === -1) white++;
					}
				}
				return {black, white};
			}
			/**
			 * 놓을 수 있는 돌 위치 가져오기
			 * @param {number} turn - 1 or -1
			 * @param {Array} [b] - 보드
			 * @param {boolean} [only_max] - 가장 많은 돌을 뒤집을 수 있는 위치만 가져올지 여부
			 * @return {Object} - {recommend, max}
			 * recommend: 놓을 수 있는 돌 위치 배열
			 * max: 놓을 수 있는 돌 위치 중 가장 많은 돌을 뒤집을 수 있는 위치의 개수
			 */
			function getRecommendPos(turn, b, only_max) {
				const recommend = [];
				let max = 0;
				for (let y = 0; y < 8; y++) {
					for (let x = 0; x < 8; x++) {
						if (board[y][x] == 0) {
							const reverses = checkReverse(x, y, turn, b);
							if (reverses.length > 0 && (!only_max || reverses.length > max)) recommend.push({x, y, reverses, count: reverses.length});
							if (reverses.length > max) max = reverses.length;
						}
					}
				}
				return {recommend, max};
			}
			/**
			 * 놓을 수 있는 돌 위치 표시
			 * @param {Array} [arr] - 놓을 수 있는 돌 위치 배열
			 * @return {void}
			 */
			function setRecommendPos(arr) {
				const beforeRecommend = document.querySelectorAll('.recommend');
				beforeRecommend.forEach((el) => el.classList.remove('recommend'));
				if (!arr) return;
				recommendCount[turn]++;
				arr.forEach(({x,y}) => {
					const cell = document.querySelector(`.cell[data-x="${x}"][data-y="${y}"]`);
					cell.classList.add('recommend');
				});
			}

			// 턴 넘김
			function getNextTurn(ms) {
				if (!ms) ms = 1000;
				const mode = document.forms[0].mode.value;
				turn *= -1;
				isPass = false;
				setRecommendPos();
				const checkEnd = setCousoleUI(turn);
				if (checkEnd) {
					clearTimeout(AITimer);
					AITimer = setTimeout(endGame, ms);
					return;
				}
				clearTimeout(AITimer);
				if (mode == 3 || (turn == -1 && mode == 2)) {
					AITimer = setTimeout(setAI, ms);
				}
			}

			// 패스함수
			function passFn() {
				// 게임 종료 여부 확인
				if (isEnd) return setConsole('게임이 종료되었습니다. 새로 시작하려면 reset 버튼을 눌러주세요.');
				// 패스 가능 여부 확인
				if (checkPass(turn)) {
					// 패스 횟수 확인
					if (isPass || passCount === 2) {
						return endGame();
					}
					// 패스
					getNextTurn();
					isPass = true;
					passCount++;
					setConsole(`턴을 패스합니다. 이제 ${turn > 0 ? '흑돌' : '백돌'} 턴입니다.`);
				} else {
					setConsole('패스 불가, 돌을 놓을 수 있는 칸이 존재합니다.');
				}
			}
			function setAI() {
				const level = document.forms[0].level.value;
				const mode = document.forms[0].mode.value;
				if (mode === '2' && turn == 1) return;

				let {recommend, max} = getRecommendPos(turn);

				setRecommendPos();
				if (recommend.length === 0) {
					setConsole('돌을 놓을 수 있는 칸이 존재하지 않습니다. 2초 뒤 턴을 패스합니다.');
					clearTimeout(AITimer);
					AITimer = setTimeout(passFn, 2000);
					return;
				}

				let res = AI(level);
				if (!res) {
					const random = Math.floor(Math.random() * recommend.length);
					res = recommend[random];
				}
				const {x, y, reverses} = res;
				setPos(x, y, turn, reverses);
				getNextTurn(500 + (500 / level));
			}
			/**
			 * AI
			 * @param {number} level - 난이도
			 * @return {Object} - {x, y, reverses}
			 * x: x좌표
			 * y: y좌표
			 * reverses: 뒤집힐 돌의 좌표 배열
			*/
			function AI(level = 1) {
				const tmpBoard = JSON.parse(JSON.stringify(board));
				const {recommend, max} = getRecommendPos(turn, tmpBoard);

				console.log('연산 중...');

				// 후보군 생성
				let candidates = [];
				candidates = candidates.concat(recommend.map(rec => Object.assign(rec, {init: rec, board: tmpBoard, d: rec.reverses.length, l: 0, t: turn})));

				// 결과 저장
				let results = [];

				let i = 0;
				let best = -Infinity;
				// 후보군이 없을 때까지 반복 또는 100000번 반복
				while (i < 100000 && candidates.length > 0) {
					/**
					 * @type {Object}
					 * @property {Object} init - 초기 위치
					 * @property {number} x - x 좌표
					 * @property {number} y - y 좌표
					 * @property {Array} reverses - 뒤집을 돌 위치 배열
					 * @property {Array} board - 보드판
					 * @property {number} d - 점수
					 * @property {number} l - 레벨
					 * @property {number} t - 턴
					 */
					const {init, x, y, reverses, board, d, l, t} = candidates.shift();
					// 후보군 연산
					const {res, board: newBoard} = setSudoPos(x, y, t, board, reverses);
					if (!res) continue;

					// 다음 턴 후보군 생성
					let {recommend, max} = getRecommendPos(-t, newBoard, true);
					// 레벨 도달
					if (l + 1 >= level && d >= best) {
						results.push({x: init.x, y: init.y, reverses: init.reverses, d});
						best = Math.max(best, d);
						i++;
						continue;
					}
					// 후보군이 없을 때
					if (recommend.length === 0) {
						// 패스 가정 후 후보군 생성
						let {recommend, max} = getRecommendPos(t, newBoard, true);
						// 후보군이 없을 때
						if (recommend.length === 0) {
							if (d >= best) {
								results.push({x: init.x, y: init.y, reverses: init.reverses, d});
								best = Math.max(best, d);
							}
							i++;
							continue;
						} else {
							// 후보군 생성
							candidates = candidates.concat(recommend.slice(0, 15 - l).map(rec => Object.assign(rec, {init, board: newBoard, d: d + rec.reverses.length, l: l + 1, t})));
						}
					} else {
						// 후보군 생성
						candidates = candidates.concat(recommend.slice(0, 15 - l).map(rec => Object.assign(rec, {init, board: newBoard, d: d - rec.reverses.length, l: l + 1, t: -t})));
					}
					i++;
				}
				results = results.filter((el) => el.d === best);
				const random = Math.floor(Math.random() * results.length);
				console.log('연산 완료!');
				return results[random];
			}
			// 게임 종료 선언
			function endGame() {
				if (isEnd) return setConsole('게임이 종료되었습니다. 새로 시작하려면 reset 버튼을 눌러주세요.');
				isEnd = true;
				const res = board.reduce((acc, cur) => acc.concat(cur), []).reduce((acc, cur) => acc + cur, 0)
				const text =  res > 0 ? '흑돌이 이겼습니다.' : res < 0 ? '백돌이 이겼습니다.' : '비겼습니다.';
				setConsole(text);
				alert(text);
			}


			// 초기설정
			mkBoardDOM();
			resetFn();
		}

		game();
	</script>
</body>
</html>