owned this note
owned this note
Published
Linked with GitHub
# KZGamer - summoning Dankshard from the tabletop.
In many tabletop games, there are often gameplay mechanics that only require a success or failure result. Boardgames, roleplaying games, and miniatures wargaming often have the notion of a "target number." If you hear phrases like "hitting on threes" or "four +es" around your table, you are experiencing one of these mechanics.
This allows for a unique situation; to play the game, you only need to know if you have succeeded or failed when the dice is rolled. The actual value on the die only matters in the context of the success criteria. Once the players know if it is a success or failure, they can proceed with the game. The value that is rolled is not strictly relevant.
If we had a device that could determine the result of a roll in success/failure terms, we could withold the values on the dice from the user. This gives us a source of entropy that can be used, but not revealed. The game mechanics act as a filter or obfuscation layer.
Examples:
* Shadowrun is a tabletop RPG set in a fantasy cyberpunk universe. All skill checks require the player to roll a number of six-sided die (D6s) and count how many were fives or sixes, which are considered hits. The players is trying to maximize the number of hits, not any particular die roll.
* Bolt-Action and its Weird-War sister Konflikt '47 are miniature wargames with similar rulesets. A key part of the combat flow is determining how hard it is to hit your target. A unit may fire on another, and only score hits for dice that come up four or more, depending on conditions like range, cover, etc. In this case, again, players only need to know the quantity of successes; dice rolled which met or exceeded the target number.
We hope to leverage this filter to contribute the same entropy that powered a game to the KZG Ceremony without revealing it to the players.
# Potential Workflow
Here's how a [dice tower](https://en.wikipedia.org/wiki/Dice_tower) could be constructed to collect the entropy from a tabletop game and only reveal the far smaller amount of information that the player needs.
1. User inputs a target condition into the dice tower.
2. D6s go into sealed dice tower.
3. Dice bounce around and land on trapdoor platform inside where they can ONLY be observed by the embedded system.
4. Embedded camera takes a picture and embedded computer reads the dice.
5. System adds new bits to the accumulated entropy.
6. System determines if those dice rolled meet target condition(s).
7. System opens trapdoor, dropping dice further through more tower and re-rolling them before returning them to the user.
A dice tower like this could likely be built out of comodity embedded systems and a touchscreen display.
# Potential Ceremony Interaction
To contribute to the ceremony, our entropy will need to be multiplied by the previous KZG ceremony output before it is destroyed. Instead of waiting in a queue of contributors, the participation will be scheduled ahead of time and the current KZG file will be provided out of band.
1. User copies KZG input file to any removable media which is readable by the system. This is a separate storage media than the one containing the operating system, KZGamer software, and kzg contribution client. This allows the commitment medium to transit the airgap.
3. Device starts up and maintains an implementation loop of watching for dice, noticing them, reading them, and returning them to the player.
4. As entropy is accumulated, it is written to the local storage containing the software, which will never transit the airgap. This should be validated by peer review of the software, and confirmation that what is running hashes to the same thing as the reviewed code.
5. When entropy collection is complete it is passed to an existing command-line client, which then multiplies it with the previous input, and produces the next contribution output file, which is written to the same medium as the incoming KZG commitment was written to. Any interim storage of entropy can and should be deleted immediately after successful commitment.
6. A message is displayed to the user indicating the ceremony is complete and the produced KZG file should be sent back to the sequencer out of band.
7. Removable media may be destroyed.
# Initial Design (and shopping list)
<img src="https://i.imgur.com/8zsuAALm.jpg" style="float: right; margin-right: 25px"/>
* A lightweight, portable, embedded computer system that can be air-gapped. It needs enough power for basic computer vision and the ability to run a KZG client
* [Raspberry PI 4, 64 bit](https://www.raspberrypi.com/products/raspberry-pi-4-desktop-kit/)
* Some kind of input system, so the user can select the target number for their rolls.
* Feedback to the user, probably a display.
* [7" touchscreen display](https://www.raspberrypi.com/products/raspberry-pi-touch-display/) handles both these problems.
* A camera that can read the dice.
* [Raspberry PI Camera 3](https://www.raspberrypi.com/products/camera-module-3/)
* A tower to house it all, a hole in the top to toss dice into, a place for them to land, a way to return the dice to the user.
* Uhhh... I have a whole woodshop, I better be able to figure this out...
# Construction Begins
<img src="https://i.imgur.com/fjljtgom.png" style="float: right; margin-left: 25px"/>
The first construction problem we encountered was making sure the camera framed the dice area correctly. The specs on the camera that estimate its field of view were close, but some trial and error was necessary to maximize how much of the camera sensor was available to look at dice. I printed out a ball and socket camera mount, which allowed me to find the optimal camera angle and distance. We wanted to maximize area covered, while avoiding views of side faces of the dice. This would later turn out to be overdesigned.
<div style="clear: right"></div>
---
The next problem was the actuator to return the dice to the user after the entropy had been collected. The first draft of that looked like this.
{%youtube qzPVzmKyRsc %}
This worked great until I started throwing lots of dice at it. The only thing holding that trapdoor level is the servo itself. Once you start dropping dice on it—which might land on either extreme from the fulcrum—you now have a decent amount of force pushing against your servo, corrupting its zero point and wearing on the gearing to the axle.
This is probably the biggest problem to be addressed in the next iteration.
I also experienced a couple of random restarts, likely due to powering the servo from the on-board gpio pins on the Raspberry Pi. Doing that is known to produce a pretty dirty power signal. For now we'll continue limping forward, but that issue needs to be addressed later.
# Computer Vision and the D6
Big shoutout to Quentin Golsteyn whose Yahtzee project was one of the first I discovered while doing initial research to see if this whole idea was even viable. [Check out his project](https://golsteyn.com/writing/dice)!
My application, however, needed to support many more than two dice, because there is nothing more satisfying than attacking your opponent with a fistful of 10 to 15 dice. It's soooo satisfying. We also need to operate in a smaller space; I am intending this dice tower to be portable so I can take it to a planned gaming event, and the area where dice land that is observed by the camera has specific needs. It must be concealed from the player and have consistent, steady lighting. It might be a little cramped in there.
## Pips
<img alt = "4 and 2 read as 5 and 1" src="https://i.imgur.com/6Kh8Sw8.png" style="float: right; margin-left: 25px"/>
It turns out, when dice land next to each other, the blobs-and-clusters technique Quentin developed runs into some problems. For example, the pips on a two can be closer to a neighboring die than they are to each other. Here we see one of the pips on the two was within clustering distance of the four pips on an adjacent die.
It's not always the twos—but it's usually the twos. Sometimes fours. Any pips that could be closer to the pips on a neighboring die than to their own pips presents a problem. It's a pretty big problem and, at this point, I got scared; the simplicity of the blobs-and-clusters technique was doing a lot of heavy lifting! I was having no problems running this on a Raspi4 on multiple frames for each roll and returning the dice to the user quickly.
I came up with two potential solutions.
1. First, maybe we introduce some edge detection to the computer vision to clearly bound the blob detection within the face of the die. That would be ideal but, as you can see from the image above, that boundary isn't always clear—you'd have to add some assumptions about boundary proportions and infer a square. Doable, but now we are straying beyond my limited Python and OpenCV capabilities which really just cover ripping off Quentin.
2. Unleash the ML. Train a model to detect what dice look like and read them. If diving deeper into traditional CV was gonna be a challenge for me, then I definitely will not have time to learn enough ML to implement it this way.
Both of these options felt like staring down a deep rabbit-hole, and I still had three armies of miniatures to paint and a board full of terrain to build before gameday when the KZG commitment window is scheduled.
<img src="https://i.imgur.com/w6e5DPYm.png" style="float: right; margin-left: 25px"/>
So, screw it. If the problem is the dice, make better dice. Bigger dice with more distance from their edge to the pip pattern would make sure the pips always cluster within the die face. Turns out, I have a friend with a laser cutter who has experience making custom dice.
He was kind enough to make me a set of custom dice on which we compressed the pip pattern toward the center of each face. Problem solved; Dankshard be praised!
<div style="clear: right"></div>
---
### Interim Status Update
{%youtube 7AMilMPYjgg %}
*I would later discover that I was quite wrong about my assumption that dice landing on the edge of the platform is ok. It is not, and we are eventually going to have to redesign that whole mechanism.*
## Computer Vision Challenges
There were a few. As you can see in the video above, there is still a lot of uncertainty around pip detection. I had heard that CV is all about controlling your environment and stripping out as much noise as possible. The rumors are true!
CV issues I had to learn about and solve:
* Resolution - If we don't use enough of the camera sensor at max resolution, blob detection becomes less consistent. Sensor noise in low-light conditions is enough to throw off the eccentricity detection that finds the pips. Camera position, resolution settings, and switching from video feed to still frame capture on an interval optimized for this.
* Lighting - Low light tricks auto exposure settings into boosting sensor sensitivity, which introduces noise. Bright light introduces glare and reflections off of surfaces we don't care about, which often get identified as blobs. To control for this, I added dimmable LED strip lights around the camera so I could tune the lighting with a dimmer external to the case.
* Contrast - Black pips on white dice are pretty ideal. How do we maximize our advantage from that? I explored a few software filtering techniques like thresholding to eliminate all the gray, but there were always reflections which ineveitably formed spheres that looked like blobs. To tame these, we painted the inside of the tower black, padded the landing deck with Gorilla Tape™ to prevent dents and paint chipping, and sealed up any light leaks from the outside. When that STILL wasn't enough, well, it was a good thing I'm really into arts and crafts. A coat of matte varnish knocked down the remaining reflections and we eventually got a nice, noiseless image.
## The Stacking Problem
Yeah, we're not done with dice issues yet. But these are mechanical, not visual! So, thats fun. In rare cases, as dice tumble down the tower, they might land on top of each other. If that happened while using a dice tray, it would be obvious and everyone would go, "Whoa, neat!" and just re-roll the die that landed on top, preserving the rest of the roll. Our concealment from the user prevents that.
But what we can do is give the user some feedback on how many dice were detected. Anything other than the number of dice expected would simply warrant a re-roll, and we'd go on with the game.
# Trapdoor needs revision
Endurance testing was not successful. If we collect two bits of entropy per die (three if we don't care about half a bit of bias), we're gonna need 512 dice tossed in there over four hours, so I would just stand there and toss dice in and watch the debug output. A smart friend questioned why I put the axle in the middle of the platform, and I didn't have a good answer. He suggested that if the axle was on one end, then you could use a latch to hold up the other end. Then you'd have a perfectly stable plane to land on and transfer no force backward through the gearing and servo. Something like this:
{%youtube GT6wM5UskyQ %}
When you have a 3D printer, though, it's easy to go a little crazy. I actually did a revision of the entire device which was printed in FDM and ... it wasn't great. The process of thinking in CAD is helpful, but doesn't really lend iteself to improvisation.
The introduction of the solenoid was going to require more current than I could provide via the GPIO pins on the RasPi, so I added a [motor driver board](https://www.adafruit.com/product/2348) to the design. This would give us a dedicated, clean current source to drive both the solenoid and a larger servo at the expense of added software complexity.
{%youtube Cn8ztnkTwO8 %}
The final version would be masonite and a dowel with a small, simple geared cam on the shaft and some glue. Here is a view from the rear access panel, showing all aspects of actuation.
{%youtube obb42EK7_kU %}
Here you can see the resin printed cams that drive the axle and a solenoid latch in the front for the opposite edge to rest on. A few more tweaks to this basic design, and we're in business. I printed a few custom housings and brackets for the solenoid and the servo to make sure they were locked in position but could still be repaired or replaced in a mid-ceremony emergency.
The last hardware issue that was causing failures over time was drifting of the camera ball joint. Vibration from dice hitting the ramp that it was mounted to eventually caused the camera position in the ball joint to shift. I tried adding a set screw to fix it in position, but the resin is just too brittle, and it loosens over time. Now that we know the proper position, though, we can replace the ball joint with a free-floating, fixed position mount. Something like this:
![](https://i.imgur.com/z5Q0d12.jpg)
Now we have a fixed position, the camera is isolated from vibration, and integrated, dimmable lighting is coplanar to the camera sensor.
![](https://hackmd.io/_uploads/S1NNyFUEn.jpg)
# We can no longer avoid building a UI
Look, I've been a backend dev for 20+ years. My day job is working on an Ethereum client (support client diversity, run Besu!). The closest I ever got to user interfaces is website hackery back in the day. Now, this project won't need anything fancy, but we do need to accept input and display feedback to our users. I'm already using Python for the CV, the Raspberry Pi is running Raspbian so we have a full-fledged desktop. We should be able to mock up a touchscreen UI pretty easily in QT.
So, lets be honest—I'm just gonna [hack this together](https://github.com/jflo/kzgamer/blob/main/MainWidget.py) with ChatGPT:
![](https://i.imgur.com/ldBNY8X.png)
That's an early version of the UI running in debug mode. Still not perfect, but the image filtering is MUCH improved from where we were before. I gotta say, ChatGPT completely changed the way I'm going to learn new programming languages. It's like having a private tutor. Not only can I feed it the code and ask it how to make changes, I can ask it why things work a certain way and even ask it to make a comparison to other languages I'm more familiar with.
# Dress Rehearsal
Ok, I think we're ready to get this out of the lab and use this in a game. At this point, I had about a week before the convention we were playing at. A week before a four-hour window during which we would use this gadget to permanently enshrine our big game of Konflikt '47 in the foundation of Ethereum's scaling strategy.
To do a proper dress rehearsal, I set up a local instance of the [KZG ceremony sequencer](https://github.com/ethereum/kzg-ceremony-sequencer) and a copy of [SCAR](https://github.com/CarlBeek/SCAR/) so that I could be absolutely sure we were executing correctly when our window opened. I went through all of the same motions that I would need to during the actual contribution window; the only difference was specifying a few alternate url options to the offline mode of the [go-kzg client](https://github.com/jsign/go-kzg-ceremony-client).
Big shoutout to all the contributors of these tools! It is remarkable how little time I had to spend on the actual crypto part of this problem, leaving me more time to play with computer vision and make terrible mechanical engineering decisions.
Here's what a game of Konflikt '47 looks like. It's pushing around army men and tanks on a cute little battlefield. Tape measures and dice. Pew pew!
![](https://i.imgur.com/KUV45A5.jpg)
Things went very smoothly as far as device function was concerned. No misfunctions; responsive performance. After about two hours of play, however, we had only collected about 300 bits of entropy. Our goal was 1024 bits, so... we're gonna have a problem at show time.
1. Perhaps the time has come to trade bias for bandwidth. Lets collect three bits of entropy per die roll, and not worry about the fact that six is only two-and-a-half bits.
2. Can we support more types of rolls with the gadget? Right now we can only use to-hit and damage rolls versus infantry.
Option one was easy to execute, but the real missed opportunity was supporting morale rolls. A morale roll in Konflikt '47 is always a roll of two dice, with the goal being to hit or get below a target number. In the beginning of the game, every unit needs to pass a morale roll to enter the battle. If they fail, they keep trying. Later in the game, as units get pinned by enemy fire, they need to make morale checks before every order they receive. If we could support morale rolls on the tower, we could add more entropy, especially at the beginning of the game.
![](https://i.imgur.com/EwsdA60.png)
Fire up ChatGPT, explain [the changes](https://github.com/jflo/kzgamer/blob/26d0409cfbf4bcec706004a690a06c59a1d93ef8/MainWidget.py#L116) we want to make to the UI, and we're good to go. Now the user needs to change the mode when selecting their target number. So, we increase the range of selectable target numbers and disable any which would be invalid.
With that last-minute feature added, we might be done! I'll just spend the rest of this last week painting minis, building terrain, and doing more endurance testing...
# Disaster
It's amazing how something as innocuous as turning the heat off overnight in your workspace can totally jeopardize weeks of work. I should just let Exhausted Past Justin explain the chain of events that struck less than 72 hours before gametime:
{%youtube RiLu5t8PrK4 %}
So that sucked.
# Whatever, lets go to the party!
We set up a camera pointed at the tower to make sure nobody tampered with it. It's a riveting four hours of dice going into a tower. I live-streamed it at the time on Twitch and also saved it to disk. The video is 19GB, though, so I'm not posting it on YouTube—but can provide it if someone is masochistic enough to want it.
I explained what was going on, how this was a fun and goofy way to contribute to a shared secret that would power the future of Ethereum scaling and everyone was happy to participate. Nobody had any issues with playing a game where they could not see their die rolls.
![](https://i.imgur.com/7kvXYaV.jpg)
That's the game board after everything is fully deployed. We are underway and collecting entropy! The only real question remaining is: Will we hit the full 1024 bits of entropy?
![](https://i.imgur.com/1xH6FjT.jpg)
By turn three, we've already collected 415 bits of entropy, a big improvement from the dress rehearsal. Troops are still moving into position and crossing the board, so we expect the rate of entropy collection to increase a lot as infantry units engage in the fight.
![](https://i.imgur.com/gBVcAGj.jpg)
And that's when the Werewolves came out. Only thing worse than Werewolves is Nazi Werewolves.
![](https://i.imgur.com/HN3Wf9R.jpg)
A bolt of red, white and blue darts across the battlefield—the Star-Spangled-Man-With-A-Plan!
The game was a smashing success, and 15 minutes before our contribution closed we crossed the 1024 bit mark. The tower said [**"CEASE HOSTILITIES - DANKSHARD BE PRAISED"**](https://github.com/jflo/kzgamer/blob/26d0409cfbf4bcec706004a690a06c59a1d93ef8/KZGamer.py#L137) and began the commitment process. Once complete, I recovered the commitment and sent it to the sequencer.
`0xF51D203536Ea8b5BFBc06b3a1c21514766b22BB1` is the address we submitted under, you can search for it at [ceremony.ethereum.org](ceremony.ethereum.org) to confirm its inclusion.
> Powers of Tau Pubkeys:
**(2^12)**: `0x9157bfd6d53e6f9cd427aa84acfe47e7f1224ab99ac2c606ccea8dec3ba85a73d3b29c74422461697e9d86e36baa7f8d02add99798a40e2f32c2cd7185b13ad86f57fb7df418c708727d9ff2f85cc220f66353408151075785caf78f3b6c4dbe`
**(2^13)**: `0x92ae7793b9ba21e8dcfa88e4722acd8b5282d0ad9f9407db13b966797d8a98474986e3beb0d6271fb4fe68ea485dd8ff05144addcf1af56f69a2c26ad5e6009b481e77f431247a03c22e458da0f5264e1d0db7e3e5aa6f1fc93cf539271b77fc`
**(2^14)**: `0x908b21c002bb0a336e8c22da6c5f3ae688c7319f72045eff8817233fbb3c29a652f04a435e4392507462b4530f80db5c102844255ec594c1b15b3b2ae9f3a3797436846f754e8769f4c5450902d2f3d704971fb998881aeea998c64d37198391`
**(2^15)**: `0xa64a105c21f2d9cff3799ef38203ae6da23b12be6949ab8dd136244de4af918b6bdb4d69537af464c3b3a156f89655720b827a331ad71d3a43ce9420b7b81be6e81b69009824de01b618ea65d2512da82215d75c99fbe9e010df5c51279416e6`
Victory came down to the very last die on the very last turn. Germany barely eked out a victory, recovering the second objective it needed at the last minute!
Huzzah! All that was left to do was destroy the media that KZGamer runs on. It had [already deleted](https://github.com/jflo/kzgamer/blob/26d0409cfbf4bcec706004a690a06c59a1d93ef8/KZGamer.py#L144) the entropy storage after completing the commitment, but just to be sure...
{%youtube x_HAOf7N7RU %}
# Epilogue
Here's a post-event tour of the tower:
{%youtube S5CrcqG5kGo %}
I think it is super cool that the Ethereum dev community arranged such a neat opportunity and that I could turn it into a really fun event for me and my friends. Shoutout [Trent Van Epps](https://twitter.com/trent_vanepps) and [Carl Beekhuizen](https://twitter.com/CarlBeek) for managing the whole KZGCeremony. Shoutout [
Ignacio Hagopian](https://twitter.com/ignaciohagopian) who quickly responded to feedback on offline features for the go-kzg client.
Shoutout [Quentin Golsteyn](https://golsteyn.com/) whose experiments in CV reading dice convinced me this stupid idea was only stupid, not crazy.
Shoutout to all my degen gamer friends who played the game in testing and production, and listened to me rubber-duck debug alllllll the issues I ran into along the way.
Most importantly, shoutout to my family who put up with me being isolated in the shed, grinding on this project for the weeks that it took.