天下一甲子園 〜10周年でサービス終了するソシャゲに打ち上げる最後の花火〜

この記事はTech KAYAC Advent Calendar 2024の25日目の記事です。

こんにちは、@commojunです。

www.kayac.com

私がサーバサイドエンジニアとしてずっと従事してきたソーシャルゲームサービス「ぼくらの甲子園!ポケット」が、2025年1月8日でサービスを終了します。

prtimes.jp

ぼくらの甲子園!ポケット(以下ぼくポケ)開発チームでは、これまで遊んでくださった皆様への感謝を伝えるため、2024年10月1日から、「天下一甲子園大会」という特別なイベントを開催していました。そしてつい数日前の12月22日、イベントのすべての内容が終了しました。

この天下一甲子園大会というイベントの開発は、たった3ヶ月のイベントのためにこれまでの運用の10年間、いっさい手を加えられなかった聖域みたいな箇所に多くの破壊的な変更を入れ、ほとんど一発勝負でリリースするというようなかなり挑戦的な開発でした。

本記事では、このイベントがどのようなイベントなのかと、開発の振り返り・感想を書いていきます。

前提となる説明

天下一甲子園とは何か…を説明するためには、まず、ぼくポケの①試合の仕組み、②ブロックとリーグという概念について、③甲子園大会についてを軽く説明しておく必要があります。

①試合の仕組みについて

ぼくポケは高校野球を題材としているので、やはり野球の試合がメインのコンテンツとなります。他のスポーツゲームと比べてぼくポケが特徴的である要素なのですが、試合の進行は、すべてサーバサイドのバッチサーバが担っています。例えば、ある打席で、選手がヒットを打った、三振になったなど、試合の進行の全ての要素はバッチサーバが抽選し、結果を決めます。

この時、ユーザが出来るアクションは、試合進行バッチの実行が予定される時刻の10分前から開始される作戦会議の時間に、パワーやミートといったキャラクターの持つ能力値を上げたり、逆に敵の能力値を下げたりするスキルを選択してセットすることや、そもそも試合開始に備えてキャラクターの能力値を「練習」であらかじめ鍛えておいたり、ガチャを引いて強いやきゅう道具を揃える・強化しておく、などといった行動です。

とにかくゲームの心臓部となる部分はサーバサイドのバッチが担っていて、キャラクターの持つ能力値をベースとした抽選が、ヒットや三振といった試合進行の要素を決定する根幹部分であるということです。

②ブロックとリーグについて

ブロック

ユーザは必ずどこかの学校(以後チーム)に所属することになります。チームには基本的に最大15人まで所属でき、9人(投手1人、野手8人以上)以上が所属することで、初めて試合に参加できるようになります。チームは、関東、関西、東北といった地理的な区分を表すブロックの中に存在しています。ユーザは自分に馴染みのある地域からチームを選択して参加できます。ぼくポケにおける大半の試合は、主にこのブロック内に存在するチーム同士で行われます。

リーグ

さらにブロックの上位概念として、ビギナー、ミドル、マスターという3つのリーグがあります。新規でゲームを開始したユーザは必ずビギナーリーグのチームに所属します。使用するキャラクターの能力値が条件となる値を満たせば、ミドルリーグ、マスターリーグのチームへ移籍できるようになります。リーグは階級のような概念になっており、異なるリーグに所属するチーム同士が試合をすることはできません。ビギナーリーグ、ミドルリーグには、試合時に発揮される能力値に上限が設定されています。鍛えたキャラクターの能力を存分に発揮させるためには、上位リーグのチームに移籍して試合に出場する必要があります。また、試合の勝利報酬は、上位のリーグほど多く設定されているため、ゲームに慣れてきたユーザは上位リーグを目指していくような設計になっています。

リーグの棲み分け現象

上位リーグを目指していく設計、と説明しましたが、ゲーム内の実態はそのようにはなりませんでした。実際には、それぞれのリーグに課された能力値の制限(と、スキル発動数などの制限もあります)が、それぞれのリーグでの試合に独特の作戦を生み出し、必ずしも上位リーグの試合のほうが楽しいというわけではなく、遊ぶ人それぞれのプレイスタイルに応じたリーグの棲み分けという現象が起こりました。過去、運営チームは様々な方法で、上位リーグに移籍することを促すような施策を行ったことがありますが、決定的な効果は無く、リーグの棲み分けはぼくポケ内の文化のように定着しました。

③甲子園大会について

ぼくポケは、2週間を1シーズンとして甲子園大会を開催していました。シーズンは、予選リーグ期間と、甲子園大会本戦で構成されます。予選リーグ期間では、同一ブロックに所属するチーム同士で1日最大4回の試合がスケジュールされ、その勝敗を基にブロック内のランキングが作られます。そして、シーズン最終日が甲子園大会本戦日となっており、各ブロックのランキング上位から選ばれたチームが本戦トーナメントに出場し、チャンピオンとなるチームを決定します。3つのリーグそれぞれでトーナメントが開催されそれぞれのリーグのチャンピオンとなるチームが計3つ、2週間毎に誕生します。

以上が大雑把ではありますが、天下一甲子園の説明の前提となるぼくポケの仕様です。10年間の運用を通して、部分的な調整はあれど、その根幹部分は変更されることがなかったとても重要な仕様です。

天下一甲子園大会とは

それでは、天下一甲子園とはどのようなイベントだったのでしょうか。お察しの通り、上記で説明した重要な仕様を壊していくことになります。

一言で説明するならば、リーグを超越して試合を行い、ゲーム内の真の最強(天下一)のチームを決定する大会です。

もう少し具体的には、以下のような大会です。

  • シーズンの予選期間では、リーグの異なるチーム同士(場合によっては同一リーグ同士)が試合を行い、勝敗に応じてリーグごとにランキングがつけられる
  • 天下一甲子園大会の本戦は2日にわたって開催される
  • 天下一甲子園大会本戦1日目は代表決定戦と呼ばれ、リーグ内のランキング上位チーム同士がトーナメント戦を行い、各リーグ代表チームを1つ決定する
  • 天下一甲子園大会本戦2日目は決勝トーナメントとなっており、各リーグの代表3チームがトーナメント戦を行い、最強のチームを1つ決定する

ビギナーリーグだから弱い・マスターリーグだから強いというわけではなく、遊びの違いによる棲み分けという文化が発生していたことを鑑み、「じゃあ実際に戦ったらどのリーグが一番強いのか決めようじゃないか!それで天下一を決めて終わろう!」といったことがテーマになります。

他にも、

  • 予選期間の一部の試合では、試合の当事者(出場者)ではないユーザも試合を観戦できる
  • 観戦者は試合の出場者に対して応援を送ることができる

という仕様もありました。こちらの実装もなかなか工夫をこらしていて面白かったのですが、今回は既存仕様の変更の話に重きを置こうと思うので、説明は省きます。

では、これらのことを実現するためにはどうしなければならないでしょうか?

リーグをまたいだ試合を実現するには

「異なるリーグに所属するチーム同士が試合をすることはできない」

この仕様に立ち向かわなければなりません。発揮できる能力値の上限がリーグによって違うので、そのまま戦っても上位リーグが勝つだけです。いわばルールが違います。ルールが違う者同士を、お互いの納得の行く方法で対戦させるにはどうすればいいでしょうか。そのために次のような方法を取りました。

  • 天下一甲子園は全3シーズンにわたって開催する
  • 1シーズン目はビギナーリーグ(ビギナールール)、2シーズン目はミドルリーグ(ミドルルール)、3シーズン目はマスターリーグ(マスタールール)の能力値上限で試合が行われる

このように、それぞれのシーズンで、対応するリーグのルールに合わせて試合に出場するキャラクターを準備してもらうようにしました。

下位リーグのユーザは上位リーグ向けのキャラクター育成を、上位リーグのユーザは下位リーグ向けのキャラクター育成をしていない問題

ビギナーリーグで主に遊んでいるユーザは、ビギナーリーグの能力値上限を超えてマスターリーグ向けのキャラクター育成をあまりしていないので、マスターリーグのルールで試合を行ったら勝てないだろう…という予想が容易にできるかと思いますが、実はその逆も然りです。

キャラクターはただ育てれば良いのではなく、各リーグの特徴に合わせて、取るべき育成方針が異なります。キャラクターの育成には(かなりインフレしたとはいえ)それなりに時間がかかるので、この不公平を解消するために、各リーグのルール向けの能力値の配分を自由に設定できる機能を新たに作成し、既存の能力値とは別のデータとして保存することにしました。なぜ既存の能力値を上書きできなかったかというと、キャラクターの能力値は、成長曲線やキャラランクといった別のパラメータと密接に結びついており、直接書き換えることによる影響が大きすぎると考えられたためです。

天下一甲子園の試合では、この新規仕様によって新たに設定された能力値を使って試合を実行します。

今自分でこれをまとめていても、とんでもないことをやってるなと思うのですが、結果的にこの選択は成功に繋がっています…。

どんな仕様変更をしなければならないか

改めて、既存の仕様をどう変更しなければならないのか、おおまかにまとめます。

  • 予選期間の試合は、リーグを超越して対戦相手をマッチングするように変更する
  • ブロックで閉じていたランキングをリーグ全体のランキングに変更する
  • ゲームの心臓部となるバッチのロジックで、現在開催中の天下一甲子園に設定されたルールに応じたキャラクターの能力値上限がかかるように変更する
  • ゲームの心臓部となるバッチのロジックで、キャラクターの能力値を今回新設したデータから参照するように変更する
  • (天下一)甲子園大会本戦を2日間にわたって開催されるように変更する
  • 決勝トーナメントは3チームのトーナメントのため、(今まで存在しなかった)シードを実装する

なんとなく、これらがどれくらい重大な仕様変更なのかを感じていただけましたでしょうか?これらの変更どれもが、ぼくポケのリリース以来、10年間誰も触れることのなかったロジックに大胆なメスを入れるとんでもない変更でした。

たった3ヶ月のイベントのためだけのこの開発(+観戦・応援機能)を、サーバエンジニア2人で、約半年で、やり切りました。

そして10月にリリースをし、完全無事故とはいきませんでしたが、大会を延期したり中止したりするような重大な事故は起こさずにイベントを完遂することができました。

なぜやりきれたのか

改めて俯瞰して見ても、10年物の巨大プロジェクトの終わり際に、こんな仕様変更を突きつけられたら担当エンジニアは普通、全力で拒否して、事故が起きたときのリスクを延々と話し始めると思います。もしくは逃げます。

ではなぜやりきれたかというと、これは自分たちで考えた施策だからです。

2023年の暮れに、現プロデューサーのibukiが、10周年を最高に盛り上げて、感謝でゲームの運営を終えられるにはどうしたらいいか、開発チーム全員に問いを投げかけてくれました。そうして全員で、こうしたらどうか、ああしたらどうかとブレストを繰り返し、最終的に生まれたのが天下一甲子園という施策です。俺が俺がとワンマンで施策を出すのではなく、全員で考えた結果で一丸となってやっていくという土壌を作って旗振りをしてくれた彼の手腕であり人柄のおかげです。だからこそ最後まで気力を失わず開発をやり遂げられました。

ただ熟練度が極まっていた

それでは、開発やりとげるために行ったスマートな工夫や、大きな事故なくイベントを完遂できた秘訣にはどんなものがあるでしょうか?そんなものはありません。ただ愚直に開発し、愚直にコードレビューし、愚直に影響範囲をしらみつぶしに修正しただけです。大事故が起こらなかったのも運が良かっただけでしょう。ただ、開発をやり切り、事故の対応に冷静に当たるための熟練度は極まっていたと思います。

私は新卒でカヤックへ入社し、そのほとんどのキャリアをぼくポケのサーバサイドエンジニアに注ぎました。天下一甲子園の開発を一緒に担当したもう一人の方も、ぼくポケの10年の運用の歴史の大半を知る方で、ともに数々の機能改善や修羅場を乗り越えてきました。そのおかげか、ぼくポケの歴史上でサーバサイドのコードを最も脳内に把握している2人になりました。今まで本当に多くの優秀なサーバエンジニアの方が運用に携わってくださいましたが、ここまで飽きずに一つのプロダクトに携わり続ける人間は他にいなかったと思います。

ここまでぼくポケのサーバサイドの熟練度が極まっていると、サーバサイドのどの部分のロジックについても、2人のうちどちらかは必ず脳内に把握しているというような状況になっており、上記の巨大な仕様変更についても、案が上がった時点でおおむねどこを修正すれば実現可能で、影響範囲がどれくらいかを予想することができました。この熟練度があってこそ実装に踏み切ることが出来た施策だと思います。

リリース後、いくつか不具合が起きてしまったのですが、どの不具合でも、発生した現象を焦ること無く淡々と観察して理解した数秒後には対策の方針が立てることができました。このスピードの速さも、自分の知るぼくポケの運用の歴史の中で最速でした。

この2人での開発やトラブルシュートの体験は、さながら最強武器でラスボスのダンジョンに挑むRPGの終盤戦のようでした。

最後に

繰り返しになっちゃいますが、新卒でカヤックへ入社し、そのほとんどのキャリアをぼくポケのサーバサイドエンジニアに注ぎました。エンジニア用語もろくに知らず、右も左もわからない状態から本当に多くの人に叩き上げてもらって最終的に10年もののサービスの終了を見届ける立場となりました。

「飽きないの?」と聞かれることも何度もありましたが、同じプロジェクトに従事し続けていても、その時その時に直面する課題は違っていて、飽きる暇なんてなかったように思います。大きな機能開発、チームビルディング、ときには企画も出しましたし、Perlのアップデート、ECS移行など、経験させてもらったことは本当に多岐にわたります。それらの経験一つ一つが全て合わさり集大成となった最後の開発が天下一甲子園だったように思います。コミットが10年以上前(つまりリリース前)の箇所のコードを本当にいくつもいくつも書き換えました。10年間の走馬灯を見ているような開発でした。本当に多くの人と多くの歴史に支えられ、愛され続けたゲームだったんだなあと思います。

ぼくらの甲子園!ポケットは、私を一人前のエンジニアに育ててくれたプロダクトであり、10年後、20年後にキャリアを振り返った時、必ず名前が挙がる、エンジニア人生の代表作になったのではないかと思います。

カヤックではエンジニアを募集しています。あなたの代表作は何ですか?

勝手にAIがプログラム書いてくれたら嬉しくない?

この記事は【カヤック】面白法人グループ Advent Calendar 2024の24日目の記事です。

おはこんばんちは。らりです。
2週間前に給湯器が壊れ、水を浴びながら生きてます。
インフルエンザも流行ってるし仕方ないですね。

はじめに

近年ではAIが進化し、もはや人間と区別がつかなくなってきました。
昨今のAIについては色々議論が巻き起こっていますが、本当に仕事を奪われそうでSFじみてきましたね。
ロボット工学三原則には「仕事を奪ってはならない」とは無いので、どこかで付け足されるのでしょうか。

ところで、簡単なプロジェクトを淡々とAIに作ってもらう検証をしてみました。
お題は電卓で、色々機能を付け足してもらえるかなと思いました。

用意

方法としては、

  1. 定期的にAIにコードを生成させる
  2. 勝手にPRを立ててもらう
  3. 勝手にページをビルドしてもらう
  4. 勝手にマージしてもらう

って順番です。
生成AIはGeminiを使い、勝手に色々するのはGitHub Actions、ページはGitHub Pagesを使います。
Geminiの生成間隔と、Actionsの実行時間を考慮すれば、全部タダです!万歳!

方法

ファイルの配置は以下のようになります。

git.ts
src/
  index.html
  ts/
    index.ts
  style/
    style.scss

git.tsの中には、コード生成からPRを立てるところまでを記述します。
PRを立てるのにはOctokitを使いました。

git.tsを走らせるためにcronを使っているのですが、課金にひよって30分に1回実行するようにしてます。
ただ、コードを変更して実際に動くか確認したいため、workflow_dispatchを用意しておくことで、GitHub上でボタン1つで実行出来るようにしてます。 docs.github.com

プロンプトの作成にはここを参考にしました。 dev.classmethod.jp 以下がプロンプトです。

下記に幾つかの要求のポリシーを示します。これに従って回答してください。

- 回答は下記JSON SCHEMAに合致するJSON形式で実施してください。
- 他の受け答えや回答は不要です。
- 返答はJSONのみで返してください。codebaseの表記に沿う必要はありません。

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "html": {
            "type": "string"
        },
        "ts": {
            "type": "string"
        },
        "css": {
            "type": "string"
        },
        "description": {
            "type": "string"
        },
        "title": {
            "type": "string"
        }
    }
}

- 上記JSON SCHEMAのプロパティの意味は下記です
    - html: HTMLコード ファイルパスは'/src/index.html'です
    - css: SCSSコード ファイルパスは'/src/style/style.scss'です
    - ts: TypeScriptコード ファイルパスは'/src/ts/index.ts'です
    - description: PRの説明文
    - title: PRのタイトル

以下の<codebase/>はコード内容です。

--------------------------------------------------------

<codebase>
\`\`\`html
${fs.readFileSync("src/index.html", "utf-8")}
\`\`\`

\`\`\`ts
${fs.readFileSync("src/ts/index.ts", "utf-8")}
\`\`\`

\`\`\`css
${fs.readFileSync("src/style/style.scss", "utf-8")}
\`\`\`
</codebase>

これらをコード踏まえ、下記の要求に合致するコードを生成してください。
- 絶対条件として、電卓が表示されるページを作成してください。
    - webページで見たり触れたりできるようにしてください。
- 既存コードから何か1つ機能を追加してください

AIは欲しい答え以外に受け答えもするので、それは全部やめていただきます。
また、その下に自分の求めている内容を記述します。

今回であれば、常に新しい機能を足してもらいたいので、それも追加しています。
これにより、いつ見ても新たな機能が足されている電卓が出来上がる想定です。

このプロジェクトでは、PRを立てるためのキーは自分自身ではなく、1つGitHub Appを作り、そのキーを用いています。 検証してませんが、自分自身のキーを使うと、コミットやPRによる草が生えてしまうかもしれないと思ったからです。

また、マージにはsecrets.GITHUB_TOKENを使っています。 Appなどのbotのマージは自動で行われないため、GitHub Actionsで行う必要があります。 docs.github.com マージ後も勝手にデプロイを行えないため、マージ前に行ってしまいます。  github.com

これらにより、コードの自動生成からPRを立て、デプロイまで走って勝手にマージされます。 終わったブランチは勝手に消えるので、立つ鳥跡を濁さずという感じです。

運用中の感想

全然上手くいかない...

というより、プロンプトが薄すぎて、出てくるコードもふわふわしたものが多い気がします。
また、cssを全然書いてくれないので、見た目が全然電卓になりません。

この辺りは足りないプロンプトを追加していって、ちょっとずつ理想に寄せていく必要があるのかなと思います。

ただ個人的に、AIにテキストベースの成果物を出してもらって何かするっていうのはどこかに応用出来そうな気がしてます。
正誤はともかく、処理に莫大な時間がかかるものとかには代替出来そうな感じがします。
量子コンピュータみたいですね。

カヤックではAIについて考えるエンジニアも募集しています