点数付けの導入でタスク管理を楽しくする[Emacs拡張]

まず趣旨!この記事はタスク管理を楽しく行なって作業能率をあげようっていう内容です。
あえてEmacsを知らない人も想定して書いていますが、この記事はEmacs Advent Calendar jp: 2011の16日目になります。15日目は[twitter:@g177564]さんのmultiple-value-blog1: Emacs Lispで式単位のコメントアウトでした。

はじめに

僕は先送り癖がひどく、テスト勉強やレポート、論文なども締切りギリギリまで行動できないタイプ。対策本を読んだ所、計画をしっかり立ててやることを明確にすると、初めの一歩を踏み出す先がわかって行動できると書いてあり、それはそうだと思う。だが、言うは易しというところで、自分の場合はあまり効果的には習慣付けられなかった。
しかし、このタスク管理をゲーム感覚で楽しめたらどうだろう?
以下のちょっとしたカスタマイズを導入した所、毎日計画を立てたり見直しをしたりするようになり、その結果として先送り癖がわりと軽減された気がする(あくまで主観)ので記事にまとめる。

本題

TODO管理を行うソフトは数多く存在する。
例えばテキストエディタであるEmacsのorg-modeにはTODO管理の機能が備わっており、タスク管理にも使うことができる(org-modeでTODO管理 - handlename's blog)。org-modeにおけるTODO管理は、タスクの終了度合いとして[X%完了][全XX個中X個完了]のような表示ができるのだが、他のソフトでもままあるように、各タスクの難しさや重要さがそれほど重視されていない。
その日はどれだけ頑張ったかを示す絶対的な指標、つまりスコアが欲しいと思った。合わせて、他の日と比べられたり、自分がどれぐらい頑張ってるかなどがわかるとタスクをこなすのが楽しくなるかも、というのが基本的なアイディア*1

やったこと

よりよいTODO管理のために、Emacsのorg-modeを拡張することにした。
具体的には、各タスクに S,A,B,C,D,E の5段階の指標を設けてその日の合計スコアを出すようにした*2


ふだんの見た目としてはこんな感じ*3



ついでなので、その日のスコアが過去のデータ中何番目にあたるかという情報を出し、さらに直近のスコアに勝つにはあと何ポイント必要かという情報も表示できるようにした。
しかしこれだと一日が終わるまで他の日と正確に比較ができず、タスクへのモチベーションを高めるという意味では不十分だと感じる。
なので、とりあえず試験的に朝8:00から夜24:30までを一日の活動時間として、単純に今の時刻と達成具合からその日の達成度を予想することにした。


コード

記事の最後に載せておきます。

使い方

だいたいは図にあるような感じでツリーを構築していってもらえば使えます。スコア計算の対象となる書式は以下*4。重要度を示す文字はS,A,B,C,D,Eが対象。

+ [ ] A:書式はこんな感じで、これは重要度Aの場合
スコアリング自体は C-c C-c によるチェックボックスのトグル関数にくっつけているので、チェックボックスをトグルした際に自動的に計算を行う。もう少し細かく言うと、現在位置のレベルの最初から次のレベルまでをまとまった区間として計算する。計算時はネスト(入れ子)も特別な扱いはしない*5
結果は、 "タスク完了分の得点/全タスクの得点" でミニバッファおよび親のアウトライン行の末尾*6に表示。




過去のスコアとの比較は、コマンド C-c C-; で行う。比較のためには、同じバッファ内で過去の得点が見えるようにしておく必要がある。
結果はミニバッファに表示する。結果の読み方は以下の通り。
score:[現在点/最大点] rank:[現在順位/全体数] ~次のランクまで必要なスコア || expected:[このペースで予想されるスコア(予想順位)] || day:[活動時間に対する現在時刻%] time:[経過時間/残り時間]

期待している効果

毎日のタスク達成度をスコアリングすることで、タスク管理を楽しくできて色々捗るかも。期待している効果は以下。

  1. 計画を立てることが楽しくなる(点数配分を考えることが新たな楽しみ要素)
  2. タスクを達成することが楽しくなる(タスクを達成すると点数が増えるから)
  3. ランキングによりタスク達成へ頑張る気持ちが生まれる(他の日の得点と比べられるから)
  4. スコアリングによってタスクの重要度が顕著になる(上の例だとタスクSはタスクEの32倍大事)
  5. 予想スコアや現在時刻と残り時間を表示することで時間を意識し、先延ばしが軽減される

そもそも計画を立ててうれしいことに関しては、

  • 計画を立てることで自分の今の状態がはっきりする
  • タスクの手順を明らかにすることで心理的負担を軽減させる

計画の立て方

うまい計画の立て方というのはやはり存在する。一応個人的に注意していることは以下。

  • タスクは必ず一定の基準を持って細分化する(サクサク感を得るため)
  • やりたくないタスクは間に簡単なタスクを挟む(はじめの一歩を簡単に)
  • 計画にないものでもやったことは評価する(減点法でなく加点法を採用する)

まとめ

ゲーム化のデザインに関してはもっといいものがあると思うし、表現ももっと色々できそう。例えば日毎のスコアの推移のグラフを出してみるとか、あるいは一日のうちのタスクをどれぐらいのペースでやっているかとか、どのタスクにどれくらい時間がかかったかを可視化するとか。
Emacsを使ってなくてここまで読んでくださった方は、これを機に導入してみるのもいいんじゃないでしょうか。

コード

以下を.emacsに記述する。自分の使用スタイルでしかバグを見てないので、環境や使い方によっては動かないかもしれませんが、その場合はごめんなさい。
基本的にはバッファ内の規定の書式を正規表現で検索して計算しているので、ツリーを非表示にしていたり独立したバッファに分割して表示していたりするとうまく動きません。

(defun org-my-calculate-score-insert ()
  (interactive)
  ;; 現在位置を保存、終了位置を探す(空行がなければバッファの終わりを代入)
  (let ((pos (point)) (score 0) (max 0)
        (end (string-match "^\*" (buffer-substring (line-beginning-position) (point-max)))))
    (if (not end) (setq end (point-max))
      (setq end (+ (line-beginning-position) end)))
    ;; レベルの始めから、優先順位付きのチェックボックスを探し、scoreとmaxに格納
    (outline-backward-same-level 0)
    (while (and (search-forward-regexp "^\\W*\+ *\[[ X-]\]\\W*[SABCDE]:" nil t) (< (point) end))
      (backward-char 1)
      (let ((pt 0) (cchar (preceding-char)))
        (if (eq cchar (string-to-char "S"))
            (setq pt (expt 2 5))
          (setq pt (expt 2 (- 5 (- cchar (- (string-to-char "A") 1))))))
        (message (format "found: %s, %d" (string cchar) pt))
        (when (string-match "\\[X\\]" (buffer-substring (line-beginning-position) (line-end-position)))
          (setq score (+ score pt)))
        (setq max (+ max pt)) ))
    ;; 何らかの点数があれば更新する
    (goto-char pos)
    (unless (< max 1)
      (outline-backward-same-level 0)
      (search-forward-regexp "^.*\]" nil t)
      ;; 文字を消したらposの位置が保証されない
      (let ((delp (point)) (pofs (- (line-end-position) (point))) (nofs 0))
        (delete-region (point) (line-end-position))
        (tab-to-tab-stop)
        (insert (format "+{%3d/%3d}" score max))
        (message (format "score/max: %d/%d" score max))
        (setq nofs (- (point) delp))
        (goto-char (+ pos (- nofs pofs)))))
    (list score max)))
;; チェックボックスをトグルしたらスコアを更新する
(defadvice org-toggle-checkbox
  (after org-toggle-checkbox-advice activate)
  (org-my-calculate-score-insert))

;; ランキング計算時に時刻補間する際の時刻設定
(defvar org-my-get-ranking-day-split 4.0)
(defvar org-my-get-ranking-day-start 8.0)
(defvar org-my-get-ranking-day-end 24.5)

;; @myfunc ランキングを表示する
(defun org-my-get-ranking ()
  (interactive)
  (let ((pos (point)) (score 0) (max 0) (expect 0) (slist (list)) (rank 1) (mrank 1) (erank 1) (num 0))
    (let ((scores (org-my-calculate-score-insert)))
      (setq score (car scores))
      (setq max (car (cdr scores))))
    (goto-char (point-min))
    (while (and (search-forward-regexp "\\+{\\W*" nil t)
                (string-match "[0-9]+/\\W*[0-9]+}" (buffer-substring (point) (line-end-position))))
      (setq slist (cons (string-to-number (buffer-substring (point) (search-forward-regexp "[0-9]+" nil t))) slist)))
    ;; 朝のorg-my-get-ranking-day-start時から夜のorg-my-get-ranking-day-end時までが活動時間とする
    (let ((next 99999) (ratio 0.0) (time 0.0) (active (- org-my-get-ranking-day-end org-my-get-ranking-day-start)))
      (setq time (+ (nth 2 (decode-time (current-time))) (/ (nth 1 (decode-time (current-time))) 60.0)))
      ;; 時間を整形:org-my-get-ranking-day-split時を一日の境目とする
      (when (< time org-my-get-ranking-day-split) (setq time (+ time 24.0)))
      (setq time (- time org-my-get-ranking-day-start))
      (when (< time 0.0) (setq time 0.0))
      (when (> time active) (setq time active))
      (setq ratio (/ time active))
      (if t (setq expect (/ score ratio))
        (setq expect score))
      (when (> expect max) (setq expect max))
    ;; 何番目か調べる:score, max, expectを同時に調べる:直近のスコアを得る
      (while slist
        (let ((top (car slist)))
          (when (< score top) (setq rank (+ rank 1)))
          (when (< max top) (setq mrank (+ mrank 1)))
          (when (< expect top) (setq erank (+ erank 1)))
          (when (and (< score top) (> next top)) (setq next top)))
        (setq num (+ num 1))
        (setq slist (cdr slist)))
      ;; score: [現在点数/最大点数]  rank: {現在/数},
      ;; expected score: 予想スコア(予想ランク), day: [経過時刻h/活動時間h](比率%)
      (message (format "score:[%d/%d] rank:[%d/%d] ~%d || expected:[%d(%d)] || day:[%.1f%s] time:[%.1fh/%.1fh]"
                       score max rank num (- next score) expect erank (* 100.0 ratio) "%%" time (- active time)))
      (goto-char pos))))

;; org-mode自体の設定
(add-hook 'org-mode-hook
	  '(lambda ()
               ;; - ここあたりはその他の設定 -
               ;; ランキング計算をショートカットキーに登録(キーバインドは適当に調節)
               (org-defkey org-mode-map (kbd "C-c C-;") 'org-my-get-ranking)))
記事の修正

2011/12/16 23:50 説明文の間違いや表現を修正, コード中の無意味なコメント行を削除し,忘れていたキーの設定を追加

*1:ゲーミフィケーションを意識

*2:得点配分は人それぞれだと思うけど、自分は S:32, A:16, B:8, C:4, D:2, E:1 の配分を採用した

*3:ちなみに自分はorg-modeのタスク管理機能(時間とか)はそこまでフル活用できてない。めんどくさいので箇条書き程度がちょうどいい

*4:ところで自分はわざわざこういう書式を打ち込むのが面倒なので、yasnippetという拡張で優先度のアルファベット一文字からチェックボックスまで展開させている

*5:最初の画像では親の書式にセミコロン(;)を用いてわざと得点計算に入れないようにしていた。得点は細分化したタスクの分で十分で、単に優先順位だけ表示したかったから

*6:この末尾に表示したスコアは、あとでランキングを調べるために用いられる