gale.quest

A generic, customizable-per-game quest system built on gale.sequence: Objective (one trackable goal), Stage (a group of objectives tracked together), Quest (a sequence of stages), and QuestLog (tracks every quest a game has, starts them, and broadcasts progress events to whichever are active).

None of this hardcodes what an objective actually means: a game defines its own progress-event keys ("wolves_killed", "herbs_collected", …) and reports progress from wherever that event actually happens.

from gale.quest import Objective, Quest, QuestLog, Stage

log = QuestLog()
log.register(
    Quest(
        "wolf_trouble",
        "Wolf Trouble",
        [
            Stage([Objective("wolves_killed", "Defeat 3 wolves", target=3)]),
            Stage([Objective("report", "Report back to the elder", target=1)]),
        ],
        on_completed=lambda: print("Wolf Trouble completed!"),
    )
)
log.start("wolf_trouble")

# Wherever a wolf actually dies in your game:
log.notify("wolves_killed", 1)

# Every frame (only active quests do anything):
log.update(dt)

A Stage completes once every one of its objectives does (or just one of them, with require_all=False, e.g. “reach the gate OR find the secret passage”), and a Quest moves to its next stage the same way a Sequence moves to its next step. Quest.notify/QuestLog.notify only ever reach the current stage of each active quest – objectives in stages that haven’t started yet, or that already finished, are left alone.

Objective is a Step underneath, so duration/advance_on_input work on it too: an objective that’s just “wait a moment” or “press a key to confirm reading the quest” needs no progress counter at all:

Objective(description="Read the notice board", advance_on_input="confirm")

Anything that doesn’t fit a simple progress counter (checking an inventory, a flag on a shared gale.ai.blackboard.Blackboard, …) is just a matter of subclassing Objective and overriding is_complete:

class HasItemObjective(Objective):
    def __init__(self, inventory, item_name, **kwargs) -> None:
        super().__init__(description=f"Obtain {item_name}", **kwargs)
        self.inventory = inventory
        self.item_name = item_name

    def is_complete(self) -> bool:
        return self.item_name in self.inventory or super().is_complete()

A Blackboard observer bridges the two systems naturally, if a game already tracks some state there:

blackboard.observe(
    "is_alerted", lambda key, old, new: log.notify("guard_alerted", 1) if new else None
)