Metadata-Version: 2.5
Name: RampantTrackGeneration
Version: 0.0.2.1.0
Summary: Rampant on the Tracks's Track generation logic, leveraging Voronout and optimized for a web service.
Project-URL: Homepage, https://github.com/jpshankar/RampantTrackGeneration
Project-URL: Issues, https://github.com/jpshankar/RampantTrackGeneration/issues
Author-email: Javas Shankar <javasshankar@gmail.com>
License-Expression: MIT
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.13
Requires-Dist: aggdraw
Requires-Dist: flask
Requires-Dist: moviepy
Requires-Dist: numpy
Requires-Dist: pillow
Requires-Dist: rustworkx
Requires-Dist: shapely
Requires-Dist: voronout
Description-Content-Type: text/markdown

# RampantTrackGeneration is the Track generation logic for [Rampant on the Tracks](https://jpshh.com/rott/pitch).

The logic is invoked by calling 

```Python
@staticmethod
def generate_track(
    diagram_width: int,
    diagram_height: int,
    num_diagram_regions: int,
    length_min_quantile: float,
    num_path_nodes_min_quantile: float,
    max_fuel_cost: float, 
    min_cycle_period: int,
    max_cycle_period: int,
    max_length_travel_duration_seconds: int,
    stop_radius: int,
    take_screenshots: bool = False
) -> Track:
```

in `TrackGenerator`.

`Track`

```Python
@dataclass(frozen=True)
class Track:
    nodes: dict[uuid4, Point]
    edges: dict[uuid4, EdgeVertexInfo]

    start_node_id: uuid4
    destination_node_id: uuid4

    start_destination_path_edges: tuple[uuid4]
    
    node_info: dict[uuid4, NodeInfo]
    edge_info: dict[uuid4, EdgeInfo]
```

describes a set of `edges`, each a connection between two `Point`s. `nodes` are the `Point`s.

`generate_track` derives the `Track` from a randomly generated Voronoi diagram. 

It preserves the organic appeal of the diagram's shape - unevenly spaced points, connected by edges of varying length - and goes on to enhance that by 

* calculating `diagram_edge_min_acceptable_length`, the `{length_min_quantile * 100}%` quantile of all edge lengths
* finding `edges_to_reconnect`, all the edges where either
    * `edge_length` < `diagram_edge_min_acceptable_length`
    * `_edge_can_be_contracted()`
        * The probability of this method evaluating to `True` is based on how many " lonely " neighbors the edge's `Point`s have.
            * A `Point` is lonely if it has only one neighbor (in this case, one of the edge's two `Point`s).
* processing `edges_to_reconnect`
    * for each `edge_to_reconnect`
        * taking edge points `A` and `B`, and determining `ML`, the one that has more lonely neighbors (making the other one `LL`, less lonely)
        * connecting all of `ML`'s neighbor points to `LL`
            * for neighbour point `NP`, we'd examine the edge `NP` <-> `ML`
                * if that edge was not created as part of the subsequent process, we'd
                    * replace the edge `NP` <-> `ML` with `NP` <-> `LL`
                    * delete `NP` <-> `ML`
        * deleting `A` <-> `B` if the `ML` edges in the last step were all valid for replacement
            * otherwise, edge removal would break the graph into subgraphs
* deleting all edges that could be deleted to break " cycles " (sets of edges that constitute loops)
* calculating edge fuel costs, number of stops, and " junction block cycle " period
* determining `start_node_id` and `destination_node_id`
* constructing and returning a `Track` from the information so far

The diagram is generated with [Voronout](https://pypi.org/project/Voronout/) and modeled with [rustworkX](https://www.rustworkx.org/).

# Track generation, visualized

`voronoi_points`:

```Python
(Point(x= 0.9463, y= 0.6669), Point(x= 0.4353, y= 0.5272), Point(x= 0.4222, y= 0.5968), Point(x= 0.9876, y= 0.5229), Point(x= 0.5794, y= 0.964), Point(x= 0.5983, y= 0.0106), Point(x= 0.687, y= 0.1437), Point(x= 0.4132, y= 0.2723), Point(x= 0.0361, y= 0.1436), Point(x= 0.062, y= 0.2417), Point(x= 0.8191, y= 0.4291), Point(x= 0.5383, y= 0.8066), Point(x= 0.5011, y= 0.365), Point(x= 0.3063, y= 0.9677), Point(x= 0.9008, y= 0.0296), Point(x= 0.2748, y= 0.7383), Point(x= 0.8428, y= 0.0683), Point(x= 0.1703, y= 0.0407))
```

Initial Voronoi diagram:

![Initial Voronoi diagram](track_generation_start.png)

Edge contraction:

![Edge contraction](track_generation_contractions.gif)

Edge deletion:

![Edge deletion](track_generation_deletion.gif)

Stop generation:

![Stop generation](track_generation_stops.gif)

Final Track:

![Final Track](track_generation_end.png)

# Track info

`node_info` contains further information about nodes

```Python
@dataclass(frozen=True)
class NodeInfo:
    num_steps_to_destination: int
    num_seconds_block_cycle: int
```

for game logic - `num_steps_to_destination` tells you how many edges away it is from `destination_node_id`'s node, while `num_seconds_block_cycle` is used in the " junction block cycle " mechanic.

`edge_info` is likewise for edges

```Python
@dataclass(frozen=True)
class EdgeInfo:
    edge_traversal_fuel_cost: float
    edge_traversal_duration: float

    edge_stop_info: tuple[StopInfo]

    edge_image_default_b64: str
    edge_image_focused_b64: str
```

`edge_traversal_fuel_cost` is the fuel spent in-game by a `Walker` traversing the edge - `edge_traversal_duration` is the time it takes to do so in seconds, used for animation purposes.

`edge_image_*_b64` are the edge's graphical representations in its default/focused states, stored in `base64`. The game converts them into sprites to show on-screen.

```
@dataclass(frozen=True)
class StopInfo:
    stop_point: Point
    stop_id: uuid4

    stop_fuel: float
```

`stop_fuel` is how much fuel a Walker can draw from the stop.
