LoginSignup
6
4

More than 1 year has passed since last update.

【GASの永続化】モルック(アウトドアスポーツ)の進行役をLINEmessagingAPIを使って作ってみた!

Last updated at Posted at 2022-08-13

目次

  • モルックとは?
  • なぜこのシステムを作ったのか
  • GASの永続化について(トラブル1個目)
  • ソースコードの解説等
  • いきなりLINE botが動かなくなる?(トラブル2個目)
  • 実際にbotを使いながらプレイしてみた!
  • 最後に

とってもいい記事を書いた(と思ってる)のでぜひ最後まで読んでください!!

1. モルックとは?

フィンランド発祥のアウトドアスポーツです。

ルールはこんな感じです。

スクリーンショット 2022-08-13 18.13.13.png
スクリーンショット 2022-08-13 18.13.01.png
スクリーンショット 2022-08-13 18.14.05.png

倒したピンを元の場所ではなく、その場で立て、1本だけ倒したなら、その倒した棒に書いてある数字、2本以上倒すと、その本数分の得点が入ります。

どちらかのチームが50点ピッタシになれば、勝利となります。
もし、50点を上回ってしまった場合は、得点が25まで戻ってしまいます。
また、ミスが3回続けば、負けとなってしまいます。

相手が倒したい棒を遠くへ飛ばしたり、頭を使った面白いアウトドアのスポーツです。
image.png

2. なぜこのシステムを作ったのか

実際に友人とする機会があり、プレイして思ったのは、「スコアの管理めんどくさくね・・?」でした。

モルックを外でプレイしながら数字を覚えながら足していって、、、って想像以上にめんどくさいです。

当たり前ですが、得点をかなり気にしながら、作戦を決めていくので
「今何点?」が何回も飛び交います

ということで、LINEを使ってモルックを円滑に進めるための「モルックの支配人bot」を作って行きましょう!!レッツ作成!

LINEbotの作り方や、GASとの連携の仕方は私が前回に書いたこちらをご覧ください。

今回はGoogle App ScriptとGoogle Spreadsheetを使って行います。
(Google App Scriptを以後GASと呼びます。)

3. GASの永続化について(トラブル1個目)

勘の鋭い方はお分かりですが、GASを使ってゲームの進行をするには少々面倒くさいです。

キーワードは「GASの永続化」です。

例えば「ゲーム開始」というとゲームが始まるとします。ユーザからのメッセージをGASが取得し、
モルックを開始し、数字をどんどん入れていくとします。

そこでネックになるのはLINEから受け取る際に動くdoPost関数です。
doPost関数は、Google Apps ScriptのページのURLに、POSTリクエストが送られたときに、関数を実行します。

LINEが送られてくるたびに、その関数が動いてしまうため、前の点数は記憶できません。

例で挙げると、

if(message==(isFinite(message)==true){ //もしユーザからの送信が数値だったら
send_message('〇〇チームの合計スコアは〇〇点です')//LINEに合計スコアをpushする
}

こういうコードがあるとします。
この場合、二点の問題が発生します。

  • グループラインにbotを入れていた場合、
    モルックと関係のない会話でもdoPost関数が動いてしまう
  • チームの合計スコアが記憶できていない

このような現象が起きてしまうと、ゲームの進行役なんてできませんよね。

そこで出てくるのがスプレッドシートです。

スプレッドシートで、以下の二点の管理をします。

  • 現在ゲームのどのフェーズにいるのか(コード内で、fease_sheetとして管理)
  • スコアやチーム名の管理(コード内で、score_sheetとして管理)

このようにして、GASの永続化をし、モルックの進行役botを作ります。

以下の文献ように、永続化の方法はいくらかあると思います。
僕はスプレットシート×GASをよく使って慣れていたのでこの実装にしました。

4. ソースコードの解説等

結構分かりやすいように、コメントアウトの追加頑張りました(笑)
制作時間はざっと20時間ほどです。

var CHANNEL_ACCESS_TOKEN = ''; //LINE Developersで取得したアクセストークンを入れる
var url = 'https://api.line.me/v2/bot/message/push';
var groupID = ''; //グループラインのID
const OutputFolder = DriveApp.getFolderById(''); //保存するフォルダ指定
const template_sheet = DriveApp.getFileById(''); //テンプレートのスプレッドシートを指定
const fease_sheet = SpreadsheetApp.openById('').getSheetByName('');//フェーズ管理のシート




function doPost(e) { //ポストで送られてくるので、送られてきたJSONをパース
  var json = JSON.parse(e.postData.contents);


  var reply_token = json.events[0].replyToken;   //返信するためのトークン取得
  if (typeof reply_token === 'undefined') {
    return;
  }


  let message = json.events[0].message.text; //送られたメッセージ内容を取得
  
 if (message == 'モルック開始'){
  fease_sheet.clear();//フェーズシートを初期化する
  var date = new Date();//現在時刻取得
  date = Utilities.formatDate(date, "Asia/Tokyo", "モルックを開始しました    yyyy/MM/dd hh時mm分");
  send_message(date)//モルックを開始した旨を送信
  create_spreadsheet()//新しくスプレッドシートを作成
  fease_sheet.getRange("A1").setValue("0")//フェーズ0を記入
  fease_sheet.getRange('G1').setValue("0")//チーム1のミスカウントの設定
  fease_sheet.getRange('H1').setValue("0")//チーム2のミスカウントの設定
  send_message('1チーム目の名前を入力してください。')
 }

if (message == 'モルックシステム説明'){
  send_message('このモルックシステムは〇〇によって作られたものです。\n\n ゲームを開始したい際には「モルック開始」、ゲームを終了したい際には「モルック終了」と入力してください。\n\nゲームの途中であれ、誰が入力しても構いません。\n\nまた、2チームor2人で行うような設定にしています。\n\n数字の入力は必ず半角でお願いします。ミスをした場合には「ミス」と入力してください。\n\nまた、モルックのルールが知りたい場合や、並び方がわからなくなった場合には「モルックルール」と送信してください。\n\n\n\n以上、システムの説明でした。もう一度聞きたい場合には「モルックシステム説明」と入力してください。')
}

if (message == 'モルックルール'){
send_message('モルックの公式ルールは以下のサイトをご覧ください。\n\n https://molkky.jp/molkky/#id-molkky-rule')
}


var new_sheet_id = fease_sheet.getRange("A2").getValue()//フェーズシートのA2にある新規シートのIDを取得
var score_sheet = SpreadsheetApp.openById(new_sheet_id).getSheetByName('a');


if(((fease_sheet.getRange("A1").getValue()) == 0) & (message != 'モルック開始')){//フェーズ0が終わっているかつユーザからのメッセージがモルックではないか
  var date = new Date();//現在時刻取得
  date = Utilities.formatDate(date, "Asia/Tokyo", "yyyy/MM/dd hh時mm分");
  score_sheet.getRange("B2").setValue(date)//スコアシートに開始時刻を入れる
  score_sheet.getRange("B3").setValue(message)//スコアシートに1チーム目を入れる
  fease_sheet.getRange("A1").setValue("1")//フェーズ1を記入
  send_message('2チーム目の名前を入力してください。')
}


if(((fease_sheet.getRange("A1").getValue()) == 1 & (score_sheet.getRange("B3").getValue() != message))){//フェーズ1が終わっているかつ前回の入力ではないか
  score_sheet.getRange("C3").setValue(message)//スコアシートに2チーム目を入れる
  fease_sheet.getRange("A1").setValue("2")//フェーズ2を記入
  fease_sheet.getRange("B1").setValue("0")//1チーム目の得点の準備をする、B1が0なら1チーム目の入力、1なら2チーム目の入力、
  send_message('チーム名の設定が終了しました。')
  Utilities.sleep(1000)
  send_message('ゲームを開始してください')
}

let team_name_0 = score_sheet.getRange("B3").getValue()
let team_name_1 = score_sheet.getRange("C3").getValue()

 if(((fease_sheet.getRange("A1").getValue()) == 2) & (fease_sheet.getRange("B1").getValue() == 0) & ((isFinite(message) == true) | (message == 'ミス'))  ){ //フェーズが2であり、1チーム目の入力であり、ユーザからの入力が数値であるか
  if((message > 12 | message < 1 | (Number.isInteger(Number(message)) == false)) & (message != 'ミス')){ //13以上の数字が入力された場合、1未満の場合、小数点がついている場合
   send_message('数字は1〜12で、自然数で入力してくださいです。')
   return
 }
  fease_sheet.getRange("B1").setValue(1)//相手チームのフェーズに設定
  game(0,team_name_0,score_sheet,new_sheet_id,message)
  return
 }


 if(((fease_sheet.getRange("A1").getValue()) == 2) & (fease_sheet.getRange("B1").getValue() == 1) & ((isFinite(message) == true) | (message == 'ミス'))){ //フェーズが2であり、2チーム目の入力であり、ユーザからの入力が数値であるか
 
   if((message > 12 | message < 1 | (Number.isInteger(Number(message)) == false)) & (message != 'ミス')){ //13以上の数字が入力された場合、1未満の場合、小数点がついている場合
    send_message('数字は1〜12で、自然数で入力してくださいです。')
   return
  }
  fease_sheet.getRange("B1").setValue(0) //相手チームのフェーズに設定
  game(1,team_name_1,score_sheet,new_sheet_id,message)
  return
 }

   if(((fease_sheet.getRange("A1").getValue()) == 2)  & (isFinite(Number(message)) == false)  & (score_sheet.getRange("C3").getValue() != message)& (message != 'モルック終了')){ //フェーズが2であり、ユーザからの入力が間違っているときであり、前回の入力ではないか、モルック終了のメッセージでないか、
send_message('入力が間違っています。半角数字でお願いします。')
 }

if (message == 'モルック終了'){
   end(new_sheet_id,'')
 }

  //return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

function create_spreadsheet(){
  let OutputFileName;
  OutputFileName = template_sheet.getName().replace('template', '') + 'モルックスコアシート_'+Utilities.formatDate(new Date(), 'JST', 'yyyyMMddhhss') //語尾に日にち追加
  var new_sheet_id = template_sheet.makeCopy(OutputFileName, OutputFolder);//テンプレートからコピー作成
  fease_sheet.getRange("A2").setValue(new_sheet_id.getId())//フェーズシートのA2に作成したスプレッドシートIDを書いておく
}


function send_message(message){
var response = UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'POST',
    'payload': JSON.stringify({
      'to': groupID,
      'messages':[{
        'type': 'text',
        "text" : message,
      }]
     })
   })
   Logger.log(response.getResponseCode());
}

function game(team,team_name,score_sheet,new_sheet_id,message){
  if((fease_sheet.getRange('B1').getValue() == 0) | (fease_sheet.getRange('B1').getValue() == 1)){
  let team_cell
  let miss_count_cell
  let goukei_score_cell
  if(team == 0){
    team_cell = 'B'
    miss_count_cell = 'G1'
    goukei_score_cell = 'B30'
  }
 else if(team == 1){
    team_cell = 'C'
    miss_count_cell = 'H1'
    goukei_score_cell = 'C30'
  }
  for (let input_count = 4; input_count < 26; input_count++){ //空白の行を探す
    let input_cell = team_cell + input_count
    let goukei_score
    if (score_sheet.getRange(input_cell).getValue() == ''){ //空白行があったら
      score_sheet.getRange(input_cell).setValue(message) //スコアを挿入

      if(score_sheet.getRange(goukei_score_cell).getValue() == ''){//はじめての合計スコア入力なら
        if(message == 'ミス'){
        let miss_count = Number(fease_sheet.getRange(miss_count_cell).getValue()) //ミスカウントを取得
        miss_count = miss_count + 1 //ミス回数を加算
        fease_sheet.getRange(miss_count_cell).setValue(miss_count) //ミス回数を記録
        message = 0
        }
        score_sheet.getRange(goukei_score_cell).setValue(message) //スコアを合計のセルに挿入
        send_message(team_name + 'チームの合計スコアは現在' + message + '点です。')
        return
        }
      else{//はじめての合計スコアではない場合
      goukei_score = Number(score_sheet.getRange(goukei_score_cell).getValue()) + Number(message) //元々入ってる合計スコア足す今回のスコア
      if(goukei_score > 50){//モルックのルールで50を超えた場合得点が25になる処理
        send_message('得点が50を上回ったので25になります。')
        goukei_score = 25

        message=message + ' 半減'
        score_sheet.getRange(input_cell).setValue(message) //半減コメントの追加
      }

      if(message != 'ミス'){
      score_sheet.getRange(goukei_score_cell).setValue(goukei_score) //ミス以外の時、合計スコアを更新
      }

      if((score_sheet.getRange('B30').getValue() == 50) & team == 1 & goukei_score != 50){//1チーム目が50点になり、2チーム目が50点に達しなかった時
        let teki_team = score_sheet.getRange('B3').getValue()
        send_message(teki_team + 'チームの勝利です。')
        end(new_sheet_id,teki_team)
        return
      }
      if(goukei_score == 50 & team == 0){ //1チーム目の得点が50に達した時
        let teki_team = score_sheet.getRange('C3').getValue()
        send_message(teki_team + 'チームが50に達しなかった場合、' + team_name + 'チームの勝利です。')//1チーム目が上がっても2チームめのターンが残っているため
        fease_sheet.getRange(miss_count_cell).setValue(0) //ミスカウントをリセット
        return
      }
      if(goukei_score == 50 & team == 1){ //2チーム目の得点が50に達した時
      if(score_sheet.getRange('B30').getValue() == 50){
        send_message('両チーム50点に達したためこのゲームは引き分けとなります。\n\nモルックアウトをプレイして勝敗を決めてください。')
        end(new_sheet_id,'draw')
        return
      }
      else{
        send_message(team_name + 'チームの勝利です。')
        end(new_sheet_id,team_name)
        return
        }
      }

      if(message == 'ミス'){//一本も倒せなかった場合
        let miss_count = Number(fease_sheet.getRange(miss_count_cell).getValue()) //ミスカウントを取得
        miss_count = miss_count + 1 //ミス回数を加算
        fease_sheet.getRange(miss_count_cell).setValue(miss_count) //ミス回数を記録
        if (miss_count == 3){//モルックのルールで三回連続でミスしたらアウト
          let teki_team = score_sheet.getRange('B3').getValue()
          send_message(team_name + 'チームがミスを三回繰り返しました。ゲームを終了します。')
          end(new_sheet_id,teki_team) //終了への関数
          return
        }
        goukei_score = Number(score_sheet.getRange(goukei_score_cell).getValue())//合計スコアの取得
        send_message(team_name + 'チームの合計スコアは現在' + goukei_score + '点です。')
        return
      }
      
      send_message(team_name + 'チームの合計スコアは現在' + goukei_score+'点です。')
      fease_sheet.getRange(miss_count_cell).setValue(0) //ミスカウントをリセット
      return
        }
      }
    }
    end(new_sheet_id,'draw')
    return
  }
}

function end(new_sheet_id,team_name){
   send_message('お疲れ様でした。')
   Utilities.sleep(1000)
   let score_sheet = SpreadsheetApp.openById(new_sheet_id).getSheetByName('a');
   if(team_name == 'draw'){ 
     score_sheet.getRange("B31").setValue('引き分け')
     score_sheet.getRange("C31").setValue('引き分け')
   }
  if(team_name == (score_sheet.getRange("B3").getValue())){
    score_sheet.getRange("B31").setValue('勝ち')
    score_sheet.getRange("C31").setValue('負け')
  }
  if(team_name == (score_sheet.getRange("C3").getValue())){
    score_sheet.getRange("C31").setValue('勝ち')
    score_sheet.getRange("B31").setValue('負け')
  }
   const scoresheet_url = SpreadsheetApp.openById(new_sheet_id).getUrl() //スプレッドシートのURL取得
   send_message('スコアシートを送信します\n\n\n'+scoresheet_url)
   fease_sheet.clear();//フェーズシートを初期化する
   return
}


大まかな流れとして

モルック開始と入力されたらゲーム開始、テンプレートのスコアシートを複製して、そのシート名を現在時刻にする
その際、複製したスプレッドシートのIDをフェーズシートに記録しておく(そのスコアシートに記録を刻んでいくため)
                    ↓
1チーム目の入力を促し、1チーム目の名前をスプレッドシートに挿入する
                    ↓
2チーム目の入力を促し、フェーズ0が終わっていれば、2チーム目の名前をスプレッドシートに挿入する
                    ↓
数字をユーザに入力してもらい、フェーズシートのB1が0なら1チーム目、1なら2チーム目のターンと判断する
                    ↓
ゲームを進めていき、50なら終了、50を超えると25、ミスを3回すればゲーム終了となる
                    ↓
ゲームを終了する際、スコアシートを見れるようにLINEに送信する

です。
以下がスコアシートのテンプレートです。
このテンプレートのシートを複製後、複製したシートの名前を変更しています。

以下がそのテンプレートシートを複製し、新しくスコアシートを作成しているコードです。
OutputFilenameに変更する新しいファイル名を入れます。
その後、makeCopyメソッドより、複製し、その返り値をフェーズシートに記憶させています。
                  ↓

function create_spreadsheet(){
  let OutputFileName;
  OutputFileName = template_sheet.getName().replace('template', '')+'モルックスコアシート_'+Utilities.formatDate(new Date(), 'JST', 'yyyyMMddhhss') //語尾に日にち追加
  var new_sheet_id = template_sheet.makeCopy(OutputFileName, OutputFolder);//テンプレートからコピー作成
  fease_sheet.getRange("A2").setValue(new_sheet_id.getId())//フェーズシートのA2に作成したスプレッドシートIDを書いておく
}

また、今回ゲームを終了する関数をend関数としています。
end関数にどちらのチームが勝ったかを引数として渡してあげることで、スプレッドシートに記録が可能です。
そして、後からどんなチーム名でどんな試合だったかを見返せるように終了の際、スプレッドシートのスコアシートをグループに送信してあげることも実装しています。
               ↓

function end(new_sheet_id,team_name){
   send_message('お疲れ様でした。')
   Utilities.sleep(1000)
   let score_sheet = SpreadsheetApp.openById(new_sheet_id).getSheetByName('a');
   if(team_name == 'draw'){
     score_sheet.getRange("B31").setValue('引き分け')
     score_sheet.getRange("C31").setValue('引き分け')
   }
  if(team_name  ==(score_sheet.getRange("B3").getValue())){
    score_sheet.getRange("B31").setValue('勝ち')
    score_sheet.getRange("C31").setValue('負け')
  }
  if(team_name == (score_sheet.getRange("C3").getValue())){
    score_sheet.getRange("C31").setValue('勝ち')
    score_sheet.getRange("B31").setValue('負け')
  }
   const scoresheet_url = SpreadsheetApp.openById(new_sheet_id).getUrl() //スプレッドシートのURL取得
   send_message('スコアシートを送信します\n\n\n'+scoresheet_url)
   fease_sheet.clear();//フェーズシートを初期化する
   return
}

モルックは、ゲームを終了する条件が多く存在し、複雑化しているため実装がやや面倒でした。

特に、前半チームが50点に達したり、ミスを3回繰り返したりなど、前半チームが終わりに差し掛かった際、後半チームはまだ最後のターンが残っていますよね。その場合の処理として、

(前半チームの合計スコアが50点であり、前半チームのフェーズである場合 → 後半チームに次のターンに50点でなければ負けることを促す
                ↓

  if(goukei_score == 50 & team == 0){ //1チーム目の得点が50に達した時
        let teki_team = score_sheet.getRange('C3').getValue()
        send_message(teki_team+'チームが50に達しなかった場合、' + team_name + 'チームの勝利です。')//1チーム目が上がっても2チームめのターンが残っているため
        fease_sheet.getRange(miss_count_cell).setValue(0) //ミスカウントをリセット
        return
      }

(前半チームの合計スコアが50点であり、後半チームのフェーズであり、後半チームの合計が50点ではない場合) → 前半チームの勝利でend関数へ進む
end関数の引数には勝利チームの名前を渡す
                ↓

 if((score_sheet.getRange('B30').getValue() == 50) & team == 1 & goukei_score != 50){//1チーム目が50点になり、2チーム目が50点に達しなかった時
        let teki_team = score_sheet.getRange('B3').getValue()
        send_message(teki_team+'チームの勝利です。')
        end(new_sheet_id,teki_team)
        return
      }

(後半チームが50点であり、後半のチームのフェーズだった場合) → 同点のためend関数へ進む
end関数の引数には'draw'を渡す
                ↓

      if(goukei_score == 50 & team == 1){ //2チーム目の得点が50に達した時
      if(score_sheet.getRange('B30').getValue() == 50){
        send_message('両チーム50点に達したためこのゲームは引き分けとなります。\n\nモルックアウトをプレイして勝敗を決めてください。')
        end(new_sheet_id,'draw')
        return
      }

5. いきなりLINE botが動かなくなる?(トラブル2個目)

実際に使っていると、いきなりbotからの返信が来なくなりました。
既読はつくものの、何もなく。
かといって、コードはどこも間違っていない。どうしよう?

doPost関数を使っていると、基本的にエラー文を見ることができません。

では、どうデバッグすれば良いのでしょうか?

結論から言うと、

GASをGCPと連携させることでエラー内容を把握することができます

私は以下の方のを参考にさせていただきました。

GCPと連携し、ログを確認した結果が以下の通りです。

スクリーンショット 2022-08-10 1.33.53.png

'You have reached your monthly limit'

だそうです。
つまり、LINE Messagin APIの送信回数が上限(1000通)にきていたわけです。
これはGCPでログを見ないと気づけませんでした。

LINE Messaging APIを使ってグループラインにテキストをpushすると
人数分の送信回数が消費されてしまいます。

グループラインが7人ならば、1メッセージにつき7がカウントされるという事です。
1ゲームで大体20〜30回メッセージが送信されるため、
1ゲームで200近くも送信回数が消費されるわけですね〜

道理で早くbotが機能しなくなるわけですね・・・

詳細は以下のサイトをご覧ください。

6. 実際にbotを使いながらプレイしてみた!

高校の友人とレッツプレイ!!!
ここからは上から順に時系列で書いてきます。


モルック開始とメッセージを送ることでゲームがスタートします。
その後、1チーム目の名前を理系クラスだった友人で、
2チーム目を文系クラスだった友人らです。

今回は僕は記録係です(笑)


理系チームの1発目です!


一発目の投球は3本倒れたようですね!!
この棒は倒れた場所で立て直します。

このように半角数字で文字を送信すると1チーム目の得点が記録されます。
もちろん、全角の数字や、日本語文字や、1〜12以外の数字の場合は弾かれるように設定しています。


文系チームの投球はなんと0本でした・・
三回連続でミスをすると失格です。

ミスの場合はミスと入力します。0点ですね、、


チーム理系チームもミスをしたようです。
ミスは0点なので前回のスコアが引き継がれ、合計スコアが3点です。


ここで文系チームが9の棒を一本倒したようです!

この場合、一本のみを倒した場合はその倒した棒の点数分が加算されます。
よって9点が得点に入るので、9を送信します。

この調子でゲームを続けていきます。


終盤に差し掛かり、理系チームが45点だったところ、5本を倒し、50点になりました!!
しかし、理系チームは前半のチームのため、後半チームにはもう1回分の投球が残っています!!
そのため、文系チームが50点に達しなかった場合、理系チームが勝利してしまうメッセージを送信しています。


ここで、後半チームの文系チームが2点しか獲得することができず、文系チームの負けが決定しました!
ゲーム終了です!!


ゲームが終了し、このように勝ったチームがメッセージとして送信され、その後にスコアシートが送信されてきます。

このゲームのスコアシートはこのような感じになっています。
シンプルですが、見やすくていいですね^^

6. 最後に

結構時間をかけて作りましたので、いいなと思ったらいいねお願いいたします!
また、これからも面白い画期的なシステムを作れたらなと思います。
React-Nativeでアプリ作りたいと思ってます〜

6
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4