Cuttlepress

Blogging about Hacker School?

Project Writeup: Spy Game

Last week, I spent almost the entirety of my time at Hacker School working on design and build for a single project, starting on Monday morning and working through until late on Friday night, with a few bugfixes on Saturday. The project was a birthday present for David, and my goal was to produce a game that he and his guests could play at his birthday party on Saturday night.

Game design

The gameplay grew out of several conversations I had with friends during the brainstorming phase of the project. I started off with the design goal that my game would be lighthearted and not necessarily obtrusive: people at the party could be as engaged with it as they wanted to be, players could enter or exit the game casually, and at no point would anybody be put on the spot or made to feel embarrassed. My early thoughts were along the lines of either a generative card game that I could generate and send, and they could print out, or something with a central computer but gameplay requiring exploration of the physical party space. Chris suggested that something less localized would be better, and that I could do a smartphone game. I was squeamish about smartphone games, because I myself do not have a smartphone, which means that 1) I couldn’t be sure that everyone at the party would and 2) I would have a pretty hard time debugging. Then Alex and Izzie floated the idea of using Twilio to do an SMS-based game.

Doing text-based communications at a party at William’s house (a cocktail-laden setting) naturally lends itself to a spy theme, so I started thinking about how to assign interesting spy tasks. One thought I had was to have agents collect images of the letters in “Happy Birthday, David!” and send them in to be collaged. This turned out to be less than possible, because when the Twilio FAQ says that “sending picture messages is as easy as sending regular text messages,” and “it costs 2¢ to send a picture message (within the 500K limit per message) and 1¢ to receive a picture message,” what they really mean is “at this time sending/receiving picture messages over Twilio US long codes is not supported.” Another idea was to send messages to pairs of agents randomly, telling them each that they were to attend a rendezvous at some location in the apartment, and that the other agent would be able to identify them by some thing they were wearing, that they would thus have to scrabble to obtain and wear. (“Agent [whatever] will identify you by your black hat.”) This seemd potentially fun, but also more obtrusive and involving more possible embarrassment than I wanted. One other idea came from my father, who suggested “a variant of what Groucho Marx used to do on the game-show ‘You Bet Your Life’ […] ‘See if anybody can be made to say [secret word].’” Which seemed like a step in the right direction, except that I, the gamemaster, would have no way of knowing when a successful play had occurred.

The next round of ideas was that each agent only had to say the word themself, but that if you thought you heard a suspicious word, you could message it in and get back some information about the identity of the agent saying it, with the goal of identifying as many other agents as possible. In this model, several agents would share the same word, so identifying information was inconclusive. The problem was that there was still no way for me to know, other than indirectly, whether an agent had said their assigned word, and in this model, there was no motivation for them to do so.

So the final iteration of gameplay brainstorming (which occurred later in the week than I’d like to admit) after talking to Becca was that each agent would have both a current secret word and a team affiliation. If a different agent reported in an occurence of the secret word, the first agent would either get a message of encouragement or a message of warning, depending on whether the agents were on the same team, with the idea that each agent would be able to piece together which agents were enemies, and limit their code-containing conversation to only friendly audiences. I also decided to, at some point in the game, give the players the ability to directly message each other, because I thought it would be great to have a situation in which an agent sends a message and then oh-so-casually looks around the room to see who reaches for their pocket.

Technical (a.k.a. “meanwhile…”)

This was pretty much my first webapp project, with the exception of a brief and broken Flask app I wrote all of the previous Friday, with ample help. So a lot of this project was learning baseline webapp skills.

Twilio itself is fairly simple to use. It can route message information to the html page of one’s choosing, via either GET or POST; from there, Flask can grab the form or arguments, process the data, and use the Python wrapper Twilio provides to create (and therefore send) an SMS response. Here’s a basic version of the SMS-handling code at the heart of this project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@app.route('/twilio', methods=['POST'])
def incomingSMS():
  fromnumber = request.form.get('From', None)
  content = request.form.get('Body', "empty text?")
  agentname = getAgentName(fromnumber, content)
  gameLogic(agentname, content)
  return "Success!"

# gameLogic advances the game state and uses
# sendToRecipient() to respond to agents

def sendToRecipient(content, recipient, sender="HQ"):
  recipientnumber = lookup(collection=players, field="agentname", fieldvalue=recipient, response="phonenumber")
  recipientcolor = lookup(collection=players, field="agentname", fieldvalue=recipient, response="printcolor")
  sendernumber = twilionumber
  time = datetime.datetime.now()
  
  try:
      message = twilioclient.sms.messages.create(body=content, to=recipientnumber, from_=twilionumber)
      transcript.insert({"time":time, "sender":sender, "recipient":recipient, "content":content, "color":recipientcolor})
  except twilio.TwilioRestException as e:
      content = content+" with error: "+e
      transcript.insert({"time":time, "sender":sender, "recipient":recipient, "content":content, "color":recipientcolor})
  return

The confusing thing about the above code is the bits about lookup(), which are there because of the other big new thing to me about this project: proper databases. A database is required because this app is deployed via Heroku, which has an ephemeral file system: other than some environmental variables (such as the ones I’m using to store my Twilio authentication information), nothing is guaranteed to persist in a Heroku app. On the bright side, offloading the things I did want to be persistent onto another site meant that data would persist across my plentiful tiny bugfixes and re-deploys, and that I would acquire new knowledge about a much more scalable way to do things; on the less bright side, I had to, you know, actually acquire that knowledge. Luckily Moshe and Kate were very helpful, and got me set up with a MongoDB (one of the options available as a free Heroku addon), a pymongo interface to it, and some information about how to query it.

The game uses three MongoDB collections: one to store players, a second to store transcript entries, and third collection for the game objects which include configuration information about the current active game, such as the possible player factions, the remaining assignable words, and whether or not various game events have occurred. Here’s most of what initializing a new player looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def newPlayer(phonenumber, content):
  r = lambda: random.randint(0,255)
  printcolor = '#%02X%02X%02X'%(r(),r(),r())
  # random color lazily grabbed from 
  # http://stackoverflow.com/questions/13998901/generating-a-random-hex-color-in-python
  name = content
  # this is just for the sake of the gamemaster
  factionlist = lookup(games, "active", "True", "affiliations")
  affiliation = factionlist[random.randint(0, len(factionlist)-1)]
  if phonenumber == mynumber:
      agentname = "Q"
      printcolor = '#008080'
  else:
      agentname = "0"+str(random.randint(10,99))
      while players.find({"agentname": agentname}).count() > 0:
          agentname = "0"+str(random.randint(10,99))
  players.insert({
      "agentname": agentname,
      "phonenumber": phonenumber,
      "printcolor": printcolor,
      "active": "True",
      "task": [],
      "affiliation": affiliation,
      "successfulTransmits":[],
      "interceptedTransmits":[],
      "reportedEnemies":[],
      "spuriousReports":[],
      "name": name,
      "knowsaboutmissions":"False",
      "squelchgamelogic":"True"
      })
  greet(agentname)
  return agentname

“Squelchgamelogic” is an inelegant hack added at the last minute, when I noticed that the structure of the game loop meant that newly initialized players were also receiving a “help” message because it was the fallthrough response. As noted below, the help message is clearly not the right solution, gameplay-wise, anyway. So that is something to change in the next iteration.

I didn’t do a lot with Flask itself. I routed the incoming messages to /twilio, generated a web console for myself (with forms to send individual messages, trigger game events, and send mass announcements), and generated a leaderboard. Most of the work of the latter two was in creating the Jinja templates; for example, to format the time stamps in the transcripts part of my console, I had to create a filter. It goes in the Flask app with a special decorator:

1
2
3
4
@app.template_filter('printtime')
def timeToString(timestamp):
    return str(timestamp)[11:16]
# naively take the slice of the time string that is the hours and minutes

and then gets used in the transcript-listing portion of the console template:

Transcript:
<ul style="list-style-type:none">
{% for post in information.find() %}
  <li style="color:{{ post.color }}">{{ post.sender }} to {{ post.recipient }}
    @{{ post.time|printtime }}: "{{ post["content"] }}"</li>
{% endfor %}
</ul>

Heroku was mostly unobtrusive to use, at least after having set it up once on the previous Friday, but the overall commit-deploy-wait-wait-wait cycle (necessary because Twilio obviously can’t interface with localhost) did add a layer of frustration to bugfixing. I also need to get a lot better at communing with Git, since the naive “just commit everything in the same branch” approach combined with having to commit every time I want to run the code is not helpful as version control.

One technical stretch goal that I did not achieve was to have timed events independent of my triggers. My brief foray into the topic was met with “well, you should probably use a Redis queue…” and the prospect of learning another database system within a day of my deadline did not appeal. So that will have to wait for another project. It turned out that having manual control over game events was probably better for gameplay anyway.

The full code is available on Github.

Betatest

By which I mean, the party itself. There was no way it was going to be anything more polished than a beta test; I did do a lot of in-progress testing, of course, but it wasn’t possible to emulate the full party in advance.

Some definite technical and design bugs emerged early and often. On the technical side, one was that I’d made a mistake in my “parser” and I was not catching incidences of “report” without a colon; a related but distinct problem is that I replaced all incidences of agent names in direct messages with “from [the sender],” leading to embarrassing output like this classic line from one agent attempting to inform on three others: “From 042: we need to figure out who our enemies are.. I have some numbers: From 042, From 042, From 042, From 042.” I also made an error in the logic of sending a birthday message to David, with the result that he (and only he!) had to try more than once to join the game.

On the design side, my default help response to unparseable input was definitely annoying — I added it in quick response to a comment from the earlier testers that it would be good to have some sort of feedback in the case of unparsed input, but it seems that the repetition quickly got on the nerves of the party crowd. This is, as we know from the world of parser interactive fiction, a finicky problem. Additionally, the text that introduces the game mission and attempts to explain gameplay needs a rewrite; reports are that it took the players a while to experiment enough to figure out what was going on.

Overall, though, the reactions I’ve heard have been enthusiastic. Direct messaging was clearly popular — arguably I could have rolled it out sooner, but the impression I get is that it dominated the party and probably would have been unsustainable in the long run. I also heard that some of the word insertions were just as hilariously obvious as I was hoping for, and some of the spurious word reports are good for a laugh as well. So I’m happy with the overall concept, and I think that the above bugs will be pretty easy to overcome.

Results

Here’s a copy of the transcript from the evening, with personal names changed to capital single letters in slashes. (Note that since the time stamps were for personal use and only relative time mattered, I did not bother to change the time zone.) And here’s the leaderboard, complete with the spurious reports list (complete with evidence of that “report” bug; how embarrassing). Here’s a chart of my Twilio usage throughout the afternoon and evening of Saturday: Spikes.

And here’s a screenshot of what my console looked like: Control center.