게임 개발

[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();