Saturday, 20 December 2014

Enhanced wifi controller for digital board game

Our digital board game idea depends heavily on a wi-fi based module, to transfer data from the game board to the tablet/smartphone/PC that is hosting the app running the game rules. We've spent a lot of time investigating lots of different methods of getting data from the board into some kind of controller, including
  • USB (suitable really only for PCs)
  • Serial (suitable for PCs and some Android tablets)
  • RF devices (a bit buggy and susceptible to noise)
  • Audio (works with all tablets, even crazy Apple iDevices)
  • Ethernet (works with everything but requires lots of wires)

Eventually, and thanks to the super-cheap ESP8266 modules that recently flooded onto the market, we went with wifi (prior to the ESP8266 we'd given up on wifi as too expensive, using either WiFly or HLK-RM04 modules).

Now there are a number of convoluted ways of getting your SSID and password into the wifi module (including setting it up as an access point, connecting your phone to it, running an app, entering the SSID/password combination then closing the app, disconnecting the phone from the AP, rebooting the wifi module in "client" mode, reconnecting your phone to your home router and starting another app, then using UDP broadcasts to find out the ip address of the wifi module) but in the end we went with something a little simpler - a screen and a rotary dial to "type in" your SSID and password into the wifi module (it retains this information during power cycles, so shouldn't need to be done too often).

Now that's been working fine for a while now, and we're at the point where we're able to share a few videos with people to gauge the level of interest for an electronically enhanced board game platform. Mostly, people have been really excited by it. But there have been maybe one or two people who didn't quite understand that the idea is to speed up tabletop gaming, by having the smart device do all the dice rolling and conflict resolution for you - they wanted to be able to roll dice and somehow enter the results into the game.

At first, we just dismissed this idea as the rambilngs of a few RGP-obsessed lunatics. But over time, we just got to thinking - well, why not? So we've added a bit of extra code to our wifi module, to allow the user to select a dice roll result, using the rotary dial and push button.

The first thing to do was draw some 1-bit bitmaps of dice. This wasn't as easy as we'd hoped (grab some images off Google, resize, reduce colour depth) and we ended up drawing each dice shape, pixel by pixel.

Of the two types, we preferred the "inverted" dice (though the "white" dice also look pretty nice, we may yet use those!) so squashed them up together (to remove any unnecessary blank pixels, to keep our RAM usage down to a minimum) then ran a simple VB script to create one big array of bitmap data.



Here's a video of it in action:




While we've provided a method for players to roll dice and enter their results into the app, it's a little bit cumbersome and not very satisfactory - stopping the game to interact with technology, to then return back to the board game, breaks up the feeling of playing a tabletop game: rolling dice and placing them over some kind of "reader" which could then automatically extract the dice result from the actual dice face would be a far more "fluid" way of playing the game. It's just a shame that we can't do that just yet!

[blog post edit: Oh yes we can - http://nerdclub-uk.blogspot.co.uk/2014/12/dice-reading-photo-image-processing.html ]

Dice reading - photo image processing

One of the methods suggested for reading back dice rolls for our electronic board game is to make use of image processing. This seemed like a massive overkill, as well as being quite nasty to implement. Sure, we're using a smart device/tablet with our board game and it has a camera in it - so holding a dice up to a camera and getting it to read the value off it makes sense. Sort of.

Until you consider how this would play out in the middle of a game. At key decision points in the game, roll a dice (or two or three) then show them, one at a time, to the camera on the smart device. Which may be a front-facing camera. Or it may be round the back. Or it might be off to one side, so you don't actually present the dice to the screen displaying the game details, but to a tiny lens on the opposite side and towards the edge of the device. Except in landscape mode, the camera isn't at that end, because the device is rotated 90 degrees the other way around..... it just gets really nasty, really quickly.

Steve pointed out that there are OV7670 UART-cameras available relatively cheaply all over the net and that at £2 each, they're only a little bit more expensive than a matrix of five reflective sensors. Our little 8-bit micro is unlikely to have the grunt-power to do much image processing (though, reading through the code below, if one could be found with enough RAM, it might just be possible) so the idea is to snap an image with the camera, stream it from the camera GRAM into the microcontroller, and send the data, byte-by-byte to the host app over wifi (using one of the ESP8266 UART-to-wifi modules).

Now we're working with the original v2 firmware on these devices, which runs at 115200bps. An image taken at 640x480 is a massive 307,200 bytes. Even using a really low colour resolution (where each colour is one byte of 3G3R2B) that would take over 20 seconds to transfer just one frame.

Luckily, the OV7670 has a number of supported modes. One of which is QQCIF and scales the captured image to 88x72 pixels.

88x72 = 6336 bytes or 50688 bits, which, at one byte per pixel, could be transferred in less than half a second over 115200bps UART-to-wifi. Even on the "higher setting" of one byte per colour (three bytes per pixel) this is about one-and-a-half seconds to transfer the image; a much more reasonable delay time.

So what does an 88x72 bitmap look like?
Here's a photo we took with a regular camera phone, of two dice on a clear plastic acrylic sheet, with the backlight on (to illuminate the face of the dice being photographed).


 When scaled down and reduced to a 1-bit image (all our image processing will be done on single-bit data) it looks like this:


Although not perfect, it retains enough information to show which dots are showing on the dice. All we need to do is process the image and extract the dice values. Which is easier said than done!

Steve suggested OpenCV and an ANE to get existing processing routines running in Flash (we're coding our native apps in Flash, the compiling the same source code down for iOS, Android and PC). This took a lot of time to set up and understand, and fail to understand, and give up on. Eventually we decided to just code our own dot recognition routine!

We can't be sure that the dice in the image are "square" to the frame - they could be at any old angle. But irrespective of the angle, we're expecting to find one or more "clusters" of dots in the image. That is, one or more instances of a dot made up of black point, surrounded by white points. So the first thing we do is scan the entire bitmap (it's only 6336 bytes remember: looping for(i=0; i<6336; i++) is actually pretty quick in AS3) and look for a black pixel, with a white pixel above, a white pixel below, a white pixel to the left and a white pixel to the right.


Whenever we find this combination of points, we compare the centre pixel to the centre pixel of previously found "dots" on the dice. If it's within a few pixels, there's a very real chance that it's actually one of the dots we've already found, so it's ignored. If it is a new dot, however, we add it to an ongoing array.

During development, we wrote a routine to draw each discovered dot in our black-and-white bitmap image. Amazingly, it correctly drew the dice dots in the right place, first time!


After parsing the image once, we end up with an array of co-ordinates where our black-dots-surrounded-by-white appear. The trick now is to group these into "clusters" of dots, to work out what the actual face values are.

We give every set of co-ordinates in the array a "cluster number" - all co-ordinate groups begin set to zero. The first time we find a co-ordinate point without a cluster number, we give it the current dice count, then loop through all the other points, looking for another dot, within a few pixels of this one, also without a "cluster number". By calling a couple of recursive functions, we can give every dot in the image a "cluster number" by working out which other dots it's closest to.

Once all dots in the image have been given their "cluster number" the values of each dice are easy to read back. If we gave three dots a cluster number of one, dice one has the value three. If five dots were given the cluster number two, it means that dice two has the value five, and so on.

The great thing about this approach is that it automatically adapts to more than one or two dice: so long as the dice are separated so that the dots appear in definite, distinct groups, there's no reason why this routine can't detect the face values of three, four, five or more dice in a single image.

Here's the code

import flash.display.Bitmap;
import flash.geom.Point;

var bmp:BitmapData=new BitmapData(imgHolder.width, imgHolder.height, true, 0x00000000);
var foundDots:Array = new Array();
var p0:int=0;
var p1:int=0;
var p2:int=0;
var p3:int=0;
var p4:int=0;

var clusterChar:int=0;
var clusterIndex:int=0;
var lastDot:Object;

function findDots(){
     var idx:int=0;
     bmp.draw(imgHolder);
   
     for(var y:int=2; y<imgHolder.height-2; y++){
           for(var x:int=2; x<imgHolder.width-2; x++){
                 // check to see if you can find a dot
                 p0 = bmp.getPixel(x,y);                
                 p1 = bmp.getPixel(x-3,y);                
                 p2 = bmp.getPixel(x+2,y);                
                 p3 = bmp.getPixel(x,y-3);                
                 p4 = bmp.getPixel(x,y+2);                
               
                 // if you find a dot, see if you've already got one within the very near vicinity
                 if(p0==0x00){
                       if(p1!=0x00 && p2!=0x00 && p3!=0x00 && p4!=0x00){
                             // this looks like a black dot on a white background
                             // but check the array to see if we've ever found a dot within a few
                             // pixels of this one (it might be the same dot)
                             // if so, skip this dot (you've already found it)          
                             // otherwise add to the array of found dots                            
                             if(similarPixel(x,y)==false){
                                   var o:Object = new Object();
                                   o.coords=new Point(x,y);
                                   o.cluster=0;
                                   o.index=idx;
                                   foundDots.push(o);
                                   idx++;
                             }
                       }
                 }                
           }
     }    
}

function drawFoundDots(){
     var circle:Shape = new Shape(); // The instance name circle is created
   
     for(var i:int=0; i<foundDots.length; i++){
           trace(foundDots[i].coords.x+","+foundDots[i].coords.y);
           circle.graphics.beginFill(0x990000, 1); // Fill the circle with the color 990000
           circle.graphics.lineStyle(1, 0x000000); // Give the ellipse a black, 1 pixel thick line
           circle.graphics.drawCircle(foundDots[i].coords.x, foundDots[i].coords.y, 4); // Draw the circle, assigning it a x position, y position, radius.
           circle.graphics.endFill(); // End the filling of the circle

     }
   
     addChild(circle); // Add a child
     circle.x=imgHolder.x;
     circle.y=imgHolder.y;
}

function parseDots(){
     clusterChar=0;
     clusterIndex=0;
     var dotValue:int=0
     var dots:Array=new Array();
   
     // find the first dot in the array that doesn't have a cluster character
     lastDot=getDotWithNoClusterChar();
     while(lastDot){
           clusterIndex++;
           dotValue=1;
           trace("found start of cluster "+clusterIndex+" at index "+lastDot.index);
           lastDot.cluster=clusterIndex;
           while(lastDot){
                 lastDot=getConnectedDotForCluster(clusterIndex);
                 if(lastDot){
                       trace("found another dot for cluster "+clusterIndex);
                       dotValue++;
                       lastDot.cluster=clusterIndex;
                 }
           }
         
           trace("dotValue = "+dotValue);
           dots[clusterChar]=dotValue;
           clusterChar++;
           lastDot=getDotWithNoClusterChar();
     }
   
     for(var i:int=0; i<dots.length; i++){
           trace("dice "+i+" value "+dots[i]);
     }
}

function getConnectedDotForCluster(indx:int){
     var o:Object=null;
     for(var i:int=0; i<foundDots.length; i++){
           if(foundDots[i].cluster==indx){
                 for(var j:int=0; j<foundDots.length; j++){
                       if(j!=i && foundDots[j].cluster==0){
                             if(Math.abs(foundDots[j].coords.x-foundDots[i].coords.x)<=7 && Math.abs(foundDots[j].coords.y-foundDots[i].coords.y)<=7 ){                      
                                   trace(i+" is connected to another dot in cluster "+indx);
                                   o=foundDots[j];
                                   break;                
                             }
                       }
                 }
                 if(o){break;}
           }
     }
     return(o);
}

function getDotWithNoClusterChar():Object{
     var o:Object=null;
     for(var i:int=0; i<foundDots.length; i++){
           if(foundDots[i].cluster==0){
                 o=foundDots[i];
                 break;
           }
     }
     return(o);
}

function similarPixel(ix:int, iy:int):Boolean {
     var found:Boolean=false;
     for(var i:int=0; i<foundDots.length; i++){
           if(Math.abs(foundDots[i].coords.x-ix) < 4 && Math.abs(foundDots[i].coords.y-iy) < 4){
                 // found a similar pixel
                 found=true;
                 break;
           }
     }
     return(found);
}

findDots();
drawFoundDots();
parseDots();

Below are the results of some of our testing. To date we've tested it on about a dozen photos of dice (all take from the same distance, since any device using this approach would have a plate at a fixed height from a fixed-position camera) and each time we've correctly reported back the dice values on the faces in the photo.

Obviously, in real use, we'd need to subtract the dice face value from seven to infer the value that was face-up on the dice (since we're taking a photo of the dice face that is face-down on the clear surface) but that is just a trivial application of our dice-reading routine.



Friday, 19 December 2014

Dice reader for 18mm dice fail

It was BuildBrighton's Open Evening tonight and the reflective sensors had arrived from Farnell just minutes before we set out, along with some 18mm dice from eBay, so we took the whole lot along and designed our first dice reading device.

It didn't take long to discover that things weren't quite going to plan.
Namely the dice are too small, or the sensors are too large to work together:


We can get a line of three sensors to pretty-well line up with the dots on the dice. Where the two don't quite line up, we could take up any gaps by placing a laser-cut tray with the dice spots cut out between the dice and the sensors.


But when the dice is oriented the other way around, the spots and sensors simply refuse to line up.
We've already placed our sensors as close together as we can get them, on a single sided board (though even on a double-sided board, there's no much extra room between the pads).


No matter how much we twist the sensors around, we can't get the sensors and dice dots to line up. Which means we either need to come up with a different way of reading our dice - or maybe just buy some bigger dice to use with the game!


Thursday, 18 December 2014

Dice reading device

About a million years ago (and on another, earlier Nerd Club blog) Evil Ben was working on an electronic cube reader. The idea was to have a cube with a number of pre-printed faces on it, which could be placed into a dedicated cube reader and read back not just the face showing (by inferring it from which face was down) but also the orientation in the reader.

Having demonstrated how our electronic board game works to a few people even nerdier and geekier than we are, a few people have suggested that we might like to allow players to roll their dice and enter the results into the game rules.

Originally we dodged this as an idea- it just seemed like a whole world of hurt and extra work. But then we thought "hey, why not?" and so made a few changes to our wifi connector device.

Now, our board game was deliberately designed to use nothing more than a single serial pin for tranferring data - specifically so that we can develop extra add-ons for the hardware without having to redesign it from the ground up. And one add-on that immediately springs to mind is a "dice reading module"; a module which can detect the presence of  dice, read which face is down (and from that, infer which face is pointing upwards) and send this information back to the host via serial.

We've already built this into our first game app - pausing the game at key points and waiting for the user to enter the dice roll result using the enhanced wifi connector. Instead of using the rotary dial to select a dice result, we can simply send the same "data message" back to the host, over serial, but from a different device plugged into one of the game board pieces.

So how do we read the value of a dice face?

These clever little QRE1113 things from Farnell are just the ticket - they are reflective sensors and are often used by robotics hobbyists for black/white line following. It's simply an IR LED and an IR phototransistor in a single package. When the sensor "sees" IR light, it activates the internal transistor. We can use this, connecting an input pin (pulled high using internal pull-up resistors) to the phototransistor collector, with the emitter connected to ground:


The simple idea is that placing an array of these sensors on a board, and placing a dice over them will result in some sensors activating (where no spot is found immediately above the sensor) and some not (where a black dot appears above the sensor, the IR will not be reflected back).

Thinking about how the spots on a dice are laid out, and allowing for the dice to  be "read" in any orientation, the first thing that springs to mind is a grid of 3x3 sensors. However, with a bit of thought, it turns out that we need only 5 sensors to be able to read every possible number on a dice face:


The image above shows how the spots on a dice could possibly be presented to a 3x3 grid of sensors. But if we use only the sensors as show on the right, we can still identify each possible number uniquely (and still allow for the dice to be rotated either horizontally or vertically).

For example, if the number two were face down, either the bottom-right or top-right sensor would be "inactive" and all other sensors would see reflected IR light from the (white/light-coloured) face of the dice. If the number three were face down, either of these sensors would be inactive, along with the  sensor in the "centre" of the 3x3 grid.

The patterns of active/inactive sensors are unique for each number on the die, as shown above. Now we're just waiting for our sensors to arrive from Farnell and we can put the theory into practice!

Saturday, 13 December 2014

Game board testing

Blog posts have been a little thin on the ground in recent weeks (compared to the usual volume of noise coming out of Nerd Towers). This has been due to a number of reasons - one of which is the inordinate amount of code we've been churning out.

Coding isn't exactly a great spectator sport - likewise, it doesn't always make for particularly interesting reading. But here's a quick video showing our pro-made PCBs interfacing with a dedicated fantasy football app.


We're placing the playing pieces on the "underside" of the board here - demonstrating that the board can be used either way up, allowing us to consider offering double-sided boards (perhaps with a space shooty-game on one side, and something like a football pitch on the other).

The app has the start of some interactive commentary in there - this needs redeveloping almost entirely, but the idea is quite fun; as you move your playing pieces across the board, two lip-synched characters give an audio description of what's actually going on, on the pitch - much like Motty and Lawro do on BBC football broadcasts!


We're hoping to have our fantasy football game finished in time for Xmas, and already GrumpyPaul(tm) is looking over some zombie co-op game rules, to see how they can be "electronified" for this system.

While we're already excited about getting our football game done, it does also mean that we're going to have to paint up about two dozen miniatures to be able to demonstrate it - on top of the forty or so zombies that Paul's no doubt got lined up for a game we've tentatively called "Last Night in Zombieville" (because the domain name was available!)


Wednesday, 10 December 2014

GW miniatures Blood Bowl

After painting up some Space Marines using, not exactly a speed-painting technique, but a faster-than-normal approach, and taking a break from writing mountains of trigonometry-based functions for line-of-sight and bullet-ricochet routines, we decided to have a go at some more GW miniatures - only this time, Blood Bowl players.

We used pretty much the same technique as for the Space Marines. Firstly, whack on a couple of base colours, keeping the palette simple. We're using Ash grey for anything that's going to be white at the end, A funny orangey-yellow for anything that will eventually be yellow, and some Crystal Blue (the same colour we used to base-coat the Space Marines) for anything blue.


At this stage, the miniatures look awful. They look pretty much like the first time I tried painting a miniature, aged about 12 years old! A quick dash of Army Painter Quickshade and the miniature is transformed


This time, because we're using mostly "warm" colours, and because we're expecting problems with painting yellow over a darker colour, we went with Strong (rather than Dark) Tone.
As with the Space Marines, the Quickshade not only picks out the shaded area but darkens the underlying colours.

After a good 36 hours drying time, a coat of Testors Dullcote kills the shine - and dulls down the "vibrancy" of the colours. The minis are looking ok at this point, but a bit dull and "dirty".


(this photo was taken after a little tidying up of the yellow. Yellow is a notoriously difficult colour to paint over miniatures. We really should have stuck with a  blue-and-grey team, as both colours offer great coverage, even over darker base paints).


This team is going to be based on the "Bright Crusaders" from the very first Blood Bowl game I ever played. The Blood Bowl game came  with a lot of "fluff" in the 80s. Example teams were given, and I just thought that painting this team as the Bright Crusaders might re-capture some of the initial excitement of seeing and playing the game, all those years ago.

Just like the Space Marines, the colours were "tidied up" by painting over the shaded colours, only this time, we went one shade brighter than the base coat. So instead of orangey-yellow, we used Sunshine Yellow (this takes two or three coats to get decent coverage). Instead of Crystal Blue, we painted the gloves and kneepads in Electric Blue. Instead of painting the white parts a pale grey and edge-highlighting in white, we just went for white from the off. This means no edge highlighting on the white parts - making the painting process a little quicker, but the finished result a little less interesting!

Finally, each miniature was finished off using our new-favourite-method for basing: a tiny dab of superglue on each foot, and glued to a clear acrylic disc!



After painting the shoulder pad and legs white, and tidying up the feet just a little (though not too much, to try to keep the appearance of muddy-white Nike trainers) we decided to keep the shirt grey instead of making that white also. This is going to be the basis for our team colours for all the other miniatures - for this team at least!

Unlike the Space Marines, which were plastic miniatures, we've glued a metal miniature to an acrylic disc. To make the bond we used Loctite Superglue (the real stuff, not the cheap 5-for-a-quid stuff from Poundland).

Putting superglue onto clear acrylic is a risky job. Superglue makes clear acrylic go cloudy, so it's really important that there's no excess squeezing out from under the feet. Obviously, we made sure that we used only a tiny drop of glue on each of the feet of the miniature. But, to discourage the glue from squelching out from under the feet, we held the miniature upside-down and let the disc rest on the feet of the miniature. This made sure that there was no real weight on the join, allowing it to go off without pushing any excess out of the sides.

Thursday, 4 December 2014

Space Marines - finished

Here's a squad of six Space Marines from Games Workshop, painted over the course of a weekend (with lots of drying time inbetween, so we've only totted up the total amount time spent actually painting!). We've been looking at getting an actual game coded up for our electronic board game idea, and decided that we're more likely to finish a Fantasy Football game (or at least, make more headway into it) than a space-shooty-type game. So before starting work on the few Blood Bowl miniatures we have here, Nick insisted that our Space Marines were finished off.




We deliberately kept the basic palette very small, to focus on getting the main bulk of the painting done in one go, not to spend hours and hours on the tiny, fiddly details, or spend too long with blending and highlighting and multiple glaze layers.
The basic approach on all the miniatures was:

  • Spray the entire miniature with Army Painter Crystal Blue primer
  • Paint the symbols on the shoulders, little skulls etc. in Ash Grey
  • Shoulder pad trims and chest plate are painted in Bronze
  • Back of legs and joints between armour plates are painted silver


At this stage, the miniature looks like a child's cariacture, with garish, bright colours and no definition to any of it's shape. Don't worry - this is all about to change, after you splosh on a coat of Army Painter Quickshade (Dark Tone, the black-based pigment goes on over blue far better than the slightly brown-y coloured Strong Tone).

(note how the colours have become much darker from their original shades)

  • After the Quickshade has dried, remove the shine (and, sadly, some of the depth of colour) with Testors Dullcote
  • Paint in Crystal Blue again on large surfaces, such as shoulder pads, armour plates fingers, and areas around the helmet. Don't be afraid to let parts of the darkened areas show through. (As we're speed painting, we didn't bother trying to blend the two shades together)
  • Paint white onto the greyed out areas (skulls, symbols on the shoulder pads etc)
  • Paint the bronzed areas with Greedy Gold (on some of the chest plates we added a wash of black ink before painting, to allow us to pick out individual feathers on the winged armour)

(our camera makes this blue colour look much brighter than it actually is; it's bright - just not this bright!)

By now, the miniature should look quite "clean" but with heavily defined outlines - a bit like a drawing, done with a black, felt-tipped-pen outline and coloured in using strong felt-tipped pen colours. (if this slightly cartoon-look isn't what you're striving far, you should have backed off after the Dark Tone!)

The last stage is to "edge highlight" the blue colours using a much brighter blue colour again - we used Electric Blue, a strong, vibrant, pale blue colour - after all, if you're going to go to the trouble of highlighting your miniatures, you may as well be able to see it!
This meant painting a thin line between any two surfaces that met at a darkened edge. We also painted along the edge of the eyes on the helmet, and individual fingers on the hands, and any raised corner or edge


notice how the small image for this character looks far better than if you click and zoom in on the model. That's how this style of painting works - with a lack of blending and too much detail painting, it maybe doesn't look the best, up-close. But when viewed at arm's length, the effects are quite striking.

Final touches (not yet done on the model above) include touching any details with a highlight colour. For example, on the seal on the leg above, the very rim of the red "rosette" would be edge highlighted with a bright orangey-red, and the cream-coloured paper stuck beneath it would be edged with pure white to give each part a little depth.

The backpacks and weapons are painted using the same technique(s) and then the whole model is assembled. Some people assemble their models before painting (we used to, and have done with our Tyranid/Genestealer aliens) but we just found because these clipped together so easily, painting them first would allow us to get into all the nooks and crannies around the weapons, to give a neater finish.

Note that the colours used are actually much brighter than the "recommended" colour scheme (by both Games Workshop, and many people on the G+ forums!) We found this worked well with our "cartoon style" painting approach; any darker, and the effect would be lost (although darker miniatures, well painted with blending and shading and drybrushing and all that stuff would probably look pretty realistic). We also had a few comments from people saying we'd used entirely the "wrong colours" (apparently, if a model has a skull here and wing there and is looking at it's feet and not into the sky, it's a something-or-other, which should only be coloured green with the pantone colour of x. We just wanted to get some nice, blue Space Marines onto the tabletop. These are Space Marines. We painted them blue! </rant>)

Finally, the miniatures need basing.
We've had a discussion about this with other wargamers online; while our paintwork isn't going to win any awards, it's nice enough for a gaming standard, and the miniatures look really great on the tabletop.

We played about with different basing approaches a while back:
While these techniques create nice-looking stand-alone models, the based miniatures didn't quite look right on our printed playing surfaces.

(some may argue otherwise, but we prefer the clear acrylic discs to the painted-and-modelled bases which, on their own, look fine, just not when used on a printed playing surface like this one!)

So we went for the easy option and laser-cut some 24mm discs out of clear 3mm acrylic. We added a 3.95mm hole in the centre, and jammed in some 4mm magnets. Because of the "kerf" on the cut edges, this means that the hole is 3.95mm on one side, and ever so slightly larger on the other - allowing the magnet to fit inside the hole, then to be jammed in place, without the use of glue or solvents.


Obviously, if these were not for our electronic board game, we wouldn't have bothered with the magnet in the middle and the final result would be a neater base. But all in all, we're quite pleased with the way these turned out.

Total time spent: 9hrs

At about an hour and a half per model (not including the time we spent going to the unit to laser cut the bases) we're not even sure if this could be classed as "speed painting" any more. But it's quicker than any other miniatures we've painted to this standard before. And it was quite fun to spend time over the weekend getting reacquainted with the miniature painting hobby. Maybe "faster painting" would be a more accurate title?