The best kittens, technology, and video games blog in the world.

Monday, March 29, 2010

Personal experience points and OSX menulets

World of Warcraft Obsession by Stacina from flickr (CC-NC-SA)

Doesn't it seem odd how people are willing to spend so much time and effort on doing everything that's best for their in-game characters, and yet they never do anything for their real lives?

And it's not just highly complex MMORPGs like World of Warcraft and Eve Online - look how many people spend how much effort on FarmVille - which is about logging it at scheduled times and clicking harvest/plow/plant seeds on a large number of virtual squares! Many of them are the same people who avoid putting any effort into their real life characters as much as possible.

There's been some interesting discussion about this real world - gaming world divide, started by this TED talk by Jane McGonigal - like vast majority of TED talks it's totally wrong, and totally worth watching for entertainment value and some intellectual stimulation:







By the way - to people who are new to this blog - links here are like on Wikipedia - they usually lead somewhere interesting.

Anyway. Assuming you want to achieve some real life outcomes but your laziness and disorganization stops you - like most people - why couldn't you simply approach life just as if it was a game? Well for one thing games give you a lot of immediate feedback that you're doing well, but real life doesn't.

So why not fix the problem and create such immediate feedback? This "feedback" is usually little more than just some numbers, progress bars, and make some badges or other icons. Let's do it!

Experience points log

Well, first thing you need is log actions which give your real life character experience points.

A log can be a simple semi-structured text file like this.
= RULES =
# Every time
+10 write a blog post
+2 read a book
+1 go to gym
+1 play with cat

# Just once
+100 setup a backup system
+500 organize free elections in Belarus

= 2010-03-27 Sat =
+10 blog post
+1 played with the cat

= 2010-03-28 Sun =
+1 played with the cat
+2 read a book
Example is completely made-up but you can think of some points for yourself. And of course you can go back and change these rules and numbers any time you want.

kitten by biwanoki from flickr (CC-NC-SA)

Recording actions

Unfortunately there are no iPhone apps for that yet, so we'll have to record our actions manually - but let's make it as easy as possible. The log is located at /home/taw/all/xp/xp.txt and I have the bound one of the keys on the most awesome keyboard in the world to open this log in case I want to edit it.

And because I almost always have terminal open, I wrote a script which appends current date header if it changed, and if you passed it any arguments it appends them to the log, otherwise it opens it. Actually now that I think about it, it wouldn't be a bad idea to get the keyboard shortcut to do this date change too... The script also edits itself if I pass it --edit

#!/usr/bin/env ruby1.9
exec "mate", __FILE__ if ARGV[0] == '--edit'

require "time"
require "date"

fn = "/home/taw/all/xp/xp.txt"
last_day = Date.parse(File.read(fn).scan(/^= (\d{4}-\d{2}-\d{2} \S{3}) =$/)[-1][0]) rescue nil

out = []
unless last_day == Date.today
  out << Date.today.strftime("\n= %Y-%m-%d %a =\n")
end
out << ARGV.join(" ") unless ARGV.empty?
unless out.empty?
  open(fn, "a"){|fh| fh.puts out.join}
end

system "mate", fn if ARGV.empty?

It's all highly portable so far as long as you adjust paths etc. - nothing OSX-specific about it.

Drawing progress bar


It's fairly simple to think of a regexp to parse experience points out of the log - the only nontrivial bit is skipping rules section. But we'd much rather have pretty icon than numbers.

Well, first let's parse XP and convert it to levels, in a typical quadratic system (code assumes Symbol#to_proc; upgrade your Ruby to 1.8.7 or paste it from core_ext if it dies).
module XP
  class << self
    def read_xp
      log = File.read("/home/taw/all/xp/xp.txt").sub(/\A.*?= \d{4}-\d{2}-\d{2} \S{3} =\n/m, "")
      log.scan(/^\+\d+/).map(&:to_i).inject(&:+)
    end
    def threshold(level)
      10 * (level ** 2)
    end
    def stats(xp=(ARGV[0] || read_xp).to_i)
      level = 0
      level += 1 while xp >= threshold(level)
      [xp, level, xp-threshold(level-1), threshold(level)-threshold(level-1)]
    end
  end
end

XP.stats returns an array of [experience ponits, level, XP since acquiring current level, XP needed from current level to next] so code drawing pictures doesn't have to think about it.

Now the drawing part. We want 32x16 icon - height is constrained by OSX menu bar height, this ratio just looks good. On the bottom we have progress bar, on the top 1-4 colored circles showing your level - levels 1-4 are green, 5-8 are cyan etc. Here are some examples:


And the code:
require "rubygems"
require 'RMagick'

canvas = Magick::Image.new(32, 16){
  self.background_color = 'transparent'
}
Magick::Draw.new.instance_eval {
  stroke('black')
  fill('none')
  rectangle(2, 9,  30, 14)

  xp, level, xp_got, xp_needed = XP.stats

  stroke('none')
  fill('#0F0')
  rectangle(3, 10, 3 + (29-3)*xp_got/xp_needed, 13)

  level_colors = %w[#0FF #FF0 #F0F #00F]
  while level > 4 and !level_colors.empty?
    fill(level_colors.shift)
    level -= 4
  end
  level.times{|i|
    x = 4+8*i
    circle(x, 4, x, 1)
  }
  draw(canvas)
}
canvas.write(ARGV[0] ? "xp-#{ARGV[0]}.png" : "/home/taw/all/xp/xp.png")

In both parts of the code weird ARGV[0] code is just there for making sample images instead of reading the actual log. Not exactly a masterpiece of modularity, but that's real world Unix scripting for you ;-)

Wich wun will teh kitty beet yous wif??? by Throcket Luther from flickr (CC-NC-ND)

Putting it in OSX menu bar

All this work is fairly useless we put the icons generated somewhere no display - like on OSX menu bar. So far everything has been reasonable system-independent - this part is highly OS-specific - unfortunately all cross-platform GUI toolkits take lowest common denominator approach, and look like shit on all systems equally. But it's really simple code. It's really easy to make the menu icon interactive but then I don't have any particular need for it to be interactive yet.

It must use OSX-shipped ruby interpretter (/usr/bin/ruby), not one from MacPorts unless you want to deal with some serious library hell.

#!/usr/bin/ruby

require "osx/cocoa"
include OSX

class Timer
  def init
    tick
  end
  def tick
    system "./draw_xp.rb"
    $statusitem.setImage(NSImage.alloc().initWithContentsOfFile("/home/taw/all/xp/xp.png"))
  end
end

app = NSApplication.sharedApplication 
$statusitem = NSStatusBar.systemStatusBar().statusItemWithLength(NSVariableStatusItemLength)
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats(1.0, Timer.new, 'tick:', nil, true)
app.run 

Alternatives

One thing I found while writing this blog is Chore Wars, which seems to be like a website for getting XP for performing real life actions - there might even be a Firefox port for constant visibility, but making it public is kinda creepy. You don't want Belarusian secret police finding out about your "+50 sent information about Lukashenko's secret Swiss bank account to WikiLeaks".

2 comments:

wordsandpictures said...

I started doing something similar this year, after realising how much time I spent playing juvenile CS:S zombie maps. The server (Geclan.net) kept detailed statistics on all players.
I then just created a spreadsheet which kept track of my time and so I struggle to up my averages for study (PhD) and gym attendance and running. I take averages, which are renewed each quarter, so each new quarter I start anew and seek to beat the previous high score!

Quickshot said...

Hmmm, it's an interesting idea. And probably is more effective, based on how much easier you can make it for people to use it.