PSO2で開催中の「8周年WEB連動パネルイベント」で公開されている獲得スコアを、GoogleSpreadsheet+GoogleAppsScript(GAS)を使って自動集計してみました。
WEB連動パネルイベントとは
技術記事寄りなので、前提情報をちょっと補足。
夏の7~8月と、年始の1月に定期的に、あとは不定期コラボなどでも開催されるゲームイベント。
指定されたクエストにおける全ユーザーの報酬獲得数を集計し、そのスコアに応じて後日発生するボーナスイベントの内容が決まるというもの。
累計数は30分毎に集計され、WEBサイト上に公表されていますが、集計スコアは画像化されています。

GASによる自動集計手順
30分おきにWEBサイトを見に行ってスコアを目視、数値を記録、というのを1人でやると破綻してしまうので、GASを使って自動化します。GCPまでは使ってないので無料で。
サービス稼働初期のイベント時(2013年頃)には、掲示板で有志が逐一報告したりしていたようですけどね。
ゲーム内の自動操作化は利用規約(第9条第1項)で禁止されていますが、WEBスクレイピングについては禁止されていないので大丈夫…なはず。付随サービスに入るのかな…。

フローとしてはこんな感じ。
Googleドライブで画像をGoogleドキュメントに返還して読み込むと、画像の内容を文書化してくれるという機能があるので、これをOCRとして使います。
GoogleDocsファイルはOCRが終われば不要なので削除しますが、オリジナルの画像ファイルは残しておきます。うまくOCR出来なかったときのエビデンスにもなりますし。
GASソースコードがこちら。(フォルダIDは隠しています)
function check8thAnivScore() {
const datetime = new Date();
var file_blob = getCountImage_8thAniv(datetime);
var ocr_text = scanOCR_8thAniv(datetime, file_blob);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("8周年WEBスコア経過");
var last_row = sheet.getLastRow();
// 集計→WEB更新までの所要時間(5~8分ほど)を引いてから30分単位切り捨て
var minute = datetime.getMinutes();
var calc_datetime = new Date();
calc_datetime.setMinutes(Math.floor( (calc_datetime.getMinutes()-8)/30)*30);
calc_datetime.setSeconds(0);
// シート末尾に記載
sheet.getRange(last_row+1, 1).setValue(datetime);
sheet.getRange(last_row+1, 2).setValue(calc_datetime);
sheet.getRange(last_row+1, 3).setValue(ocr_text);
var dist_formula = "=C" + (last_row+1) + "-C" + last_row
sheet.getRange(last_row+1, 4).setValue(dist_formula);
}
// 7周年イベントのカウンタ画像を取得し、Driveに保存する
function getCountImage_8thAniv(datetime) {
// 取得元URL
var url = "http://pso2.jp/players/event/8th_anniversary/webevent/panel/count.png"
// 保存先フォルダ
var folder_id = "*********************************"
var folder = DriveApp.getFolderById(folder_id)
// 保存日時(=保存ファイル名)
var file_name = Utilities.formatDate(datetime, "JST", "yyyyMMdd_HHmmss") + ".png"
var response = UrlFetchApp.fetch(url);
var file_blob = response.getBlob().setName(file_name);
var file = folder.createFile(file_blob)
return file_blob;
}
// OCRで数字画像を数値として読み取る
function scanOCR_8thAniv(datetime, file_blob) {
var file_name = Utilities.formatDate(datetime, "JST", "yyyyMMdd_HHmmss")
var resource = {
title: file_name, // ファイル名
mimeType: "image/png" // ファイルのMIMEタイプ
};
// OCRの設定
var optional_args = {
ocr: true,
ocrLanguage: 'en'
};
// 保存先フォルダ
var tmp_file = Drive.Files.insert(resource, file_blob, optional_args);
var ocr_doc = DocumentApp.openById(tmp_file.id);
var ocr_text = ocr_doc.getBody().getText();
var folder_id = "*********************************"
var folder = DriveApp.getFolderById(folder_id)
// 余計な文字を除外
ocr_text = ocr_text.replace(/^\n/, "");
ocr_text = ocr_text.replace(/[.,]/g, "");
folder.addFile(DriveApp.getFileById(tmp_file.id));
// Docsファイルは削除
var file_id = tmp_file.id
Drive.Files.trash(file_id)
return ocr_text;
}
なおGoogleDriveに画像を保存するため、GoogleAppsScriptの「リソース > Googleの拡張サービス」から、Drive APIを有効化する必要があります。

check8thAnivScore関数がメイン関数なので、これを実行すれば処理が回ります。
自動化するためトリガー設定で、30分おきの時間主導型に設定します。

ただ今回のケースの特殊なところが、30分毎に集計・公開されていると言っても、流石に向こうの処理にも時間が掛かるようで、17:30の集計内容がWEB上に表示反映されるのは17:40前後。
「○○おき」の時間主導型トリガーは、トリガー保存した時点から周期を取り始めるので、保存タイミングを調整する必要があります。
今回の場合は16分のマージンを取ってトリガー保存して、毎時16分・46分頃に取得するようにしました。
そうして集計しているGoogleスプレッドシートはこちらで公開しています。実際に動いている物なので、イベント終了まで集計し続けます。
スコア推移から見えるもの
時系列推移
この記事を書いている3週目時点でのグラフにするとこんな感じ。赤の棒グラフが30分間の増分、青の折れ線グラフが累計数の推移です。

増分0が3箇所あるのはメンテナンス中でゲームが動いていないため。
時間帯別の推移傾向
手修正を加えて別のBIツールを使って時間別の推移を見ると、19時頃から急激に伸び始めて、23時がピーク、25時を過ぎると低い水準になります。

緊急クエストによる増減具合
ただ、推移が滑らかにならず、土日19時や日曜21時などに凹みが見えるのは、「緊急クエスト」が予告発生している時間帯のため。
「緊急」が「予告」されるっておかしな話1)元々は突発的に発生する大型クエストとして売りにしていたのが、 サービス開始3年目くらいから事前にスケジュール発表されるようになりメインコンテンツ化ですが、目玉アイテムなどを求めてそちらに人が流れるのです。
緊急クエストはWEB連動イベントのスコア集計に入っていないので、緊急クエスト中はスコアが減っている、という格好。
予告緊急クエスト発生時間と照らし合わせると、こんな感じになります。予告回数が少ない(3週間中で2回以下)ものは除外。

スコアが平時(緊急クエストが無いとき、上表の「NULL」列)より伸びが鈍いところほど赤く記載しています。 緊急クエストに行っている人が多そうという指標。
直近で難易度が引き上げられた「異界に紡がれし知の化身」や「悲劇を歌う怨嗟の虚影(ペルソナ緊急)」が特に人気そう。
ペルソナ緊急に至ってはクリア後ブーストをスコア稼ぎに活用する人も多いようで、その時間帯にあたる「ペルソナ後30分」は平時以上にスコアが伸びている青文字が並んでいます。
Ship別ポイントランキングの効果
時間帯による波はありつつ、日を追う毎に緩やかに下がってきている…と思いきや、7/18に急に盛り上がりましたが、これは「Ship別ポイントランキング」集計時間に入ったため。
パネルによる累計集計と別に、個人別の集計もされているのですが、7/18午後から7/19午前の24時間は、個人スコアをShip別に集計した順位を競うというもの。
1週目土日の同時間帯は8万pt/時ほどまで減っていたのが、対抗戦期間中の2週目は45万pt/時を割り込まなかったので、寝る間を惜しんで稼いだ人が結構居たのでしょうね。
頑張る人は頑張るのだろうとは思っていましたが、深夜帯の稼働を5倍近く上げるというのは予想以上で驚きました。
ちなみに第1回の10位ボーダーを見て回ったところ2)これはゲーム内でしか参照できないので手動、Shipによって差はありますが、見た限りだと12000~16000ptくらいでした。
普通に回ると7分くらい毎に20ptほどですが、 最低でも毎時500ptペースということは、トリガーアイテム3)特別なクエストを受けることが出来る消費アイテム。こちらだと4分で60ptくらい稼げるを使い続けたりしたのでしょう。
8/1午後~8/2午前に第2回が開催予定ですが、毎月2日は更にブーストされる「PSO2の日」とも重なっているので、これ以上の伸びが見られるかもしれません。
ゲームも、個人プログラム・データ分析も、まずは自分のため、自分の手を伸ばせる範囲で楽しみながらやってます。
他にやってることだと、週次のスケジュール管理をGoogleフォーム+スプレッドシートで管理するとか。
PSO2以前にはゲーム攻略サイトを作ったりもしてましたが、手を広げるのに疲れてしまったので、今はこうやってニッチに細々と。広げ続けられる人は偉いと思う。
新型コロナウイルスの経過とかも、興味はあるけど、実際にどうこう出来る立場には居ないですからね。それは最前線の人に任せつつ。
でもこうして積み重ねながら、チャンスが降って来たときに掴める力は蓄えておきたいなと。
補足
| 1. | ↑ | 元々は突発的に発生する大型クエストとして売りにしていたのが、 サービス開始3年目くらいから事前にスケジュール発表されるようになりメインコンテンツ化 |
| 2. | ↑ | これはゲーム内でしか参照できないので手動 |
| 3. | ↑ | 特別なクエストを受けることが出来る消費アイテム。こちらだと4分で60ptくらい稼げる |
