タイムカードアプリに「ねこ」を住まわせた話 第1回 ― ご褒美システムの仕組みを作る

自分用のタイムカードアプリ「MyTIMECAT」に、タスクをがんばった分だけねこが着飾っていくご褒美システムを実装しました。この記事はシリーズの第1回として、まず「仕組みそのもの」をどう設計し、どう作り直していったかをまとめます。

やりたかったこと

タスクの実行時間を記録するだけだったアプリに、「毎日ちゃんと目標時間を達成できたら、ねこにごほうびをあげられる」というモチベーション装置を足そうと思い立ちました。

最終的な仕組みはこうです。

  • その日にスケジュールされたタスクの実績合計が目標を上回れば「達成日」
  • 達成日が7日分たまるごとに、ねこのおめかし(首輪→リボン→王冠)かえさ(カリカリ→ちゅーる→ささみ→猫草)のどちらかを1つ選んで解禁できる
  • 達成が30日連続続くと、伝説のねこが仲間になる

週間集計→日単位の積み上げへ

最初は「週間達成」という単位で設計していました。ところが実際に使ってみると、「今日より前の日付を選んでタスクを追加したときも、その日が達成率100%なら⭐を付けてほしい」という要望が出てきました。つまり、後から過去の記録をいじっても、正しく反映されてほしいということです。

そこで、日単位の達成を積み上げていく方式に作り直しました。

「毎回ゼロから全期間を再計算する」という設計判断

ここが今回の設計の肝だったと思っています。

最初、evaluateRewards()(達成状況を再評価する関数)は「前回評価済みの日から順に、1日ずつ確定していく」方式でした。しかしこれだと、過去日への遡り追加に弱いという問題が残ります。

そこで思い切って、「呼ばれるたびに、追跡開始日から現在まで丸ごと再計算する」方式に変えました。

達成日リストを空にする
trackingStartDateから今日まで1日ずつ見て
  その日の実績が目標以上なら達成日リストに追加
  (今日だけは「今まさに達成しているか」をその都度チェックし、まだ確定させない)

一見非効率に見えますが、過去日の追加・編集・削除が起きても、次に呼ばれたときに勝手に正しい状態へ収束してくれるので、差分計算のための複雑なロジックを一切書かずに済みました。個人開発の規模であれば、この「毎回全部やり直す」の割り切りはかなり効きます(記録が何年分も溜まってきたら再検討が必要かもしれませんが、それは将来の自分に任せることにしました)。

副産物として、trackingStartDate(集計対象の開始日)自体も自動で過去に拡張されるようにしました。当初は「初めてevaluateRewards()が呼ばれた日」で固定していたのですが、これだと「アプリを使い始めた後に、それより前の日付の記録を追加しても集計対象外になる」というバグがありました。記録に存在する最も古い日付まで自動的に遡って追跡開始日を広げる(一度広がったら縮まない)ようにしたことで解消しました。

プレゼントの解禁ロジック

達成日数を7で割った商が「これまでに解禁されているべきプレゼントの総数」。これを「実際に解禁済みの数」(単調増加のみ、どんな事情でも減らない)と比較して、差があればその場で埋める、というシンプルな仕組みにしました。

  • おめかし・えさの両方にまだ枠が残っていれば、ユーザーに選んでもらう
  • 片方しか枠が残っていなければ、選択なしで自動解禁
  • 両方解禁済みなら何もしない

後日、3つ目のカテゴリ(かざりつけ、後に廃止)を追加する話が出た際も、カテゴリを配列(REWARD_CATEGORIES)にして汎用ロジックで回すように書いていたおかげで、追加・削除ともにロジック自体はほぼ変更せずに対応できました。「最初から3択目線で作っていたわけではないけれど、決め打ちを避けておいたら後で助かった」という好例です。

一度手に入れたものは失わない

プレゼントや連続達成記録は、後から記録を削除して未達成に戻しても取り消されない設計にしています。「せっかく手に入れたごほうびが、記録整理の副作用で消える」という体験を避けたかったためで、プロトタイプとして割り切った判断です。

曜日ベースの達成判定

達成可否は、タスク側の「実行する曜日」設定を基準にしています。曜日設定に含まれない日は、そのタスクにとって「そもそも目標なし」として扱う形です。これが本当にユーザーの時間管理スタイルに合っているかは今後の検討課題として残していて、いったんは動くものを優先しました。

まとめ

仕組み部分でいちばん効いた判断は、「差分更新をがんばらず、毎回全部を再計算する」というところだったと思います。複雑な同期処理や取り消し処理を作り込まずに済み、後続のドット絵の合成表示や外部サービス連携(次回・次々回の記事で書きます)に集中する余裕が生まれました。

次回は、ねこに着せるドット絵をどう作って、どう合成表示させたかについて書きます。

Diary の記事一覧へ戻る