学習の備忘録です。タスク管理アプリを作っています。
今回は、スタンプ獲得機能を実装する際の仕様について、その考え方を思考の整理がてら書いていきます。
「実際にこういうコードで実装する」みたいなものではなく、考え方の記事なので具体的なコードはありません。(そういう実装手順についてはまた後日記事にするつもりです。)
現状でのスタンプ獲得の仕様については以下です。
- 達成したタスク数/その日の全体のタスク数*100で達成率を割り出す
- 達成率100%ならスタンプ獲得
- スタンプが10個溜まったら「ごほうびチケット」を獲得
本記事は個人の学習記録です。内容の正確性を保証するものではありませんので、あらかじめご了承ください。
1日に何度も獲得できてしまう問題
一日のタスクの達成率は、現状以下のように計算しています。
//達成率計算
public static function calAchievementRate($tasks) {
$total = $tasks->count();
if ($total === 0) {
return 0;
}
$completed = $tasks->where('is_completed', 1)->count();
return (int)floor(($completed / $total) * 100);
}
要するに、その日一日のタスクの合計を出して、そのうち達成しているタスク(is_completed)がいくつあるかで割合を出しています。
そして達成率が100%になったらスタンプを獲得できる仕組みです。
ただ、スタンプの獲得については制御をかけないといけません。
達成率100%になった時点でスタンプ獲得させると、以下のような問題が起きてしまいます。
①最初に決めていたタスクを全部完了させてスタンプ獲得
まずは一番最初に入っているタスクを完了させます。
以下の画像では、毎日自動で登録される「日課タスク」を完了させた状態です。タスクが1つしかないので、それを完了させた時点で達成率は100%になります。

達成率100%時点でスタンプ獲得の設定にしてしまうと、この時点でスタンプを1つ獲得することになります。
②追加分のタスクを完了させることでスタンプ獲得
現状では、当日のタスク変更は「編集チケット」の消費が必要です。
ただ、「当日の日付で、タスクが日課タスクしかない場合は、変更チケットの消費なしでタスクを追加できる」という仕様になっています。
よって、もしも前日にその日のタスクを登録していない場合は、「タスクを登録する」ボタンから、編集チケットの消費なしでタスクを登録できます。
すると全体のタスク数は3つになるので、達成率は33%となります。


そうやって増やしたタスクを全部完了させると、再び達成率が100%になり、またスタンプが獲得できます。
③変更チケットが続く限りスタンプ獲得ができる

その後も、編集チケットが続く限り達成率の操作ができるので、一日に何度もスタンプを獲得することができます。
スタンプを10個獲得したら「ごほうびチケット」を獲得できますが、1日に何回もスタンプを獲得できるなら、ごほうびチケットも簡単に手に入ってしまいます。
タスク管理アプリは「毎日継続して頑張っていく」という感じにしたいので、こういうスタンプの獲得方法は趣旨に合いません。
ということで、「1日に何度もスタンプを獲得できないようにする」という仕様で設計していく必要があります。
そこで、とりあえず以下の3つの方法を考えました。
1.深夜のバッチ処理で達成率を確認する
たとえば日付が変わった瞬間(00:00)に達成率を確認して、もしも100%だったら、次にアプリが開かれた時にスタンプを獲得します。
「〇日のタスク完了! おめでとう!」などの演出と共に、スタンプカードにスタンプが押されるアニメーションが入るイメージです。
メリット
- システム的にデータの矛盾が起きない
- アプリを開いた瞬間に褒められることで、継続のモチベーションに繋がる
デメリット
- 全タスク完了時点(リアルタイム)でスタンプ獲得できないので、もどかしい部分も
2.1日1個以上スタンプ獲得できないようロックをかける
最初に達成率が100%になってスタンプを獲得したら、それ以降に達成率が変動して再び100%になっても、スタンプが獲得できないようにします。
メリット
- リアルタイムの達成感を維持できる
デメリット
- 仕様の矛盾(矛盾したデータ)が生まれやすい
「今日の分のスタンプは獲得したのに、その日の達成率は33%しかない」みたいな事態になる。見た目ちょっと気持ち悪い。
3.「タスク完了」ボタンを付ける
画面下部などに「タスク完了」ボタンを設置し、タスクが100%になったらユーザー自身がそのボタンを押下し、その日のタスクの完了を確定します。自己申告方式です。
- 達成率が100%になるまで、ボタンはグレーアウトさせる(クリックさせない)
- 達成率が100%になったら押下でき、「完了しますか?」モーダルが開く
- 「はい」を押したらスタンプ獲得
- 一度完了させたら「タスク完了」ボタンは非表示にする。また、編集ボタンもグレーアウトさせる(完了後に達成率を操作できないようにする)
こんな感じで制御します。
メリット
- 自分の手で行うので達成感を得やすい
- タスク追加との相性がいい
※ユーザー自身が、「まだ後でタスクを増やすかもしれないから、完了ボタンは夜に押そう」とコントロールできる
デメリット
- 達成率100%でもタスク完了の申告を忘れる場合があり、やる気低減に繋がる
まとめ:深夜のバッチ処理方式を採用
3つの方法の中で、私はバッチ処理方式での実装を選択しました。
理由としては、以下が一番大きいです。
- 画面制御(ボタンの表示非表示など)の分岐が少ない
- バグが一番起きにくい
処理の流れとしては、以下のようになります。
- 深夜00:00(バックエンド処理)
昨日100%を達成したユーザーの「達成スタンプ数」を+1し、「スタンプ獲得のアニメーション演出をしたかどうか」の判定でfalseを入れる - ユーザーの次回ログイン
ユーザーがアプリを開いた際、コントローラーで「スタンプ獲得のアニメーション演出をしたかどうか」の判定を確認。 - フロントエンドでの演出
もしも「スタンプ獲得のアニメーション演出をしたかどうか」の判定がfalseなら、画面が開いた瞬間に「昨日は達成率100%おめでとう!」などのモーダルを自動表示して、スタンプがポンッと押されるアニメーションを実行。
その後「スタンプ獲得のアニメーション演出をしたかどうか」の判定をtrueに変更。
こんな感じで実装すれば、「1日1回だけスタンプを獲得する」という仕様を自然に実現できそうです。
また、これらの実現のためにはスタンプ獲得の履歴テーブルが欲しいので「stamp_histories」などのテーブルを新規で追加するつもりです。
実際に機能付けたら、それはそれで記事にしようと思います。


コメント