見出し画像

kintoneで初めてのゲーム作成に挫折した話

この記事は、「すごくない」kintone その3 Advent Calendar 2024 20日目の記事です。

「すごくない」kintone Advent Calendar 2024はこちらです。

「すごくない」kintone その2 Advent Calendar 2024はこちらです。


ゲーム作りを取り入れればkintoneに取り組みやすくなるのでは

今年の夏にLT登壇してから、kintoneもエンジニアとして挑戦していこうと思いました。しかし、業務で担当している訳でもないのでなかなか時間を割くことが出来ていなかったです。
今回のAdvent Calendar募集を見て、kintoneでミニゲーム作成に挑戦できれば少しは身に着ける事ができるのではと思い、テーマ選定して参加しました。

初めてのゲーム

初めてなので〇×ゲームなら、少ない日数でも取り組めるのではないかと思いました。
・スペースをボタンとして扱う事。
・文字列(複数行)でメッセージを表示できる事。

必要最小限のkintoneのUIでミニゲームが実現できるはずだと。


kintoneで〇×ゲームを作成しようと始めた所


スペースが消える・・・

「レコードを追加する」で文字列(複数行)は表示されるのに、ボタンと見立てた複数のスペースが消えてしまう。遅々として進みませんでした。

挫折

きっと忙しいだろうと12月下旬に担当日を設定したのですが、なかなか思うように時間がさけずに今日を迎えてしまいました。

魔が差して手を出してしまった、生成AI

ChatGPT先生に「kintoneのJavaScriptカスタマイズで、〇×ゲームを作ってください。」と聞いてみました。

(function () {
  "use strict";

  kintone.events.on("app.record.index.show", function (event) {
    // 既存のビュー内容をクリア
    const spaceElement = kintone.app.getHeaderSpaceElement();
    if (!spaceElement) return;

    spaceElement.innerHTML = "";

    // ゲームボードの作成
    const gameContainer = document.createElement("div");
    gameContainer.style.display = "grid";
    gameContainer.style.gridTemplateColumns = "repeat(3, 100px)";
    gameContainer.style.gridGap = "5px";
    gameContainer.style.margin = "20px auto";
    gameContainer.style.width = "max-content";

    // ゲームの状態管理
    let board = Array(9).fill(null);
    let isXTurn = true;

    // 勝利判定
    function checkWinner() {
      const winningCombinations = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
      ];

      for (const [a, b, c] of winningCombinations) {
        if (board[a] && board[a] === board[b] && board[a] === board[c]) {
          return board[a];
        }
      }
      return board.includes(null) ? null : "Draw";
    }

    // マス目をクリックしたときの動作
    function handleClick(index, cell) {
      if (board[index] || checkWinner()) return;

      board[index] = isXTurn ? "X" : "O";
      cell.textContent = board[index];
      isXTurn = !isXTurn;

      const winner = checkWinner();
      if (winner) {
        setTimeout(() => {
          alert(winner === "Draw" ? "引き分けです!" : `${winner}の勝ち!`);
          resetGame();
        }, 100);
      }
    }

    // ゲームリセット
    function resetGame() {
      board = Array(9).fill(null);
      isXTurn = true;
      gameContainer.querySelectorAll(".cell").forEach((cell) => {
        cell.textContent = "";
      });
    }

    // マス目の作成
    for (let i = 0; i < 9; i++) {
      const cell = document.createElement("div");
      cell.className = "cell";
      cell.style.width = "100px";
      cell.style.height = "100px";
      cell.style.border = "1px solid #000";
      cell.style.display = "flex";
      cell.style.alignItems = "center";
      cell.style.justifyContent = "center";
      cell.style.fontSize = "24px";
      cell.style.cursor = "pointer";

      cell.addEventListener("click", () => handleClick(i, cell));
      gameContainer.appendChild(cell);
    }

    // ゲームコンテナをヘッダースペースに追加
    spaceElement.appendChild(gameContainer);

    return event;
  });
})();

人間が〇と×の両方を担当するので、プロンプトに追加しました。

人間が〇で、コンピューターが×を選択していく、〇×ゲームに変更してください。コンピューターの賢さは3段階からゲーム開始時に選べるようにしてください。

(function () {
  "use strict";

  kintone.events.on("app.record.index.show", function (event) {
    // ヘッダースペースの初期化
    const spaceElement = kintone.app.getHeaderSpaceElement();
    if (!spaceElement) return;
    spaceElement.innerHTML = "";

    // コンピューターの賢さを選択するUI
    const difficultyContainer = document.createElement("div");
    difficultyContainer.style.marginBottom = "20px";
    difficultyContainer.innerHTML = `
      <label>コンピューターの賢さ: 
        <select id="difficulty">
          <option value="easy">低</option>
          <option value="medium">中</option>
          <option value="hard">高</option>
        </select>
      </label>
    `;
    spaceElement.appendChild(difficultyContainer);

    // ゲームボードの作成
    const gameContainer = document.createElement("div");
    gameContainer.style.display = "grid";
    gameContainer.style.gridTemplateColumns = "repeat(3, 100px)";
    gameContainer.style.gridGap = "5px";
    gameContainer.style.margin = "0 auto";
    gameContainer.style.width = "max-content";

    // ゲームの状態管理
    let board = Array(9).fill(null);
    let isGameActive = true;

    // 勝利判定
    function checkWinner() {
      const winningCombinations = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
      ];

      for (const [a, b, c] of winningCombinations) {
        if (board[a] && board[a] === board[b] && board[a] === board[c]) {
          return board[a];
        }
      }
      return board.includes(null) ? null : "Draw";
    }

    // コンピューターの手を決定
    function computerMove() {
      const difficulty = document.getElementById("difficulty").value;

      // ランダム選択(低レベル)
      if (difficulty === "easy") {
        let emptyCells = board.map((value, index) => (value === null ? index : null)).filter(v => v !== null);
        return emptyCells[Math.floor(Math.random() * emptyCells.length)];
      }

      // 勝利/ブロックを考慮(中レベル)
      if (difficulty === "medium") {
        const winningCombinations = [
          [0, 1, 2],
          [3, 4, 5],
          [6, 7, 8],
          [0, 3, 6],
          [1, 4, 7],
          [2, 5, 8],
          [0, 4, 8],
          [2, 4, 6],
        ];

        // 勝てる手を探す
        for (const [a, b, c] of winningCombinations) {
          if (board[a] === "X" && board[b] === "X" && board[c] === null) return c;
          if (board[a] === "X" && board[c] === "X" && board[b] === null) return b;
          if (board[b] === "X" && board[c] === "X" && board[a] === null) return a;
        }

        // ブロックすべき手を探す
        for (const [a, b, c] of winningCombinations) {
          if (board[a] === "O" && board[b] === "O" && board[c] === null) return c;
          if (board[a] === "O" && board[c] === "O" && board[b] === null) return b;
          if (board[b] === "O" && board[c] === "O" && board[a] === null) return a;
        }

        // ランダム選択
        let emptyCells = board.map((value, index) => (value === null ? index : null)).filter(v => v !== null);
        return emptyCells[Math.floor(Math.random() * emptyCells.length)];
      }

      // 最適解選択(高レベル)
      if (difficulty === "hard") {
        function minimax(board, isMaximizing) {
          const winner = checkWinner();
          if (winner === "X") return { score: 10 };
          if (winner === "O") return { score: -10 };
          if (winner === "Draw") return { score: 0 };

          let bestMove = isMaximizing ? { score: -Infinity } : { score: Infinity };

          board.forEach((value, index) => {
            if (value === null) {
              board[index] = isMaximizing ? "X" : "O";
              const result = minimax(board, !isMaximizing);
              board[index] = null;

              if (isMaximizing) {
                if (result.score > bestMove.score) {
                  bestMove = { score: result.score, index };
                }
              } else {
                if (result.score < bestMove.score) {
                  bestMove = { score: result.score, index };
                }
              }
            }
          });

          return bestMove;
        }

        return minimax(board, true).index;
      }
    }

    // マス目をクリックしたときの動作
    function handleClick(index, cell) {
      if (!isGameActive || board[index]) return;

      // 人間の手
      board[index] = "O";
      cell.textContent = "O";

      const winner = checkWinner();
      if (winner) return endGame(winner);

      // コンピューターの手
      const computerIndex = computerMove();
      board[computerIndex] = "X";
      const computerCell = gameContainer.children[computerIndex];
      computerCell.textContent = "X";

      const winnerAfterComputer = checkWinner();
      if (winnerAfterComputer) endGame(winnerAfterComputer);
    }

    // ゲーム終了
    function endGame(winner) {
      isGameActive = false;
      setTimeout(() => {
        alert(winner === "Draw" ? "引き分けです!" : `${winner}の勝ち!`);
        resetGame();
      }, 100);
    }

    // ゲームリセット
    function resetGame() {
      board = Array(9).fill(null);
      isGameActive = true;
      gameContainer.querySelectorAll(".cell").forEach((cell) => (cell.textContent = ""));
    }

    // マス目の作成
    for (let i = 0; i < 9; i++) {
      const cell = document.createElement("div");
      cell.className = "cell";
      cell.style.width = "100px";
      cell.style.height = "100px";
      cell.style.border = "1px solid #000";
      cell.style.display = "flex";
      cell.style.alignItems = "center";
      cell.style.justifyContent = "center";
      cell.style.fontSize = "24px";
      cell.style.cursor = "pointer";

      cell.addEventListener("click", () => handleClick(i, cell));
      gameContainer.appendChild(cell);
    }

    // ゲームコンテナを追加
    spaceElement.appendChild(gameContainer);

    return event;
  });
})();



えぇ、スペースは使わないの!?

以下は、人間が「〇」を選び、コンピューターが「×」を選択する〇×ゲームの実装です。ゲーム開始時にコンピューターの賢さを3段階で選択できるように変更しました。
概要
賢さレベル
低レベル: ランダムで選択。
中レベル: 勝利条件を少し考慮する。
高レベル: ミニマックスアルゴリズムで最適解を選択。
ゲームフロー
人間がマスをクリックすると「〇」が記入されます。
コンピューターが「×」を選びます(賢さに基づくアルゴリズムで選択)。
勝敗が決定するとメッセージを表示し、ゲームをリセットします。

ChatGPT


勝敗判定もちゃんとしてくれる


kintoneの記事というより生成AIの話になってしまいました。
ChatGPT先生の出力してくれたソースコードを読み込んで、理解を深めたいと思います。完敗です。

この記事は以上です。

いいなと思ったら応援しよう!