Skip to content

Elder Futhark Runes

An Elder Futhark rune-casting engine with the 24-rune deck plus single-rune and Norns spreads. Casting is RNG-driven. Reversals are optional via allow_reversals, and the eight symmetrical runes are never reversed.

from fortune_telling_core import RandomRng, ReadingRequest
from fortune_telling_core.traditions.runes import RUNE_DECK, NORNS, build_engine

request = ReadingRequest(
    deck_id=RUNE_DECK.id,
    spread_id=NORNS.id,
    options={"allow_reversals": "true"},
)
reading = build_engine().read(request, rng=RandomRng(seed=42))

runes

RUNE_DECK module-attribute

RUNE_DECK = Deck(
    id="runes.deck.elder-futhark.v1",
    symbols=tuple(
        (
            Symbol(
                id=rune.symbol_id,
                name=rune.name,
                attributes={
                    "slug": rune.slug,
                    "glyph": rune.glyph,
                    "letter": rune.letter,
                    "aett": rune.aett,
                    "keyword": rune.keyword,
                    "cycle_index": str(rune.index),
                    "reversible": "true"
                    if rune.reversible
                    else "false",
                },
            )
        )
        for rune in RUNES
    ),
)

NORNS module-attribute

NORNS = Spread(
    id="runes.spread.norns.v1",
    name="Norns",
    positions=(
        Position(
            "urdr",
            "Urðr",
            "That which has become: the past.",
        ),
        Position(
            "verdandi",
            "Verðandi",
            "That which is becoming: the present.",
        ),
        Position(
            "skuld",
            "Skuld",
            "That which shall be: the future.",
        ),
    ),
)

SINGLE_RUNE module-attribute

SINGLE_RUNE = Spread(
    id="runes.spread.single.v1",
    name="Single Rune",
    positions=(
        Position(
            "focus",
            "Focus",
            "The central message of the casting.",
        ),
    ),
)

RuneEngine

Bases: AbstractEngine

Elder Futhark rune-casting engine.

The engine draws from the bundled 24-rune deck into a single-rune or Norns (past/present/future) spread, with optional reversals through the request option allow_reversals=true. The eight symmetrical runes have no reversed form and are always cast upright, even when reversals are enabled.

id class-attribute instance-attribute

id = 'runes.elder-futhark.engine'

version class-attribute instance-attribute

version = '0.1.0'

deck

deck(request: ReadingRequest) -> Deck

Return the Elder Futhark deck for a request.

Parameters:

Name Type Description Default
request ReadingRequest

Reading request whose deck_id must match RUNE_DECK.id.

required

Returns:

Type Description
Deck

The bundled rune deck.

Raises:

Type Description
ValidationError

If the request names an unsupported deck.

Source code in src/fortune_telling_core/traditions/runes/engine.py
def deck(self, request: ReadingRequest) -> Deck:
    """Return the Elder Futhark deck for a request.

    Args:
        request: Reading request whose ``deck_id`` must match
            ``RUNE_DECK.id``.

    Returns:
        The bundled rune deck.

    Raises:
        ValidationError: If the request names an unsupported deck.
    """

    if request.deck_id != RUNE_DECK.id:
        raise ValidationError(f"unsupported rune deck: {request.deck_id}")
    return RUNE_DECK

spread

spread(request: ReadingRequest) -> Spread

Return the rune spread requested by id.

Parameters:

Name Type Description Default
request ReadingRequest

Reading request whose spread_id selects a bundled rune spread.

required

Returns:

Type Description
Spread

SINGLE_RUNE or NORNS.

Raises:

Type Description
ValidationError

If the spread is not supported.

Source code in src/fortune_telling_core/traditions/runes/engine.py
def spread(self, request: ReadingRequest) -> Spread:
    """Return the rune spread requested by id.

    Args:
        request: Reading request whose ``spread_id`` selects a bundled
            rune spread.

    Returns:
        ``SINGLE_RUNE`` or ``NORNS``.

    Raises:
        ValidationError: If the spread is not supported.
    """

    if request.spread_id == SINGLE_RUNE.id:
        return SINGLE_RUNE
    if request.spread_id == NORNS.id:
        return NORNS
    raise ValidationError(f"unsupported rune spread: {request.spread_id}")

draw

draw(request: ReadingRequest, rng: Rng) -> Draw

Cast runes for the requested spread.

Parameters:

Name Type Description Default
request ReadingRequest

Reading request. Set options={"allow_reversals": "true"} to include orientation modifiers.

required
rng Rng

Random source used to shuffle the deck and choose reversals.

required

Returns:

Type Description
Draw

A draw containing one selection per spread position.

Raises:

Type Description
ValidationError

If the requested deck or spread is unsupported.

ExhaustedRngError

If the supplied RNG cannot provide enough values.

Source code in src/fortune_telling_core/traditions/runes/engine.py
def draw(self, request: ReadingRequest, rng: Rng) -> Draw:
    """Cast runes for the requested spread.

    Args:
        request: Reading request. Set ``options={"allow_reversals":
            "true"}`` to include orientation modifiers.
        rng: Random source used to shuffle the deck and choose reversals.

    Returns:
        A draw containing one selection per spread position.

    Raises:
        ValidationError: If the requested deck or spread is unsupported.
        ExhaustedRngError: If the supplied RNG cannot provide enough values.
    """

    deck = self.deck(request)
    spread = self.spread(request)
    order = rng.shuffle(len(deck.symbols))
    allow_reversals = (
        request.options.get("allow_reversals") == "true" if request.options else False
    )

    selections: list[Selection] = []
    for position, symbol_index in zip(spread.positions, order[: spread.size], strict=True):
        symbol = deck.symbols[symbol_index]
        modifiers: dict[str, str] = {}
        if allow_reversals:
            # Consume one float per position so the stream stays aligned,
            # but only invertible runes can actually take a reversed form.
            turned = rng.random() < 0.5
            reversible = symbol.attributes.get("reversible") == "true"
            modifiers["orientation"] = "reversed" if turned and reversible else "upright"
        selections.append(
            Selection(
                position_id=position.id,
                symbol_id=symbol.id,
                modifiers=modifiers,
            )
        )

    return Draw(deck_id=deck.id, spread_id=spread.id, selections=tuple(selections))

build_engine

build_engine() -> RuneEngine

Create an Elder Futhark rune engine.

Returns:

Type Description
RuneEngine

A new RuneEngine instance.

Source code in src/fortune_telling_core/traditions/runes/engine.py
def build_engine() -> RuneEngine:
    """Create an Elder Futhark rune engine.

    Returns:
        A new ``RuneEngine`` instance.
    """

    return RuneEngine()