Continuing in the steampunk fantasy theme from the last post, I created a couple of images of airships and sail-ships with the same seed while slightly modifying the prompts.

For all the composite images below the prompts for the two tiles in the upper row differ by one word only. The prompt for the upper left one has “steampunk” while the upper right one has none. The impact is quite visible, the “steampunk” keyword adds some intricate structures and indeed steampunk feel to the picture.

The bottom tiles of each composite image have several changes compared to the upper row: “shooting”, “on fire” and “battle”. Indeed the bottom row tiles follow the prompt and contain flames, flashes and smoke.

The difference between the bottom left and bottom right tiles is that the prompt for the former one contains “steampunk”, while for the latter contains “sail-ship”. It’s easy to see that the bottom right tile contains more sails, as expected.

Overall I’m very impressed with the quality of the AI generated images and how the Stable Diffusion model maps the words to the image style and content.

AI art generators like Dall-E, Midjourney or Stable Diffusion are a pretty fun way of making your ideas materialize. Here are some images that I have generated using SD.

Niagara dragon
A lady in a forest
A lady in a forest – same seed, different parameters – note the difference
Witcher under a parachute
After a hard day at work
Scythe/Iron Harvest-like image
Crazy steampunk again
Not enough steampunk
What’s in the fog?
Scary
Very Polish ruins
Cavalry
Windmills
Cybergirl
Maria Skłodowska-Curie

I do happen to own another IR-controlled illumination device. My cheap RGB LED strip came with a 24-key IR remote. Apparently it uses the same NEC codes as the 24-button remote described here. The color shades may be a bit different, but the general logic is the same: colors on buttons below the red button become more and more yellow, colors below the green button become more blue, colors below the blue button become more violet. But it looks like my remote’s functions are misnamed – the smooth function is actually a red/green/blue flash which is not at all smooth.

The structure of the IR codes for my remote & receiver is not NEC compliant, for example the “BRIGHT+” code is 0x00F700FF, while the NEC standard requires the second byte (here: 0xF7) to be the binary complement of the first byte (here: 0x00). In fact I started with the same address/complement codes as for the LED bulb, but seeing no effect I modified my IR research program on Raspberry Pi and tried using the these NEC codes which worked.

As you can easily notice, the function codes can have 32 values (all possible upper 5 bit combinations) while the remote has 24 buttons. So again I went through the entire list to find any hidden features or colors and this time I managed to find something – at least two colors that cannot be set up via the remote’s buttons! The lowest unassigned code, 24 (0x18) resulted in a beautiful shade of pink. Same for 56, 88 and 120. Then 152 (0x 98) resulted in a “special” shade of blue. Same for 184 and 216. But strangely, 248 turned on normal red. And when I revisited the unassigned codes the pink was no more. I did manage to get the “special” blue and then white, but this stopped working too and finally all I was able to get by the unassigned codes was red/green/blue flash.

Something was missing here. When I pressed any existing color button, the LED strip always turned on the appropriate color using the current brightness. But the same unassigned codes sometimes turned on different colors depending on what “real” buttons were pressed in between. Therefore it looked like the IR receiver had some kind of internal state which determined the actual color to show in the response to an unassigned IR code. The question now was: how to manipulate the state?

Well, I was aware that the IR receiver actually did maintain some state: it was the brightness level. I started systematically to check the behavior of the LED strip in the following way:

  1. Reduce the brightness to minimum
  2. Send “red” code (or use “red” color button on the remote)
  3. Send 24 (0x18) code
  4. Repeat sending 24 (0x18) code until the colors stop cycling, write down the color(s)
  5. Send “red” code (or use “red” color button on the remote)
  6. Send “brightness +” code (or use the dedicated button)
  7. If the max brighness is not reached, go to 3

In reality keeping the log of things was hard to do because there is little difference between the brightness levels and thus little visual indication if the “brightness +” code worked. Initial tests indicated there were like 19 brightness levels, subsequent procedure resulted in 15 “behaviors”, I would assume the real number of levels is more likely 16 (as a power of 2).

|Brightness level | Function of code 24 (0x18)                   |
|        1        | cycle red/blue                               |
|        2        | cycle(?) red/white                           |
|        3        | cycle blue/green/red                         |
|        4        | cycle SPECIAL BLUE/white                     |
|        5        | cycle white/SPECIAL BLUE/greenish            |
|        6        | cycle(?) red/light blue                      |
|        7        | cycle light violet/light yellow              |
|        8        | cycle red/greenish                           |
|        9        | cycle red/turquoise/blueish                  |
|       10        | cycle yellowish/red                          |
|       11        | yellow                                       |
|       12        | red then cycle yellow/blue/violet/turquoise  |
|       13        | red then cycle yellow/PINK/turquoise         |
|       14        | RGB flash (same as smooth)                   |
|       15+       | RGB flash                                    |

The table above is a bit imprecise because sometimes the unassigned code 24 resulted in a red color and then it cycled between other colors and when taking notes I have not always indicated if the red is a part of the color cycle. Note that cycling the color required sending the code again, only “RGB flash” actually changed the red/green/blue colors automatically.

I did not have time to check all the unassigned colors, however it seemed like the other codes only turned on a single, unchanging color, while the code 24 was unique in that it sometimes cycled (switched between) the colors. This was why I actually started my research with that code. Anyway I was able to find two colors that I was previously unable to create using buttons, marked above as PINK and SPECIAL BLUE.

So was I able to decode the function of the unassigned code 24? A simple test of the above table was to go down from the maximum brightness to the minimal and check the effect. And at first I got the RGB flash and one step below the code resulted in a very fast white flash on the LED strip and the receiver went unresponsive! No buttons or codes worked, not even turn on/off codes, the only way to disable the flashing was to unplug the strip. Would it turn on again in the “hung up” state and become unusable? Luckily after reconnecting the power, the LED strip went back to the RGB flash, as usual after a power loss (by the way, not a great choice when there is a temporary power cut in the night and you are woken by the RGB flash).

After further experimenting and running a couple of times more into the “hung up” rapid flashing state I could not find any rhyme and reason for the function of the code 24. Perhaps it might be some bug in the IR receiver which never expects this code and runs into undocumented behavior. Some evidence for that is that sometimes the IR receiver becomes unresponsive. For the curious if the unassigned codes might be actually placeholders for some buttons from a larger remote, here is the map of all buttons and codes in the numerical order, I have indicated the physical row of buttons on the 24-button remote, starting with 0 for the top row.

Row  |Column 1 code & button   |Column 2 code & button   |Column 3 code & btn  |Column 1 code & button
-----+-------------------------+-------------------------+---------------------+-----------------------
Row 0|  0 0x00 00000xxx BRIGHT+|128 0x80 10000xxx BRIGHT-| 64 0x40 01000xxx OFF|192 0xc0 11000xxx ON 
Row 4|  8 0x08 00001xxx R4     |136 0x88 10001xxx G4     | 72 0x48 01001xxx B4 |200 0xc8 11001xxx FADE
Row 2| 16 0x10 00010xxx R2     |144 0x90 10010xxx G2     | 80 0x50 01010xxx B2 |208 0xd0 11010xxx FLASH
Row ?| 24 0x18 00011xxx ???!   |152 0x98 10011xxx ???    | 88 0x58 01011xxx ???|216 0xd8 11011xxx ???
Row 1| 32 0x20 00100xxx R1     |160 0xa0 10100xxx G1     | 96 0x60 01100xxx B1 |224 0xe0 11100xxx WHITE
Row 6| 40 0x28 00101xxx R5     |168 0xa8 10101xxx G5     |104 0x68 01101xxx B5 |232 0xe8 11101xxx SMOOTH
Row 3| 48 0x30 00110xxx R3     |176 0xb0 10110xxx G3     |112 0x70 01110xxx B3 |240 0xf0 11110xxx STROBE
Row ?| 56 0x38 00111xxx ???    |184 0xb8 10111xxx ???    |120 0x78 01111xxx ???|248 0xf8 11111xxx ???

As I have bought LED RGB 230V bulb (9W, 720lm) with an an infrared remote, I started to wonder if it can be controlled from a computer using an IR diode. In fact there are many successful such projects described online, some of them basing on Raspberry Pi, which was a good fit for my old RPi2.
The diode that I used for this project was TSAL6400. GPIO current is too weak to directly control devices, so an amplifier circuit mut be built, in my case based on this article.

Take caution! If you have a different transistor than the 2N2222 from the original circuit,
you probably may use it but you need to verify the datasheet to check the limits and the transistor legs layout on your electronic part.
In my case I only had BC639 transistor available and the “base” leg is number 3 on my transistor and not the middle leg as on 2N2222. Check everything!
BC639 and 2N2222 datasheets.

Other changes in my circuit: I used GPIO23 and reduced the current limiting resistor from 200 Ohm to 100 Ohm. I believe I can go down to 30 Ohm with my setup and some people skip adding this resistor at all, at the risk of reduced lifetime for the IR diode.

A useful RPi pinout reference.

As for the software, I followed the advice from this 2016 blog article and used pigpio library instead of LIRC.

Pigpio installation guide.

Note that pigpio was already present and enabled in my Raspberry Pi OS. Your mileage may vary.

The author of the blog article promoting pigpio has also kindly provided ready to use C examples.

Many similar electronic devices use NEC protocol for infrared communication, which requires sending 32 bits:

  • an address byte e.g. 0x00
  • a logical negation of the address byte e.g. 0xFF
  • a command byte e.g. 0xC0
  • a logical negation of the command byte, e.g 0x3F

I had no IR receiver for my RPi so I could not check the codes sent by the original remote for the light bulb. However many reports on the internet suggested that the cheap devices use an address of zero or ignore it alltogether.


With this assumption I added a loop to the ir-slinger NEC example to go through all 256 commands and immediately got a hit. Command 0 increased the brightness! But the next few commands caused no reaction. I wondered if the signal was too weak, then with command 8 the bulb changed the color.
No further reaction until command 16 – I could definitely see the pattern emerging.

It turned out that my light bulb used only the upper 5 bits for the command byte. This meant that 32 commands were theoretically possible, while the remote only had 24 buttons. I had high hopes that I would discover some hidden features in the light bulb!

After I mapped all the commands it turned out that none of the unmapped codes had effect on the light bulb. So unless the device needs some combination of the hidden codes, there are no extra features. Moreover the MODE button does not seem to do anything different from the R1 (full red) button, it immediately switches the bulb to red color.

So my experience was in fact very similar to the one described in this article.

Here is the mapping of the buttons as arranged on the actual remote. Please note that to use the number listed below as the command, you need to multiply the number by 8 (or shift left by 3 bits). For example OFF command is 96=12*8. What is interesting is that the buttons are arranged in 4 columns and the 2 upper bits indicate the column while the lower 3 bits indicate the row, although both the rows and the columns are not in an increasing numerical order.

[+] 0 00000                    [-]  8 01000   WHITE  24 11000 
                               OFF 12 01100   ON     28 11100   

R1  2 00010     G1 18 10010    B1  10 01010
R2  6 00110     G2 22 10110    B2  14 01110   FLASH  30 11110
R3  1 00001     G3 17 10001    B3   9 01001   STROBE 25 11001
R4  5 00101     G4 21 10101    B4  13 01101   SMOOTH 29 11101
R5  3 00011     G5 19 10011    B5  11 01011   MODE   27 11011

However this is not the only RGB LED device we have at home. In the next installment we will try to solve the mystery of the missing colors on a LED strip.

I have recently upgraded from my old Raspberry Pi Zero to Raspberry Pi Zero 2. My little homegrown project uses Pi, Python 3, Pygame and a USB camera for monitoring purposes. After I loaded the latest Raspberry Pi OS onto an SD card in the new Zero 2 I was pleasantly surprised that all the dependencies I needed were already installed. However my script immediately ran into problems:

Traceback (most recent call last):
  File "/path/to/my/script.py", line 22, in <module>
    pygame.camera.init()
  File "/usr/lib/python3/dist-packages/pygame/camera.py", line 44, in init
    from pygame import _camera
SystemError: set_controls() method: bad call flags

Some googling helped to determine that I needed to dump the Pygame 1.9 delivered with Raspberry OS. Here is a confirmed working way of upgrading to the latest Pygame 2.1.2:

python3 -m pip install pygame==2.1.2

This upgrade allowed me to run the script to acquire and display the camera image. However not everything was working correctly for me:

    pygame.image.save(surface, path)
NotImplementedError: saving images of extended format is not available

It turned out that the Pygame I have installed via pip does not support so called “extended” formats i.e “PNG” or “JPEG” while my project uses the latter. One workaround is to recompile the Pygame with the extended formats support. Another way is to use a “simple” format like “BMP” and convert it to “JPEG” via command line tools like ImageMagick’s convert. I settled on doing the conversion within a script, as the PIL library is already provided in Raspberry Pi OS. PIL Image class can determine the type of the saved file basing on the extension, similarly to the Pygame’s image.save function.

from PIL import Image 
...
path = "/your/path/image.jpg"
...
data = pygame.image.tostring(surface, 'RGB')
pil_img = Image.frombytes('RGB', surface.get_size(), data)
pil_img.save(path)

I hope that this install hint and the code snippet can save your time when upgrading Pygame on Raspberry Pi or other systems.

Here is a static screenshot from my AWK script that can be used for visualising numeric geographical data. The script input is a text file with each line containing the country name and some numerical value. The actual globe is animated and rotates.

world.jpegThe script is provided in a single line form, so you can paste it into your terminal without installing anything.

I’m also providing a static map in a Mercator projection, as well as a map of larger or unique airports. What’s more all the data for this project was prepared using standard Unix tools, nothing more advanced than AWK.

What was probably most demanding: the country shapes were provided as polygons, so I had to use ray casting to determine which coordinates were inside each country. Also encoding and decoding scheme was made complicated by the fact that awk does not have built in ord() function, it is trivial to be built with an associative array and printf’ed characters, bu that consumes space in the script.

The link to the repository.

And the animation.

globe_filled

If you haven’t heard about Advent of Code, it is a really great Advent calendar of programming puzzles, some simple, some challenging. Doing them makes you a better programmer (or so I hope).

Below is the link to my solutions to Advent of Code 2019 . In the README I provide description about how I solved every puzzle.

https://github.com/quiath/advent2019

 

I have already owned the original Gamebuino – now named Classic – but I have not skipped the opportunity to upgrade my experience with the new Gamebuino Meta sporting more memory and a color screen!

Here is the box after opening.

 

And the contents of the deluxe package, including a real wood sticker! There is also a pouch (not shown).

 

Here is a closeup of the screen including pixel art created by my kid using the recommended palette.

 

The programming environment is practically the same as for Gamebuino Classic, only with a new board, driver and library. Very quickly I managed to patch together an app that displays a background and a large animated moving sprite – the pony above:

I’m in love with the speed and the color screen. I see so many opportunities. Stay tuned for updates!

In the previous post (size-efficient maze generation) I showed how to write a simple and short, if suboptimal, program generating a maze. Now here’s how to put it to work and create a simple game for BBC micro:bit.

Micro:bit game

Micro maze

The game incorporates:

  • random selection of maze “branching factor”
  • 20×20 maze generation,
  • random placement of the target,
  • controlling the player dot by tilting the device,
  • on completion, display of the time used to solve the maze

The design of the game has been heavily influenced by the limitations of the micro:bit platform. The game does not include any sound effect, since the device has no built-in speaker (though you can add one). The 5×5 screen is extremely limited, at least every pixel has 10 lightness levels, so the walls can be distinguished from the player dot.

Yet what caused me the most headaches was the memory size limit both for the program itself and for the runtime. It turned out that it is extremely easy to add a few lines to a correctly working Micropython program, create a .hex file, download it to micro:bit, only to realize that it would not work. The device would display nothing and the all the info I found on the web was that probably the device ran out of memory while compiling Micropython to byte code. Even if the program run, it might still stop due to a MemoryException when allocating the memory required for the maze.

For example, I could have easily doubled the size of the maze while using the same amount of memory by storing 2 locations (cells) per byte, not one. Sadly, this made the program longer and more complicated, so it never “compiled” successfully. As it is, I keep the maze as one large bytearray where each byte corresponds to one cell. I encode the type of the cell in the lowest 2 bits of a byte. Support of “branching factor” requires that during the generation I know what was the original direction from which a maze location was originally entered. This takes 2 bits more – only for generation. This leaves 4 bits per byte unused.

On other Micropython implementations I used const keyword which saved memory. It seems to be unsupported on the Micropython version on http://python.microbit.org. I noticed that replacing Python “constant” variables by constants did reduce memory usage, however at the cost of reduced readability. I kept the number of named “constant” variables to minimum. I have also kept the Game class even though I felt that replacing it via some global variables would save runtime memory. That would be a bad programming example though.

Now about the game itself. After the start or reset, a number 0-9 is randomly selected and displayed. This is the “branching factor” where 0 has many branches and 9 has more long corridors. The maze is generated for a few seconds while this number is being shown. After it’s completed, the player position is shown as a bright pixels, walls are medium-light pixels, corridors are dark, the target is flashing. If A button is pressed, the minimap is displayed, with just the player (steady dot) and the target (blinking dot). Tilting the device will move the player in the direction of the tilt as long as it does not run into a wall.

And here’s me actually finding the target – yes, the whole search did take 300 seconds including the hidden part.

How to play?

  • copy the code below.
  • paste it into the editor on Python micro:bit website
  • name the program in a window to the right
  • press Download
  • save the .hex file to a directory of your choice
  • connect your micro:bit to your computer
  • drag and drop the .hex file from your computer to micro:bit
  • the code will be flashed
  • the game will start, display a number and after a short while you will be able to play

Enjoy!


# -*- coding: utf-8 -*-

import microbit as m
from random import randrange as rr

WH = 20
SH = 5

def clamp(a, b, c):
    return max(min(c, b), a)

class Game:
    def __init__(self):
        self.world = bytearray(WH * WH)
        self.player = [ 1, 1 ]
        self.pos = [0, 0]

    def getworld(self, x, y):
        return self.world[x + y * WH]

    def setworld(self, x, y, s):
        self.world[x + y * WH] = s

    def world2screen(self, p):
        return [ p[0] - self.pos[0], p[1] - self.pos[1] ]

    def screen2world(self, p):
        return [ p[0] + self.pos[0], p[1] + self.pos[1] ]

    def getscreen(self, x, y):
        p = self.screen2world((x, y))
        if p == self.player:
            return 4
        else:
            return self.getworld(p[0], p[1])

    def trymove1axis(self, p, d, axis):
        q = [ p[0], p[1] ]
        q[axis] += d
        if q[axis] < 0 or q[axis] >= WH:
            return False, p
        if self.getworld(q[0], q[1]) != 0:
            return False, q
        return True, q

    def updatepos(self):
        self.pos[0] = clamp(0, self.player[0] - SH // 2, WH - SH)
        self.pos[1] = clamp(0, self.player[1] - SH // 2, WH - SH)

    def moveplayer(self, dxy, ax):
        for i in range(2):
            if dxy[ax] != 0:
                r, q = self.trymove1axis(self.player, dxy[ax], ax)
                if r:
                    self.player = q
                    self.updatepos()
                    return True
            ax = 1 - ax
        return False        

XX = "2101"
YY = "1210"

CACTGOAL = 3
CAVAIL = 2
CWALL = 1
CEMPTY = 0

def mazesearchactive(g, k):
    for x in range(1, WH - 1):
        for y in range(1, WH - 1):
            if g.getworld(x, y) & 0x03 == CACTGOAL:
                if k == 0:
                    return x, y,
                k -= 1
    return None, None

def mazeupd(g, x, y, active, d):
    p = g.getworld(x, y)
    if p & 0x03 == CACTGOAL:
        g.setworld(x, y, CWALL)
        active -= 1
    elif p == CAVAIL:
        g.setworld(x, y, CACTGOAL + (d << 2))         active += 1          return active      def maze(g, prob_max, prob_t):     active = 0     for x in range(WH):         for y in range(WH):             if x == 0 or y == 0 or x == WH - 1 or y == WH - 1:                 g.setworld(x, y, CWALL)             else:                 g.setworld(x, y, CAVAIL)                      g.setworld(1, 1, CACTGOAL)     active += 1          nx, ny = -1, -1          while active > 0:

        if nx == -1:
            x, y = mazesearchactive(g, rr(active))
        else:
            x, y = nx, ny
            nx, ny = -1, -1

        lastd = g.getworld(x, y) >> 2

        g.setworld(x, y, CEMPTY)
        active -= 1

        for d in range(4):
            nactive = mazeupd(g, x + int(XX[d]) - 1, y + int(YY[d]) - 1, active, d)
            if lastd == d and nactive > active and rr(prob_max) < prob_t:
                nx, ny = x + int(XX[d]) - 1, y + int(YY[d]) - 1
            active = nactive

    g.setworld(x, y, CACTGOAL)
    return x, y

h = rr(10)
m.display.show(str(h))
g = Game()
gx, gy = maze(g, 10, h)

f = 0    

while True:

    if m.button_a.is_pressed():
        m.display.clear()
        m.display.set_pixel(g.player[0] * SH // WH, g.player[1] * SH // WH, 9)
        m.display.set_pixel(gx * SH // WH, gy * SH // WH, 8 * (f % 2))
    else:

        for y in range(SH):
            for x in range(SH):
                v = g.getscreen(x, y)
                if v == CACTGOAL and f % 2 == 1:
                    v = 0
                m.display.set_pixel(x, y, v * 2)

        if abs(g.player[0] - gx) + abs(g.player[1] - gy) <= 1:             break         dx = m.accelerometer.get_x()         dy = m.accelerometer.get_y()         ax = int(abs(dy) > abs(dx))
        dx = clamp(-1, dx // 200, 1)
        dy = clamp(-1, dy // 200, 1)
        if dx != 0 or dy != 0:
            g.moveplayer((dx, dy), ax)

    m.sleep(200)
    f += 1

while True:
    m.sleep(500)
    m.display.scroll("{}s".format(f // 5))

The task for today’s exercise is to build a maze and keep the size of the source code as well as memory under some limits. There is an array “world” of size WW times WH and it needs to be converted into a maze, where some cells are passable and some are walls. The outer rows and columns must become walls and the maze needs to be “perfect” – no loops and all passable cells are reachable. Let’s assume that the start is always in position (1,1) and there is one special cell being the goal.

labexample

In the above example the white square in the left upper corner, (1, 1) is the starting point and (22, 44) in is the light grey goal. Dark grey squares are walls and black cells are passable. Walls surround the maze as required. Some cells that are not empty are not reachable however (very dark grey). As long as there are few such cells a maze is good enough even if not perfect.

Generating a maze is generally not a challenging task. The assumption for today is that we are running the maze generation on a device with limited memory for both the code and the variables. Namely, we can store several bits of information in each maze cell, above that we can only use several simple variables. How does it affect our approach to maze generation algorithm?

These restrictions mean that using recursion or stack based algorithms directly is out of question. One of the possible approaches would be to use a method similar to BFS (Breadth First Search) or DFW (Depth First Search) where the next square to be visited would be randomly selected from the queue or the stack, respectively. The data structure would require some additional memory for storing “active” square coordinates, which does not match our requirements.

The idea is that we can trade time for memory size and simply mark the active squares in the same memory that we are using for storing the maze itself. Randomly selecting the next active cell to be processed requires:

  • keeping track of the count of all active cells, e.g. active
  • select randomly the index k from 0 to active-1 (the next cell to be processed)
  • search through all the possible squares, stopping the search on kth active cell

If there are N cells, this makes the algorithm complexity to be N^2. For example a 5×5 maze with 25 cells would require at worst 25×25 = 625 operations in the worst case. So this method can be used only for rather small mazes, but we are limited by the memory anyway.

This approach leads to mazes with high branching i.e. there are many shorter corridors. If longer straight corridors are desirable, we can incorporate such a feature by storing the direction of the travel in each active cell and adding a probability P that this direction will be continued next time the cell is processed.

Here is an example of the algorithm execution for the simpler version without directionality.

In the example below:

  • W: wall
  • ?: available unprocessed cell
  • A: active cell
  • : empty cell

The initial state; walls surround the available cells.

+---+---+---+---+--
|Y/X| 0 | 1 | 2 |
+---+---+---+---+--
| 0 | W | W | W | W
+---+---+---+---+--
| 1 | W | ? | ? | ?
+---+---+---+---+--
| 2 | W | ? | ? | ?
+---+---+---+---+--
|   | W | ? | ? |

Cell (1, 1) is made active and the active counter is set to 1.

+---+---+---+---+--
|Y/X| 0 | 1 | 2 |
+---+---+---+---+--
| 0 | W | W | W | W
+---+---+---+---+--
| 1 | W | A | ? | ?
+---+---+---+---+--
| 2 | W | ? | ? | ?
+---+---+---+---+--
|   | W | ? | ? |

The index of the active cell to be processed is randomly selected, however there is only 1 at the moment. The algorithm scans the whole table and finds the active cell. The current active cell changes state to empty and squares up, down, right and left are examined. Walls up and left are not changed. Available cells to the right and down become active. Active cell count is updated (2).

+---+---+---+---+--
|Y/X| 0 | 1 | 2 |
+---+---+---+---+--
| 0 | W | W | W | W
+---+---+---+---+--
| 1 | W |   | A | ?
+---+---+---+---+--
| 2 | W | A | ? | ?
+---+---+---+---+--
|   | W | ? | ? |

In the next step the index of the active cell to be processed is randomly selected; for example 2nd: (1, 2). Table is scanned to find that cell. The current active cell changes state to empty and squares up, down, right and left are examined. Wall to the left and empty square up are not changed. Available cells right and down become active. Active cell count is updated (3).

+---+---+---+---+--
|Y/X| 0 | 1 | 2 |
+---+---+---+---+--
| 0 | W | W | W | W
+---+---+---+---+--
| 1 | W |   | A | ?
+---+---+---+---+--
| 2 | W |   | A | ?
+---+---+---+---+--
|   | W | A | ? |

Next cell is randomly selected and found by scanning, e.g. (2, 1). The current active cell changes state to empty and squares up, down, right and left are examined. Neighboring walls and empty squares are unchanged. Available cells to the right becomes active. Neighboring active cell becomes a wall, since it would otherwise link two different empty squares and we do not want loops. Active cell count is updated – it is reduced to 2.

+---+---+---+---+--
|Y/X| 0 | 1 | 2 |
+---+---+---+---+--
| 0 | W | W | W | W
+---+---+---+---+--
| 1 | W |   |   | A
+---+---+---+---+--
| 2 | W |   | W | ?
+---+---+---+---+--
|   | W | A | ? |

This short example showed how this algorithm is able to build paths and walls with minimal additional storage.

Here is the actual Python code, extended with the feature of longer straight corridors.

Values to be stored in the maze cell. Available (unprocessed) and active (on the currently processed list) cells codes can be reused. MASK and SHIFT are used for encoding the codes (lower two bits) and travel direction (higher two bits).

MASK = 0x03
SHIFT = 2

CEMPTY = 0
CAVAIL = 1
CWALL = 2
CACT = 3
CGOAL = 3

Size efficient mapping of the numerical direction code 0…3 to x and y deltas. Deltas are -1, 0 or 1, so it’s necessary to add 1 for encoding and subtract 1 for decoding. For example if the direction d is 1, XX[1] is “1”, 1 – 1 == 0; YY[1] is “2”, 2 -1 == 1; thus the direction vector is (0, 1) i.e. down.

XX = "2101"
YY = "1210"

This is the function that scans the “world” searching for the kth active cell. The variable g is the “world” object that exposes getworld(x, y) and setworld(x, y, state) member functions. This function returns the coordinates of the kth active cell.


def mazesearchactive(g, k):
    for x in range(1, g.WW - 1):
        for y in range(1, g.WH - 1):
            if g.getworld(x, y) & MASK == CACT:
                if k == 0:
                    return x, y,
                k -= 1
    return None, None

Maze update function. g is the “world” object as above, x and y are the coordinates of the cell being updated, active and avail are counts of the active and available cells respectively. Strictly speaking available cells do not need to be counted, unless we want to implement checking if any cells were left unprocessed. So this code can be still simplified if the the target architecture requires it. d is direction (0…3), the index to XX and YY strings.


def mazeupd(g, x, y, active, avail, d):
    p = g.getworld(x, y)
    if p & MASK == CACT:
        g.setworld(x, y, CWALL)
        active -= 1
    elif p == CAVAIL:
        g.setworld(x, y, CACT + (d << SHIFT))
        active += 1
        avail -= 1

    return active, avail

Finally, the maze generating function. g is the “world” object, while prob_t / prob_max is the probability that the direction of the corridor will be extended. The higher this probability, the more straight corridors. Specifying this as a ratio avoids floating point numbers that might not be available on the destination platform. The last square to be emptied is marked as the goal. This tends to be distant from the origin, however if the probability of straight corridors is higher, the last square may turn out to be close and the maze will be easy to solve.


def maze(g, prob_max, prob_t):
    avail = 0
    active = 0
    for x in range(g.WW):
        for y in range(g.WH):
            if x == 0 or y == 0 or x == g.WW - 1 or y == g.WH - 1:
                g.setworld(x, y, CWALL)
            else:
                g.setworld(x, y, CAVAIL)
                avail += 1

    g.setworld(1, 1, CACT)
    active += 1
    avail -= 1

    nx, ny = -1, -1

    while active > 0:

        if nx == -1:
            x, y = mazesearchactive(g, rr(active))
        else:
            x, y = nx, ny
            nx, ny = -1, -1

        lastd = g.getworld(x, y) >> SHIFT

        g.setworld(x, y, CEMPTY)
        active -= 1

        for d in range(4):
            nactive, avail = mazeupd(g, x + int(XX[d]) - 1, y + int(YY[d]) - 1, active, avail, d)
            if lastd == d and nactive > active and rr(prob_max) <span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>< prob_t:
                nx, ny = x + int(XX[d]) - 1, y + int(YY[d]) - 1
            active = nactive

    g.setworld(x, y, CGOAL)
    return x, y

This code was used in a Micropython application and should be easily portable to embedded Lua or C/C++.

And here are mazes generated with various likelihoods of direction reusing. Low values have higher branching, higher tend to have straight long corridors.

0%

lab0

10%

lab10

20%

lab30

30%

lab30

40%

lab40

50%

lab50

60%

lab60

70%

lab70

80%

lab80

90%

lab90

Design a site like this with WordPress.com
Get started