Servo-Operated Ball Maze: Marble Mayhem

Daniel Koch

Issue 46, May 2021

This article includes additional downloadable resources.
Please log in to access.

Log in

A servo-operated adaptable Labyrinth to test your skills and your sanity.


Most people have played with a ball maze at some point. Some are tiny ones wrapped around the barrel of a pen, while some are keychain or novelty sized. Others are more serious toys, hand-held or two-handed with a bit more build quality. There are even arcade console-sized versions around. All have one thing in common: A maze track which a ball must roll through, and the maze needs to be tiled to achieve this.

The design we present here is a combination of many other ideas that we have seen over time. While some motorised examples can be found, we haven’t seen the inside of one. That means we have no idea if they work with servos or not, but that’s what we’re going with. There are a few other features here that we have come up with ourselves but may well already be out there. We apologise if you beat us to it and we just don’t know!


We have deliberately designed this build to be accessible. That means the materials are readily available or substitutable, or both. The primary build material is foam-cored cardboard, which you can find in dollar shops or office supply stores. Be careful, because there are craft versions and art versions, and they are priced accordingly. We bought a packet of five A3 sheets for under $20, while some art versions with special paper surfaces sell for that for a single sheet. Have a bit of a look around before you commit to buying. Foam-cored cardboard is strong enough but still easy to tool and machine. It can even be drilled. You can use timber or plywood if your skills are up to it.

Additionally, we have designed it to be adaptable. There is no one maze design that would suit everyone. By using a drop-in system, the one gimbal rig can be used to house many mazes, from the simple to the insanely complex. This also takes into account different build skills. Maybe you want a maze with curved paths, inspired by the classical representations of King Minos’ structure used to imprison the Minotaur on Crete in Greek Mythology. Many other Labyrinthian or maze-like concepts are associated with other cultures, notably Celtic and those descended from it.

If you’re not thinking of a culturally specific shape, then the maze type that emerged in the Renaissance period may be more your style. These were the straight-lined, multipathed version with dead ends and blind passages that we are most familiar with today. They were made of hedges in estate gardens and were navigable by people. This concept is, incidentally, closer to the literary descriptions of the ancient Greek Labyrinth on Crete despite the artistic representations from shortly after and onwards all depicting the single-path, curved version. The later hedge mazes are the style we now know and see scaled down and printed in kids’ activities or as more complex versions for adults.

Another relevant type of maze is not always thought of as such. An open area with obstructions strewn through it can be thought of as a maze. In this way, paintball and laser tag fields may be considered mazes, as well as many pinball machines. This offers a very different maze experience when combined with our mounting and gimbal system.

The advantage of a drop-in maze in a holder is that you can change the maze as often as needed. You can have several complexities and adapt for the user, or you can just make another when you get too good at solving one. You can also adapt it for different ball sizes. You can also experiment with different styles, from the curved single path to the familiar multipath straight walled type, to the obstacle field variety.


We’ve based our build on an Arduino Uno, partly because it’s ubiquitous and partly because, with the need for a breadboard for other connections, there was nothing to be gained from choosing a tiny footprint. Any other microcontroller would work too.

Added to this are two small servos, and a joystick. The whole assembly is powered by a USB battery bank but could be run from any other USB supply. The servos will require centering before use. To do this, we used the servo tester from Issue 24 in July 2019.

The joystick we chose has two potentiometers with a spring mount, with one pot travelling perpendicular to the other. It is designed so that the potentiometers are at the centre of their travel when the stick is centred, and are used as voltage dividers. As such, when supplied with 5V, the centre position for the joystick gives 2.5V nominally at the signal pins for each axis. Moving the stick one way on a given axis draws that voltage down to 0V, while moving the other way increases the signal to 5V. Anything in between is proportional.

Joystick module XC4422 from Jaycar

This means we can use the analog pins to read the voltage and create a 1024-bit variable. This can be mapped and scaled so that full travel on the joystick gives the number of degrees of travel that we want as our maximum to tilt the maze bed. This will be nowhere near the 90° either side of centre that the servo is capable of, hence the need to map and scale.

Other types of joystick use limit switches and so are only digital controls. If you’re going to build this maze gimbal into an arcade machine-sized unit, the bigger limit-switch joysticks may suit you more. That said, non-proportional movement where the bed tilts all the way to its designed limit might be more reminiscent of the 80s style game play that those arcade machines replicate anyway. Think of a pinball machine. Pushing a button half way does not move a flipper or paddle half way.

The Build:

Electronically, the build involves a small breadboard to make connections practical, and an Arduino Uno. The two servos mount on the gimbal chassis, and joystick on its own little palm-sized base plate. While we could 3D Print a case for the joystick, it’s engineering for the sake of it seeing as we’re already going to have quite a bit of foam-cored card lying around.

Parts Required:JaycarAltronicsCore Electronics
1 x Solderless BreadboardPB8820P1002CE05102
14 x Plug-to-plug Jumper Leads*WC6027P1017PRT-12795
4 x Plug-to-socket Jumper Leads*WC6028P1021PRT-12794
1 x Arduino Uno or Compatible BoardXC4410Z6280A000066
2 x 180° Mini ServosYM2758Z6392SER0006
1 x Joystick ModuleXC4422Z6363SS101020028
1 x USB CableWC7704P1901CFIT0265

Parts Required:

* Quantity shown, may be sold in packs.

It could be argued that we should 3D-print the gimbal too, but again, it’s engineering for the sake of it. While it’s a reasonable assumption today that most makers have a 3D printer or have access to printing services, there is still a need to keep designs below 150mm x 150mm x 150mm for something we want to call ‘accessible’. While bigger printers are common and getting cheaper, they’re not ubiquitous yet. For that reason, our build went with a built-from-scratch option. If you want to, you could design a 3D-printed chassis and gimbal.


The electronics are simple enough not to need step-by-step instructions for most builders. Carefully follow the Fritzing and Schematic to assemble the core build. Note that one of the servos needs extension wiring, so use plug-to-socket jumper wires if you don’t want to cut and solder. We went down this road. The breadboard is a small variety with no rails.

The USB supply can be made from a cut-up USB cable with two jumpers twisted or soldered on. Be careful of polarity as some USB cables use non-standard or even no wire colouring. Use your multimeter to double-check, looking for the ‘-’ symbol in front of the 5V when the meter is set to voltage and the USB plug inserted in a suitable power source. If you see 5V, you have the positive and negative to the right USB wires. If you see -5V, you’re the wrong way around.

Before you do anything else, make sure the servos are centred.

You can do this with the servo tester from Issue 24, or by using the code and changing the mapping to 0 and 180. These are the preferred methods. The other option is to manually rotate the servo with a servo horn attached. Mark the end points, then return to centre. If there is any error with indexing in the servo, this method may not work. That happened to us, as the servo’s travel was actually close to 200° and the 180° nominal was not centred within that. Whichever method you choose, when the servo is centred, take off the horn and position it so that the long axis is parallel to the body of the servo.

One last task is to extend the wires for the joystick. There are pins on the stick to take standard jumper wires so it makes sense to extend these. We chose ribbon cable, and used lots of heatshrink. This way, we can still change out the joystick if anything happens or if we want to find one of a different size. We carried these to the length we needed, then joined the original pins from the jumper wires back on. We left two longer ones, to reach the power distribution on the breadboard, and two shorter ones to reach the Uno.


The joystick is something we envisage being handheld, so we made up a small holder for it out of the same foam-cored cardboard we’re building the machine out of. We used scraps left over after we built, and you might like to wait too. It’s fairly simple, with a small plate big enough to grip, and low walls to secure the joystick. One end is open to pass out the cable. We secured it in place with double sided tape. The other two options here are 3D printing an enclosure for the joystick, or mounting it to the front of the machine on the baseplate instead of having it handheld.


Start by deciding how big your mazes are going to be. We decided to make ours 180mm x 180mm. You can easily make smaller mazes to fit into a bigger gimbal, but the reverse is not true. Cut one piece of foam-cored cardboard to that size.

It is a good idea to give a little clearance around your maze, so that it can slide into and out of the gimbal with relative ease. We chose a 3mm clearance in total for each axis, with 1.5mm per side. That means the inside of the next frame you build needs to be your maze size plus your clearance size. We decided to make our frame from four identical pieces, rather than two long and two short. We therefore measured the thickness of the foam board, which was 5.5mm, and added that to our maze and clearance. That makes the length 180 + 5.5 + 3mm = 188.5mm.

The frame height is 25mm in our case. Also cut four corner braces that will both hold the drop-in mazes, and keep the frame square. Fix these so that the bottom face is at the same height as, or slightly above, the servo where it will mount later. The bottom of the servo will be flush with the bottom edge of the frame, and our servo is 12mm thick. That means the underside of our corner pieces sit 12mm above the frame. The extra thickness of the corners gives room above the servo for adding hot melt glue if it needs a bit more securing. We’ve already mounted the servo so you can see what we’re on about but that’s in the next step.

The servo needs to mount so that the shaft is at the middle of the frame, enabling even pivot. Because of the way the sides were joined end-to-face, this isn’t the middle of a given piece. As such, take the measurement with the frame assembled. Mark the middle line, but then hold the servo underneath and mark the cut-out. The shaft of a servo is rarely in the middle of the body. Cut this space out so that the servo mounts into the frame with the underside flush with the bottom of the frame. Glue it in place. We used hot melt glue but that choice is up to you. Measure the height from the bottom of the frame to the centre of the shaft of the servo. On the other side, mark the middle but then make a mark at the same height as the servo shaft, which will be quite a way below centre. For the sake of imagery, we did this on the inside of the frame so you can see it.

Attach a servo horn to the servo, and measure the distance between the top of this and the edge of the frame. In our case, 7.9mm is as good as 8mm. This will be the clearance between the inner and outer frame. Measure the size of the outside of the inner frame, with the clearance either side. For us, that came to 195 + 8 + 8mm = 211mm. That’s the inside dimensions of the frame, so adding the 5.5mm thickness of the foam-cored cardboard, we need four lengths, 216.5mm long. However, we need clearance for the inner frame to tilt, so this frame has to be deeper and may need to be wider. We need enough clearance to cope with the sides of the maze that will slope when tilted.

We’re expecting our maze to need to depress 10° in order to get the ball to roll, but different designs may need more, particularly curved mazes where 10° in one direction may not be very much in another that we don’t have control over. As a worst case scenario, we’re suggesting 20°. Use a protractor or, if you’re wanting the mental exercise, trigonometry, to work out how high the axles are when the inner frame is tilted to 20°. The axels are slightly above the base of the frame but the difference will be negligible. This will need to be factored in soon though. The outer frame must have at least this clearance between where the axles mount (at the top, in reverse of the inner frame) and the top of the bracing triangles which will go at the bottom of the outer frame.

In our case it was 36.25mm, and this is where we need to factor in the axle height. While it won’t make much of a difference to the trig calculations, we do need to make sure the clearance starts at the axles, not the top of the frame. This means our outer frame needs to be at least 43mm deep. We’ll go with 45, and round it to 50mm to allow for the thickness of the bracing pieces.

There is one more consideration, and it involves some more trigonometry. As the inner frame tilts, its walls are no longer vertical. They now lean out further than the nominal length of the frame which was our hypotenuse earlier. We calculated again according to the diagram presented, and found that the extra room needed was 8.5mm. Because 20° was a worst-case scenario (our code is set up for 10°), and the clearance is only 0.5mm less than what the calculations said we needed, we left it unchanged. Your sizes and choices may lead to a different situation, however, so consider this. The most likely way to solve the problem would be to mount the servo so the flanges mount outside the inner frame, not inside it as we have.

Finally, we can cut four lengths of foam-cored cardboard 216.5mm long and 50mm wide. We can also cut triangles to brace the frame. These are glued together in the same way as the inner frame, with the exception of the brace pieces, which mount at the bottom instead of part-way up. This frame needs no cut-out, as the servo for this axis mounts on the base frame. It will, however, need an axle mounting of its own, and another to receive the non-driven axle from the inner frame. For this, mark a line along the inside of two opposite sides of the frame that is 7mm down from the top. Find the middle and mark that too. Do the same for the outside of the bottom edge: 7mm up, dead centre.

Now comes the biggest part: Making the base. This is a static frame that holds the inner and outer dynamic (moving) frames, and will also feature a ball return race, space for the breadboard and Arduino, and two trays for marbles or bearing balls. Making a return tray that slopes in two directions is very difficult with a knife and foam-core, because it involves slopes in two directions on one piece of board and the resulting triangle calculations are going to be a bit harder than those above. Instead, we’re going to make a ramp in one plane and a sloped channel in the other.

Start by deciding how high your frame will be. It should be at least the same as the outer dynamic frame plus enough room to give fall on the ball return ramp and channel. We’re going with an overall height of 80mm. The size of the inside of the frame is dictated by the clearance of the servo, which this time mounts with the flanges inside the frame but the preponderance of the body extending outward. That amounts to 16mm clearance. Taking the 222mm outer frame dimensions (the glue added a little), and adding two lots of 16mm, the inner frame size must be 254mm. Add another 5.5mm for the thickness of the card, and we end up with 259.5mm, as near to 260mm that it makes no difference. So, we can cut four 260mm x 80mm pieces of foam core.

These are assembled in the way we’re now familiar with, and we included two bracing pieces in the corners to keep it rigid until we glue it to a base. The other side is brace-free to fit the ball channel. We decided to make our ball channel 15mm wide, to easily cope with 12mm or ½ inch bearings and marbles. After a little experimenting, we found that a fall of the 5.5mm thickness of the foam-core across the length of our frame (255mm internal) was enough to make marbles move. Your situation may be different but that translates to an angle of 1.24°. In other words, not a lot. Additionally, Pythagoras’ theorem shows that the hypotenuse of our triangle (the ramp face) is only 0.06mm longer than the nominal 255mm base with a 6mm side, so we can ignore that.

To make the channel, cut a section of foam-core cardboard the length of the inside of your static frame and the width you want the channel to be plus the width of a piece of foam-core. That made ours 255mm x 21mm. Slice another piece in a triangle with one side 255mm and the other 5.5mm (or the width of your foam core). This will stop smaller bearings like 4mm or 5mm ones getting jammed. Finally, cut a small spacer of foam core from a scrap, and glue the three pieces together as shown.

Cut a hole in the side of your static frame the same width as your ball race. For ours, this was 15mm. Add the height of the foam-cored cardboard ramp to the width to get the height of the opening. We decided not to chamfer the end of the channel, partly because it was such a shallow angle, and partly because we wanted the thickness of the channel end to act as a backstop so that marbles cannot roll back inside even a little way. Glue the channel inside so that it sits hard against the frame. We used hot melt glue and glued it to the frame along the long side of the triangle, and the higher end.

The overall frame will be mounted on a base plate. Because the centre of this will be completely hidden and has no function, it can be cut out to form the ramp that leads balls or marbles to the channel. Place the static frame where you want it to sit on a large enough piece of foam-cored cardboard. We have left room at the back for the breadboard and Uno, room at the side for the ‘winning ball’ to drop, and room at the front for the ‘fail’ balls to roll out into. With the frame in place, trace around the outside and inside. Take it away, and extend the inner lines where the bracing is, so that all lines meet in a square. Carefully cut this out.

Take the inner cut-out and make sure it fits inside the static frame. Trim it if need be, then cut the width of your channel off one side. When this piece becomes a ramp, it will now end at the edge of the channel. Cut two small supports the same height as your channel. Ours was 11mm. Deciding on how much height you need to cause a reasonable slope on the ramp, cut a support piece to this height. On our build, it just so happened that the piece cut off the ramp will be the right height when sitting on the corner braces. Unless you’ve made significant alterations to the design yours should too, even if you’ve scaled up or down proportionally. Glue these supports in place. Dry fit the ramp to make sure everything lines up, and glue the ramp in place.

At this point, it’s time to take a break from frame assembly and start working on assembling the gimbal. While on one side of each axis, the servo is the axle, there is a need for an axle on the other side. There are some great options out there but with accessibility and ease of working in mind, we went with bamboo skewers. These can be cut with side cutters and are available at supermarkets, so they satisfy both criteria.

For the inner frame, apply glue to the servo horn and position it at the mark on the inside of the outer dynamic frame. Carefully line it up so that the centre of the shaft is over the centre mark made earlier. On the other side, insert a skewer and apply glue to the outer frame so that the skewer cannot rotate in this bit. Glue here helps stop any sagging as the weight of the inner frame and maze tends to push down and make these axles tilt. Notice we used a metal skewer to make the holes in the photos, but replaced it with a wooden skewer after. When the glue is properly set, you can trim the skewer so that it projects inwards a little way on the inner frame, but finishes flush with the outside of the outer dynamic frame.

The process is similar for mounting the outer dynamic frame, which now has the inner frame mounted to it, into the static frame. The chief difference is that the axles are at the lower edge of the outer dynamic frame, and the top edge of the static frame. Start by finding the centre of the sides along which you are going to mount your axle. Use the servo to determine the cut-out required when the shaft is at the middle of the side. The body will be offset. Cut this out to a depth that will allow the body of the servo to sit flush with the top edge of the frame. Mark the position on the opposite side for the axel. It will be in the middle, and half the depth of the servo body from the top of the static frame.

Apply glue to the servo horn, and attach it firmly to the marks made earlier on the outside of the outer dynamic frame. Then, insert the skewer through the outside of the static frame, locate the outer dynamic frame into position, and push the skewer into that too. Hold the skewer to support the weight, and apply glue to the inside of the servo cut-out and the back of the flanges on the servo body. Insert the servo into its position and hold everything steady until the glue sets. When it does, you can glue around the axle skewer as in the dynamic frames. Trim it flush on the outside when you’re done.


There are different ways of making mazes. One method involves holes which the ball can fall through, before completion or at the end. That’s the purpose of the ramp and channel under the frame. However, we decided to add a channel under the axle that holds the outer dynamic frame to the static frame. This will be the ‘winning ball’ and is only underneath one hole. That does mean each maze that uses it has to finish in the same place, but the starting position and path can be different for each one.

We think only some people will want to go down this road, so if you do, take a close look at the photos. The channel has to be mounted low enough to clear the tilting frames above, but high enough to let balls and marbles pass underneath into the channel. Ours rolls out through a hole in the side into its own ‘winner’s box’. Even if you want to have holes in your maze, but not have a fished finish point, you can skip this. The advantage is that you can keep track of how many attempts it takes to get a win, because each ‘fail’ ball comes out the main ramp and channel and only the ‘winning’ ball exits this place.

The other thing we have added is a box around the ramp/channel output, to catch and store balls and marbles coming out of here. The Uno and breadboard are mounted to the back of the unit, using double-sided tape. Finally, the servo cables are glued into place. Be careful to route the cable from the inner servo in such a way that it does not interfere with balls on the ramp, or the movement of the frames of the gimbal.

Last of all, plain white foam-core is boring. We spray-painted ours to give a better finish. In retrospect, that really needed to be done before assembly, but only in a few places is it obvious that the paint didn’t reach.


As hinted at, you can make a maze with holes for the ball to disappear in. This can take two main forms. You can have holes as traps along the way, for the ball to fall into. Alternatively, you can have a hole only at the end. If you go with the latter option, the maze will be easier and you won’t need the ‘winning ball’ channel described above. If you make mazes with no holes at all, then you can forgo the entire ramp and channel system. With foam core, you can use a drill to make holes, but the edges will be messy. We found it was easier to drill holes and clean them up with a sharp knife, than it was to cut round holes with the knife from scratch.

Maze forms can be anything that fits into the footprint in the top of the gimbal. We added small silicone pads to our corners in the gimbal so that we could keep a little clearance with the maze plate but still not have it slide around. Further, while the maze plate has to be the size of the gimbal, the maze on it does not. We have a few mazes here. One is simple with a hole at the end. One has trap holes and therefore a broader path, and one is the open field type. This also utilises trap holes. To make a curved maze, you won’t be using foam-cored cardboard. Perhaps thick plain cardboard would work, or thin plastic. You could also 3D print any of these options.

On that note, you can find online maze generators. These are usually free for personal use and some have licensing options but none were for editorial use. Because of this, even though we’re not distributing any mazes as downloadable files, we made the ones in the photos from scratch. It’s quite a process to think of a good one, so ours probably don’t look as good as ones you can make with the generators. If you are making your own, we found grid paper to be invaluable, and you can download that for free online too.


With a maze loaded into the gimbal, it’s time to power up your creation. You most likely unplugged the servos during construction of the gimbal and frame, so plug those back in. After uploading the code to your Uno, you’ll have to figure out which axis you want to be ‘x’, and which you want to be ‘y’. This will depend on your preference but most makers will likely want ‘x’ to be left/right and ‘y’ to be forward/backward. If moving your joystick does not produce these results, swap the two servo pins at the Arduino. If the axes are correct but the direction is wrong, such as moving the joystick left moves the gimbal right, then you can change the code. The code as presented uses mapping for the joystick to the servos, and we’ll discuss how to change that in the following paragraphs.


The code relies on the availability of a library to run the servo. This normally complex job is rendered very simple by the use of this library, called ‘Servo.h’. While we prefer to rely on in-code commenting to explain code aspects, so that code can be updated and changed without rendering the article obsolete, some things need more words than can fit reasonably in a comment. If you’re already comfortable with coding, you probably won’t find anything new in the following paragraphs.

The first two lines under the addition of the library assign that library to two objects, which we named ‘xaxis’ and ‘yaxis’. These are, of course, the servos. They are named for the axis they are attached to and therefore the direction of movement they impart. While ‘x-axis’ would have been a clearer name, the ‘-’ symbol is part of the syntax of other areas of the C language, and therefore causes an error if used in an object name.

#include <Servo.h>
Servo xaxis;      
Servo yaxis;          
int xpot = A0;       
int ypot = A1;   
int xvalue;     
int yvalue;

Under that is a series of integers which we define. Two, ‘xpot’ and ‘ypot’ are the pins that connect to potentiometers in the x- and y-axes of the joystick, respectively. These are analog pins A0 and A1. The other two integers are variables used later to store the value of the potentiometer, both as it gets read from the pin, then after scaling. These are ‘xvalue’ and yvalue’. We cannot use a hyphen, for the same reasons as before.

In the ‘void setup’ section, a less familiar function is found: ‘attach’. Without getting too complex, the name of this function is pretty accurate. We named our servo objects earlier as ‘xaxis’ and ‘yaxis’ and the ‘attach’ function just assigned the objects with their library links to the pins we want to call for the servo to move. We’re using the higher-speed Arduino PWM pins, pins 9 and 10. The other two operations in this section are to define analog pins A0 and A1 as inputs. While this isn’t strictly necessary, analog pins will not give an accurate reading if they have been made outputs before. There are a few other unusual circumstances too and while we don’t see many or any occurring in this code, it’s a good habit to be in to keep everything stable and performing as it should.

void setup() {
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);

In the main loop, there are two nearly identical groups of code. It’s the same thing repeated, with only the ‘x’ and ‘y’ axis letters changing. The ‘analogRead’ line reads the value of ‘xpot’, which is the integer we assigned to A0. It stores the value in the integer we defined as ‘xvalue’. The number will be between 0 for 0V and 1023 for 5V.

The next line is a little harder to follow if you're not used to it. This line alters the number stored in ‘xvalue’. The ‘map’ function scales the 1024-bit number into a number between two variables. We first tell the function which integer stores the information we want (xvalue), then tell it what this number is currently between 0 and 1023 (written as 0, 1023,). After this, we tell it what we want that value to be scaled to. For a servo with 180° rotation, this would normally be between 0 and 180, and would appear as 0, 180). The whole syntax would be xvalue = map(xvalue, 0, 1023, 0, 180);

void loop() {
  xvalue = analogRead(xpot); 
  xvalue = map(xvalue, 0, 1023, 80, 100);
  value = analogRead(ypot);
  yvalue = map(yvalue, 0, 1023, 100, 80);

However, notice that in the code shown here, the numbers ‘80’ and ‘100’ appear instead. This is because we want all the travel of the joystick, which is the whole 0 to 1023 range, to translate to only 20° of movement, 10° either side of the centre. Because 90° is the centre, 80° is ten degrees less, and 100° is ten degrees more. Feel free to change these values to suit your build, but we found going below 5° was problematic. Be careful of the travel of your maze being too great or too small.

If the direction of your joystick movement was uncomfortably mirrored, for example left joystick causing right-handed movement, then this part of the code is that you need to change. Just swap the numbers so that instead of 80° to 100° (or whatever angle range you have chosen), yours is 100° to 80°.

At the end is a 15ms delay, which allows time for the servo to move before the process can repeat. Then the code includes the same four lines again, changed for the y-axis this time.


You could get adventurous and build an automatic leveller into your maze. The servos sometimes do get out of alignment. While we suggested our Servo Tester from issue 24 as a tool to overcome this, it does mean disconnecting the servos for readjustment.

Theoretically they should not, but they manage to anyway. An automatic leveller could use optical or inductive sensors at the corners of the gimbal, in such a way as they are only triggered when the bed is dead level. Code would then be used to reset the zero index for the servos, or create an offset.

Besides that, it’s a matter of how complex your maze can be, from the simple to the crazy dense curved versions. That’s entirely up to you, your skills, your patience, and your sanity.