見出し画像

【作成編】コードを一行も書かずに、LINE上でChatGPTと対戦できるリバーシを作ってみた

前回の記事でLINE上でChatGPTと対戦できるリバーシの紹介をしました。

実際に対戦してみたい方は以下のQRコードから読み取って、友達追加してみてください。

AIリバーシちゃん

その記事の最後で説明したように、今回は【作成編】ということで、実際の作り方を紹介します!


・事前に準備が必要なもの

前回の記事でも紹介しましたが、準備が必要なものは以下の通りです。

  1. Open AI API:LINEからのメッセージをChatGPTが受け取るために必要(その逆も)(初回登録時に無料枠があるが基本的には有料

  2. Google Apps Script(とGoogleスプレッドシート):LINEからのメッセージを受け取り、リバーシの盤面を作成、更新、保存したのち、ChatGPTに送るためのメッセージを編集するプログラミングツール(基本無料で使用可能

  3. LINE Messaging API:LINEを使ってChatGPTにメッセージを送るために必要(その逆も)(基本無料で使用可能

今回は、これらの準備方法から紹介します。

まず、Open AI のAPIキーを取得します。

1. ChatGPT側の準備(登録からAPI keyの取得まで)【無料特典あり!】

はじめに、ChatGPT側の準備を行なっていきます。
ChatGPTのアカウントを既に持っている方であったとしても、別の画面からログインする必要があるので注意が必要です。
(APIとは・・・異なるソフトウェアやプログラムを連携させる機能。そのためには連携させたいソフトフェアのAPI keyを取得する必要がある。)
まず、ChatGPTのホームページに移動し、画面上側にある「Products」をクリックします。

APIの項目から、「API login」をクリックします。

すると、通常のログイン画面に移るのでChatGPTアカウントを既に持っている方はここでログインします。(持っていない方はアカウント作成から行います)

ログインに成功すると、ChatGPTかAPIか選択させられるので、APIをクリックします。

はじめは下のようなチュートリアル画面が出るため、「Get started」をクリック

画面左側の一覧にある「API keys」をクリック

「Create new secret key」をクリックし新しいAPI keyを作成します。

すると、長い文字列のAPI keyが作成されるので、Copyをクリックして保存しておきます。
【注意!】
1. API keyは作成時の画面しか確認できません。なので忘れたりしてしまうとそのkeyは二度と使えなくなるため、ちゃんとメモをしておきましょう。
2. 他人にkeyが知られてしまうと簡単に悪用できてしまうため、管理には細心の注意が必要です。

API keyが作成できましたが、まだ使用できません。
支払い方法を設定する必要があります。
【無料特典あり!】
ChatGPTの新規登録ユーザーには登録から3ヶ月だけ使える5ドル分の無料枠があります!(私は存在を知らず、期限切れでした・・・)
無料で使えるのでぜひ使いましょう!

支払い方法を設定は右上から設定画面をクリック(1)
左側にあるBillingをクリック(2)
はじめはFree trialになっています(3)
「Add payment details」をクリック(4)

個人か企業か聞かれるので、個人開発の場合は「Individual」を選択

支払い方法を設定していきます。

設定が完了するといくら分の枠を購入するか確認されるので、はじめは最低金額の「5ドル」にしておきます。(1)
枠が一定金額を下回った場合に自動で追加分を購入するか確認されますが、開発中はOFFで問題ないかと思います。(2)

購入が完了すると、下のように購入した金額が表示されます。

以上で、ChatGPT側でAPI keyを使用して、連携する準備が整いました!

2. LINE Messaging APIの取得

続いて、LINE MessagingのAPIキーを取得します。

初めに、LINE Business IDが必要になるため、新しくアカウントを作成します。

以下のリンクからLINE Business IDの作成をしていきます。

初めにログイン方法を確認されるので、「アカウントを作成」をクリックします。

「アカウントを作成」をクリック

次に、登録方法を求められるので、「メールアドレスで登録」を選択します。

開発者名、メールアドレスを入力してアカウントを作成すると、登録したメールアドレスに以下の登録用リンクが届くので登録画面に進んでいきます。

【注意】2024/9/3より2段階認証が必要となりました。
ログインする際に登録したメールアドレスにログインコードが届くので、届いたコードを入力してください。

アカウントのトップページで新規プロバイダーの作成を求められるので、
新規プロバイダー作成」をクリックします。

プロバイダー名を入力し、作成をクリックします。

Messaging API」を選択します。

2024/9/4から「Messaging API」を使用するためには、LINE公式アカウントの作成が必要になりましたので、「LINE公式アカウントを作成する」をクリックします。

SMS認証が必要のため、携帯番号を入力します。

すると、公式アカウントの作成ができますので、必要情報を入力して、アカウントの作成を行います。

以下のようにチャネルの作成ができたら、OKです。

作成したチャネルから「Messaging API設定」を選択し、一番下にある、「チャネルアクセストークン」を発行します。

ここで発行した「チャネルアクセストークン」も「Open AI API」と同じく、覚えておくようにしましょう。(管理には注意が必要なのも同じです)

3. Google Apps Script(GAS)でコードの編集

次に、Googleスプレッドシートの拡張機能であるGoogle Apps Script(通称:GAS)を開きます。

Googleスプレッドシートを新しく作成し、拡張機能タブApps Scriptをクリックします。(シートは無編集で構いません)

以下のような画面になるので、自由に名称を変更する(以下のスクショで「人口統計」になっているところ)

コード.gs」の部分が編集できる状態になっていれば準備は完了です。

・ChatGPTでのコード作成

ここまで準備ができたら、実際にプログラムコードを作成していきます。

・・・と言っても、私自身はコードが書けませんので、ChatGPTにお願いして書いてもらいます。

一番最初のプロンプトはこう⬇︎

あなたはプロのプログラマーです。LINE API、Google Apps Script、ChatGPT APIを使ってChatGPTのチャット機能を有したオセロゲームを作りたいです。LINE APIおよびChatGPT APIのキーは取得していることを前提として、作成方法、必要であればコードを作成してください

ChatGPTにした一番最初の質問

で、出てきたコードがこれ⬇︎

const LINE_CHANNEL_ACCESS_TOKEN = 'YOUR_LINE_CHANNEL_ACCESS_TOKEN';
const CHATGPT_API_KEY = 'YOUR_CHATGPT_API_KEY';
const CHATGPT_API_URL = 'https://api.openai.com/v1/chat/completions';

// Webhookハンドラ
function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const userMessage = json.events[0].message.text;
  const userId = json.events[0].source.userId;

  // オセロのゲーム状態を取得(必要に応じてスプレッドシートなどを使用)
  const gameState = getGameState(userId);
  
  // ChatGPT APIを呼び出して次の手を決定
  const chatGPTResponse = callChatGPT(userMessage, gameState);

  // オセロゲームの進行
  const updatedGameState = updateGameState(userMessage, gameState, chatGPTResponse);

  // 更新されたゲーム状態を保存
  saveGameState(userId, updatedGameState);

  // LINEにメッセージを送信
  replyToLine(json, updatedGameState);
}

// ChatGPT APIを呼び出す関数
function callChatGPT(userMessage, gameState) {
  const data = {
    model: "gpt-4",
    messages: [
      {role: "system", content: "あなたはオセロゲームのアシスタントです。"},
      {role: "user", content: `現在のゲーム状態: ${JSON.stringify(gameState)}\nユーザーの入力: ${userMessage}`}
    ],
    max_tokens: 50
  };
  
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: `Bearer ${CHATGPT_API_KEY}`
    },
    payload: JSON.stringify(data)
  };
  
  const response = UrlFetchApp.fetch(CHATGPT_API_URL, options);
  const jsonResponse = JSON.parse(response.getContentText());
  return jsonResponse.choices[0].message.content.trim();
}

// ゲーム状態を更新する関数
function updateGameState(userMessage, gameState, chatGPTResponse) {
  // ここにオセロゲームのロジックを実装
  // ユーザーとChatGPTの入力に基づいてゲームを進行させる
  return updatedGameState;
}

// ゲーム状態を取得する関数
function getGameState(userId) {
  // ここでユーザーごとのゲーム状態を取得
  // 例えばスプレッドシートやFirebaseを使用
  return {};
}

// ゲーム状態を保存する関数
function saveGameState(userId, gameState) {
  // ここでユーザーごとのゲーム状態を保存
}

// LINEにメッセージを返信する関数
function replyToLine(json, message) {
  const replyToken = json.events[0].replyToken;
  const url = 'https://api.line.me/v2/bot/message/reply';

  const payload = JSON.stringify({
    replyToken: replyToken,
    messages: [{
      type: 'text',
      text: message
    }]
  });

  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${LINE_CHANNEL_ACCESS_TOKEN}`
    },
    payload: payload
  };

  UrlFetchApp.fetch(url, options);
}

しかし、そのまま貼り付けるのではなく、さらに改良を加えてもらいます。

プロンプトがこれ⬇︎

この出力を60点とします。100点の出力にするために足りないものを列挙した上で、100点の出力はどのようなものか出力してください。

60点プロンプト

俗に言う、60点プロンプトというやつです。

・ChatGPT作成コードをGoogle Apps Scriptに貼り付け

ChatGPTに作成してもらったコードをそのままコピーして、作成しておいたGoogle Apps Scriptに貼り付けます。

その後、「プロジェクトの保存」→「デプロイ」→「新しいデプロイ」を順に押していきます。

新しいデプロイ」の画面で「ウェブアプリ」を選択します。

以下の3つを設定し、「デプロイ」を押す。
・説明 → アプリの名称
・次のユーザーとして実行 → 自分
・アクセスできるユーザー → 全員

すると、承認画面が現れるので、以下に従って承認していく。(画面は英語です。Googleアカウントの初期設定によって、日本語の場合もありますが、クリックする位置は同じ)

①アクセスを承認
②Advanced(詳細)→ ③Apps Scriptの名称部分をクリック
④Allow(承認)ボタンを押す
⑤ウェブアプリのURLが作成されれば完了!

最終的にWebアプリのURLが表示され、デプロイが完了します。(上の画面のようになっていたらOK!)

このウェブアプリのURLをコピーします。

・ウェブアプリのURLを「Messaging API」のWebhook URLに貼り付け

次に、LINEの「Messaging API」に戻り、Messaging API設定に移ります。

Webhook URLにコピーしたウェブアプリのURLを貼り付けて更新を押します。

以上で、Messaging APIからApps Scriptを呼び出して動かすことができるようになりました。

・実際に公式アカウントを友達登録して試してみる

Apps Scriptを動かす準備ができましたので、実際に作成した公式アカウントを友達登録して、期待通りに動くか試してみます。

Messaging API設定にあるQRコードから友達登録をします。

最後に自分で友達登録してみて、動きを確認します。

以上で作成は完了です!

でも・・・

・本当に大変なのはここから

残念ながら、これだけでうまくいくことはほぼありません。

ここからが戦いです。

やることはもうこれだけ⬇️

ChatGPTに質問して、
書いてもらったコードをGASに貼り付けて、
デプロイして、
得られたURLをWebhook URLに貼り付けて、
実際に動くかやってみて、
うまくいかないところをChatGPTに報告して、
またコードを書いてもらって・・・

これを、ひたすら、繰り返すのみ。

この間、私はコードを一行も書いていません。

しかし、自分の納得のいくまで繰り返します。

私の場合、最終的なコードは以下になりました。

デプロイの試行回数

・コードの試行回数 62回(バージョン62)⬇︎

function getLineChannelAccessToken() {
  const scriptProperties = PropertiesService.getScriptProperties();
  return scriptProperties.getProperty('LINE_CHANNEL_ACCESS_TOKEN');
}

function getChatGptApiKey() {
  const scriptProperties = PropertiesService.getScriptProperties();
  return scriptProperties.getProperty('CHATGPT_API_KEY');
}

// 取得した値を使用する
const LINE_CHANNEL_ACCESS_TOKEN = getLineChannelAccessToken();
const CHATGPT_API_KEY = getChatGptApiKey();
const CHATGPT_API_URL = 'https://api.openai.com/v1/chat/completions';

function doPost(e) {
  try {
    const json = JSON.parse(e.postData.contents);
    const userMessage = json.events[0].message.text.trim();
    const userId = json.events[0].source.userId;
    let gameState = getGameState(userId) || initializeGameState();

    if (userMessage === "開始") {
      gameState = initializeGameState();
      saveGameState(userId, gameState);
      const flexMessage = createBoardFlexMessage(gameState);
      replyWithFlexMessage(json, flexMessage, "ゲームを開始します!\nあなたは赤です。");
      return;
    }

    const upperCaseMessage = userMessage.toUpperCase();
    if (upperCaseMessage === "パス") {
      gameState.currentPlayer = 'B';  // ユーザーの手番をパスしてChatGPTの手番に
      saveGameState(userId, gameState);

      // ChatGPTの手番処理
      try {
        const chatGPTResponse = callChatGPT(gameState);
        updateBoard(chatGPTResponse.move, gameState);
        const chatGPTBoardMessage = `私の番です。${chatGPTResponse.move}に青を打ちました。${chatGPTResponse.isRandomMove ? '(ランダムで選ばれた手)' : '(ChatGPTが選んだ手)'}`;

        if (isGameOver(gameState)) {
          const winner = determineWinner(gameState);
          const finalBoardFlexMessage = createFinalBoardFlexMessage(gameState);
          replyWithFinalBoardAndMessage(json, finalBoardFlexMessage, `${winner === '勝者: 赤' ? 'おめでとうございます!あなたの勝ちです。' : winner === '勝者: 青' ? '残念ですが、あなたの負けです。再挑戦お待ちしてます!' : '引き分けです。'}`);
          resetGameState(userId);
          return;
        }

        const flexMessage = createBoardFlexMessage(gameState);
        replyWithFlexMessage(json, flexMessage, chatGPTBoardMessage);
      } catch (error) {
        console.error(error);
        if (error.message === "有効な手がありません。") {
          gameState.currentPlayer = 'R';  // 再びユーザーの手番に
          saveGameState(userId, gameState);
          replyToLine(json, "有効な手がないため、ChatGPTはパスします。あなたの番です。");
          const flexMessage = createBoardFlexMessage(gameState);
          replyWithFlexMessage(json, flexMessage);
        } else {
          replyToLine(json, "エラーが発生しました。ゲームを終了します。");
          resetGameState(userId);
        }
        return;
      }
      saveGameState(userId, gameState);
      return;
    }

    if (!isValidMove(upperCaseMessage, gameState)) {
      replyToLine(json, "無効な手です。再度お試しください。");
      return;
    }

    updateBoard(upperCaseMessage, gameState);

    if (isGameOver(gameState)) {
      const winner = determineWinner(gameState);
      const finalBoardFlexMessage = createFinalBoardFlexMessage(gameState);
      replyWithFinalBoardAndMessage(json, finalBoardFlexMessage, `${winner === '勝者: 赤' ? 'おめでとうございます!あなたの勝ちです。' : winner === '勝者: 青' ? '残念ですが、あなたの負けです。再挑戦お待ちしてます!' : '引き分けです。'}`);
      resetGameState(userId);
      return;
    }

    // ChatGPTの手番(常に青)
    try {
      const chatGPTResponse = callChatGPT(gameState);  // ChatGPTは常に青
      updateBoard(chatGPTResponse.move, gameState);
      const chatGPTBoardMessage = `私の番です。${chatGPTResponse.move}に青を打ちました。${chatGPTResponse.isRandomMove ? '(ランダムで選ばれた手)' : '(ChatGPTが選んだ手)'}`;

      if (isGameOver(gameState)) {
        const winner = determineWinner(gameState);
        const finalBoardFlexMessage = createFinalBoardFlexMessage(gameState);
        replyWithFinalBoardAndMessage(json, finalBoardFlexMessage, `${winner === '勝者: 赤' ? 'おめでとうございます!あなたの勝ちです。' : winner === '勝者: 青' ? '残念ですが、あなたの負けです。再挑戦お待ちしてます!' : '引き分けです。'}`);
        resetGameState(userId);
        return;
      }

      const flexMessage = createBoardFlexMessage(gameState);
      replyWithFlexMessage(json, flexMessage, chatGPTBoardMessage);
    } catch (error) {
      console.error(error);
      if (error.message === "有効な手がありません。") {
        gameState.currentPlayer = 'R';  // ユーザーの手番は常に赤
        saveGameState(userId, gameState);
        replyToLine(json, "有効な手がないため、ChatGPTはパスします。あなたの番です。");
        const flexMessage = createBoardFlexMessage(gameState);
        replyWithFlexMessage(json, flexMessage);
      } else {
        replyToLine(json, "エラーが発生しました。ゲームを終了します。");
        resetGameState(userId);
      }
      return;
    }
    saveGameState(userId, gameState);

  } catch (error) {
    console.error(error);
    replyToLine(json, "エラーが発生しました。もう一度お試しください。");
  }
}

function callChatGPT(gameState) {
  const validMoves = getValidMoves(gameState);
  if (validMoves.length === 0) {
    throw new Error("有効な手がありません。");
  }

  const boardString = getLabeledBoardString(gameState);

  const data = {
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `
        あなたはリバーシ(オセロ)ゲームの青(B)側のプレイヤーです。8x8のボードで、現在の盤面が与えられます。
        ゲームのルールに従って、最も戦略的に有利な手を選んでください。以下の点を考慮してください:
        1. 可能であれば、端や角に石を置くことを優先してください。
        2. 相手の石をできるだけ多く反転させる手を選んでください。
        3. 将来的に自分の石が多く反転されないよう、次の手を計画してください。
        回答は必ず "最適な手は [座標] です。" という形式で、[座標]の部分を実際の座標(例:D3)に置き換えて返してください。
        有効な座標は A1 から H8 の範囲内です。
        `
      },
      { 
        role: "user", 
        content: `現在の盤面は以下の通りです。青(B)の手番として、最適な手を選んでください:

        ${boardString}
        
        有効な手は以下の通りです:${validMoves.join(', ')}
        `
      }
    ],
    max_tokens: 50
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: `Bearer ${CHATGPT_API_KEY}`
    },
    payload: JSON.stringify(data)
  };

  let attempts = 0;
  const maxAttempts = 3;

  while (attempts < maxAttempts) {
    try {
      const response = UrlFetchApp.fetch(CHATGPT_API_URL, options);
      const jsonResponse = JSON.parse(response.getContentText());
      const content = jsonResponse.choices[0].message.content.trim();

      // 正規表現で座標を抽出
      const moveMatch = content.match(/[A-H][1-8]/);
      if (moveMatch) {
        const move = moveMatch[0];

        // 再度、有効な手か確認
        if (validMoves.includes(move) && isValidMove(move, gameState)) {
          console.log(`ChatGPTが選んだ手: ${move}`);
          return { move, isRandomMove: false };
        }
      }

      // 無効な手の場合、再度試行
      attempts++;
      data.messages.push({ role: "assistant", content: content });
      data.messages.push({ role: "user", content: `その手は無効です。有効な手(${validMoves.join(', ')})から選んでください。` });
    } catch (error) {
      console.error("APIリクエストエラー:", error);
      attempts++;
    }
  }

  // 最大試行回数を超えた場合、ランダムに有効な手を選択
  const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)];
  console.log(`ランダムに選ばれた手: ${randomMove}`);
  return { move: randomMove, isRandomMove: true };
}

function isValidMove(move, gameState) {
  // 座標形式が正しいか確認
  if (!/^[A-H][1-8]$/.test(move)) {
    return false;
  }

  const col = move.charCodeAt(0) - 'A'.charCodeAt(0);
  const row = parseInt(move[1]) - 1;

  // 盤上での有効性を確認
  return isValidMoveDirection(row, col, gameState);
}

function isValidMoveDirection(row, col, gameState) {
  // 既に石が置かれている場合は無効
  if (gameState.board[row][col] !== "") {
    return false;
  }

  const directions = [
    [-1, -1], [-1, 0], [-1, 1],
    [0, -1],          [0, 1],
    [1, -1], [1, 0], [1, 1]
  ];
  const currentPlayer = gameState.currentPlayer;
  const opponent = currentPlayer === "R" ? "B" : "R";
  let valid = false;

  for (const direction of directions) {
    let r = row + direction[0];
    let c = col + direction[1];
    let foundOpponent = false;

    while (r >= 0 && r < 8 && c >= 0 && c < 8) {
      if (gameState.board[r][c] === opponent) {
        foundOpponent = true;
      } else if (foundOpponent && gameState.board[r][c] === currentPlayer) {
        valid = true;
        break;
      } else {
        break;
      }
      r += direction[0];
      c += direction[1];
    }

    if (valid) {
      return true; // 少なくとも1つの方向で有効なら手は有効
    }
  }

  return false;
}

function getLabeledBoardString(gameState) {
  const rows = gameState.board.map((row, rowIndex) => {
    const labeledRow = row.map(cell => cell || "_").join(",");
    return `${rowIndex + 1} ${labeledRow}`;
  });

  const header = "  A,B,C,D,E,F,G,H";
  return [header, ...rows].join("\n");
}

function getValidMoves(gameState) {
  const validMoves = [];
  for (let row = 0; row < 8; row++) {
    for (let col = 0; col < 8; col++) {
      const move = String.fromCharCode(65 + col) + (row + 1);
      if (isValidMove(move, gameState)) {
        validMoves.push(move);
      }
    }
  }
  return validMoves;
}

function updateBoard(move, gameState) {
  const col = move.charCodeAt(0) - 'A'.charCodeAt(0);
  const row = parseInt(move[1]) - 1;
  gameState.board[row][col] = gameState.currentPlayer;
  flipDiscs(row, col, gameState);
  gameState.currentPlayer = gameState.currentPlayer === "R" ? "B" : "R";
}

function flipDiscs(row, col, gameState) {
  const directions = [
    [-1, -1], [-1, 0], [-1, 1],
    [0, -1],          [0, 1],
    [1, -1], [1, 0], [1, 1]
  ];
  const currentPlayer = gameState.currentPlayer;
  const opponent = currentPlayer === "R" ? "B" : "R";
  directions.forEach(direction => {
    let r = row + direction[0];
    let c = col + direction[1];
    let discsToFlip = [];
    while (r >= 0 && r < 8 && c >= 0 && c < 8 && gameState.board[r][c] === opponent) {
      discsToFlip.push([r, c]);
      r += direction[0];
      c += direction[1];
    }
    if (r >= 0 && r < 8 && c >= 0 && c < 8 && gameState.board[r][c] === currentPlayer) {
      discsToFlip.forEach(([flipRow, flipCol]) => {
        gameState.board[flipRow][flipCol] = currentPlayer;
      });
    }
  });
}

function isGameOver(gameState) {
  return getValidMoves(gameState).length === 0 && getValidMoves({...gameState, currentPlayer: gameState.currentPlayer === "R" ? "B" : "R"}).length === 0;
}

function determineWinner(gameState) {
  let redCount = 0;
  let blueCount = 0;
  for (const row of gameState.board) {
    for (const cell of row) {
      if (cell === "R") redCount++;
      if (cell === "B") blueCount++;
    }
  }

  if (redCount > blueCount) {
    return "勝者: 赤";
  } else if (blueCount > redCount) {
    return "勝者: 青";
  } else {
    return "引き分け";
  }
}

function getGameState(userId) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("OthelloGameState");
  const data = sheet.getDataRange().getValues();
  for (const row of data) {
    if (row[0] === userId) {
      return JSON.parse(row[1]);
    }
  }
  return null;
}

function saveGameState(userId, gameState) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("OthelloGameState");
  const data = sheet.getDataRange().getValues();
  let updated = false;
  for (let i = 0; i < data.length; i++) {
    if (data[i][0] === userId) {
      sheet.getRange(i + 1, 2).setValue(JSON.stringify(gameState));
      updated = true;
      break;
    }
  }
  if (!updated) {
    sheet.appendRow([userId, JSON.stringify(gameState)]);
  }
}

function initializeGameState() {
  return {
    board: [
      ["", "", "", "", "", "", "", ""],
      ["", "", "", "", "", "", "", ""],
      ["", "", "", "", "", "", "", ""],
      ["", "", "", "B", "R", "", "", ""],
      ["", "", "", "R", "B", "", "", ""],
      ["", "", "", "", "", "", "", ""],
      ["", "", "", "", "", "", "", ""],
      ["", "", "", "", "", "", "", ""]
    ],
    currentPlayer: "R"
  };
}

function resetGameState(userId) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("OthelloGameState");
  const data = sheet.getDataRange().getValues();
  for (let i = 0; i < data.length; i++) {
    if (data[i][0] === userId) {
      sheet.deleteRow(i + 1);
      break;
    }
  }
}

function replyToLine(json, message) {
  sendLineMessage(json.events[0].replyToken, [{ type: 'text', text: message }]);
}

function replyWithFlexMessage(json, flexMessage, additionalMessage = "") {
  const messages = additionalMessage ? [{ type: 'text', text: additionalMessage }, flexMessage] : [flexMessage];
  sendLineMessage(json.events[0].replyToken, messages);
}

function replyWithFinalBoardAndMessage(json, flexMessage, resultMessage) {
  sendLineMessage(json.events[0].replyToken, [flexMessage, { type: 'text', text: resultMessage }]);
}

function sendLineMessage(replyToken, messages) {
  const url = 'https://api.line.me/v2/bot/message/reply';
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${LINE_CHANNEL_ACCESS_TOKEN}`
    },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: messages
    })
  };
  UrlFetchApp.fetch(url, options);
}

function createBoardFlexMessage(gameState) {
  const rows = [];
  for (let i = 0; i < 8; i++) {
    const row = [];
    for (let j = 0; j < 8; j++) {
      const cell = gameState.board[i][j];
      const position = String.fromCharCode(65 + j) + (i + 1);

      row.push({
        type: "box",
        layout: "vertical",
        width: "40px",
        height: "40px",
        backgroundColor: cell === "" ? "#808080" : cell === "R" ? "#FF0000" : "#0000FF",
        borderColor: "#000000",
        borderWidth: "1px",
        justifyContent: "center",
        alignItems: "center",
        action: {
          type: "message",
          label: position,
          text: position
        },
        contents: [{
          type: "text",
          text: " ",  // 空のテキストにして見た目を揃える
          color: cell === "R" ? "#FFFFFF" : "#000000",
          align: "center",
          gravity: "center",
          size: "xxl"
        }]
      });
    }
    rows.push({
      type: "box",
      layout: "horizontal",
      spacing: "none",
      contents: row
    });
  }
  return {
    type: "flex",
    altText: "ゲーム盤",
    contents: {
      type: "bubble",
      size: "giga",
      body: {
        type: "box",
        layout: "vertical",
        spacing: "none",
        backgroundColor: "#FFFFFF",
        paddingAll: "xs",
        contents: rows
      }
    }
  };
}

function createFinalBoardFlexMessage(gameState) {
  return createBoardFlexMessage(gameState);  // 最終盤面表示用のFlex Message
}

・最後に・・・AI時代に必要なのは「想像力」と「質問力」

今回はいかがでしたでしょうか。

LINEでゲームが作りたい!」今回の目標はここでした。

実際にやってみたら、形になるものができました。

AIを活用すれば、「今までやってみたかったけど、できなかったこと」ができるようになる。

これは、素晴らしいことだと思います。

必要なのは「想像力」と「質問力

作ってみたいものが明確に想像できて、しっかりとした言葉で表せられる。

AI時代には必要なスキルなのかなと思いました。

ここまで読んでくださり、ありがとうございます。


この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?