My Saturday just disappeared, quickly, and barely with any trace. You could find leftover scripts all over my hard drive here and there, all pointing to Hackmud. One moment I had plans for the day, and the next it was well past midnight, I was listening to the 90s hacker movies inspired soundtrack while cracking locks and farming for credits with a script I crafted.

I’ve spent ten hours in game, and it was glorious.

Hackmud is a text-only MUD (multi-user dungeon) set in a not so distant future, where humanity was wiped out by a combination of Welsh Measels, killer rabbit outbreak, and multiple other disasters. All that’s left is a trace of old corporations, dead user accounts, and AIs roaming the net. That’s you, an AI.

Game has a heavy 90s vibe to it, and the atmosphere reminds me of Ready Player One. References to popular TV shows and dial up sounds included.

You start in a training area, a vLAN without access to the rest of the network and other players. You quickly learn all-text interface, and simple bash-like command line - it’s based on JavaScript, with a few syntax changes. Hackmud is very much a puzzle game, and lets you take your time with a tutorial: you’ll need a few hours to go through all the puzzles, cracking locks, installing upgrades, learning about security, and finally - escaping vLAN.

Here are some basics I picked up:

  • You can have multiple user accounts within a game, and so can everyone else. Be on the lookout.
  • “Locs” are account locations, something similar to an IP or a domain name. You can launch an attack against a user or a dead account if you know their loc.
  • Scripts are just that - scripts written in JavaScript (outside of the game). Hit #help and Google to get started, but at least some basic programming background is necessary here.
  • Locks protect accounts - one can be behind multiple locks. Lock usually requires you to guess the correct combination, like a color, correct lower order prime, or a keyword.
  • Upgrades are installed on user accounts and include locks, script slots, character limits, etc.

At some point I successfully struggled through the tutorial and escaped vLAN. And that’s where the open-ended social nature of the game shines. You’re thrown into a global chat channel with users trying to trick you into running their malware, players and corporations providing banking services, selling breached account information, locks, and scripts.

The game encourages lying, betrayal, deception, ponzi schemes, money laundering - you name it. And players take advantage of such freedoms to the full extent:

It looks rather confusing, but tutorial prepares you well for the chaotic flow of characters on the screen.

I really wasn’t sure what to do next. The tutorial taught me how to get money out of dead accounts, so I decided to try to find one or two. After asking about in the main channel and being offered a few dozen “risk free scripts to get locs worth millions”, I decided to set out on a search. I looked through a list of scripts.fullsec, and found a few corporations.

After digging about in their files for a good few hours, I found a way to get to employee registry and get a dozen of dead user locs. The locs were protected by different kinds of locks, and I went through the first batch manually. It took me a long time to get all the combinations and pull out some cash out of the accounts.

I found my second batch of locs shortly after, and decided to try automating some manual attempts through scripting. After struggling with syntax and being aggravated by having only 120 seconds at a time to crack a lock (in line with the 90s feel of the game, one needs to be connected to a “hardline”), I wrote a first sample script, and started to play around with game concepts.

Hackmud is in no way an accurate hacking simulator, but it’s a really fun puzzle game, and it doesn’t make any outrageous mistakes. It allows you to write your own scripts to use in game using valid JavaScript with access to a game library. Game developer (it’s a one person project) implemented his own JavaScript interpreter, so it has bugs here and there - one should be careful using obscure language features.

The worst part about scripting was character limitations: one needs quite a bit of money to be able to install first upgrades, and default limit of one active script and 500 characters per script is straight-up rage inducing. Before getting my first 1MGC to finally upgrade my rig, I wrote a bunch of tiny scripts, one per lock type, struggling to keep to the character limit. I ended up with ugly monstrosities like this, manually passing results from one script to another when encountering multiple accounts:

function(o, g) {
  var
    a = {},
    b = "ez_35",
    c = ["open", "release", "unlock"],
    d = "digit",
    r = "",
    s = false,
    t = g.target.call;

  for (var x in g) {
    if (x != "target") {
      a[x] = g[x];
    }
  }

  for (var i = 0; i < c.length; i++) {
    a[b] = c[i];
    r = t(a);
    if (r.indexOf(d) > -1) {
      for (var k = 0; k < 10; k++) {
        a[d] = k
        r = t(a);
        if (r.indexOf(d) == -1) {
          var m = b + ":\"" + a[b] + "\", " + d + ": \"" + a[d] + "\"";
          return { ok:true, msg:r + "\n\n" + m };
        }
      }
    }
  }
  return { ok:s, msg:r };
}

It was really interesting working with the limitations:

  • I couldn’t write scripts longer than 500 characters (spaces not included).
  • I couldn’t have access to one script without unloading the other first.
  • I had to debug and test the scripts within 120 second windows.

Few more batches of locs later, I was finally able to afford to initialize my system to tier 1 and install upgrades I bought and accumulated from hacking accounts:

Now I can write all the scripts I want! Excited, I built my first t1 (tier 1) lock breaker, which automatically cracks a loc with any number of t1 locks on it:

function(context, func_args) { // {target:"#s."}

  var digits = Array();
  for (var i = 0; i < 10; i++) {
    digits.push(i);
  }

  var
    COLORS = [
      "purple", "blue", "cyan", "green", "lime", "yellow", "orange", "red"],
    DIGITS = digits,
    LOCKS = ["open", "release", "unlock"],
    // ez_prime lock seems to only request low order primes, hardcoding this
    PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,
      61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137,
      139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193];

  var hackMultLevels = function(target, args, levels) {
    for (var i = 0; i < levels.length; i++) {
      var
        item = levels[i].item,
        itemDesc = levels[i].itemDesc,
        itemList = levels[i].itemList;
      for (var k = 0; k < itemList.length; k++) {
        args[item] = itemList[k];
        ret = target(args);
        if (ret.indexOf(itemDesc) == -1) {
          break;
        }
      }
    }
    return ret, args;
  }

  var hackEz21 = function(target, args) {
    return hackMultLevels(
      target,
      args,
      [
        { item: "ez_21", itemDesc: "command", itemList: LOCKS }
      ]);
  };

  var hackEz35 = function(target, args) {
    return hackMultLevels(
      target,
      args,
      [
        { item: "ez_35", itemDesc: "command", itemList: LOCKS },
        { item: "digit", itemDesc: "digit", itemList: DIGITS }
      ]);
  };

  var hackEz40 = function(target, args) {
    return hackMultLevels(
      target,
      args,
      [
        { item: "ez_40", itemDesc: "command", itemList: LOCKS },
        { item: "ez_prime", itemDesc: "prime", itemList: PRIMES }
      ]);
  };

  var hackC001 = function(target, args) {
    return hackMultLevels(
      target,
      args,
      [
        { item: "c001", itemDesc: "correct", itemList: COLORS },
        { item: "color_digit", itemDesc: "digit", itemList: DIGITS }
      ]);
  }

  var hackC002 = function(target, args) {
    return hackMultLevels(
      target,
      args,
      [
        { item: "c002", itemDesc: "correct", itemList: COLORS },
        { item: "c002_complement", itemDesc: "complement", itemList: COLORS }
      ]);
  }

  var hackC003 = function(target, args) {
    return hackMultLevels(
      target,
      args,
      [
        { item: "c003", itemDesc: "correct", itemList: COLORS },
        { item: "c003_triad_1", itemDesc: "first", itemList: COLORS },
        { item: "c003_triad_2", itemDesc: "second", itemList: COLORS }
      ]);
  }

  var
    args = {},
    i = 0,
    locks = [
      { name: "EZ_21", func: hackEz21 },
      { name: "EZ_35", func: hackEz35 },
      { name: "EZ_40", func: hackEz40 },
      { name: "c001", func: hackC001 },
      { name: "c002", func: hackC002 },
      { name: "c003", func: hackC003 }
    ],
    ret = "",
    target = func_args.target.call,
    unlocked = [];

  ret = target(args);

  while (true) {
    i++;
    var flag = true;

    for (var k = 0; k < locks.length; k++) {
      if (ret.indexOf(locks[k].name) > -1 &&
          unlocked.indexOf(locks[k].name) == -1) {
        ret,  args = locks[k].func(target, args);
        unlocked.push(locks[k].name);
        flag = false;
      }
    }

    if (flag === true) {
      return { ok: true, msg: ret };
    }

    if (i > 10) {
      return { ok: false, msg: ret }
    }
  }
}

Rather rough around the edges, not easiest to read, but it works - and I was really proud of finishing it (and too tired to go back and refactor). With this, next dozen of t1 locs took me a few minutes to crack open. Success!

That was the logical conclusion of my Saturday, and left me feel really satisfied. The game is rough around the edges, and has numerous bugs here and there. But the text-only world of Hackmud is alive and atmospheric, and puzzles and exploration of the derelict world through randomly-generated documents pulls you in, making you lose track of time.

Disclaimer: This post was not endorsed by Pebble, nor I am affiliated with Pebble.

About a year ago I’ve tried out almost all wearables Pebble had to offer at a time - the original, Pebble Time, and my favorite - Pebble Time Round. I wouldn’t call myself a fanboy, but Pebble watches are pretty damn great.

Back then I was on a market for a smartwatch - something stylish, inexpensive, and durable, to show time and notifications. After some research I immediately ruled out all other wearables on the market: some were too much centered around fitness (not my market), and some tried to put a computer with a tiny screen on your hand (I’m looking at you, Apple and Google). I wasn’t interested in either, and I was charmed with simplicity of Pebble.

First, I’ve gotten the original Pebble. I was blown away by the battery life which neared two weeks, beautiful minimalist design, and the absence of touch screen. The last one is probably the main reason why I’m still using Pebble.

It’s a watch, it has a tiny screen the size of my thumb. How am I supposed to control it with gestures with 100% accuracy? Unlike with a phone, I interact with my watch often during more demanding activities - cycling, running, gym, meeting, etc. Having physical buttons doesn’t require me to look at a screen as I perform an action (quick glance to read a notification is enough - I can reply or dismiss without having to look at the watch again).

After using the original pebble for a few weeks I was curious to try out Pebble Time. I enjoyed having colorful display, the battery life was almost as long, and the watch felt smaller than the original one. It was a decent choice, but still couldn’t help but feel like a square digital watch doesn’t fit my style.

That’s when I decided to try out Pebble Time Round. It’s the smallest of the three, and definitely one of the thinnest smartwatches available (at only 7.5 mm). I went for a silver model with a 14 mm strap. Initially there was a lack of affordable straps, but after some time GadgetWraps filled in that niche.

It’s been a year now, and it’s still going strong. Pebble Time Round (or PTR as people call it) doesn’t have the longest battery life, averaging at about 2 days until hitting the low power mode (when Pebble watches run low on juice they only display time). I usually charge it daily, since charging 56 mAh battery doesn’t take long (it gets a full day of use from 15 a minute charge).

PTR is much more a watch than anything else: it looks good, and it shows time. All the necessary things are available at a glance - calendar, notifications, texts, weather, music controls, timers and alarms. I use voice dictation to send out an occasional text.

I work in a corporate setting, with sometimes difficult to manage number of meetings, constant Hangouts pings, and a stream of emails. Pebble helps me easily navigate hectic daily routine without having to pull up my phone or my laptop to look up the next conference room, meeting attendees, or reply to a quick ping.

Due to app marketplace similar to Google Play Store (with most if not all the apps and watchfaces free) I find it easy to customize Pebble based on a situation I’m in. I’m traveling and need to be able to pull up my flight? Check. Need to call an Uber from my wrist? Check. Get walking directions? Check.

To my delight Pebble as a platform is rather close to Linux ideology. Pebble apps are modular and tend to focus on one thing and do one thing well.

Recently a Kickstarter for Pebble 2 has been announced. It’s rather unfortunate PTR is not getting an updated version, but to be honest it doesn’t really need to. It’s a fantastic combination of hardware and software which fills in a specific niche: a stylish smartwatch for displaying relevant chunks of information.

Distributing mobs in a dungeon based on player’s level (or some dungeon level difficulty factor) was somewhat straightforward, but I would still like to document the progress. I needed to place a mob that’s somewhat within the difficulty level I want, plus minus a few difficulty levels to spice it up.

Above you can see three rats, three cats, a dog (r, c, d, all level 1), a farmer (f, level 2), and a lonely bandit (b, level 3) in a level 1 dungeon.

Without going straight into measure theory, I generated intervals for each mob based on the diff of desired level and their level, and then randomly selected a point within the boundaries. Here’s the abstract code:

import bisect
import random


def get_random_element(data, target, chance):
    """Get random element from data set skewing towards target.

    Args:
        data   -- A dictionary with keys as elements and values as weights.
                  Duplicates are allowed.
        target -- Target weight, results will be skewed towards target
                  weight.
        chance -- A float 0..1, a factor by which chance of picking adjacent
                  elements decreases (i.e, with chance 0 we will always
                  select target element, with chance 0.5 probability of
                  selecting elements adjacent to target are halved with each
                  step).

    Returns:
        A random key from data with distribution respective of the target
        weight.
    """
    intervals = []  # We insert in order, no overlaps.
    next_i = 0
    for element, v in data.iteritems():
        d = max(target, v) - min(target, v)
        size = 100
        while d > 0:  # Decrease chunk size for each step of `d`.
            size *= chance
            d -= 1
        if size == 0:
            continue
        size = int(size)
        intervals.append((next_i, next_i + size, element))
        next_i += size + 1
    fst, _, _ = zip(*intervals)
    rnd = random.randint(0, next_i - 1)
    idx = bisect.bisect(fst, rnd)  # This part is O(log n).
    return intervals[idx - 1][2]

Now, if I test the above for, say, a 1000000 iterations, with a chance of 0.5 (halving probability of selecting adjacent elements with each step), and 2 as a target, here’s the distribution I end up with:

target, chance, iterations = 2, 0.5, 1000000

data = collections.OrderedDict([  # Ordered to make histogram prettier.
    ('A', 0), ('B-0', 1), ('B-1', 1), ('C', 2), ('D', 3), ('E', 4),
    ('F', 5), ('G', 6), ('H', 7), ('I', 8), ('J', 9),
])

res = collections.OrderedDict([(k, 0) for k, _ in data.iteritems()])

# This is just a test, so there's no need to optimize this for now.
for _ in xrange(iterations):
    res[get_random_element(data, target, chance)] += 1

pyplot.bar(
    range(len(res)), res.values(), width=0.7, align='center', color='green')
pyplot.xticks(range(len(res)), res.keys())
pyplot.title(
    'iterations={}, target={}, chance={}'.format(
        iterations, target, chance))
pyplot.show()

You can see elements B-0 and B-1 having roughly the same distribution, since they have the same weight.

Now, if I decrease the chance, likelihood of target being selected increases, while likelihood of surrounding elements being selected decreases:

I works the opposite way as well, increasing the chance decreases likelihood of the target being selected and increases the probability for surrounding elements.

For the sake of completeness, it works with 0 chance of surrounding elements being picked:

And an equal chance of picking surrounding elements:

After playing around with the configuration in Jupyter Notebook, I cleaned up the algorithm above and included it into mob placement routine.

After generating a few good looking random dungeons, I was puzzled with randomly placing mobs on resulting maps. To make it fair (and fun) for the player mobs should be evenly distributed.

The obvious idea was to pick coordinates randomly within the rectangular map boundaries, and then place mobs if they have floor underneath them. But this way I lose control of the number of mobs and risk having a chance of not placing any mobs at all. Plus, dungeons with bigger surface area would get more mobs - which sounds somewhat realistic, but not entirely what I was aiming for.

I could improve on the above by rerunning enemy placement multiple times and select the most favorable outcome - but the solution would be rather naive.

To have control over the number of mobs I decided to place them as I generate the rooms of the dungeon. There’s a trick one can use to get a random element with equal probability distribution from a sequence of an unknown size:

import random


def get_random_element(sequence):
    """Select a random element from a sequence of an unknown size."""
    selected = None
    for k, element in enumerate(sequence):
        if random.randint(0, k) == 0:
            selected = element
    return selected

With each iteration the chance of the current element to become a selected item is 1 divided by number of elements seen so far. Indeed, a probability of an element being selected out of a 4-item sequence:

1 * (1 - 1/2) * (1 - 1/3) * (1 - 1/4) = 1/2 * 2/3 * 3/4 = 6/30 = 1/4 

Now all I had to do is to modify this to account for multiple mob placement. Here’s a generalized function above which accounts for selecting n elements from the sequence with even distribution.

import random


def get_random_element(sequence, n):
    """Select n random elements from a sequence of an unknown size."""
    selected = [None for _ in range(n)]
    for k, element in enumerate(sequence):
        for i in range(n):
            if random.randint(0, k) == 0:
                selected[i] = element
    return selected

I incorporated logic above into the room generation code, accounted for duplicates, and ended up with decent distribution results.

After playing with random dungeon generation for a bit, I seem to be satisfied with the results:

The above is displayed using ADOM notation - # represents a wall, . is a floor tile, and + is meant to be a closed door. After fiddling about with a few random dungeon generation ideas, I settled with the following.

The algorithm

  1. Start with a random point on a canvas.
  2. Create a room with random width and height. Don’t worry about walls yet.
  3. Select a few points on the sides of the room, put those in a stack. Save the direction in which the potential doors would point.
  4. Go through the stack, and try to add a room or a corridor (corridor is just a room with a width or a height of 1). Higher chance of corridors seems to look better and results in those wiggly passageways.
    1. Try to add a room a few dozen times with different random configurations. If no luck - give up and grab a new item from the stack. If couldn’t generate a continuation to the corridor - mark it as a loose end, we’ll clean those up later.
    2. If a room was added successfully - add a door where it connects to the previous room/corridor. Add a floor section if it’s a corridor connecting to another corridor.
    3. At this point one ends up with quite a few interconnected corridors, merged rooms, and all kinds of fun surroundings (my desired goal).
  5. Do the above until the stack is empty or a desired number of rooms has been generated.
  6. Clean up the loose ends from step 4.1. Remove loose corridor segments one by one until intersection with another room/corridor is detected.
  7. Add walls around the rooms and corridors, while also cleaning up any extra doors we may have left behind when creating merged corridors or rooms.
    1. I simply used slightly modified depth first search starting inside any room and drawing walls wherever I could find floor/door not connected to anything.
    2. When encountering a door - check if it’s surrounded by walls or doors from the opposite sides. If not - remove the door and replace it with a floor tile. If any doors were adjucent to the removed door - requeue the door check on them.
  8. Perform steps 1-7 a few hundred times, saving the resulting dungeons each time. Pick the best candidate with the most desired features - like a number of rooms, breadth, square footage, longest corridors, etc.

A more detailed explanation of the steps is below. For now, here are a few more dungeons generated using this method:

I think dungeon generation is far more art than science, and I had a lot of fun tweaking all the different input parameters:

  • Room size boundaries.
  • Corridor lengths.
  • Frequency of corridor occurrences.
  • Number of exits from the room.
  • Number of room generation attempts.
  • Number of dungeon generation attempts.
  • Final dungeon picking heuristics.

Last item on the list is the most interesting one - with few hundred dungeons as an input, picking the right one is rather important. I ended up settling on using max priority queue with a rough surface area of the dungeon as a key (it’s more of a breadth, really - how wide and tall it is). Then I’d sift through some top results and pick the one with the most rooms available. This results in the most fun-looking map which feels up most of the screen, while still usually not being too cluttered.

Here’s a breakdown of a simple scenario:

Steps 1 and 2

Pick a random spot on a canvas and generate a room of random size (4 x 3):

....
....
....

Step 3

Select potential spots for doors, let’s label them 1, 2, 3.

....2
....
....1
  3

I went for a uniform distribution by unfolding the rectangle and folding it back in to get a proper coordinate on the perimeter. Now, stack contains coordinates of [1, 2, 3] (along with the directions in which they are pointing).

Steps 4 and 5

Add a room or a corridor to a connector number 3. We’ll be adding the room to the right of number 3. Let’s assume random sends a corridor of length 5 our way. We’re happy with the corridor pointing either up, down, or right - so we let the random decide again: up.

     4
     .
     .
....2.
.... .
....3.
  1

We add the end of the corridor to the stack as number 4 (now [1, 2, 4]). We also mark 4 as a loose end, in case we end up not adding a room to it. Dangling corridors are never pretty.

Now, to replace number 3 with a door:

     4
     .
     .
....2.
.... .
....+.
  1

Adding another random corridor of length 2 to the point 4, pointing right. Replace number 4 with a floor segment, since point 4 was the end of another corridor. Remove point 4 from loose ends, add point 5.

     ...5
     .
     .
....2.
.... .
....+.
  1

Take point 5, generate a room of size 3 x 6. 5 becomes a door. Loose ends list is empty.

     ...+...
     .   ...
     .   ...
....2.   ...
.... .   ...
....+.   ...
  1

For simplicity’s sake, let’s assume we don’t want any more exits from this room. Back to the stack of [1, 2]. Point 2 seem to not have much room for growth. After a few unsuccessful attempts to place a room or a corridor there, we give up:

     ...+...
     .   ...
     .   ...
.... .   ...
.... .   ...
....+.   ...
  1

Now for point 1: we get another corridor of length 3. Point 6 is now added to the loose ends list.

     ...+...
     .   ...
     .   ...
.... .   ...
.... .   ...
....+.   ...
  +
  .
  .
  .
  6

Let’s assume we run out of space and can’t add anything to the end of 6. We’re done generating the dungeon. Our stack is empty, and our loose ends contains coordinates of 6.

Step 6

Start with the loose end, and remove items one by one until a tile with multiple neighbors is encountered:

     ...+...
     .   ...
     .   ...
.... .   ...
.... .   ...
....+.   ...
  X
  X
  X
  X
  X

Viola:

     ...+...
     .   ...
     .   ...
.... .   ...
.... .   ...
....+.   ...

Steps 7 and 8

There are no rogue doors in this scenario, so all we need to do is add the walls:

     #########
     #...+...#
     #.###...#
######.# #...#
#....#.# #...#
#....#.# #...#
#....+.# #...#
######## #####

All of the steps above should be repeated a few hundred times with different dungeons, and then the best dungeon should be picked as a final one.

Did I miss anything? Was cleaning up “loose ends” too much of a hack? What should have I done differently?