AI 탐구노트

한글 십자말풀이 게임 도구 만들기 본문

DIY 테스트

한글 십자말풀이 게임 도구 만들기

42morrow 2024. 11. 16. 10:44

 

1.시작 : Crossword 퍼즐 다시 생각하기

 

아무래도 지난 번 Crossword 퍼즐은 만드는 과정이 많이 복잡했던 것 같습니다. 자동화를 하려다보니 알고리즘도 다양한 부분에서 필요했고 기타 부가적인 기능도 많이 필요했던거죠. 

 

 

십자말풀이 게임 생성 테스트

1.개요어느 순간부터 우리는 퀴즈와 퍼즐에 매료되기 시작했습니다. 바쁜 일상 속에서 짧은 여유를 만끽하며 두뇌를 자극할 수 있는 활동은 큰 매력으로 다가옵니다. 그중에서도 크로스워드 퍼

42morrow.tistory.com

 

 

 

그래서, 이번에는 좀 쉽게 만들 수 있는 방법을 생각해 봤습니다. 다음과 같이 작동하도록 말이죠.

  1. 격자 그리드를 생성 (크기는 제공)
  2. 원하는 위치에 단어 수동 입력 -> 반복
  3. 기록된 단어에 대한 문제 작성 (수동이 기본. 향후 ChatGPT API 이용?)

 

향후에는 단어 입력을 수동에서 자동으로 변경할 수도 있을 겁니다. 그렇게 되면 단어 배치 패턴도 자동으로 생성될테니 자신이 원하는 모양으로 만드는 것은 어려울 텐데 대안으론 단어 선정과 배치를 수동으로 하거나 놓일 위치만 수동으로 지시하고 해당하는 길이의 단어는 자동으로 입력되도록 하는 방법이 가능하겠습니다. 

 

 

2.코드 만들기

2.1.요구사항

이번에 해 보고자 하는 것은 격자 그리드 상에 내가 원하는 위치에 수동으로 단어를 입력해서 크로스워드 퍼즐을 만들어 보는 것입니다. 가능하면 기능을 단순화하려고 했고, ChatGPT에게는 아래와 같은 요구사항을 제시했습니다. 제법 많은 시행착오가 있었습니다.

 

2.1.1. 화면 레이아웃

  • 상단에는 가로, 세로의 격자의 크기를 입력 받는 부분을 두고, 그리드 생성 버튼, 입력 완료 버튼이 있어야 합니다. 
  • 퍼즐 격자를 그리되 격자의 기본 배경색은 회색, 글자 배경은 흰색, 격자 구분선은 검정색입니다. 
  • 퍼즐의 아래 쪽에는 가로문제와 세로문제를 표시하는 텍스트 박스가 있습니다.
  • 문제 텍스트 박스 아래에 '퍼즐 저장' 버튼이 있습니다. 

2.1.2.동작 

  • 특정 격자에서 마우스 왼쪽 버튼으로 클릭하면 내용(단어, 가로/세로) 입력을 받는 창이 뜹니다. 
    예를 들어 '가로단어:h', '세로단어:v' 가 됩니다. 
  • 입력받은 내용을 격자들에 반영합니다.
    - 입력이 되면 바탕은 흰색, 글자는 검은색으로 표시합니다. 
    - 가로면 가로 방향으로 옮겨가며 격자에 단어의 글자를 채웁니다.
    - 세로면 세로방향으로 옮겨가며 격자에 단어의 글자를 채웁니다. 
  • 입력받은 내용은 브라우저의 local storage에 저장됩니다. 
    - 프로그램이 다시 시작되면 저장된 파일의 내용을 읽어와서 퍼즐 격자와 문제 박스의 내용을 채워야 합니다. 
    - 기존 단어가 있는 상태에서 변경을 하게 되면 local storage 상의 내용도 변경되어야 합니다. 
  • 입력 완료 버튼을 클릭하면 퍼즐 상의 텍스트는 사라지고, 해당 위치를 알려주는 숫자만 표시되며, 문제 텍스트 박스에는 문제 번호와 저장된 답이 표시됩니다. 
  • 최종 '퍼즐저장' 버튼이 클릭되면 퍼즐, 문제박스의 내용이 local storage에 기록되며, 퍼즐 상단의 입력창과 버튼들이 숨겨집니다. (최종 화면 출력을 위해) 그리고, 문제의 번호는 격자의 진행 방향 (왼쪽->오른쪽, 위->아래)에 맞춰 번호를 재설정해야 합니다. 


2.2.코드

위의 요구사항이 반영된 코드는 다음과 같습니다. 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>십자말풀기 게임</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      margin: 0;
      padding: 0;
    }
    h1 {
      text-align: center;
    }
    #grid-container {
      display: grid;
      grid-gap: 1px;
      background-color: transparent;
      margin: 20px;
    }
    .grid-item {
      width: 40px;
      height: 40px;
      background-color: #d3d3d3;
      text-align: center;
      line-height: 40px;
      font-size: 20px;
      border: 1px solid black;
      position: relative;
    }
    .filled {
      background-color: white;
      color: black;
    }
    .number {
      position: absolute;
      top: 2px;
      left: 2px;
      font-size: 12px;
    }
    #controls {
      display: flex;
      justify-content: center;
      gap: 10px;
      margin: 10px;
    }
    #questions-container {
      width: 80%;
      max-width: 600px;
    }
    textarea {
      width: 100%;
      height: 100px;
      resize: vertical;
    }
    #save-button {
      margin-top: 10px;
      display: block;
      width: 100%;
      padding: 10px;
      font-size: 16px;
    }
  </style>
</head>
<body>
  <h1>십자말풀기 게임</h1>
  <div id="controls">
    <label for="rows">Rows: </label>
    <input type="number" id="rows" min="1" max="50">
    <label for="columns">Columns: </label>
    <input type="number" id="columns" min="1" max="50">
    <button onclick="createGrid()">Create Grid</button>
    <button onclick="completeInput()">Complete Input</button>
  </div>
  <div id="grid-container"></div>
  <div id="questions-container">
    <h3>가로 문제</h3>
    <textarea id="horizontal-questions"></textarea>
    <h3>세로 문제</h3>
    <textarea id="vertical-questions"></textarea>
    <button id="save-button" onclick="savePuzzle()">퍼즐저장</button>
  </div>

  <script>
    let wordList = [];

    document.addEventListener('DOMContentLoaded', loadSavedData);

    function createGrid() {
      const rows = document.getElementById("rows").value;
      const columns = document.getElementById("columns").value;
      const container = document.getElementById("grid-container");
      container.style.gridTemplateColumns = `repeat(${columns}, 40px)`;
      container.style.gridTemplateRows = `repeat(${rows}, 40px)`;
      container.innerHTML = "";

      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < columns; c++) {
          const cell = document.createElement("div");
          cell.classList.add("grid-item");
          cell.dataset.row = r;
          cell.dataset.column = c;
          cell.addEventListener("click", () => onCellClick(cell));
          container.appendChild(cell);
        }
      }

      fillGridFromWordList();
    }

    function onCellClick(cell) {
      const input = prompt("Enter a word and direction (e.g., 'word:h' for horizontal, 'word:v' for vertical):");
      if (!input) return;
      const [word, dir] = input.split(":");
      const direction = dir === 'h' ? 'horizontal' : dir === 'v' ? 'vertical' : null;
      if (!word || !direction) return;

      const startRow = parseInt(cell.dataset.row);
      const startCol = parseInt(cell.dataset.column);

      for (let i = 0; i < word.length; i++) {
        const currentCell = direction === "horizontal" 
          ? document.querySelector(`[data-row='${startRow}'][data-column='${startCol + i}']`)
          : document.querySelector(`[data-row='${startRow + i}'][data-column='${startCol}']`);
        if (currentCell) {
          currentCell.textContent = word[i];
          currentCell.classList.add("filled");
        }
      }

      if (checkOverlap(word, direction, startRow, startCol)) {
        alert("The word overlaps with an existing word.");
        return;
      }

      wordList.push({ word, direction, startRow, startCol });
      saveData();
    }

    function checkOverlap(word, direction, startRow, startCol) {
      return wordList.some(({ direction: dir, startRow: sr, startCol: sc, word: w }) => {
        if (dir !== direction) return false;
        if (direction === "horizontal") {
          return sr === startRow && sc <= startCol + word.length - 1 && sc + w.length - 1 >= startCol;
        }
        return sc === startCol && sr <= startRow + word.length - 1 && sr + w.length - 1 >= startRow;
      });
    }

    function saveData() {
      localStorage.setItem("crosswordData", JSON.stringify(wordList));
      localStorage.setItem("horizontalQuestions", document.getElementById("horizontal-questions").value);
      localStorage.setItem("verticalQuestions", document.getElementById("vertical-questions").value);
    }

    function loadSavedData() {
      wordList = JSON.parse(localStorage.getItem("crosswordData")) || [];
      createGrid();
      document.getElementById("horizontal-questions").value = localStorage.getItem("horizontalQuestions") || "";
      document.getElementById("vertical-questions").value = localStorage.getItem("verticalQuestions") || "";
    }

    function completeInput() {
      const horizontalQuestions = new Map();
      const verticalQuestions = new Map();

      // Assign numbers to words based on their position in the grid
      wordList.sort((a, b) => {
        if (a.startRow === b.startRow) {
          return a.startCol - b.startCol;
        }
        return a.startRow - b.startRow;
      });

      wordList.forEach(({ word, direction, startRow, startCol }, index) => {
        const cell = document.querySelector(`[data-row='${startRow}'][data-column='${startCol}']`);
        if (cell) {
          cell.innerHTML = `<span class='number'>${index + 1}</span>`;
        }
        if (direction === 'horizontal') {
          horizontalQuestions.set(word, index + 1);
        } else if (direction === 'vertical') {
          verticalQuestions.set(word, index + 1);
        }
      });

      // Update question text areas without duplication
      document.getElementById('horizontal-questions').value = '';
      document.getElementById('vertical-questions').value = '';
      horizontalQuestions.forEach((number, word) => {
        document.getElementById('horizontal-questions').value += `${number}. ${word}\n`;
      });
      verticalQuestions.forEach((number, word) => {
        document.getElementById('vertical-questions').value += `${number}. ${word}\n`;
      });

      // Clear all letter content except numbered cells
      document.querySelectorAll('.grid-item').forEach(cell => {
        if (!cell.querySelector('.number')) {
          cell.textContent = '';
        }
      });
    }

    function fillGridFromWordList() {
      wordList.forEach(({ word, direction, startRow, startCol }) => {
        for (let i = 0; i < word.length; i++) {
          const currentCell = direction === "horizontal" 
            ? document.querySelector(`[data-row='${startRow}'][data-column='${startCol + i}']`)
            : document.querySelector(`[data-row='${startRow + i}'][data-column='${startCol}']`);
          if (currentCell) {
            currentCell.textContent = word[i];
            currentCell.classList.add("filled");
          }
        }
      });
    }

    function savePuzzle() {
      saveData();
      document.getElementById("controls").style.display = "none";
    }
  </script>
</body>
</html>

 

3.실행 - 퍼즐 상에 단어 입력

일단 외관상으로는 요구했던 사항이 반영된 것 같습니다. 아직도 버그나 구현되지 않은 세세한 것들이 수면 아래 숨어 있는데 그런 것은 언급하지 않고 그냥 넘어갑니다. ^^;

 

맨 처음 격자 크기를 지정한 격자를 생성합니다. 그리고 난 후 원하는 위치에 단어를 입력합니다. 아래는 예시입니다. 

 

 

입력이 다 완료되면 'Complete Input'을 실행합니다. 다음과 같이 나오죠. 

 

 

 

 

 

참고) 

엥? html인데 로컬에 파일로 저장된다고? 보안 상 그게 문제가 없나 하시겠지만, 실제 여기서 말하는 local storage는 브라우저가 사용하는 key/value 형태의 저장소입니다. 강제로 날려 버리지 않으면 브라우저 종료 후 다시 로딩해도 해당 key값을 이용해 다시 불러올 수 있습니다. 클릭해서 입력받은 내용이 어떻게 저장되는지는 크롬 브라우저의 개발자 도구에서 확인 가능합니다.

개발자도구 > Application 탭 > Storage / Local Storage 상에 'crosswordData'라는 키로 저장

그림 : 브라우저의 캐시 역할을 하는 local Storage에 저장된 데이터

 

4.실행 - ChatGPT로 문제 출제

가로, 세로 문제의 정답을 알고 있으니 ChatGPT에게 이 정답에 대한 문제를 제출해 달라고 할 수 있습니다. 각각 그렇게 진행했고 다음과 같이 결과를 받을 수 있습니다. 

 

 

이 둘을 결합한 최종 결과는 다음과 같습니다. 

 

그림 : 단어 배치, 정답을 이용한 문제 적용까지 완료한 예시

 

 

5.후기

지난 번 십자말풀이를 자동화 코드로 테스트 한 이후에 편의성에 대한 약간의 갈증이 있었는데 해소된 것 같습니다. 사실, 이렇게 만들거면 그냥 PPT나 워드 같은데서 만들면 되는데 왜 굳이 이렇게 해야 하나 싶기도 합니다. 여기에 대해서는, 현재까지 되어 있는 것 가운데 부분부분을 자동화 코드로 변경만 하면 훨씬 더 손쉽게 만들 수 있는 도구로 발전할 수 있다는 가능성으로 변명을 대신 해야할 것 같습니다.