海馬社長にカードだけじゃなくてビジネスも教わりたい

# 海馬瀬人のおさらい
海馬瀬人は、アニメ「遊戯王デュエルモンスターズ」に登場するキャラクターです。
主人公の武藤遊戯のライバル的存在で、玩具やゲームの開発・販売を行う海馬コーポレーションの社長でもあります。デュエルで使用するソリッド・ヴィジョン・システムも海馬コーポレーションで開発されました。

海馬は自信家で人に対しては見下した態度も取りますが、常に真剣で弟思い、そして情熱的で勝利に貪欲という魅力的な人物です。

```plantuml
@startuml
<style>
root {
  Padding 0
  Margin 0
}
footer {
  FontColor black
  FontSize 20
}
</style>
footer コナミ公式サイトより
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/b190679d-d44a-0e44-ce23-5385aed69348.png>
@enduml
```

## こんな人たちへ向けたLINE botです
- 海馬イズムを注入してほしい
- 海馬社長にビジネス知識を教わりたい
- 海馬社長に見下されたい

# 開発環境
- Node.js (ver 20.8.1)
- openai (ver 4.19.1)
- line/bot-sdk　(ver 8.0.1)

# OpenAIのAPIを発動し海馬瀬人を召喚
ChatGPTを利用するときと同様に、プロンプトから海馬社長をつくっていきます。

:::note warn
これだけだと一人称が「拙者」になったりと変な人格が芽生えたので何行か追加しました。アニメでも偽海馬がいたのを思い出しました。
```
 ## あなたはアニメ「遊戯王デュエルモンスターズ」の海馬瀬人です。
 ## 海馬社長らしく自信たっぷりで偉そうに応答してください。
```
:::

:::note info
一人称や口癖などを設定。頻度も与えるとよりgood！
```
 ## あなたはアニメ「遊戯王デュエルモンスターズ」の海馬瀬人です。
 ## 海馬社長らしく自信たっぷりで偉そうに応答してください。
 ## 海馬社長らしくストイックで勝利に貪欲な雰囲気で応答してください。
 ## 海馬社長らしく少し馬鹿にするような雰囲気でビジネス用語を詳しく解説してください。。
 ## あなたの一人称は「俺」か「俺様」で、「私」や「僕」や「拙者」とは言いません。
 ## あなたが相手を指すときは「貴様」で、「あなた」や「君」とは言いません。
 ## 応答の冒頭は高確率で「ふん...」と言ってください。
 ```
:::

<details><summary>実装例</summary>

```js
'use strict';

// モジュールの読み込み
const line = require('@line/bot-sdk');
const OpenAI = require('openai');
const express = require('express');
// const dotenv = require('dotenv'); //
// dotenv.config();

// 設定
const config = {
  channelSecret: process.env.CHANNEL_SECRET || 'CHANNEL_SECRET',
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || 'CHANNEL_ACCESS_TOKEN',
};

// GPTのAPIKEY
const apiKey = process.env.OPENAI_API_KEY || 'OPENAI_API_KEY';
const openai = new OpenAI({apiKey});

const makeCompletion = async (userMessage) => {
  const prompt = {
      role: "system", 
      content: `## あなたはアニメ「遊戯王デュエルモンスターズ」の海馬瀬人です。
                ## 海馬社長らしく自信たっぷりで偉そうに応答してください。
                ## 海馬社長らしくストイックで勝利に貪欲な雰囲気で応答してください。
                ## 海馬社長らしく少し馬鹿にするような雰囲気でビジネス用語を詳しく解説してください。
                ## あなたの一人称は「俺」か「俺様」で、「私」や「僕」や「拙者」とは言いません。
                ## あなたが相手を指すときは「貴様」で、「あなた」や「君」とは言いません。
                ## 応答の冒頭は高確率で「ふん...」と言ってください。`
  };

  const sendMessage = [prompt, userMessage];
  console.log(sendMessage);

  const generated = await openai.chat.completions.create({
  messages: sendMessage,
      model: "gpt-3.5-turbo-1106",
  });
  return generated
};

const client = new line.messagingApi.MessagingApiClient(config);
// メッセージイベントの処理
async function handleEvent(event) {
  // テキストメッセージ以外は無視
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  const userMessage = {
    role: "user",
    content: event.message.text
  };

  // ChatGPT APIにリクエストを送る
  try {
    const completion = await makeCompletion(userMessage);
    // レスポンスから返答を取得
      const reply = completion.choices[0].message.content;
      // 返答をLINEに送る
      return client.replyMessage({
        replyToken: event.replyToken, 
        messages:[{
          type: 'text',
          text: reply
        }]
      });
  } catch (error) {
    // エラーが発生した場合はログに出力
    console.error(error);
    return Promise.resolve(null);
  }
}

const port = process.env.PORT || 3000;
const app = express()
  .get('/', (_, res) => res.send('hello LINE Bot'))
  .post('/webhook', line.middleware(config), (req, res) => {
    if (req.body.events.length === 0) {
      res.send('Hello LINE BOT! (HTTP POST)');
      console.log('検証イベントを受信しました');
      return
    } else {
      console.log(req.body.events);
    }
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
  })
app.listen(port);
console.log(`PORT${port}番でサーバーを実行中です`)

```
</details>

# デュエルスタンバイ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/761f3632-b19e-6c80-9774-4b46a0eb1890.png" height=50%>
海馬社長想像できますね。ちょっと優しさが垣間見えますが...まあ良いでしょう。

# 海馬社長からビジネスを学びたい
海馬社長にCSRという言葉を聞いてみました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/e869fafb-ab0a-770b-f5d5-4f54564d0c59.png" height=70%>

しっかり教えてくれました。

ゲームを開発しているということで技術的な質問もしてみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/0a2fd85b-8e34-099b-34fb-04990245dfc8.png" height=70%>
こちらも流石です。カードとも絡めてくれました。

# シークレットワードの追加
コミック、アニメに触れた人なら聞きたい「あのセリフ」があるはず。こちらもいくつか追加しました。
今回は、「ダイヤモンド・ドラゴン」と「エネミーコントローラー」を紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/381f641b-046e-060d-c4df-e92cb2fca7b0.png)


はじめは表記揺れがあるといけないと思い、
- **「ダイヤモンド・ドラゴン」は「ダイヤモンド」**
- **「エネミーコントローラー」は「コントローラー」**

といった具合に近い言葉でもシークレットワードが出るようにしたのですが、海馬社長がそんなこと許すはずがないと思い、正式名称のみを受け付けるようにしました。
```
## 「ダイヤモンド・ドラゴン」という言葉があったら「ほう…ダイヤモンド・ドラゴン......よくこの程度のカードで虚勢が張れたものだ...(こんなカード オレは三十六枚持っているよ...)」と返答してください。
## 「エネミーコントローラー」という言葉があったら「マジックカード エネミー・コントローラー発動！ ライフ1000ポイント払い 左・右・A・B ！！」と返答してください。
```

# おまけ：オリジナルカードをつくろう
遊戯王風のオリジナルカードをつくろうとしましたが、こちらは著作権を侵害する可能性があるのでNG。
代わりに海馬社長の有名なセリフをカードのイラストとして生成する機能を付けました。リッチメニューからイラストを押すと[dall-e-3](https://openai.com/dall-e-3)で生成した画像をLINEで送信します。
生成できるカードは2種類 **「粉砕！玉砕！大喝采ー！」** と **「強靭！無敵！最強！」** です。後者はオフィシャルにありますが、新たにどんなイラストが出るか楽しめます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/41505671-a709-855b-dece-3779bc369627.png" height=50%>

さらにもうひと枠は「海馬のヒトコト」です。これから大事な勝負事がある人などは海馬に活を入れてもらいましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/94c3562f-cec9-8d81-6e0d-05e28b8df712.png" height=50%>

カードの効果もつくってみたいですね。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/60cf7a86-0264-3e35-78d2-b3b885697f03.png" height=50%>
 「強靭！無敵！最強！」のカードイラスト


<details><summary>実装例</summary>

```js
'use strict';

// モジュールの読み込み
const line = require('@line/bot-sdk');
const OpenAI = require('openai');
const express = require('express');
// const dotenv = require('dotenv'); //
// dotenv.config();

// 設定
const config = {
  channelSecret: process.env.CHANNEL_SECRET || 'CHANNEL_SECRET',
  channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || 'CHANNEL_ACCESS_TOKEN',
};

// GPTのAPIKEY
const apiKey = process.env.OPENAI_API_KEY || 'OPENAI_API_KEY';
const openai = new OpenAI({apiKey});

const makeCompletion = async (userMessage) => {
  const prompt = {
      role: "system", 
      content: `## あなたはアニメ「遊戯王デュエルモンスターズ」の海馬瀬人です。
                ## 海馬社長らしく自信たっぷりで偉そうに応答してください。
                ## 海馬社長らしくストイックで勝利に貪欲な雰囲気で応答してください。
                ## 海馬社長らしく少し馬鹿にするような雰囲気でビジネス用語を詳しく解説してください。
                ## あなたの一人称は「俺」「俺様」で、「私」や「僕」や「拙者」とは言いません。
                ## あなたが相手を指すときは「貴様」で、「あなた」や「君」とは言いません。
                ## 応答の冒頭は高確率で「ふん...」と言ってください。
                ## 戦いに関連する言葉が出てきたら「戦いのロード」という言葉を稀に入れて返答してください。
                ## 「ダイヤモンド・ドラゴン」という言葉があったら「ほう…ダイヤモンド・ドラゴン......よくこの程度のカードで虚勢が張れたものだ...(こんなカード オレは三十六枚持っているよ...)」と返答してください。
                ## 「エネミーコントローラー」という言葉があったら「マジックカード エネミー・コントローラー発動！ ライフ1000ポイント払い 左・右・A・B ！！」と返答してください。
                ## 「ブルーアイズ」という言葉があったら「出でよ！ ブルーアイズ・ホワイト・ドラゴン！」と返答してください。
                ## 「海馬のヒトコト」というメッセージに対しては一日の成功を応援する一言を海馬社長らしく応答してください`
  };

  const sendMessage = [prompt, userMessage];
  console.log('userMessage');
  console.log(userMessage.content);
  if (userMessage.content=='粉砕！玉砕！大喝采ー！'||userMessage.content=='強靭！無敵！最強！') {
    const generated = await openai.images.generate({
      model: "dall-e-3",
      prompt: userMessage.content,
      size: "1024x1024",
      quality: "standard",
      n: 1,
    });
    return generated
  }else{
    const generated = await openai.chat.completions.create({
      messages: sendMessage,
      model: "gpt-3.5-turbo-1106",
    });
    return generated
  };
};

const client = new line.messagingApi.MessagingApiClient(config);
// メッセージイベントの処理
async function handleEvent(event) {
  // テキストメッセージ以外は無視
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  const userMessage = {
    role: "user",
    content: event.message.text
  };

  // ChatGPT APIにリクエストを送る
  try {
    const completion = await makeCompletion(userMessage);
    console.log("completion");
    console.log(completion);
    if (completion.choices) {
    // レスポンスから返答を取得
      console.log(completion.choices[0].message.content); // レスポンスをみたいときにコメントアウトを外してください
      const reply = completion.choices[0].message.content;
      // 返答をLINEに送る
      return client.replyMessage({
        replyToken: event.replyToken, 
        messages:[{
          type: 'text',
          text: reply
        }]
      })
    } else {
      const url = completion.data[0].url;
      const reply = await fetch(url);
      
      // 返答をLINEに送る
      return client.replyMessage(
        event.replyToken, 
        {
          type: 'image',
          originalContentUrl: url,
          previewImageUrl: url
        }
      );

    }
  } catch (error) {
    // エラーが発生した場合はログに出力
    console.error(error);
    return Promise.resolve(null);
  }
}

const port = process.env.PORT || 3000;
const app = express()
  .get('/', (_, res) => res.send('hello LINE Bot'))
  .post('/webhook', line.middleware(config), (req, res) => {
    if (req.body.events.length === 0) {
      res.send('Hello LINE BOT! (HTTP POST)');
      console.log('検証イベントを受信しました');
      return
    } else {
      console.log(req.body.events);
    }
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
  })
app.listen(port);
console.log(`PORT${port}番でサーバーを実行中です`)
```
</details>