게임 개발
[Web] 스도쿠 구현하기
ris
2024. 11. 14. 23:48
Python 로직으로만 구현해보니 웹으로 구현해보고 싶어서 하루를 갈아서 만들어봤습니다.
퀄리티는 조악하지만 그래도 하루만큼의 값어치는 하는 것 같아서 만족합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Document</title>
</head>
<body>
<h1 class="title">Sudoku</h1>
<div class="container">
<div class="board-container">
<div class="grid-container" id="grid-container"></div>
<button id="StartButton">Start</button>
<button id="RestartButton" style="display: none;">Restart</button>
<span id="heart1" style="display: none;">❤️</span>
<span id="heart2" style="display: none;">❤️</span>
<span id="heart3" style="display: none;">❤️</span>
</div>
<div class="number-container" id="number-grid">
<div class="number-item">1</div>
<div class="number-item">2</div>
<div class="number-item">3</div>
<div class="number-item">4</div>
<div class="number-item">5</div>
<div class="number-item">6</div>
<div class="number-item">7</div>
<div class="number-item">8</div>
<div class="number-item">9</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* 전체 페이지의 가운데에 배치 */
body, html {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-family: Arial, sans-serif;
flex-direction: column;
}
.title {
font-size: 36px;
font-weight: bold;
color: #4CAF50; /* Same color as the Start button */
margin-bottom: 20px;
text-align: center;
}
/* 컨테이너를 flex로 설정하여 보드와 숫자 그리드를 나란히 배치 */
.container {
display: flex;
align-items: flex-start; /* 세로로 정렬 */
justify-content: center;
}
/* 9x9 그리드 컨테이너 스타일 */
.grid-container {
position: relative;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(9, 50px); /* 9개의 열 */
grid-template-rows: repeat(9, 50px); /* 9개의 행 */
gap: 5px; /* 셀 간격 */
margin-bottom: 20px; /* 그리드와 버튼 사이 여백 */
}
/* 각 그리드 항목 스타일 */
.grid-item {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
border: 1px solid #ccc; /* 셀 경계 */
font-size: 20px;
font-weight: bold;
text-align: center;
color: #333;
background-color: #f9f9f9; /* 기본 배경 색상 */
transition: background-color 0.3s ease, transform 0.2s ease; /* 배경 색상과 transform 효과 */
cursor: pointer; /* 클릭 가능한 영역 */
}
/* 홀수 셀 배경색 */
.grid-item:nth-child(odd) {
background-color: #e9ecef;
}
/* 짝수 셀 배경색 */
.grid-item:nth-child(even) {
background-color: #f9f9f9;
}
/* 숫자 그리드 스타일 (세로로 배치) */
.number-container {
display: grid;
grid-template-columns: repeat(1, 50px);
grid-template-rows: repeat(9, 50px);
gap: 5px;
margin-left: 20px;
}
/* 숫자 항목 스타일 */
.number-item {
font-size: 22px; /* 글자 크기를 조금 키움 */
font-weight: bold;
color: #333;
display: flex;
justify-content: center;
align-items: center;
background-color: #d3d3d3; /* 숫자 그리드 배경 색상 */
border: 1px solid #bbb; /* 더 부드러운 경계선 색상 */
border-radius: 5px; /* 둥근 모서리 */
transition: background-color 0.3s ease, transform 0.3s ease; /* 부드러운 효과 추가 */
}
/* 숫자 항목 hover 효과 */
.number-item:hover {
background-color: #d4e9d2; /* 미세하게 다른 배경색 */
box-shadow: 0 0 10px rgba(0, 128, 0, 0.6); /* 그림자 추가 */ /* hover 시 배경색 변경 */ /* 숫자 색상 변경 */
cursor: pointer; /* 클릭 가능한 손 모양 커서 */
transform: scale(1.1); /* hover 시 크기 약간 확대 */
}
/* 숫자 칸 배경색 */
.number-item {
background-color: #f9f9f9;
}
/* Start 버튼 스타일 */
button#StartButton {
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
background-color: #4CAF50; /* 초록색 */
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
height: 60px;
margin-top: 10px;
}
button#RestartButton {
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
background-color: #4CAF50; /* 초록색 */
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
height: 60px;
margin-top: 10px;
}
/* 버튼 hover 효과 */
button#StartButton:hover {
background-color: #45a049;
}
button#RestartButton:hover {
background-color: #45a049;
}
/* 클릭된 셀 스타일 */
.grid-item.clicked {
background-color: #4CAF50; /* 클릭된 셀 배경색 */
color: white; /* 숫자 색상 변경 */
}
/* 호버 시 배경색 변화와 그림자 */
.grid-item:hover {
background-color: #d4e9d2; /* 미세하게 다른 배경색 */
box-shadow: 0 0 10px rgba(0, 128, 0, 0.6); /* 그림자 추가 */
}
/* 그리드 항목에 숫자가 들어간 경우 텍스트 스타일 */
.grid-item.clicked span {
color: white; /* 클릭 시 숫자 색상 */
}
/* 각 하트 아이콘을 스타일링 */
#heart1, #heart2, #heart3 {
font-size: 48px; /* 하트 크기 */
color: #ff0000; /* 하트 색상 */
margin-left: 10px; /* 버튼과 하트 사이 간격 */
display: inline-block; /* 텍스트가 버튼과 같은 라인에 오도록 설정 */
vertical-align: middle; /* 세로 정렬 */
transition: transform 0.3s ease; /* 하트 애니메이션 추가 */
}
/* 하트에 마우스를 올렸을 때 크기 변화 */
#heart1:hover, #heart2:hover, #heart3:hover {
transform: scale(1.2); /* 크기 확대 */
}
/* 각 하트를 다르게 배치 */
#heart1 {
margin-right: 10px; /* 첫 번째 하트 오른쪽 여백 */
}
#heart2 {
margin-right: 10px; /* 두 번째 하트 오른쪽 여백 */
}
.game-over-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 0, 0, 0.8);
color: white;
font-size: 24px;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
}
.game-success-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 255, 100, 0.8);
color: white;
font-size: 24px;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
}
// grid-container에 접근
const gridContainer = document.getElementById('grid-container');
// 9x9 정사각형을 생성하여 grid-container에 추가
for (let i = 0; i < 81; i++) {
const gridItem = document.createElement('div');
gridItem.classList.add('grid-item');
gridContainer.appendChild(gridItem);
}
// 셔플 함수
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// 보드 초기화 및 랜덤 위치에 숫자 표시
function display_board() {
const gridItems = document.querySelectorAll('.grid-item');
// 먼저 그리드의 모든 셀을 초기화
gridItems.forEach((item) => {
item.innerText = ''; // 셀을 초기화
item.style.backgroundColor = ''; // 배경 색도 초기화
});
let positions = [];
while (positions.length < 30) {
let randomIndex = Math.floor(Math.random() * 81);
if (!positions.includes(randomIndex)) {
positions.push(randomIndex);
}
}
// 보드의 값을 그리드에 맞게 업데이트
positions.forEach((index) => {
const i = Math.floor(index / 9);
const j = index % 9;
const gridItem = gridItems[index];
// 숫자가 있을 때만 표시
if (board[i][j] !== 0) {
gridItem.innerText = board[i][j];
}
});
}
// Sudoku 보드 관련 데이터 초기화
let board = Array.from({ length: 9 }, () => Array(9).fill(0));
let row = Array.from({ length: 9 }, () => Array(10).fill(0));
let col = Array.from({ length: 9 }, () => Array(10).fill(0));
let diag = Array.from({ length: 9 }, () => Array(10).fill(0));
let end = false;
let gameStarted = false;
let selectedCell = null; // 마지막으로 선택한 칸을 추적
// 보드 생성
function board_init() {
let num_list = Array.from({ length: 9 }, (_, i) => (i + 1));
let nums = shuffle(num_list);
let idx = 0;
for (let i = 0; i < 9; i += 3) {
for (let j = 0; j < 9; j += 3) {
board[i][j] = nums[idx];
row[i][nums[idx]] = 1;
col[j][nums[idx]] = 1;
diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][nums[idx]] = 1;
idx += 1;
}
}
}
// 재귀적으로 보드를 채움
function fill_board(k) {
if (k >= 81) {
end = true;
display_board();
return;
}
let i = Math.floor(k / 9);
let j = k % 9;
if (board[i][j] !== 0) {
fill_board(k + 1);
return;
}
for (let n = 1; n <= 9; n++) {
if (row[i][n] === 0 && col[j][n] === 0 && diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][n] === 0) {
board[i][j] = n;
row[i][n] = col[j][n] = diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][n] = 1;
fill_board(k + 1);
if (end) return;
board[i][j] = 0;
row[i][n] = col[j][n] = diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][n] = 0;
}
}
}
let clicked = false; // 이미 숫자가 채워진 곳을 클릭했을 때만 false;
// Start 버튼 클릭 시 보드 초기화 및 숫자 표시
document.getElementById('StartButton').addEventListener('click', function () {
gameStarted = true;
board = Array.from({ length: 9 }, () => Array(9).fill(0));
row = Array.from({ length: 9 }, () => Array(10).fill(0));
col = Array.from({ length: 9 }, () => Array(10).fill(0));
diag = Array.from({ length: 9 }, () => Array(10).fill(0));
end = false;
board_init();
fill_board(0);
document.getElementById('StartButton').style.display = 'none';
document.getElementById('RestartButton').style.display = 'inline-block';
document.getElementById('heart1').style.display = 'inline-block';
document.getElementById('heart2').style.display = 'inline-block';
document.getElementById('heart3').style.display = 'inline-block';
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach((item) => {
item.addEventListener('click', function () {
if (gameStarted && item.innerText === '') {
// 이전에 선택한 칸의 색을 원래대로 복원
if (selectedCell) {
selectedCell.style.backgroundColor = '';
}
// 현재 클릭한 칸을 녹색으로 설정하고 selectedCell에 저장
item.style.backgroundColor = 'green';
selectedCell = item;
clicked = true;
}
});
});
});
// number-container 안의 각 number-item에 클릭 이벤트 리스너 추가
document.querySelectorAll('.number-item').forEach((numberItem) => {
numberItem.addEventListener('click', function () {
if(selectedCell && clicked){
const gridItems = document.querySelectorAll('.grid-item');
const index = Array.from(gridItems).indexOf(selectedCell);
// index를 이용해 row, col 계산
const rowIndex = Math.floor(index / 9);
const colIndex = index % 9;
// 'board' 배열에서 해당 셀의 값을 가져옴
const boardValue = board[rowIndex][colIndex];
// 'numberItem'의 innerText와 'board' 배열의 값이 같을 때만 실행
if (numberItem.innerText == boardValue) {
selectedCell.innerText = numberItem.innerText;
selectedCell.style.backgroundColor = '';
checkIfBoardFilled();
} else{
numberItem.style.backgroundColor = 'red';
// 1초 후 배경색을 원래대로 되돌림
setTimeout(() => {
numberItem.style.backgroundColor = ''; // 원래 배경색으로 복구
}, 1000);
let hearts = [document.getElementById('heart3'), document.getElementById('heart2'), document.getElementById('heart1')];
if (hearts[1].style.display == 'none'){
displayGameOverMessage();
}
for (let i = 0; i < hearts.length; i++) {
if (hearts[i].style.display !== 'none') {
hearts[i].style.display = 'none'; // 첫 번째로 보이는 하트를 숨김
break; // 하나만 숨기고 멈춤
}
}
}
}
})
});
// Restart 버튼 클릭 시 보드 초기화
document.getElementById('RestartButton').addEventListener('click', function () {
gameStarted = false;
board = Array.from({ length: 9 }, () => Array(9).fill(0));
row = Array.from({ length: 9 }, () => Array(10).fill(0));
col = Array.from({ length: 9 }, () => Array(10).fill(0));
diag = Array.from({ length: 9 }, () => Array(10).fill(0));
end = false;
display_board();
selectedCell = null; // 리셋할 때 선택된 칸도 초기화
document.getElementById('StartButton').style.display = 'inline-block';
document.getElementById('RestartButton').style.display = 'none';
document.getElementById('heart1').style.display = 'none';
document.getElementById('heart2').style.display = 'none';
document.getElementById('heart3').style.display = 'none';
const gameOverMessage = document.querySelector('.game-over-message');
if (gameOverMessage) {
gameOverMessage.remove(); // 게임 오버 메시지 삭제
}
});
function displayGameOverMessage() {
const gameOverMessage = document.createElement('div');
gameOverMessage.classList.add('game-over-message');
gameOverMessage.innerText = 'Game Over!';
// 게임 오버 창을 body에 추가
document.body.appendChild(gameOverMessage);
// 메시지가 화면에 표시되면, 3초 후 자동으로 사라지게 설정
setTimeout(() => {
gameOverMessage.remove();
}, 3000);
}
function checkIfBoardFilled() {
let isFilled = true;
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach((item) => {
if (item.innerText === '') { // 빈 칸이 있으면 false로 설정
isFilled = false;
}
});
// 보드가 다 채워지면 성공 메시지 표시
if (isFilled) {
displaySuccessMessage();
}
}
function displaySuccessMessage() {
const existingMessage = document.querySelector('.game-success-message');
if (!existingMessage) {
const successMessage = document.createElement('div');
successMessage.classList.add('game-success-message');
successMessage.innerText = 'Success!';
// 성공 메시지를 body에 추가
document.body.appendChild(successMessage);
// 메시지가 화면에 표시되면, 3초 후 자동으로 사라지게 설정
setTimeout(() => {
successMessage.remove();
}, 3000);
}
}
# 주석 제거 버전
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<title>Document</title>
</head>
<body>
<h1 class="title">Sudoku</h1>
<div class="container">
<div class="board-container">
<div class="grid-container" id="grid-container"></div>
<button id="StartButton">Start</button>
<button id="RestartButton" style="display: none;">Restart</button>
<span id="heart1" style="display: none;">❤️</span>
<span id="heart2" style="display: none;">❤️</span>
<span id="heart3" style="display: none;">❤️</span>
</div>
<div class="number-container" id="number-grid">
<div class="number-item">1</div>
<div class="number-item">2</div>
<div class="number-item">3</div>
<div class="number-item">4</div>
<div class="number-item">5</div>
<div class="number-item">6</div>
<div class="number-item">7</div>
<div class="number-item">8</div>
<div class="number-item">9</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
body, html {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-family: Arial, sans-serif;
flex-direction: column;
}
.title {
font-size: 36px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 20px;
text-align: center;
}
.container {
display: flex;
align-items: flex-start;
justify-content: center;
}
.grid-container {
position: relative;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(9, 50px);
grid-template-rows: repeat(9, 50px);
gap: 5px;
margin-bottom: 20px;
}
.grid-item {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
border: 1px solid #ccc;
font-size: 20px;
font-weight: bold;
text-align: center;
color: #333;
background-color: #f9f9f9;
transition: background-color 0.3s ease, transform 0.2s ease;
cursor: pointer;
}
.grid-item:nth-child(odd) {
background-color: #e9ecef;
}
.grid-item:nth-child(even) {
background-color: #f9f9f9;
}
.number-container {
display: grid;
grid-template-columns: repeat(1, 50px);
grid-template-rows: repeat(9, 50px);
gap: 5px;
margin-left: 20px;
}
.number-item {
font-size: 22px;
font-weight: bold;
color: #333;
display: flex;
justify-content: center;
align-items: center;
background-color: #d3d3d3;
border: 1px solid #bbb;
border-radius: 5px;
transition: background-color 0.3s ease, transform 0.3s ease;
}
.number-item:hover {
background-color: #d4e9d2;
box-shadow: 0 0 10px rgba(0, 128, 0, 0.6);
cursor: pointer;
transform: scale(1.1);
}
.number-item {
background-color: #f9f9f9;
}
button#StartButton {
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
height: 60px;
margin-top: 10px;
}
button#RestartButton {
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
height: 60px;
margin-top: 10px;
}
button#StartButton:hover {
background-color: #45a049;
}
button#RestartButton:hover {
background-color: #45a049;
}
.grid-item.clicked {
background-color: #4CAF50;
color: white;
}
.grid-item:hover {
background-color: #d4e9d2;
box-shadow: 0 0 10px rgba(0, 128, 0, 0.6);
}
.grid-item.clicked span {
color: white;
}
#heart1, #heart2, #heart3 {
font-size: 48px;
color: #ff0000;
margin-left: 10px;
display: inline-block;
vertical-align: middle;
transition: transform 0.3s ease;
}
#heart1:hover, #heart2:hover, #heart3:hover {
transform: scale(1.2);
}
#heart1 {
margin-right: 10px;
}
#heart2 {
margin-right: 10px;
}
.game-over-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 0, 0, 0.8);
color: white;
font-size: 24px;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
}
.game-success-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 255, 100, 0.8);
color: white;
font-size: 24px;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
}
const gridContainer = document.getElementById('grid-container');
for (let i = 0; i < 81; i++) {
const gridItem = document.createElement('div');
gridItem.classList.add('grid-item');
gridContainer.appendChild(gridItem);
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function display_board() {
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach((item) => {
item.innerText = '';
item.style.backgroundColor = '';
});
let positions = [];
while (positions.length < 30) {
let randomIndex = Math.floor(Math.random() * 81);
if (!positions.includes(randomIndex)) {
positions.push(randomIndex);
}
}
positions.forEach((index) => {
const i = Math.floor(index / 9);
const j = index % 9;
const gridItem = gridItems[index];
if (board[i][j] !== 0) {
gridItem.innerText = board[i][j];
}
});
}
let board = Array.from({ length: 9 }, () => Array(9).fill(0));
let row = Array.from({ length: 9 }, () => Array(10).fill(0));
let col = Array.from({ length: 9 }, () => Array(10).fill(0));
let diag = Array.from({ length: 9 }, () => Array(10).fill(0));
let end = false;
let gameStarted = false;
let selectedCell = null;
function board_init() {
let num_list = Array.from({ length: 9 }, (_, i) => (i + 1));
let nums = shuffle(num_list);
let idx = 0;
for (let i = 0; i < 9; i += 3) {
for (let j = 0; j < 9; j += 3) {
board[i][j] = nums[idx];
row[i][nums[idx]] = 1;
col[j][nums[idx]] = 1;
diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][nums[idx]] = 1;
idx += 1;
}
}
}
function fill_board(k) {
if (k >= 81) {
end = true;
display_board();
return;
}
let i = Math.floor(k / 9);
let j = k % 9;
if (board[i][j] !== 0) {
fill_board(k + 1);
return;
}
let num_list = Array.from({ length: 9 }, (_, i) => (i + 1));
let nums = shuffle(num_list);
for (let num of nums) {
if (!row[i][num] && !col[j][num] && !diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][num]) {
board[i][j] = num;
row[i][num] = 1;
col[j][num] = 1;
diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][num] = 1;
fill_board(k + 1);
if (end) return;
board[i][j] = 0;
row[i][num] = 0;
col[j][num] = 0;
diag[Math.floor(i / 3) * 3 + Math.floor(j / 3)][num] = 0;
}
}
}
function start_game() {
board_init();
fill_board(0);
gameStarted = true;
document.getElementById('StartButton').style.display = 'none';
document.getElementById('RestartButton').style.display = 'inline';
}
function restart_game() {
board = Array.from({ length: 9 }, () => Array(9).fill(0));
row = Array.from({ length: 9 }, () => Array(10).fill(0));
col = Array.from({ length: 9 }, () => Array(10).fill(0));
diag = Array.from({ length: 9 }, () => Array(10).fill(0));
end = false;
gameStarted = false;
selectedCell = null;
document.getElementById('StartButton').style.display = 'inline';
document.getElementById('RestartButton').style.display = 'none';
display_board();
}
document.getElementById('StartButton').addEventListener('click', start_game);
document.getElementById('RestartButton').addEventListener('click', restart_game);
display_board();