#ActsOfSuperness Twitter Game
Three days ago at about 3 a.m. I woke up with an idea to make people be nicer to each other. I know it might sound kind of corny, but I wanted to build a Twitter game to encourage people to do (and tweet) good things.
The game is called #ActsOfSuperness, and it centers around getting people to retweet the nice things they do for others. The more an action is retweeted the more points they get.
As players get higher scores they get moved up in the superhero rankings. Eventually they become Batman, and everyone wants to be Batman.
But really, the payout is more about having this fun little excuse to be nice to other people. Maybe I'm horribly naive, but that sounds like enough reward to me.
Building the game
I want to write up how I made this project, and how others can reproduce it. As usual, I've put the source code on github. I really would like other people to clone this and build it out or duplicate it. You could imagine making clones with any kind of positive (or negative) message as the hashtag. #ActsOfFreshness, #ActsOfMerriness (for Christmas) You get the idea.
The project can be broken into two parts—a twitter-facing "pseudo-backend" that trawls Twitter and a web-facing frontend that displays the leaderboard.
The Twitter facing "backend"
The actual game-play really just boils down to a string of tweets. Many people have built tweetbots, but I thought it would be a good idea (read free) to host it on Google App Engine so I looked for people who had done that before.
To get started, I borrowed heavily from this Twitter Bot example by BillTheLizard. It leverages tweepy to automatically update a twitter account's status. With just a few modifications Bill's code formed the backbone of my project.
Sending Tweets with Tweepy
To connect to the Twitter API, you'll first have to set up an API authentication key, which is explained nicely in this tutorial. I put the keys into a config file like BillTheLizard did, and then I load them in.
config = ConfigParser.RawConfigParser()
CONSUMER_KEY = config.get('Twitter OAuth', 'CONSUMER_KEY')
CONSUMER_SECRET = config.get('Twitter OAuth', 'CONSUMER_SECRET')
ACCESS_TOKEN_KEY = config.get('Twitter OAuth', 'ACCESS_TOKEN_KEY')
ACCESS_TOKEN_SECRET = config.get('Twitter OAuth', 'ACCESS_TOKEN_SECRET')
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
api = tweepy.API(auth)
After authenticating the twitter, all of the tweeting work is performed in a single line.
api.update_status("@%s Congrats! You've moved up to #%s." % (name,namesake),f.id)
To actually set up the Google App Engine part, I only had to adjust Bill's app.yaml file to point to different programs on the frontend and "backend". I'd used GAE before so this was no problem.
- url: /favicon\.ico
- url: /tweet_super
- url: /js
- url: /im
- url: /css
- url: .*
- name: webapp2
The searching for tweets part
My backend app was going to need to take in twitter info in order to process tweets and find people to tweet back.
To keep things recent, I save the last tweet I read in sid and use the since_id parameter in tweepy.search.
found = api.search('%23ActsOfSuperness',since_id=sid)
for f in found:
name = f.user.screen_name
id = f.user.id
# do something with name and id
storing user scores
The hard part is that I need to keep track of twitter @users and their scores. Now I could do this every time the page loads by searching all the tweets with #ActsOfSuperness and recalculating the totals. But, of course, that would be very slow if there are many tweets to process. Since I want to at least pretend that other people will use this someday, I had to find another way of retaining score over time.
If I'm only periodically reading a recent chunk of tweets, I need to update a persistent database of scores.
Behold the Cloud!
Fortunately, since I'm running this on Google App Engine, I get to use 1GB of free space on the Google Datastore. I'm betting that not too many people will be playing so my GAE Datastore Account won't fill up. But even if I start to get a lot of users playing, GAE will let me expand easily. That would be a good thing, and I could throw AdSense up there to pay for the increased storage space.
I referred to Google's Datastore Python Guestbook Demo to get started. The one issue with the datastore and eventual consistency is that you aren't guaranteed that the Datastore is up to date when you access it. That meant that if the user had tweeted more than once, the following code could run multiple times accidentally, producing multiple copies of the same user.
hero = Hero.all().filter('id =', id).fetch(10)
... Do stuff
hero = Hero()
hero.id = id
hero.handle = name
hero.score = 1.0
To fix those kinds of duplication problems I decided on the (slightly hacky) fix that would eventually kick out the duplicates if there were any. I now that you can do this somehow with ancestory in GAE, but that apparently limits the number of Datastore calls you can make in a given time window so I didn't want to do that.
hero = Hero.all().filter('id =', id2).fetch(10)
for i in range(1,len(hero)):
hero = hero
At this point we just have to update user scores every time we see a new user. This code increments the score and puts it back in the datastore. And if the user has "leveled up" I send them a tweet.
points = 10.0/(ff.user.followers+1.0)
sup = floor((hero.score+points)/10)
namesake = HERO_LIST
namesake = HERO_LIST[-1-val]
api.update_status("@%s Congrats! You've moved up to #%s. " % (name,namesake),f.id)
hero.score = hero.score+points
Cron for updates
I'm polling Twitter every half hour to get new tweets to score. Automation like this is accomplished really simply by setting up a cron.yaml file in app engine.
- description: repeated tweet test
schedule: every 30 mins
The web-facing site
I built the site around a neat bootply theme. My main.py just fills in one dynamic section in the middle, creating a list of @users by sorting the datastore
hero = Hero.all().order('-score').fetch(100)
for h in hero:
MID_HTML = MID_HTML + h.handle
It's simple and neat looking and gets the job done. I also embedded a copy of the online list that I'm using to decide the superhero order. (Thank God Batman was number 1)
Eventually, I'm hoping to build something for people to look up any users score eventually, but I might not get to that. It really depends if anyone ever starts using this thing.
So Go Play Now
I really had fun making #ActsOfSuperness, and I'm excited for people to try it out.
I really hope that this game can inspire a few people to go and do some nice things for others. We have so many silly incentives in this day-and-age that promote everyone being "interesting." Hopefully this app will inspire some people to do actual good.