LINEでミニ神経衰弱を作った【GAS✖️スプレッドシート 】

# はじめに
[リッチメニューエディター](https://richmenu.app.e-chan.me/)を使えば、ボタンの数も形も関係なくリッチメニューが作成できることを知り、トランプゲームはできないかと思いつきました。

https://twitter.com/ped_yi/status/1548345617432612864

# 使用したもの
スプレッドシート 
Google Apps Script
LINE messaging API

【ルール（通常の神経衰弱と同じ）】
1) めくるトランプを選ぶ（リッチメニューをタップ）
2) 2枚選び、ペアであれば「１ペア！」違えば「残念」と返ってくる
3) 2枚同じトランプは選べない
4) 既にペアになったトランプを選ぶと「既に選んだカード」と返ってくる
5) 全ペア（ここでは６ペア）作れたらクリア！ランキングに登録すると順位が返ってくる
6) 「ゲームを初めからやる」を押すとカードがシャッフルされる

# 準備
- スプレッドシートの作成→中に"common", "user", "prize"の３シートを準備![スクリーンショット 2022-07-18 18.59.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653579/22b0c003-7848-8afe-6226-ba9ac2a9a0f8.png)

- LINEアカウントの作成
- トランプ表面（今回はハートのJQK、クローバーのJQK)の画像のダウンロード
  (URLは[GithubPagesで画像のホスティング](https://qiita.com/cog1t0/items/0cc21346f1ebba5ce7f0)で取得)

# ルールに従って実装

まずはuserからアクションがあった時にオリジナルのスプレッドシートをコピーして、userごとのスプレッドシートを作成する。
[データベースをユーザーごとに用意する](https://qiita.com/Cyber_Hacnosuke/items/445ee64b3e9594a828ed)を参考にしました。

## ①めくるトランプを選ぶ
[リッチメニューエディター](https://richmenu.app.e-chan.me/)でトランプごとに領域を区切り、順番に番号を振る。
選ばれたカードの画像をLINEで返す。
```javascript
const number = Number(json.events[0].message.text);
// 返す画像URL
image = commonSheet.getRange(number + 1, 3).getValue();
```
commonSheetはこのようになっています。![神経衰弱qiita用commonsheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653579/4be63a1b-b199-b70c-7830-e795d0c5b195.png)
※H列ではなくK列を使用していることには特に意味はありません。

## ②-④カードを選んだ時の挙動
２枚選ぶということは１枚目と２枚目の挙動が異なるので、スプレッドシート上で今１枚目なのか２枚目なのかを確認する。
（私はuserSheetに「奇数」「偶数」を表示するセルを作りました。）
- 奇数（１枚目を引いた）の場合
```javascript
if (userSheet.getRange("G2").getValue() === "奇数") {
          messages = [
            {
              type: "image",
              originalContentUrl: image,
              previewImageUrl: image,
            },
            {
              type: "text",
              text: "次の１枚を選んでください。",
            },
          ];
          userSheet.getRange("G2").setValue("偶数");
```
- 偶数（２枚目を引いた）の場合
奇数の時よりやや複雑になります。
   - １枚目と同じカードを引いている場合
   - １枚目と違うカードを引き、絵柄がそろった場合
      - これでゲームクリアの場合「クリアです」「○回でクリアできました」「ランキングに登録しますか？」と返す
      - クリアではない場合「１ペア成立！」と返す
   - １枚目と２枚目の絵柄が違う場合

１枚目と２枚目のカードの見比べができるように選んだカードをuserSheetに記録していきます。
![神経衰弱userSheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653579/963acaed-42cf-58c6-957f-0204cbe0a909.png)

★既にペアになったカードかどうかの判定
ペアができた時にそのカードを「済」に変更しておく。
commonSheetD列（例えばB２）が=COUNTIF(B2:B13,B2)になっており、ペアになったカードの相方であればD列が１になるため、１になったD列と同じ行のB列を「済」に変更する。
```javascript
commonSheet.getRange(number + 1, 2).setValue("済");
              for (let i = 1; i < 13; i++) {
                if(commonSheet.getRange(i + 1, 4).getValue() == 1){
                  commonSheet.getRange(i + 1, 2).setValue("済")
                }
```
```javascript
var card = commonSheet.getRange(number + 1, 2).getValue();
      if (card === "済") {
        messages = [
          {
            type: "text",
            text: "既にペアになったカードです。",
          },
        ];
      }
```


（コードは全体コード↓参照。）
## ⑤ランキングに登録すると順位が返ってくる
オリジナルのスプレッドシートのprizeSheetから順位を引っ張ってきます。
```javascript
var prizelastrow = prizeSheet
                        .getRange(1, 1)
                        .getNextDataCell(SpreadsheetApp.Direction.DOWN)
                        .getRow();
prizeSheet.getRange(prizelastrow+1, 1).setValue(json.events[0].postback.data)
prizeSheet.getRange("C1").setValue(json.events[0].postback.data)
var prize = prizeSheet.getRange("C2").getValue()
      messages =[
        {
          type: "text",
          text: "あなたの順位は"+prize+"位です！",
        }
      ]
```
prizeSheetはこんな感じです。![神経衰弱prizeSheet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653579/bab1fd53-430c-f4cf-d772-0c6be9c1b972.png)
## ⑥「ゲームを初めからやる」を押すとカードがシャッフルされる
commonSheetにMath.random関数を用いて、カードの順番を変える。
前回選んだカードの記録を全て消す。
そろったペアの数を０にする。
```javascript
if (usermessage == "ゲームを初めからやる") {
      for (let i = 1; i < 13; i++) {
        x = Math.random();
        commonSheet.getRange(i + 1, 6).setValue(x);
      }
      for (let i = 1; i < 13; i++) {
        commonSheet
          .getRange(i + 1, 2)
          .setValue(commonSheet.getRange(i + 1, 11).getValue());
      }
      userSheet.getRange("D3:D302").clear();
      userSheet.getRange("G4").setValue(0);
      userSheet.getRange("G2").setValue("奇数");
      messages =[{ type:"text",text:"スタートです！"}]
    } 
```
以上がポイントです。

### 全体
```javascript
var CHANNEL_ACCESS_TOKEN ="アクセストークン";
var line_endpoint = "https://api.line.me/v2/bot/message/reply";
var line_endpoint_profile = "https://api.line.me/v2/bot/profile";
var messages;
var image;
const prizeSheet = SpreadsheetApp.openById("originalsheetのID").getSheetByName('prize')

function doPost(e) {
  var json = JSON.parse(e.postData.contents);
  var reply_token = json.events[0].replyToken;
    if (typeof reply_token === "undefined") {
      return;
    }

  if (json.events[0].type == "message") {
    var user_id = json.events[0].source.userId;
    var SpreadSheet = getSpreadSheet(user_id);
    const commonSheet = SpreadSheet.getSheetByName("common");
    const userSheet = SpreadSheet.getSheetByName("user");
    const usermessage = json.events[0].message.text;

    if (usermessage == "ゲームを初めからやる") {
      for (let i = 1; i < 13; i++) {
        x = Math.random();
        commonSheet.getRange(i + 1, 6).setValue(x);
      }
      for (let i = 1; i < 13; i++) {
        commonSheet
          .getRange(i + 1, 2)
          .setValue(commonSheet.getRange(i + 1, 11).getValue());
      }
      userSheet.getRange("D3:D302").clear();
      userSheet.getRange("G4").setValue(0);
      userSheet.getRange("G2").setValue("奇数");
      messages =[{ type:"text",text:"スタートです！"}]
    } else {
      const number = Number(json.events[0].message.text);

      // 返す画像URL
      image = commonSheet.getRange(number + 1, 3).getValue();
      var lastRow = userSheet
        .getRange(1, 4)
        .getNextDataCell(SpreadsheetApp.Direction.DOWN)
        .getRow();
      // 何を引いたか
      var card = commonSheet.getRange(number + 1, 2).getValue();
      if (card === "済") {
        messages = [
          {
            type: "text",
            text: "既にペアになったカードです。",
          },
        ];
      } else {
        userSheet.getRange(lastRow + 1, 4).setValue(card);
        userSheet.getRange(lastRow + 1, 5).setValue(number);
        userSheet.getRange("A9").setValue(userSheet.getRange("G2").getValue());
        if (userSheet.getRange("G2").getValue() === "奇数") {
          messages = [
            {
              type: "image",
              originalContentUrl: image,
              previewImageUrl: image,
            },
            {
              type: "text",
              text: "次の１枚を選んでください。",
            },
          ];
          userSheet.getRange("G2").setValue("偶数");
        } else {
          var last = userSheet.getRange(lastRow, 4).getValue();
          var lastnumber = userSheet.getRange(lastRow, 5).getValue();
          var now = userSheet.getRange(lastRow + 1, 4).getValue();

          if (lastnumber === number) {
            messages = [
              {
                type: "text",
                text: "同じカードは選べません。",
              },
            ];
          } else if (last === now) {
            var pair = userSheet.getRange("G4").getValue();
            var newpair = pair + 1;
            var clearnumber = userSheet.getRange("G8").getValue();
            userSheet.getRange("G4").setValue(newpair);
            
            if (newpair == 6) {
              messages = [
                {
                  type: "image",
                  originalContentUrl: image,
                  previewImageUrl: image,
                },
                {
                  type: "text",
                  text: "クリアです！！",
                },
                {
                  type: "text",
                  text: clearnumber + "回でクリアできました。",
                },
                {
                  type: "text",
                  text: "ランキングに登録しますか？",
                  quickReply: {
                    items: [
                      {
                        type: "action",
                        action: {
                          type: "postback",
                          label: "登録する",
                          text: "ランキングに登録しました。",
                          data: clearnumber,
                        },
                      },
                      {
                        type: "action",
                        action: {
                            type: "message",
                            label: "しない",
                            text: "登録せずに記録を破棄します。"
                          }
                      },
                    ],
                  },
                },
              ];
            } else {
              messages = [
                {
                  type: "image",
                  originalContentUrl: image,
                  previewImageUrl: image,
                },
                {
                  type: "text",
                  text: "１ペア成立！！",
                },
              ];
              commonSheet.getRange(number + 1, 2).setValue("済");
              for (let i = 1; i < 13; i++) {
                if(commonSheet.getRange(i + 1, 4).getValue() == 1){
                  commonSheet.getRange(i + 1, 2).setValue("済")
                }
              }
              userSheet.getRange("G2").setValue("奇数");
            }
          } else if (lastRow == 299) {
            messages = [{ type: "text", text: "ゲームオーバーです。" }];

            userSheet.getRange("D3:D302").clear();
            userSheet.getRange("G4").setValue(0);
            userSheet.getRange("G2").setValue("奇数");
            for (let i = 1; i < 13; i++) {
              commonSheet
                .getRange(i + 1, 2)
                .setValue(commonSheet.getRange(i + 1, 11).getValue());
            }
          } else {
            messages = [
              {
                type: "image",
                originalContentUrl: image,
                previewImageUrl: image,
              },
              {
                type: "text",
                text: "残念。頑張って！！",
              },
            ];
            userSheet.getRange("G2").setValue("奇数");
          }
        }
      }
    }
  }else if(json.events[0].type == "postback"){
      var prizelastrow = prizeSheet
                        .getRange(1, 1)
                        .getNextDataCell(SpreadsheetApp.Direction.DOWN)
                        .getRow();
      prizeSheet.getRange(prizelastrow+1, 1).setValue(json.events[0].postback.data)
      prizeSheet.getRange("C1").setValue(json.events[0].postback.data)
      var prize = prizeSheet.getRange("C2").getValue()
      messages =[
        {
          type: "text",
          text: "あなたの順位は"+prize+"位です！",
        }
      ]
    }
      // bagSheet.getRange(3,1,100).clear()
      UrlFetchApp.fetch(line_endpoint, {
        headers: {
          "Content-Type": "application/json; charset=UTF-8",
          Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
        },
        method: "post",
        payload: JSON.stringify({
          replyToken: reply_token,
          messages: messages,
        }),
      });
  return ContentService.createTextOutput(
    JSON.stringify({ content: "post ok" })
  ).setMimeType(ContentService.MimeType.JSON);
}

function getSpreadSheet(user_id) {
  var sid = PropertiesService.getScriptProperties().getProperty(user_id);
  if (sid == null) {
    return createSpreadSheet(user_id);
  } else {
    try {
      return SpreadsheetApp.openById(sid);
    } catch (e) {
      return createSpreadSheet(user_id);
    }
  }
}

function createSpreadSheet(user_id) {
  var ori_sheet = SpreadsheetApp.openById(
    "originalSheetのID"
  );
  var spreadSheet = ori_sheet.copy(getUserDisplayName(user_id));
  PropertiesService.getScriptProperties().setProperty(
    user_id,
    spreadSheet.getId()
  );
  var file = DriveApp.getFileById(spreadSheet.getId());
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  // logSheet.getRange("A2").setValue("createsheet")
  return spreadSheet;
}

function getUserDisplayName(user_id) {
  var res = UrlFetchApp.fetch(line_endpoint_profile + "/" + user_id, {
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    method: "get",
  });
  return JSON.parse(res).displayName;
}

```

# おわりに
各部分でもっといいやり方がありそうな気がしますが、自分の実力ではこれが限界でした。
ぜひアドバイスなどあればコメントお願いします！！！！