Programming a State Machine

A monster modeled after a dog in my neighborhood.
A monster modeled after a dog in my neighborhood.

My attempts at game programming usually turn into impenetrable spaghetti code: “If the player walks through this door, then have him talk to the princess, unless he’s killed a guard, in which case the guards attack, or if he comes out of the secret passage…”

The game I’m working on now is pretty simple, but I’ve kept it really clean (so far) by using a state machine to keep track of what should happen when. Basically, each scene in the game is a state. There’s an overarching state machine which runs the current state on each tap. A state can either return itself or choose a new state to run next time.

In Objective C (+cocos2d), a state looks like this:

@interface State : NSObject {
    CCLayer *ui;
}

-(id) init:(CCLayer*)layer;
-(Class) processTouch:(UITouch*)touch withEvent:(UIEvent*)event;

@end

The processTouch function either returns nil, which means “run me again next time” or the next state to run. The other half is a “machine” to run the states:

// -----------------
// Interface
// -----------------

@interface StateMachine : NSObject {
    State *currentState;
    CCLayer *ui;
}

-(id) init:(CCLayer*)layer;
-(BOOL) processTouch:(UITouch*)touch withEvent:(UIEvent*)event;

@end

// -----------------
// Implementation
// -----------------

@implementation StateMachine

// Initialize the state machine by setting currentState to the first state
-(id) init:(CCLayer*)layer {
    self = [super init];

    if (self) {
        ui = layer;
        currentState = [[FirstState alloc] init:ui];
    }

    return self;
}

// Run this from the UI's touch dispatcher: it runs the current state's processing code
-(BOOL) processTouch:(UITouch*)touch withEvent:(UIEvent*)event {
    Class nextState = [currentState processTouch:touch withEvent:event];

    if (nextState != nil) {
        currentState = [(State*)[nextState alloc] init:ui];
    }
}

@end

Then, you might have an implementation like this for a swords & sorcery RPG:

@interface PalaceDungeonState : State {
    Guard *guard;
}

@implementation PalaceDungeonState

-(id) init:(CCLayer*)layer {
    // Use ui to render a dungeon
}

-(State*) processTouch:(UITouch*)touch withEvent:(UIEvent*)event {
    if (guard.alive) {
        [guard updatePosition];
    }

    CGPoint touched = [ui convertTouchToNodeSpace:touch];

    switch (touched) {
    case GUARD:
         [guard dies];
         break;
    case STAIRWAY:
         return PalaceStairwayState;
    case SECRET_PASSAGE:
         return SecretPassageState;
    }

    return nil;
}

@end

I’m not thrilled with doing so much work in the init, so for this type of game I’d probably move that to a start method that would be called by StateMachine on state changes.

Regardless, I’ve found this makes it a lot easier to make a complicated sequence of events while keeping my code readable.

Mad Art Skillz

With all this free time, I’ve been working on an iOS game. I’m not even close to done yet, but I’ve wrestled Objective C into submission and now and I’m working on some assets. It’s going to be musical, so here’s Beethoven:

beethoven

And here’s a demon (it plays the piano):

monster2

And the player character, Calliope:

calliope

If you ever need to make some vector art, Chris Hildenbrand’s blog, 2D Game Art for Programmers, is fantastic. It teaches you how to create awesome vector art using Inkscape (which is free). I had never done vector art before and his instructions are perfect for beginners.

Finished The Definitive Guide

Or at least the writing it, it still has to be tech edited, “real” edited, illustrated, formatted, etc. The second edition is going to be about 400 pages (almost twice the length of the first edition), with majorly expanded sections on sharding, replication, and server administration.

Phew.

Now, some mea culpas:

To those of you who sent me schemas: I’m sorry if I never got back to you! I decided to go in a different direction and ended up not using any of them. Sorry to waste people’s time (but they were fascinating to read).

To those of you who sent in a schema and I asked for your mailing address: I forgot to forward those emails to my personal account before leaving 10gen so I’ve lost the addresses. Please resend your address to my personal email (k dot chodorow at gmail dot com).

Screen Shot 2013-03-08 at 1.57.54 PM

Databases & Dragons

Here are some exercises to battle-test your MongoDB instance before going into production. You’ll need a Database Master (aka DM) to make bad things happen to your MongoDB install and one or more players to try to figure out what’s going wrong and fix it.

This was going to go into MongoDB: The Definitive Guide, but it didn’t quite fit with the other material, so I decided put it here, instead. Enjoy!

Tomb of Horrors

Try killing off different components of your system: mongos processes, config servers, primaries, secondaries, and arbiters. Try killing them in every way you can think of, too. Here are some ideas to get you started:

  • Clean shutdown: shutdown from the MongoDB shell (db.serverShutdown()) or SIGINT.
  • Hard shutdown: kill -9.
  • Shut down the underlying machine.
  • If you’re running on a virtual machine, stop the virtual machine.
  • If you’re running on physical hardware, unplug the machine.

A slightly more difficult twist is to make these servers unrecoverable: decommission the virtual machine, firewall a box from the network, pick up a physical machine an hide it in a closet.

@markofu‘s suggestion: make netcat bind to 27017 so mongod can’t start back up again:

$ while [ 1 ]; do echo -e "MongoDB shell version: 2.4.0nconnecting to: testn>"; nc -l 27017; done

DM’s guide: make sure no data is lost.

The Adventure of the Disappearing Data Center

Similar to above, but more organized. You can either have a data center go down (shut down all the servers there) or you can just configure your network not to let any connections in or out, which is a more evil way of doing it. If you do this via networking, once your players have dealt with the data center going down, you can bring it back and make them deal with that, too.

Note that any replica set with a majority in the “down” data center will still have a primary when it comes back online. If your players have reconfigured the remainder of the set in another data center to be primary, these members will be kicked out of the set.

Find the Rogue Query

There are several types of queries that you can run that will pound on your system. If you’d like to teach operators how to track these types of queries down and kill them, this is a good game to play.

To test a query that stresses disk IO, run a query on a large collection that probably isn’t all in memory, such as the oplog. If you have a large, application-specific collection, that’s even better as it’ll raise less red-flags with the players as to why it’s running. Make sure it has to return hundreds of gigabytes of data.

Kicking off a complex MapReduce can pin a single core. Similarly, if you can do complex aggregations on non-indexed keys, you can probably get multiple cores.

Stressing memory and CPU can be done by building background indexes on numerous databases at the same time.

To be really tricky, you could find a frequently-used query that uses an index and drop the index.

DM’s guide: players should re-heat the cache to speed up the application returning to normal.

THAC0, aka Bad System Settings

Try setting readahead to 65,000 and watch MongoDB’s RAM utilization go down and the disk IO go through the roof.

Set slaveDelay=30 on most of your secondaries and watch all of your applications w: majority writes take 30 seconds.

Use rs.syncFrom() to create a replication chain where every server only has one server syncing from it (the longest possible replication chain). Then see how long it takes for w: majority writes to happen. How about if everyone is syncing directly from the primary?

LEROY JENKINS!

What happens if your MongoDB instance gets more than it can handle? This is especially useful if you’re on a multi-tenant virtual machine: what’s going to happen to your application when one of your neighbors is behaving badly? However, it’s also good to test what might happen if you get a lot more traffic than you expect. You can use the Linux dd tool to write tons of garbage to the data volume (not the data directory!) and see what happens to your application.

Server Concealment

Try using a script to randomly turn network on and off using iptables. For increased realism, it’s more likely that you’ll lose connectivity between data centers than within a data center, so be sure to check that.

Network issues will generally cause failovers and application errors. It can be very difficult to figure out what’s going on without good monitoring or looking at logs.

The Google Interviews

When I was a college senior I applied for a job at Google. During the in-person interview, the interviewer asked me to find the median of an infinite series of numbers and I just stared at the board, having no idea what to do. Every idea I came up with that was reasonable for a finite series fell flat when faced with an infinite series.

After one more excruciating (but less memorable) interview, they politely showed me out.

So, I was a bit nervous this time around. However, I am pleased to report that I never was at a loss for what to do and all of the questions seemed pretty fair and reasonable. Most of the questions were basically variations on things in Cracking the Coding Interview, so I figured I’d share them. I got a few more creative questions which are not listed below (I don’t want to ruin a anyone’s “personal” question) but they weren’t any harder or easier than the others.

Note: if you’re planning to interview somewhere soon, you might want to save this, do your other prep, and then go through these questions as a mock-interview.

Here’s what I was asked:

  • Given this weird tree-like thing:

    semitree

    Find greatest sum on a path from root to leaf (that can only touch one node per level—no going up and down).

    I did a recursive solution that was about six lines long and then she asked me to come up with a way to do it iteratively.

  • Consider a power series:

    a0 + a1x + a2x2 + …

    Suppose we have an interface as follows to get the coefficients: a0, a1, a2, etc:

    class PowerSeries {
    public:
        virtual double next();
    };
    

    You can take the product of two power series:

    (a0 + a1x + a2x2 + …) * (b0 + b1x + b2x2 + …)

    Implement next() so it gives you the next coefficient in the product of two power series:

    class PowerProduct : public PowerSeries {
    public:
        PowerProduct(PowerSeries* A, PowerSeries* B);
        virtual double next();
    };
    
  • Reverse bytes in an int.

    This was in my last interview of the day, and mental fatigue had reduced me to such a state that I assumed four bits in a byte. I don’t even know where that came from, but that was really embarrassing when I realized it (well after the interview).

  • Write a function to find if a set A is subset of a set B, B is a subset of A, the two sets are equal, or none of the above (you are allowed to use set operations like union and intersection).
  • Part 2 of the previous question: suppose you are given a list of sets. You wish to filter out any set in the list that are a subset of another set in the list. Use your solution from above to find the smallest possible result list as efficiently as possible.
  • Implement atoi (convert a string to an integer).
  • Given a string, find the logest substring of only two distinct characters. For example, given “aabacccaba”, you would return “accca”.
  • Design a cache.
  • Suppose you are on a Cartesian coordinate system. Find all paths from (0,0) to (m,n) that only go through each point once. For example, if you were given m=2, n=2, you’d have the following:

    paths

    This would be one possible path:

    paths2

    Return the number of possible paths. Then extend for negative coordinates.

  • Given two integers, a and b, find the smallest square integer in the range (or return -1, if there is no such square).

Guide to Tech Interviews

I’ve been interviewing people for programming jobs for five years and I’ve recently gotten a look at the interview process from the other side. Here are some suggestions for acing tech interviews.

Read Cracking the Coding Interview (available for free from here, Google Play, and various other places). It is incredible, it basically covers every questions that a sane interviewer could ask. That’s really my main suggestions, but here are a few other supplementary tips:

Pre-Prep

Go onto Glassdoor and see what people say about the interview process at the company. Often they’ll list questions they were asked and interviewers are not that creative: if there are any questions listed whatsoever, make sure you can code them up perfectly. Google, too, can be helpful: “<company> phone screen” or “<company> interview” often will give you other possible questions.

Once you’ve plumbed the internet, time to refresh the other stuff you might be asked. For data structures, make sure you know linked lists, trees, tries, heaps, sets, and hashtables.* For algorithms, make sure you still remember dynamic programming (I certainly didn’t).

*The answer is always “hashtables.” Use them early and often, they almost always make the problem easier.

Non-Technical Questions

You might be a brilliant coder, but you also have to come up with comprehensible answers stuff like, “Tell me about something you’ve debugged recently” or “Tell me about a project that shows your strengths.” I wasn’t sure how to prep for these questions in a general way, but Cracking the Coding Interview had a great system: make a table of the projects you’ve done and possible questions and fill it out. For example:

Project 1 Project 2 Project 3 Project 4
Most challenging
What you learned
Most interesting
Hardest bug
Enjoyed most
Conflicts with teammates

This pretty much covers all of the “soft” questions you’re likely to get. I did mine using a private Noodlin board, which worked out well for me (but a Google Spreadsheet would probably work fine, too):

NoodlinMatrix

Note that mine’s a bit sparse and that’s fine. It’s actually even sparser than it looks, as a lot of the stories in the same column are very similar. I’d say come up with at least two projects for each row, though.

Then practice your responses out loud! At least for me, saying things out loud is very different than saying them in my head. I’d go off on random tangents complaining about things and then realize how it sounded halfway through.

Keep your responses short (1-5 minutes) and talk about your coding. For example, you could say: “All of the code was in a giant switch statement, so I abstracted it all out into <data structure> and then traversed it.” A bad answer would be “I used <some framework> and <hot new tech>.”

Prepping for Technical Questions

For the technical interview, try to answer all of the questions in Cracking the Coding Interview without looking at the solutions. If you can’t even get started on a problem, see if you can see any similarities between it and a more common problem. How would you solve it brute-force? There are answers at the back of the book if you’re really stuck.

For all of the questions you can get, come up with alternate answers. Can you optimize for space? Can you optimize for time? Generally you can find a fast solution that uses lots of space and a slow solution that uses very little. If you did it recursively, could you do it iteratively (and visa versa)?

Make sure you’re actually coding up these answers, too. TopCoder is pretty good for this. Google around for SRM challenges covering specifc areas, or check out the problems linked to in the algorithm tutorials. However, you can also just code up answers in a plaintext editor and run them on your own test cases.

Before the Phone Screen

Try to schedule interviews for when you peak: e.g., I can barely function before noon, so I tried to schedule all interviews for the late afternoon or evening (super handy if you’re applying to places in CA from NY).

Once your interview is set up, make sure you’re all set 15 minutes before the prearranged time. If they didn’t tell you it would be a tech interview, assume it will be and have your computer ready with internet connection. Have a cellphone with hands-free headset (I got this one on Amazon for $10 and it worked fine).

Now get a nice, big, pad and write down some questions you’d like to ask them. Leave lots of free space on the pad to take notes. Try to keep taking notes and doodling while they’re talking: you absorb more info when you’re doodling than when you’re just listening.

Make sure you’re somewhere you can spread out, so you can lay your phone beside you and quickly switch between laptop and pad as needed. On one phone interview, I accidentally hit a button on my phone and started recording the conversation (I was so embarrassed and had no idea how to turn it off).

Finally, drugs! I make a cup of coffee about 15 minutes before the interview and drink most of it, but leave a little. I’ve found it’s nice to have a booster (or at least something for my mouth to do) if I get totally stuck on a question.

A minute or two before they’re scheduled to call, take some deep breaths, relax, and try to imagine you’re expecting a call from someone who you really like and with whom you enjoy talking about technical stuff (maybe a coworker or friend). They’re just calling to hear about the cool stuff you’ve been working on and get some tech advice on a problem they’ve been having.

Executing the Tech Questions

During the interview, take the coding in several phases:

  1. Understand the question. Hopefully it will only take a few minutes to clarify what they’re asking, but make sure you have a clear mental model. Having interviewed tons of people, the difference between people who really grasp the problem and those that do not is staggering.
  2. Get the algorithm down. This is the tough part.
    • As you think of edge cases, make notes about them (or clarify with the interiewer). You might realize you have to handle overflow 20 lines in, but have forgotten by line 30. Just add a // TODO: handle overflow and then make a pass through after to take care of all the TODOs.
    • If you aren’t sure you can keep the logic straight, make comments. For instance, for a connection pool problem, you might do something like:
      Connection* getConnection() {
          // Check if pool is empty
          // If so, create new connection
          // If not, return connection from pool
      }
      

      This lets your interviewer know what you’re thinking even if the code doesn’t come out right (and it turns the question into kind of a fill-in-the-blank exercise for you).

    • If you get to a part that is more complex than the surrounding area, just make it a function call, at least for the time being. For example, something like this:
      if (valid) {
          counter++;
      }
      else {
          while (x[i] == '') {
               int j;
               for (j = counter; j >= 0; --j) {
                    // oh crap, this is going to be even more complicated... 
               }
          }
      }
      

      should be turned in this:

      if (valid) {
          counter++;
      }
      else {
          counter += chompNulls(x, i);
      }
      

      Then go write chompNulls.

  3. Check your work. There is an almost irresistable temptation to say “okay, done” when all you have is a first draft. Once you’ve coded the last line, walk through your code twice before saying “done.”
    • First, check edge cases. Can you pass in null? Zero? Empty string? Negative numbers? Really short strings? Long strings? Answer at beginning/middle/end? You can probably find most bugs with a few quick checks here.
    • Make sure your method signature still makes sense. Often you realize halfway through that you’re returning something different than you thought or you don’t need a parameter.
    • Go through any test case they gave you, make sure it works. (If they gave multiple, you don’t have to go through them all, just choose one.)
  4. Report when your done. Keep your interviewer in the loop the whole time, of course, but it’s especially important once you stop programming and you’re just sitting there in silence (checking things over). “Now let me check the case where n is negative” will go a long way towards keeping the interviewer happy.

    Then let them know “Okay, I think that should do it” once you’re done (please don’t just sit there in silence).

The Long Haul

For all-day in-person interviews, be prepared to be mentally exhausted. If you have the time/dedication, try doing a “mock interview” where your friend asks you technical questions for five hours. Use this to identify when you start getting tired and what you do when you’re tired: stop checking edge cases? Make too many assumptions about the problem? Get cranky with the interviewer?

Whatever your response is to mental exhaustion, try to figure out ways to counteract it: take a 5-minute break after each interview (ask to use the restroom if they schedule them back-to-back), drink coffee, or boost your blood sugar with a quick snack.

Also, if you aren’t familiar with coding on a whiteboard, try answering some Cracking the Coding Interview answers longhand before you go in. Make sure you leave lots of whitespace between lines, even when you don’t thing you’ll need it. I’ve found that if I concentrate on spreading things out, my brain unconsciously leaves more space where I’ll need to add things later (e.g., checking for edge cases, adding more conditions to an if, etc.). Your milage may vary.

Finally, don’t get too hung up on making things flawless. In five years, I’ve had three interviewees that did perfectly. However, I’ve recommended hiring tons of other people (and I certainly didn’t perform flawlessly… more on that in my next post about the questions I was asked). So, don’t stress out too much if you mess up.

Afterwards

Take care of yourself. Do something special to celebrate getting it over with (even if you don’t feel like you did that well). Resist the urge to follow up with the company, try to put the whole thing out of your mind. You probably won’t be able to, but remind yourself that there is nothing you can do and concentrate on other things. (Also, feel free to write thank you notes, but I’ve never known a programmer at a geeky company who gave two shits whether you did or not.)

If you get a rejection, it really hurts. If you’re at work, take a few minutes to yourself to recover. Go out for lunch, call a friend, or just walk around a bit. Remember that you’re awesome, you did everything you could, it’s their loss, and it’s probably their flawed interview process’s fault (hint: everyone’s interview process is deeply flawed).

If you get an acceptance, congratulations! They love you. Now you just have to figure out what to do next. For negotiating (and more general interview/resume/cover letter) advice, I highly recommend Ask a Manager.

Find this useful? Please comment/upvote on Hacker News!

Goodbye 10gen, Hello Google

Mongo_map_from_Flash_Gordon

After five wonderful years, I’ve decided to leave 10gen and join Google. I’m going to miss working with all of my coworkers and the community tons, you guys are awesome.

I will hopefully continue blogging, but Snail in a Turtleneck will probably not be as MongoDB-focused anymore. If you’re looking for some good MongoDB reads, I recommend checking out Planet Mongo, an aggregator of MongoDB-related blogs, and in particular:

If I’ve missed any MongoDB blogs you find helpful, let me know in the comments and I’ll add them to the list!

I will be finishing the second edition of MongoDB: The Definitive Guide in a few weeks and it should be out by the end of the year.  Once I’m done with that, hopefully I’ll have a few weeks to relax.

Intro to Fail Points

This is probably exclusively of interest to my coworkers, but MongoDB has a new fail points framework. Fail points make it easier to test things that are hard to fake, like page faults or network errors. Basically, you create a glorified boolean called a fail point, which you can turn on and off while mongod is running.

To show how this works, I’ll modify the humble “ping” command. “ping” is fairly simple:

> db.runCommand({"ping" : 1})
{ "ok" : 1 }

I’d like to make the response include a "pong" : 1 field, on command.

The ping command is defined in src/mongo/db/dbcommands_generic.cpp. To add a fail point, we first have to include the fail points header at the top of the file:

#include "repl/multicmd.h"
#include "server.h"
#include "mongo/util/fail_point_service.h"

namespace mongo {

Then, below the namespace mongo line, add a declaration for the fail point:

namespace mongo {

    MONGO_FP_DECLARE(pingPongPoint);

Feel free to call your failpoint whatever you want.

The ping command is defined lower down in the file in a section that looks like this:

    class PingCommand : public Command {
    public:
        PingCommand() : Command( "ping" ) {}
        virtual bool slaveOk() const { return true; }
        virtual void help( stringstream &help ) const { help << "a way to check that the server is alive. responds immediately even if server is in a db lock."; }
        virtual LockType locktype() const { return NONE; }
        virtual bool requiresAuth() { return false; }
        virtual void addRequiredPrivileges(const std::string& dbname,
                                           const BSONObj& cmdObj,
                                           std::vector* out) {} // No auth required
        virtual bool run(const string& badns, BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) {
            // IMPORTANT: Don't put anything in here that might lock db - including authentication
            return true;
        }
    } pingCmd;

Now, in the run() method of the code above, you can trigger certain actions when the fail point is turned on:

        virtual bool run(const string& badns, BSONObj& cmdObj, int, string& errmsg, BSONObjBuilder& result, bool) {
            // IMPORTANT: Don't put anything in here that might lock db - including authentication
            if (MONGO_FAIL_POINT(pingPongPoint)) {
                result.append("pong", 1.0);
            }
            return true;
        }

Now recompile the database. By default, mongod doesn’t allow failpoints to be run. To even allow the possibility of fail points being triggered, you have to run mongod with the --setParameter enableTestCommands=1 option.

$ ./mongod --setParameter enableTestCommands=1

Note: as of this writing, you cannot enable failpoints with the setParameter command, you must start the database with this option.

The failpoint still isn’t turned on, so if you run db.runCommand({ping:1}), you can see that there’s still just the “ok” field. You can enable the fail point with the configureFailPoint command:

> db.adminCommand({"configureFailPoint" : 'pingPongPoint', "mode" : 'alwaysOn'})
{ "ok" : 1 }
> db.runCommand({ping:1})
{ "pong" : 1, "ok" : 1 }
> db.adminCommand({"configureFailPoint" : 'pingPongPoint', "mode" : 'off'})
{ "ok" : 1 }
> db.runCommand({ping:1})
{ "ok" : 1 }

Possible modes are "alwaysOn", "off", and {"times" : 37} (which would be on for the next 37 times the fail point is hit… obviously the value for “times” is configurable).

This is a derpy example, but I’ve found it super helpful for debugging concurrency issues where I need to force a thread to block until another thread has done something. You can do that with something like:

while (MONGO_FAIL_POINT(looper)) {
    sleep(0);
}

If you wanted to merely delay something, say, immitate a slow connection, you can use MONGO_FAIL_POINT_BLOCK to pass in information:

MONGO_FAIL_POINT_BLOCK(pingPongPoint, myDelay) {
    const BSONObj& data = myDelay.getData();
    sleep(data["delay"].numberInt());
}

Then you’d pass in a delay as so:

> db.adminCommand({"configureFailPoint" : 'pingPongPoint', "mode" : 'alwaysOn', "data" : {"delay" : 5}})
{ "ok" : 1 }

Now, if you run the ping command, it’ll take 5 seconds to return.

MongoDB Puzzlers #1

Suppose that the collection test.foo contained the following documents:

{"x": -5}
{"x": 0}
{"x": 5}
{"x": 10}
{"x": [0, 5]}
{"x": [-5, 10]}
{"x": [-5, 5, 10]}

x is some combination of -5, 0, 5, and 10 in each document. Which documents would db.foo.find({"x" : {"$gt" : -1, "$lt" : 6}}) return?

Click here to show answer

jQuery(‘#puzzlers-0-toggle’).toggle(
function() {
jQuery(this).text(“Click here to hide answer”);
jQuery(‘#puzzlers-0-answer’).css(‘display’, ‘block’);
},
function() {
jQuery(this).text(“Click here to show answer”);
jQuery(‘#puzzlers-0-answer’).css(‘display’, ‘none’);
}
);

Humans are difficult

My web app, Noodlin, has two basic functions: 1) create notes and 2) connect them, so I tried to make it blindingly obvious how to do both in the UI. Unfortunately, when I first started telling people about it, the first feedback I got was, “how do you create a connection?”

The original scheme: you clicked on the dark border to start a connection.

At that point, the way you created a connection was to click on the border of a note (a dark border would appear around a note when the mouse got close). Okay, so that wasn’t obvious enough, even though the tutorial said, “click on my border to create a connection.” I learned a lesson there: no one reads tutorials. However, I didn’t know what users did expect.

I started trying things: I darkened the color of the border, I had little connections pop out of the edge and follow your mouse as you moved it near a note. I heard from one user that she had tried dragging from one note to another, so I made that work, too. But people were still confused.

The next major revision: click on a square to start a connection.

I turned to UX Stack Exchange and people pointed me to several other graphing tools, which generally had little boxes on the border you could start connections with. I changed the “click anywhere on the border” to “click on a box on the border.”

Users continued to ask, “how do I make connections?”

At this point, I was tempted to give up. Luckily, a user tipped me off that he thought the little boxes were resize handles and suggested circles, instead. I tried that and it was a huge improvement. Finally, I decided that no one was going to read my damn tutorial, so I’d animate how you make a connection and play it on an endless loop. Try to ignore that, movement-tracking humans!

And it worked.

I have gotten exactly 0 inquiries about how to make connections since I posted on my blog about it (which yielded about 50x the amount of traffic I was averaging before). Phew.

The current scheme, which seems to be working.

And if you want to try it out for yourself, check out Noodlin!