Map

Not every narrative needs a Map.

However if you want the player to explore your story world, they need to be able to change their location.

Then you have to consider how travel works. What locations can be reached from here? How do we identify the possible directions? Can I give this pathway a name? Did I just make that pathway a real place by naming it?

The design goal for Balladeer’s topological model is to be extremely flexible, while remaining as simple as possible.

Compass

The Compass is a traditional direction system. It is very common in text adventures.

class balladeer.lite.compass.Compass

A state to represent the eight points of the compass.

    N = ["North", vector(+0, +1)]
    NE = ["Northeast", "North East", vector(+1, +1)]
    E = ["East", vector(+1, +0)]
    SE = ["Southeast", "South East", vector(+1, -1)]
    S = ["South", vector(+0, -1)]
    SW = ["Southwest", "South West", vector(-1, -1)]
    W = ["West", vector(-1, +0)]
    NW = ["Northwest", "North West", vector(-1, -1)]
property back

Return the opposite point:

>>> Compass.NE.back
Compass.SW
__new__(value)
classmethod Compass.bearing(*args) float

Calculate the angular bearing of the endpoint of a route as measured from the beginning.

>>> Compass.bearing(Compass.E)
90.0
>>> Compass.bearing(Compass.N, Compass.E, Compass.E, Compass.SW)
90.0

Traffic and Transits

Sometimes in a story a way is blocked, only to be opened later. Or perhaps there’s a slippery slope which can be taken only in one direction.

Traffic is a simple state definition to model that behaviour.

class Traffic(State, enum.Enum):
    blocked = "No traffic"
    forward = "Traffic flow forward"
    reverse = "Traffic flow reversed"
    flowing = "Traffic flows freely"

A Transit is an object which describes the navigation between two places in the world. It inherits from Entity so it may be anonymous, or a fully described feature of your story.

transit = Transit(
    names=["Door", "Wooden Door"],
    type="Door",
    aspect="locked",
    sketch="A {0.name}. It seems to be {aspect}.",
).set_state(Traffic.blocked)
>>> transit.description
"A Wooden Door. It seems to be locked."

Caution

Compact idiom for attribute swapping and state allocation.

At the the expense of readability, this one-liner will:

  • Change the state of the door

  • Copy the value of aspect to revert

  • Change the value of aspect to “unlocked”

>>> transit.set_state(Traffic.flowing).aspect, transit.revert = "unlocked", transit.aspect

With the subsequent result:

>>> transit.description
"A Wooden Door. It seems to be unlocked."
>>> transit.get_state("Traffic")
<Traffic.flowing>
>>> transit.revert
"locked"

MapBuilder

class balladeer.lite.compass.MapBuilder

This class is the base for the map of your story world.

It is responsible for routing and navigation. It helps your characters find their way around.

To populate a new map, create a subclass of MapBuilder. Give the new class an attribute called spots. This must be a dictionary suitable for the construction of an enum.Enum.

Override the build method of your class. This method generates Transit objects.

Internally the map object will derive four state types from the spot data you supplied to it:

Spot

This state represents an absolute position, eg: map.spot.kitchen.

Into

This state represents motion towards the position, eg: map.into.kitchen.

Exit

This state represents motion away from the position, eg: map.exit.kitchen.

Home

This state represents an affinity to the position, eg: map.home.kitchen.

These states are available for use with transits and other entities.

class Map(MapBuilder):
    spots = {
        "bedroom": ["bedroom"],
        "hall": ["hall", "hallway"],
        "kitchen": ["kitchen"],
        "stairs": ["stairs", "stairway", "up", "up stairs", "upstairs"],
        "inventory": ["inventory"],
    }

    def build(self, **kwargs):
        yield from [
            Transit(name="bedroom door").set_state(
                self.exit.bedroom, self.into.hall, Traffic.flowing
            ),
            Transit().set_state(
                self.exit.hall, Compass.N, self.into.stairs, Traffic.flowing
            ),
            Transit(name="kitchen door").set_state(
                self.exit.kitchen, Compass.SW, self.into.hall, Traffic.flowing
            ),
        ]
__init__(spots: dict, config=None, **kwargs)
property topology: Generator[tuple[State, State, Entity, State]]

Generates the topological mesh of the map.

Each item is a tuple representing an arc from one spot to another, if permitted by a transit. Compass direction, when known, is the second element of the tuple.

The built map of the previous example generates the following six arcs:

1(<Exit.bedroom: ['bedroom']>, None, Transit(names=['bedroom door'], ...), <Into.hall: ['hall', 'hallway']>)
2(<Into.hall: ['hall', 'hallway']>, None, Transit(names=['bedroom door'], ...), <Exit.bedroom: ['bedroom']>)
3(<Exit.hall: ['hall', 'hallway']>, <Compass.N: ['North', (0, 1, 0)]>, Transit(names=[], ...), <Into.stairs: ['stairs', ...]>)
4(<Into.stairs: ['stairs', ...]>, <Compass.S: ['South', (0, -1, 0)]>, Transit(names=[], ...) <Exit.hall: ['hall', 'hallway']>)
5(<Exit.kitchen: ['kitchen']>, <Compass.SW: ['Southwest', 'South West', (-1, -1, 0)]>, Transit(names=['kitchen door'], ...) <Into.hall: ['hall', 'hallway']>)
6(<Into.hall: ['hall', 'hallway']>, <Compass.NE: ['Northeast', 'North East', (1, 1, 0)]>, Transit(names=['kitchen door'], ...) <Exit.kitchen: ['kitchen']>)
options(spot: State) set

Returns a set of all the permitted transits from the supplied spot. Each item of the set is a tuple of three elements. The first is a compass heading if one is defined, otherwise it’s an integer unique in the result set. The second element is the destination spot. The third is the viable transit.

Using the example above, this line of code will return a set with three items:

>>> map.options(map.spot.hall)
{(<Compass.NE: ['Northeast', 'North East', (1, 1, 0)]>, <Spot.kitchen: ['kitchen']>, Transit(names=['kitchen door'], ...),
(<Compass.N: ['North', (0, 1, 0)]>, <Spot.stairs: ['stairs', ...]>, Transit(names=[], ...)),
(1, <Spot.bedroom: ['bedroom']>, Transit(names=['bedroom door'], ...))}
route(start: State, end: State) list[State]

Return a list containing the shortest route between the spots start and end. The endpoints are included in the output.

>>> map.route(map.spot.kitchen, map.spot.bedroom)
[<Spot.kitchen: ['kitchen']>,
 <Spot.hall: ['hall', 'hallway']>,
 <Spot.bedroom: ['bedroom']>]