02: Voronoi
The third Procedural Notes - describes my Python module Voronout and how Rampant on the Tracks can use it.
This is the third Procedural Note.
The second promised, between 11/21 - 12/05, tooling for the procedural generation of Rampant on the Tracks' Tracks.
This delivers on the first of the three promised points, Track generation itself - it shows off both the Python module built for that, and the previous gameplay mechanics demo, updated to use the module's output.
The module's name is Voronout..
.. and can be found at https://github.com/jpshankar/voronout.
It's based on the concept of a Voronoi diagram - a set of cells in a 2D plane generated with respect to an input set of sites. Each cell is a region corresponding to a site, containing all points in the plane closer to that site than any other.
An example can be seen from this Voronout output, drawn via Matplotlib:

(The x and y-axis here reflect the convention of, on a device screen, determining positional coordinates by taking (0, 0) as the top-left corner.)
This diagram is calculated via SciPy's Voronoi functionality.
Voronout is a wrapper that..
- .. facilitates intuitive site specification - providing input points where
0 <= x <= 1and0 <= y <= 1. One can describe a site as being " middle of the upper-left " -(x = 0.25, y = 0.25)- and scale the resulting diagram output up appropriately later, without having to calculate the absolute x/y values at the time of input. - .. trims the generated diagram to fit within the plane - it is possible that calculated region edges could have
x < 0 || x > 1and/ory < 0 || y > 1. Voronout takes such edges, determines where they would intersect the containing plane's boundaries, and trims the edge down to that intersection point - keeping the diagram manageable within the plane.
It outputs the calculated diagram as JSON that describes..
- the sites
- the edge vertices
- diagramVertices, within the region
- boundaryVertices, the results of trimming done (as needed) on the diagram
- the regions
- the site
- the border edges (vertices, neighboring region)
This JSON indexes vertices by UUIDs..
"diagramVertices": {
"87cd7568-4e74-425f-9b24-403d832ab8f4": {"x": 0.5952, "y": 0.2042},
"d2ae8f18-8293-410d-9ad7-7a6774c3f6fb": {"x": 0.8034, "y": 0.4349},
"09c2367e-e884-4a84-b6a7-986d711517b5": {"x": 0.6976, "y": 0.3837},
"6a1cd69c-c065-4b70-94d3-263902cc5fe8": {"x": 0.8188, "y": 0.7709},
"c48ce7f8-3a82-47ae-b187-b09acb9c5b91": {"x": 0.6694, "y": 1.0},
"9476a821-f602-4e21-8dcc-9cea99dbb987": {"x": 0.3629, "y": 1.0},
"c89b2c7b-8b5d-44bd-a24c-3c2e0c425f42": {"x": 0.4934, "y": 0.6435},
"edfd247f-8535-4909-92d8-98b9174f0348": {"x": 0.1894, "y": 0.4783},
"f4933e54-4690-4e70-9dc6-1876b9a551c7": {"x": 0.315, "y": 0.5249}
},
"boundaryVertices": {
"6fbbdec4-2f4f-4a15-a12a-fec889997095": {"x": 1.0, "y": 0.4333},
"19e0356f-4213-4914-a3c7-b505665352a5": {"x": 0.4899, "y": 0.0},
"9d4f7a9a-834c-4fb8-96e4-43580661a42c": {"x": 0.6694, "y": 1.0},
"0f8c99f9-db13-41f8-8aa0-2e3488a96c16": {"x": 0.0, "y": 0.1136},
"42946db9-1742-4603-a26a-03d2f5c8fa17": {"x": 1.0, "y": 0.8143}
}
.. and then describes region edges in terms of those IDs:
{
"vertexIdentifier0": "d2ae8f18-8293-410d-9ad7-7a6774c3f6fb",
"vertexIdentifier1": "09c2367e-e884-4a84-b6a7-986d711517b5",
"neighborSiteIdentifier": "f65119de-e596-4757-b20b-c6440bfab364"
}
{
"vertexIdentifier0": "6a1cd69c-c065-4b70-94d3-263902cc5fe8",
"vertexIdentifier1": "42946db9-1742-4603-a26a-03d2f5c8fa17",
"neighborSiteIdentifier": "cca71e3f-c7c0-4f4c-b0ee-e1fcb5a79de9"
}
(neighborSiteIdentifier indicates the Voronoi region bordering the edge's site, on the edge.)
This JSON is efficient in both describing the Voronoi diagram and facilitating usages of that description.
Rampant on the Tracks' game mechanics demo uses this JSON.
You can see that at https://jpshankar.github.io/rampant-on-the-tracks-game-mechanics.
(You can see the code at https://github.com/jpshankar/rampant-on-the-tracks-game-mechanics, and how it works with Voronout's JSON:
const pointMapData = this.cache.json.get("walker_map");
const verticesData = { ...pointMapData.boundaryVertices, ...pointMapData.diagramVertices };
const verticesEntries: [string, PointData][] = Object.entries(verticesData);
this.verticesMap = this._makeIdCoordsMap(verticesEntries);
const regionsData: RegionData[] = pointMapData.regions;
for (const { edges } of regionsData) {
for (const { vertexIdentifier0, vertexIdentifier1 } of edges) {
const vertex0 = this.verticesMap.get(vertexIdentifier0);
const vertex1 = this.verticesMap.get(vertexIdentifier1);
..
const neighborLine = new Phaser.Geom.Line(vertex0.x, vertex0.y, vertex1.x, vertex1.y);
lineGraphics.strokeLineShape(neighborLine);
}
}
)
Previously, the points that the Walker was moving across were a uniform grid:

Now, the Walker's on Tracks:

This is the same diagram Matplotlib visualized above, with the edges as paths.
Gameplay mechanics remain the same - the only change is that Redirect is now represented by coloring the line:

(For now, this works better than angling the " Redirect " sprites to indicate the correct direction.)
This Track has visual variance - scattered points with paths of unequal length between them - instead of that grid's uniformity.
It also has variance in travel routes - unlike the grid, each point is not necessarily connected to every point that it apparently looks like it could be.
(Traveling to one point P will lock it out of traveling to some other points S, until it - likely via Blocks or Redirects - gets to some other point T from which it can visit a point in S.)
These variances, procedurally generated, will enhance Rampant on the Tracks' " get Walkers to their destinations " gameplay.
Another demo will demonstrate this.
The previous Note promised full Track generation by 12/05. This is amending that plan - the next step will be to have a more fully featured gameplay demo.
By 12/19, there'll be a demo with one stage of the game implemented - mechanics that provide non-trivial challenge, graphics that're a little more than placeholder, UI that isn't just text, and maybe audio assets.
(That'll provide a more definite basis from which to build generation tools - not just the Tracks, but the Walkers and constraints imposed on Blocks/Redirects.)
Subscribe if you'd like to be notified about the first Rampant on the Tracks demo in your inbox.
(Or if you're just curious.)