03: Voronout
The fourth Procedural Note - describes fixes to Voronout + a REST API using the module + how the Rampant on the Tracks mechanics demo uses the first two.
This is the fourth Procedural Note.
The third promised, by 12/19, a full gameplay demo of Rampant on the Tracks' mechanics.
As in the second Note, that promise is being deferred - this time to January 2026. This Note explains why by showing off the reason - fixes to Voronout, the Python module I built for easily generating Voronoi diagrams - and demonstrating a further use of Voronout that'll show up in that demo.
Voronout is still available on Github.
It's also installable via pip install voronout now - it's been uploaded to PyPi at https://pypi.org/project/Voronout/.
There were a couple open issues previously - fixing them required revisiting SciPy's Voronoi diagram representation, further understanding how that worked, more clearly comprehending the exact gaps between it and Voronout, and then doing the work to narrow those gaps.
The results of that can be seen when we compare
created from the previous Voronout with
basePoints = ({"x" : 0.9676, "y": 0.4927}, {"x": 0.2163, "y": 0.7649}, {"x": 0.936, "y": 0.7093}, {"x": 0.206, "y": 0.4837}, {"x": 0.2662, "y": 0.5927}, {"x": 0.4211, "y": 0.7802}, {"x": 0.5706, "y": 0.663}, {"x": 0.5134, "y": 0.3368}, {"x": 0.7245, "y": 0.2413}, {"x": 0.0938, "y": 0.9428}, {"x": 0.79, "y": 0.1978}, {"x": 0.9625, "y": 0.7223}, {"x": 0.0454, "y": 0.804}, {"x": 0.7317, "y": 0.5099}, {"x": 0.1314, "y": 0.9227})
to the same basePoints being processed by the latest Voronout:

The latest Voronout has eliminated an issue where bounding was not being correctly handled - instead of being applied to each point on an edge going outside the plane, it was being applied to one point and then subsequent points were just following from that.
Additionally, scaling from basePoints' percentages is done as part of the returned output - you simply provide planeWidth/Height parameters when you create the diagram, and you get scaled-up values in the returned points/vertices fields.
(boundaryVertices and diagramVertices in the output have been melded into a single vertices, for ease of use.)
These changes make Voronout easier to use as a module - and as a service.
Voronout's also a REST API now.
I've set a single endpoint up on PythonAnywhere, getDiagram.
It can be curl'd with a request like
curl --request POST \
--url 'http://jpshh.pythonanywhere.com/getDiagram?=' \
--header 'Content-Type: application/json' \
--data '{
"planeWidth": 600,
"planeHeight": 600,
"planeSites": [
{"x": 0.6473, "y": 0.5131},
{"x": 0.159, "y": 0.1831},
{"x": 0.9664, "y": 0.664},
{"x": 0.558, "y": 0.263}
]
}'
This returns the diagram in JSON:
{
"points": {
"8799bb6d-2ad9-4696-98bb-236fafa7eecc": {
"x": 388.38,
"y": 307.86
},
"f56f94a0-8513-4123-b7e6-03a54f06d356": {
"x": 95.4,
"y": 109.86
},
"c4d90d8d-0155-4087-92bc-69b02426d1a3": {
"x": 579.84,
"y": 398.4
}
},
"vertices": {
"ebc3f2a5-33ee-44e2-9d23-2618acbdf9b4": {
"x": 600.0,
"y": 108.12
},
"f4ca8ea5-ec32-4acb-b5a7-3cc7a42084a0": {
"x": 367.44,
"y": 600.0
},
"b08292a7-8d01-4efd-95c3-007cd3f7e362": {
"x": 383.04,
"y": 0.0
},
"5bec39d7-3bcd-451e-8ba9-679baf7a53f6": {
"x": 0.0,
"y": 566.88
},
"648dfc6e-8e1b-4ce2-bf1d-dd851b625c8a": {
"x": 488.94,
"y": 0.0
}
},
"regions": [
{
"siteId": "8799bb6d-2ad9-4696-98bb-236fafa7eecc",
"edges": [
{
"vertex0Id": "f4ca8ea5-ec32-4acb-b5a7-3cc7a42084a0",
"vertex1Id": "ebc3f2a5-33ee-44e2-9d23-2618acbdf9b4",
"neighborSiteId": "c4d90d8d-0155-4087-92bc-69b02426d1a3"
},
{
"vertex0Id": "5bec39d7-3bcd-451e-8ba9-679baf7a53f6",
"vertex1Id": "b08292a7-8d01-4efd-95c3-007cd3f7e362",
"neighborSiteId": "f56f94a0-8513-4123-b7e6-03a54f06d356"
}
]
},
{
"siteId": "f56f94a0-8513-4123-b7e6-03a54f06d356",
"edges": [
{
"vertex0Id": "5bec39d7-3bcd-451e-8ba9-679baf7a53f6",
"vertex1Id": "b08292a7-8d01-4efd-95c3-007cd3f7e362",
"neighborSiteId": "8799bb6d-2ad9-4696-98bb-236fafa7eecc"
},
{
"vertex0Id": "648dfc6e-8e1b-4ce2-bf1d-dd851b625c8a",
"vertex1Id": "648dfc6e-8e1b-4ce2-bf1d-dd851b625c8a",
"neighborSiteId": "c4d90d8d-0155-4087-92bc-69b02426d1a3"
}
]
},
{
"siteId": "c4d90d8d-0155-4087-92bc-69b02426d1a3",
"edges": [
{
"vertex0Id": "f4ca8ea5-ec32-4acb-b5a7-3cc7a42084a0",
"vertex1Id": "ebc3f2a5-33ee-44e2-9d23-2618acbdf9b4",
"neighborSiteId": "8799bb6d-2ad9-4696-98bb-236fafa7eecc"
},
{
"vertex0Id": "648dfc6e-8e1b-4ce2-bf1d-dd851b625c8a",
"vertex1Id": "648dfc6e-8e1b-4ce2-bf1d-dd851b625c8a",
"neighborSiteId": "f56f94a0-8513-4123-b7e6-03a54f06d356"
}
]
}
]
}
The API was set up via Flask.
The idea's doing procedural generation - of Tracks - on a Python backend, then retrieving the generated Track and displaying it on the frontend via Python.
The idea's not novel, but it is efficient.
Track generation has been set up in the game mechanics demo.
This can be seen at https://jpshankar.github.io/rampant-on-the-tracks-game-mechanics/.
Mechanics work as they did previously - the difference is that now, on every page load of the demo, a getDiagram call:
_getVoronoiDiagramData() {
// sites.length
const sitesData = Array.from({ length: numDiagramSites }, _ => {
return { x: Math.random(), y: Math.random() };
});
const diagramParams = {
planeWidth: this.sceneWidth,
planeHeight: this.sceneHeight,
planeSites: sitesData
};
return fetch("https://jpshh.pythonanywhere.com/getDiagram",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(diagramParams)
})
.then(response => response.text())
.then(responseText => responseText.replaceAll("'", "\""))
.then(responseTextCleanedUp => JSON.parse(responseTextCleanedUp));
}
..
const pointMapData = await this._getVoronoiDiagramData();
..
.. ensures that the Track looks different between each run.
See one..

.. and another:

This is a very shallow use of procedural generation - just dropping the Voronoi diagram in as a Track, without any effort to " tune it up " in terms of the gameplay mechanics (Blocks, Redirects) or smooth over any of the UX problems (Points clustered too close together, the overall too-geometrical visual effect).
It serves as a quick demonstration of what Voronout is - something more impressive's coming in January 2026.
What's that going to be?
As previously promised, one level of gameplay - with the caveat now that the level, Tracks and Destinations and Walkers, is generated differently every time the page is reloaded.
Look forward to that on 1/9/2026 - subscribe if you'd like to be notified when that time comes.
Or if you're just curious.