It’s time to write more about my adventures. I’ve been traveling and living in my car for nearly a year now, mostly full-time (excluding hotels on infrequent business trips, occasional friends’ couches, or AirBnB a few times I was sick or wanted a home). I’d love to share what I’ve learned over the past year.

Here’s how a typical conversation with someone who just learned I’m living in my car goes:

You live in that?

Yes I do. I think I need to start from the beginning.

About a year ago I wasn’t in a very good place emotionally. I’ve been going through a divorce, my dream company declined my job application (spoiler: I got the job this year, yay), and my landlord decided that 12 tiny houses on a property isn’t enough and it’s time to kick everyone out to build a more dense apartment complex. I didn’t do much outside of going to work and sleeping.

I think I saw an opportunity to do something drastic with my life, and I decided to take a leap of faith. I gave away everything I couldn’t fit in the back of the car, and set on a road trip across continental United States (after spending a month doing test drives and adjusting my setup dozens of times).

The experience turned out to be more enjoyable than having a house.

How do you even fit in there?!

Everyone I talk to thinks the space is really cramped up, and I have to sleep curled into a ball in the trunk (or something along those lines).

I’m 5’11”, and I have a flat twin-width bed in that’s around 7 feet long (it’s been awhile since I measured). I sleep on an inflatable pad wrapped in a sleeping bag, on top of another sleeping bag all wrapped in a silk sleeping bag cover - my composite mattress is about 3 inches thick. I use regular blankets and pillows, adding or removing a few depending on the temperature outside.

It’s one of the most comfortable beds I’ve ever slept on.

Poor soul, want to crash on my couch?

This is an offer I get as soon as somebody learns I’m living in a car (it’s right up there with “You must be saving so much money not paying rent!”). Most people are really nice, and offer a place to stay. I think it stems from an understandable notion that one probably would only live in a car because of being in a bad situation.

Oddly enough I really enjoy the lifestyle, and I always reject the offers (always appreciate them though). Sometimes I contemplate getting a small RV or a van, but I’m way to attached to air conditioning on demand and 50 mpg on the highway. But I probably won’t need to crash on your couch.

I think the TL;DR here is that majority of people are really nice, once you get out and get to interact with them enough.

Why on Earth are you still living in a car?

There’s more than one reason at this point. Part of it is that it has become a habit now. The biggest reason is probably all the fun things it has forced me to do.

I’ve met a lot of cool people, tried out so many things I would’ve never considered trying, and found exciting hobbies I would’ve never had otherwise. I’m also now in a great shape physically since I usually have to hit up the gym to shower.

It also made me confront my inner demons, since car living tends to consistently provide me with time to be alone with my thoughts - be it driving somewhere, or falling asleep in an area where I wouldn’t want to use electronics. I feel more in tune with myself, and I can close my eyes without being afraid of stream of thoughts keeping me awake at night.

That’s cool! What else is inside?

The front

I try to keep driver’s seat and front passenger seat area clean and empty, with storing some low key items in passenger seat leg space – portable 12V vacuum, water jug, water boiler/thermos.

The front seats are separated from the back by a dark curtain. Unless you shine a (powerful) flashlight inside a car it’s almost impossible to tell the curtain is there.

The back

One of the passenger seats is lowered to make a bed. The bed is extended with a wooden panel specifically made for this purpose which creates a flat surface about 7 foot long. A mattress with a blanket and some throws creates /a nice homey look.

Under the bed is “the basement” – large clear container where I store winter clothes and things I rarely use. If I’m camping extra water jugs go here as well. And some bicycle maintenance stuff, and extra pairs of shoes.

I use the other rear passenger seat as my primary relaxing area when I’m inside the car. There’s a little trashcan stuck to the floor in between the seats.

Cargo and the bike

There’s an old suitcase I use for organizing various cooking and camping supplies. It’s split into nine compartments by some separators I’ve put together. This is where I have some freeze dried veggies, spices, grains and pasta, cooking oils, camping pots and pans, equipment for cleaning the dishes, etc. On top of the suitcase is a backpack where I keep clothes. There’s also a propane burner, camping table and a chair, as well as a toiletry kit. Dirty laundry goes in a dry bag.

I also have a bike strapped to the outside of the car, exploring places by bike after arriving to a new city is very rewarding!

Isn’t it dangerous?

You often read stories of people suffocating in their own cars, and it’s something to be aware of. There are a few rules I follow to be safe, and there are three main dangers here:

  1. Carbon monoxide poisoning.
  2. Carbon dioxide poisoning.
  3. Extreme temperatures.

I sleep with carbon monoxide alarm not far from my head, and my car is a hybrid – meaning it only turns itself on once in a while when batteries need to be recharged. Sometimes I sleep with my car on, and sometimes not. The alarm never went off so far (it works, I check regularly).

Carbon dioxide (stuff you exhale) poisoning is not a silent killer (unlike carbon monoxide), humans are pretty good at detecting high concentration of CO2 – panic attack on par with a headache are a rather clear sign of this happening. I keep windows cracked open most of the time, plus my AC is often on.

I love sleeping at low temperatures (50-60F is my comfort zone for snuggling up under a blanket), but if local forecast suggests the temperature will drop below 50F at night - I make sure to sleep with AC on. If it’s below freezing - I roll up the windows, otherwise I still keep them cracked open a little.

And if it’s really cold and I don’t want to run AC all night, or I just feel like I want to stay inside for a bit - I can just get AirBnB for a couple of nights. Speaking of, by pure chance I stayed at Gayle Laakmann McDowell’s guest house few weeks ago, and I left with great memories and a signed copy of Cracking the Code Interview. Bonus point for meeting cool people.

How does Prius AC work anyway?

This question pops up often enough to address here (along with condensation concerns from seasoned vandwellers).

It’s a hybrid with two electric motors assisting the internal combustion engine. The electric motors power up from a ~1.3 kWh battery, which is recharged using the engine and regenerative brakes. That battery is connected to the 12V battery, and as long as there’s gas in the car – both batteries will be charged. In practice, if the car isn’t moving, engine turns on every half an hour or so for a little under a minute to recharge the batteries.

This allows one to keep the car on and use the AC throughout the night. AC also keeps the moisture level at bay, preventing condensation. I also noticed that when I don’t use the AC cracked windows help with condensation (I have rain guards for stealth and keeping the rain out).

Um, how do you do bathroom stuff?

If we’re talking about changing - there’s plenty of space in the back of the car to change.

For hygiene - I use gym showers (I take quite a lot of classes), or wet wipes if I’m out camping or shower is otherwise unavailable. I’m now kind of an expert on all sorts of wipes. Any restroom works for brushing my teeth at night, and in US it’s very difficult not to be within the vicinity of a public bathroom.

Where do you park at night?

Rest stops are the best when I travel, since everyone on a rest stop is just resting/sleeping in their car.

There are so many places to park: since this is just a passenger car, anywhere where cars can park is good. Usually somewhere with either a lot of foot traffic (or the exact opposite). Parking lots of 24/7 business, residential areas, anywhere and anytime.

What do your friends and coworkers say?

Most colleagues and acquaintances don’t know, since it’s not really a topic that pops up during a normal conversation. I go to work and social events well groomed, with clean clothes, and (I’d like to think) demonstrating a sense of style.

Few friends and colleagues that do know that I live in the car don’t see it as anything out of the ordinary (past the initial shock of course). I can’t remember the last time conversation got stirred towards car living with any of my close friends. A few are rather jealous but reluctant to make the move. I try not to preach.

My love life surprisingly didn’t suffer. Maybe it’s because I tend to pursue women just as odd as me, but me living in a car didn’t have any negative effects, and even got me a date or two. If someone doesn’t want to go out with you because of living arrangements - they would probably not make a good partner in a long run.

Do strangers bother you?

When you observe people you start noticing how much everyone’s concerned with what people think of them. In fact, most people so concerned with themselves - that they don’t look around. Encounters with people who notice I live in a car are nearly non-existent. Nobody cares to look inside one of the hundred cars in a parking lot or parked on the side of the street.

I’ve had a few rather friendly encounters with parking lot security, with people usually just demonstrating concern. Until I share my setup that is, then concern is often replacement with mild jealousy.

Most encounters with strangers approaching me happen at rest stops, since I usually don’t hide much – there are enough misfits and travelers. Every encounter like that I’ve had was positive, with people commenting on how amazing my setup is, how they wished they could do a similar thing, or sharing a story about their own travels.

How do you not go crazy?

This is where the fun part starts. You see, the thing about living in the car is how boring it is. There’s really not much you can do in a confined space. So I have to get out of my way to entertain myself.

I take a lot of classes like various martial arts or archery. I tried miniature painting, rock climbing, long distance cycling. There are numerous things I’m “forced” to do in order to keep myself entertained.

There are a lot of things I do inside the car too. Writing or coding (even though I prefer coffee shops), rare indulgence in video games (I recently picked up a gaming laptop for this), guitalele (it’s a ukulele-sized guitar), reading. Drawing, keeping a journal, or just relaxing.

And of course there are people. When I travel I end up meeting a lot of different people. I often get invited to join somebody at a campsite, and we’d cook a meal together or share some stories. Or somebody sitting next to me in a coffee shop works in the same industry and we grab lunch to mingle and share interesting ideas. Wherever I go I get lucky enough to make friends for a couple of minutes, hours, days, and sometimes months.

What’s next for you?

For now, I’m used to living in a car. I sometimes pop by AirBnBs or friends’ places for a couple of days, but I wouldn’t say I enjoy being tied down to a single location for a long period of time.

I’m contemplating getting an RV to be able to stand up inside and have more room and privacy, but potential difficulties with air conditioning at night, atrocious gas mileage, and inefficient and noisy generators for my power usage are a concern of mine.

Once in a while I contemplate getting myself an apartment. Since it’s now winter I end up staying in AirBnBs more often, but I feel like having a place to call home makes it harder for me to a fight a lazy person inside me. I like when that person is forced to learn, explore, and create.

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) {
    a = {},
    b = "ez_35",
    c = ["open", "release", "unlock"],
    d = "digit",
    r = "",
    s = false,
    t =;

  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++) {

    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++) {
        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) {
    return ret, args;

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

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

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

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

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

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

    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 =,
    unlocked = [];

  ret = target(args);

  while (true) {
    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);
        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.

        data   -- A dictionary with keys as elements and values as weights.
                  Duplicates are allowed.
        target -- Target weight, results will be skewed towards target
        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

        A random key from data with distribution respective of the target
    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:
        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
    range(len(res)), res.values(), width=0.7, align='center', color='green')
pyplot.xticks(range(len(res)), res.keys())
    'iterations={}, target={}, chance={}'.format(
        iterations, target, chance))

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.