Skip to content

1. TopKit for Python: The Guide

Julio Toboso García edited this page Oct 9, 2026 · 3 revisions

The TopKit Guide

Tag-Oriented Programming for people. Read this before the Specification.

This guide teaches TOP by building one thing: a character sheet the way a tabletop game builds it. Species, class, background and feats are separate choices, and the sheet is what they compose. Every code block runs, in order, on Python 3.12 or later, with nothing installed but TopKit.

If you want the laws, read the Specification. If you want to see the whole thing at once, run examples/dnd_character.py. This document is for learning, one pattern at a time.


The one idea

A hero is one character. Over a game she becomes a Human, a Wizard, a Fighter, a Harper. She stays the same hero. In TOP you write each of those meanings as a Tag, and you apply Tags to an object you already have. The object keeps its identity. Its meaning grows.

Two words from Spanish say the whole model. A Tag is something you are (ser): a noun, durable. A Record is how you are right now (estar): an adjective, changeable. Wizard is a Tag. Hit points are a Record.

from TopKit import (
        Action, Contract, Delete, Flag, Form, Imprint, Keyword, Operation,
        Outline, Pin, Post, Postcondition, Pre, Precondition, Public,
        Record, Report, Requirement, Rip, Scope, Secret, Tag, Tags,
        Underlay,
        TagCompositionError, TagPostconditionError, TagPreconditionError,
        TagResolutionError,
        )


class Character:
    """The host: ordinary Python. TOP never asks you to change it."""

    def __init__(self, name, level=1):
        self.name = name
        self.level = level

Ten minutes

A Tag is a category. Applying it is membership.

class Wizard(Tag):
    pass


ari = Character("Ari", level=3)
Wizard(ari)

assert ari in Wizard                 # Ari is a Wizard now
assert list(Wizard) == [ari]         # the Field: everyone who is one
assert Wizard                        # "is anyone a Wizard?" (yes)

An empty Tag is already useful. It defines a category and a population, and that is the primary thing a Tag does. Everything else is extra.

Tags give the Agent things: Records and Actions

class Wizard(Tag):

    @Record
    def spell_slots(agent):
        return 2                          # built once per Agent, stored on it

    def Attack(agent):                    # a plain method is an Action
        return f"{agent.name} casts a spell"


ari = Character("Ari", level=3)
Wizard(ari)

assert ari.spell_slots == 2
assert ari.Attack() == "Ari casts a spell"

ari.spell_slots -= 1                      # a Record is an ordinary attribute
assert ari.spell_slots == 1

Notice the first parameter is agent, not self. A Tag's methods act on someone else's object. The name is your choice; the position is the rule.

Tags compose. The last one applied is what you see.

class Fighter(Tag):

    def Attack(agent):
        return f"{agent.name} cuts with a blade."


Fighter(ari)

assert ari.Attack() == "Ari cuts with a blade."       # Fighter came last
assert ari.Wizard.Attack() == "Ari casts a spell"   # the view after Wizard

ari.Wizard is a view: the sheet as it was right after Wizard applied. It never changes, whatever comes later. Wizard[ari] is the same view by class, exact even if two Tags share a name.

Ask "Attack" @ ari whether that named Contribution is present, without calling it. Its kind and value do not affect presence. See Presence: a name at an Agent for Post guards, host Contributions and queries through Tag views.

A Tag can be taken away. What it gave stays.

del Fighter[ari]                          # Rip: leave the Field

assert ari not in Fighter
assert isinstance(ari, Fighter)           # ever a Fighter, always a Fighter
assert ari.Attack() == "Ari cuts with a blade."   # sticky: the Action stays

That last line surprises people. TOP calls this a Rogue Agent: it has what it learned, but no active membership. If a Tag must clean up after itself, it says so with @Rip (pattern 9).

That is the whole kernel. The rest of this guide is patterns.


Patterns

Each pattern has the same shape: when you want something, write this, and watch out for that.

Pattern 1 · A category with no code

When you want to say what something is, so other code can ask.

class Undead(Tag):
    pass


ghoul = Character("Ghoul")
Undead(ghoul)

corruption = 0
for creature in Undead:                 # every sound member
    corruption += creature.level

if not Undead:                          # nobody is Undead: nothing to do
    print("This area is safe")
else:
    print(f"This area is corrupted by a level of {corruption}")

Watch out. for creature in Undead walks the sound population: the members whose conditions hold (pattern 6). Undead[:] is everyone, regardless of conditions.

Pattern 2 · State that belongs to the role

When you want each Agent to carry its own value, fresh, set by the Tag.

class Wizard(Tag):

    @Record
    def spell_slots(agent):
        return 2

    @Record
    def spellbook(agent):
        return []                       # a new list for every Agent


ada = Character("Ada")
bob = Character("Robert")
Wizard(ada)
Wizard(bob)

ada.spellbook.append("Light")

assert bob.spellbook == []                # not shared

Watch out. A Record builder runs once, when the Tag applies. It is not a property; it does not recompute. Reapplying an active Tag does nothing, so a Record is never silently reset. To reset, Rip and apply again.

Pattern 3 · Pile things up from several Tags

When you want a value that several independent Tags contribute to: a spell list from species, class and background; hit points from class and a feat.

Declare a second parameter on the builder. It receives what is already stored under that name, or None the first time.

class Elf(Tag):

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Light"]


class Wizard(Tag):

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Magic Missile", "Shield"]

    @Record
    def hit_points(agent, stored):
        return (stored or 0) + 6 * agent.level


class Tough(Tag):

    @Record
    def hit_points(agent, stored):
        return (stored or 0) + 2 * agent.level


ari = Character("Ari", level=3)
Elf(ari)
Wizard(ari)
Tough(ari)

assert ari.spells == ["Light", "Magic Missile", "Shield"]
assert ari.hit_points == 18 + 6

You write the merge. stored + new, max(stored, new), stored | new: whatever the domain means. There is nothing to configure.

Watch out. A builder without the second parameter replaces. If it replaces a Record that an unrelated Tag put there, TopKit warns, because something another meaning relied on was overwritten.

Pattern 4 · Extend behaviour instead of replacing it

When you want a later Tag to add to an Action, not overwrite it.

class Person(Tag):

    def Attack(agent):
        return "Attack!"


class Elf(Person):                      # Elf is a Shape of Person

    @Action
    @Underlay
    def Attack(agent, underlay):
        return "With elven grace, " + underlay()


class Paladin(Person):

    @Action
    @Underlay
    def Attack(agent, underlay):
        return underlay() + " For the oath!"


ari = Character("Ari")
Elf(ari)
Paladin(ari)

assert ari.Attack() == "With elven grace, Attack! For the oath!"
assert ari.Elf.Attack() == "With elven grace, Attack!"

@Underlay hands you the Action as it was just before yours. It is captured when the Tag applies and never changes, so every view stays true.

class Elf(Person) makes Person a Base of Elf: applying Elf applies Person first. Form(Elf) lists that order; f"{Elf:form}" prints it.

Watch out. An Action that calls another Action through the Agent (agent.Attack() inside Combat) uses whatever is visible now, not what was captured. Underlay is for the same name; the Agent is for the current sheet.

Pattern 5 · Gate the door

When you want a Tag that not everyone may take. Think of a door with a bouncer. Before a Tag is applied, the Tag looks at the Agent and decides: in, or not. That look is a Precondition, written with @Pre. It is a function that receives the Agent and answers True (come in) or False (stay out).

class Wizard(Tag):

    @Pre
    def Can_Study(agent):
        return agent.level >= 1


class War_Caster(Tag):

    @Pre
    def Is_A_Caster(agent):             # synergy: needs another Tag
        return agent in Wizard


bruk = Character("Bruk", level=2)

try:
    War_Caster(bruk)
except Precondition.Is_A_Caster:
    pass                                # Bruk is no caster: nothing changed

assert bruk not in War_Caster

Wizard(bruk)
War_Caster(bruk)                        # now it takes

Three things to notice.

  • A refused Agent is untouched. When a Precondition says no, the tagging stops before anything is written. Bruk is exactly as he was.
  • The error carries the name you wrote. except Precondition.Is_A_Caster catches that one refusal and nothing else. except TagPreconditionError catches every refusal. Both work; the named one reads better. A name you never declared, such as a typo, is an error at the except line, so a silent handler that never fires cannot happen.
  • A gate can ask about other Tags. agent in Wizard is an ordinary check, so a Tag can require another Tag. That is how synergies are written: the second Tag needs the first.

Inputs travel with the tagging. Sometimes the gate needs information that is not on the Agent yet, like a code number. You give it at the call: MI6(bond, code="007"). TopKit then hands code, by name, to every Precondition, Record builder and Imprint of that Tag that has a parameter called code. The usual use is a Record that simply keeps the input:

class MI6(Tag):

    @Pre
    def Has_A_Code(agent, code):
        return code is not None

    @Record
    def code(agent, *, code):           # keyword-only: "this comes from the call"
        return code


bond = Character("Bond")
MI6(bond, code="007")

assert bond.code == "007"

Read the * as a blank space. A Record's function has three places, in order: the Agent, what was already stored under that name (pattern 3), and what the call brings. Here nothing was stored, so the star holds that seat empty: def code(agent, *, code) says "agent, nothing stored, then code from the call". If you wrote def code(agent, code), Python would put code in the stored seat, and TopKit refuses that at tagging time and prints the spelling above. Preconditions and Imprints have no stored seat, so there def Has_A_Code(agent, code) is enough.

Watch out. A gate must answer True, False, or nothing at all. Do not return a number and hope: a count of 0 is not False to TopKit, and it will refuse the raw value. Write the comparison you mean, such as return agent.slots > 0. This keeps a real zero from being mistaken for a refusal.

Pattern 6 · Promise, then repair

When you want a Tag to guarantee something about the finished Agent. Postconditions inspect the product after everything applied.

class Wizard(Tag):

    @Post
    def Has_Spellbook(agent):
        assert agent.spellbook is not None


newt = Character("Newt")
newt.spellbook = None

try:
    Wizard(newt)
except Postcondition.Has_Spellbook:
    pass                                # the Tag stays; Newt is defective

assert newt in Wizard                   # a member
assert not newt                         # whose promise is broken
assert newt in ~Wizard                  # waiting in the repair queue

for broken in ~Wizard:                  # the repair loop
    broken.spellbook = []

assert newt                             # sound again
assert newt in list(Wizard)             # back in the working population

This is the factory rule: a bad product is not melted back to materials. It is flagged, repaired, or thrown away (del Wizard[newt]).

if agent: reads "are this agent's promises holding". When you need the name of the broken one, Contract.Display(agent) or f"{agent:contract}" prints them.

Necessary to enter, necessary to stay. Stack @Pre and @Post on one function and it is both: a gate at the door and a promise kept after.

class Elf(Tag):

    @Pre
    @Post
    def Alive(agent):
        return agent.alive


ghost = Character("Ghost")
ghost.alive = False

try:
    Elf(ghost)
except Precondition.Alive:
    pass                                # not alive: cannot become an Elf

ari.alive = True
Elf(ari)
ari.alive = False                       # an Elf who died is a broken Elf
assert ari in ~Elf

Being alive is necessary to be an Elf, not sufficient: other living things are not Elves. That is the difference between a condition and a Tag.

When the claim reads better as one necessity than as two checks, write @Requirement instead of the pair. It means exactly the same thing:

class Elf(Tag):

    @Requirement
    def Alive(agent):
        return agent.alive

It still fails under two names, because you repair the two differently: Precondition.Alive is someone who may not come in, Postcondition.Alive is an Elf already in and now broken.

Watch out. A Shape should promise at least what its Base promised. Use @Post @Underlay and return base() and .... Overriding a Base's promise without it weakens the contract; TopKit allows it and warns.

Pattern 7 · Keywords for rules written as data

When you want rules that live in tables or files and check words, not classes. Mark the Tags that are words with @Flag.

@Flag
class Undead(Tag):
    pass


@Flag
class Flying(Tag):
    pass


ghoul = Character("Ghoul")
Undead(ghoul)

assert "Undead" in ghoul                # by name
assert Undead in ghoul                  # by class
assert "Flying" not in ghoul

TURNABLE = ["Undead", "Fiend"]          # a rule as data
assert any(word in ghoul for word in TURNABLE)

assert Keyword(ghoul, "Undead")         # the function form: any object
assert not Keyword(Character("x"), "Undead")

When one word is not enough, list the others. The Tag's own name always counts; the words are its variants and neighbours:

@Flag("Wolf", "Lycanthrope", "Beast")
class Werewolf(Tag):
    pass


howler = Character("Howler")
Werewolf(howler)

assert "Wolf" in howler                 # a word
assert "Werewolf" in howler             # the name, always
assert Keyword(howler, "Beast", "Lycanthrope")

HUNTED = ["Beast", "Fiend"]             # the table never imports Werewolf
assert any(word in howler for word in HUNTED)

A word is a public name, not a membership: if there is also a Beast Tag, "Beast" in howler is True while howler in Beast is False. That is the point, a secret identity with a public face, but it is yours to keep straight. When a word needs a Field to walk, a gate or a promise, make it a Tag and a Base (class Werewolf(Beast)); when rules only match it, a word is enough.

Watch out. A string never asks membership. An ordinary Tag never answers to its name: "Wizard" in agent says nothing about being a Wizard, and is True only while an active Flag is named Wizard or lists the word. Words are strings, @Flag("Beast"); @Flag(Beast) is the bare form applied to the class Beast, not a word.

One seat, one meaning. A Flag cannot join an object that already answers in: a list, a party that iterates its members (__iter__), or an Agent carrying a Tag with a __contains__ or __iter__ Action. Nor can such a Tag join an Agent that carries a Flag. TopKit refuses either way, and the message names both sides, so "alice" in party never changes meaning in silence. A keyed sheet (sheet["STR"], only __getitem__) has no working in, and a Flag may take it. So may an old-style sequence that answers only line[0], line[1], …, and there "alice" in line stops meaning membership: give such a class a real __iter__ if its in matters.

A class that writes __contains__ = None tells Python its in is unavailable, and a Flag may then take the seat. The line belongs to the whole class, not to one Agent. Once a Flag lands, the class's own x in self answers keywords, and so do inherited container methods that ask in (the comparisons of a collections.abc.Set). Write it only for a class whose in nobody uses for membership.

Pattern 8 · Shared things, and who may reach them

When you want one value or one behaviour for the whole Field, and a clean line between what the Agent shows and what the Tag keeps.

class Agency(Tag):

    @Public
    @Report
    def colour(tag):
        return "navy"                   # one copy, published on the Agent

    @Report
    def HQ(tag):
        return "London"                 # one copy, Tag-side only

    @Public
    @Operation
    def dispatch(agency, sender, message):
        return f"{sender.name}: {message}"   # only members ever get here

    @Secret
    @Record
    def clearance(agent):
        return "top"

    @Action
    def status(agent):
        return agent.clearance          # inside: the secret resolves


bond = Character("Bond")
Agency(bond)

assert bond.colour == "navy"            # published Report, read-only
assert bond.dispatch("hello") == "Bond: hello"
assert bond.status() == "top"
assert not hasattr(bond, "HQ")          # Tag-side stays Tag-side
assert not hasattr(bond, "clearance")   # secret from outside

send = bond.dispatch                    # a captured handle
del Agency[bond]

assert bond.status() == "top"           # his own Action: he keeps it

try:
    send("late")
except TagResolutionError:
    pass                                # the Agency's: revoked with membership

The rule in one line: what the Agent does is public, what the Agency keeps is internal. @Secret hides an Agent member; @Public shows a Tag member. Both are modifiers: they stack on @Record, @Action, @Report or @Operation in either order.

A published member answers members only, and only sound ones. A Rogue Agent (he left) keeps what he became, his own Actions and Records, and loses what the Agency lent him: TagRogueAccessError. A defective Agent (a promise broke, any promise, whoever made it) is turned away until repaired, and the refusal names the broken promise. You never write either check, and the second one gives you the autofix pattern: catch the promise, repair what it names, try again.

class Sworn(Tag):

    @Post
    def Has_Oath(agent):
        return agent.oath is not None


Agency(bond)
bond.oath = None
try:
    Sworn(bond)
except Postcondition.Has_Oath:
    pass                                # a member, but defective

try:
    bond.dispatch("hello")
except Postcondition.Has_Oath:          # the refusal names what is broken
    bond.oath = "for Queen and country"
    assert bond.dispatch("hello") == "Bond: hello"

Notice the symmetry. A Report is written exactly like a Record, with the Tag instead of the Agent as its first input; it runs once per Tag and its value is shared by the whole Field. def hit_die(tag, inherited) extends the Base's value, as def spells(agent, stored) extends what is stored.

Watch out. A secret resolves only while one of the Agent's own Actions or protocols is running. A handle to a secret Action captured inside and called outside fails, on purpose.

Pattern 9 · Roles that end

When you want a Tag to clean up when it leaves, or a role that lasts for a block of code.

class Sentry(Tag):

    @Imprint
    def post(agent):
        agent.on_duty = True

    @Rip
    def stand_down(agent):
        agent.on_duty = False


guard = Character("Guard")

with Scope(guard, Sentry):              # applies on entry
    assert guard.on_duty

assert not guard.on_duty                # Ripped on exit, even on error
assert guard not in Sentry

@Imprint runs after the Tag applies; @Rip runs after it leaves. They are constructor and destructor, __enter__ and __exit__.

A role's conditions do not leave with it on their own. What the Agent became stays (pattern 1's Rogue Agent), and so does what the role required: a promise that outlives its Tag fails loud, never silently. If a promise should end with the role, you say so, in the role's own @Rip protocol, one name at a time:

class Knight(Tag):

    @Post
    def Has_Sword(agent):
        return agent.sword is not None

    @Rip
    def Undub(agent):
        Contract.Delete(agent, "Has_Sword")    # the promise ends here, visibly

    def Salute(agent):
        return f"{agent.name} salutes"


lance = Character("Lance")
lance.sword = "Excalibur"
Knight(lance)

del Knight[lance]                       # dubbed no more
lance.sword = None

assert lance.Salute() == "Lance salutes"   # what he became: stays
assert lance                               # what the role required: ended by you

The other way is a guard inside the promise, if agent not in Knight: return True, which lets the promise follow any membership you like; the Contracts Guide shows both.

Watch out. Python does not promise to run finalizers at shutdown, so del agent is best-effort. Scope is the guaranteed path.

An object's own __del__ is a Layer. It keeps running when the object is tagged. A Tag may replace it, or wrap it with @Underlay; the Tags' teardowns always run first, and a __del__ never stops them:

farewells = []


class Lantern:
    def __del__(self):
        farewells.append("wick out")


class Carried(Tag):
    @Rip
    def Put_Down(agent):
        farewells.append("put down")


class Enchanted(Tag):
    @Underlay
    def __del__(agent, underlay):
        farewells.append("spell fades")
        underlay()                          # the Lantern's own, or nothing


lamp = Lantern()
Carried(lamp)
Enchanted(lamp)
del lamp

assert farewells == ["put down", "spell fades", "wick out"]

At interpreter exit only the __del__ Layers run. To have teardowns run at exit too, register the Agent with At_Exit(agent).

Pattern 10 · Build the sheet from pieces

Put it together. This is the D&D example in miniature.

class Species(Tag):

    @Record
    def speed(agent):
        return 30


class Elf(Species):

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Light"]


class Class(Tag):

    @Report
    def hit_die(tag):
        return 8


class Wizard(Class):

    @Report
    def hit_die(tag):
        return 6

    @Pre
    def Can_Study(agent):
        return agent.level >= 1

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Magic Missile"]

    @Record
    def hit_points(agent, stored):
        return (stored or 0) + agent.level * Wizard.hit_die

    @Post
    def Knows_Spells(agent):
        return len(agent.spells) > 0

    def Attack(agent):
        return f"{agent.name} casts {agent.spells[-1]}"


class Fighter(Tag):

    @Record
    def spells(agent, stored):
        return (stored or []) + ["Identify"]


class War_Caster(Tag):

    @Pre
    def Is_A_Caster(agent):
        return agent in Wizard

    @Action
    @Underlay
    def Attack(agent, underlay):
        return underlay() + " behind a shield"


ari = Character("Ari", level=3)

for choice in (Elf, Wizard, Fighter, War_Caster):
    choice(ari)

assert ari.spells == ["Light", "Magic Missile", "Identify"]
assert ari.hit_points == 18
assert ari.Attack() == "Ari casts Identify behind a shield"
assert ari                                      # every promise holds
assert f"{ari:tags}" == "Elf, Wizard, Fighter, War_Caster"

print(Outline(ari))
# Character
#   Species
#     Elf
#   Class
#     Wizard
#   Fighter
#   War_Caster

Pattern 11 · Categories of categories

When you want to say something about a Tag itself: this class is rare, that species is homebrew, this rule is deprecated. A Tag is an object, so a Tag can be tagged. A Tag marked @Pin applies to Tags, and the Tag it is applied to becomes its Agent: it joins a Field, it can be gated, it can promise, it can leave.

@Pin
class Rare(Tag):

    @Record
    def rarity(tag):                        # the receiver is the Tag
        return "rare"

    @Action
    def Describe(tag):
        return f"{tag.__name__} is {tag.rarity}"


Rare(Wizard)                                # pin the Tag

assert Wizard in Rare                       # membership, from the Pin's side
assert list(Rare) == [Wizard]               # a Field of Tags
assert Wizard.rarity == "rare"              # one value, held on the Tag
assert Wizard.Describe() == "Wizard is rare"
assert not hasattr(ari, "rarity")           # never on the Tag's Agents

for tag in Rare:                            # walk the rare Tags
    print(tag.Describe())

Notice the first parameter is tag, because the receiver is a Tag. What a Pin gives lands on the Tag the way a Report or an Operation does: one value shared by the whole Field, inherited by the Tag's Shapes (War_Caster.rarity is "rare" too), and never on the Tag's Agents.

A Pin can also patch what a Tag shares. Say a Tag has an Operation that controls an engine, and the engine changed. A Pin Action of the same name replaces it, with the old one as its Underlay, and every Agent that goes through the Tag gets the new control at once:

class Engine(Tag):

    @Operation
    def Control(tag, level):
        return f"{tag.__name__} v1 at {level}"

    def Drive(agent):
        return Engine.Control(agent.level)      # through the Tag, at call time


kit = Character("Kit", level=2)
Engine(kit)
assert kit.Drive() == "Engine v1 at 2"


@Pin
class Firmware_2(Tag):

    @Action
    @Underlay
    def Control(tag, underlay, level):
        return underlay(level).replace("v1", "v2")

    @Public                                 # reach every Agent of the Tag, too
    @Record
    def firmware(tag):
        return "v2"

    @Rip
    def Unpatch(tag, original):             # the originals, handed to you
        tag.Control = original.Control


Firmware_2(Engine)

assert kit.Drive() == "Engine v2 at 2"      # actualized, no per-Agent work
assert kit.firmware == "v2"                 # published onto the Field

del Firmware_2[Engine]                      # un-patch: one deliberate line
assert kit.Drive() == "Engine v1 at 2"

Watch out. A Pin may replace what a Tag shares (its Operations and Reports), never what its Agents do (its Actions, Records and conditions); TopKit refuses that at the gate, because on a class the two live in one dictionary. if Wizard: still asks whether anyone is a sound Wizard, not whether Wizard's own promises hold; ask those from the Pin's side, Wizard in ~Rare. A Pin applies only to Tags, and an ordinary Tag only to objects, so a Field is never a mix of the two. A Flag Pin is a keyword on the Tag: "Deprecated" in Wizard. Prefer it to Wizard.deprecated = True for the same reason you prefer an Undead Tag to asleep = True: a value can be flipped back, membership is a state of the architecture, with a Field to walk and a history that stays.


Reading an Agent

Question Spelling
Is it a Wizard now? agent in Wizard
Was it ever? isinstance(agent, Wizard)
Does it carry the keyword? "Undead" in agent, Keyword(agent, "Undead"), a Flag's words too
Which Tags, in order? Tags(agent), f"{agent:tags}"
The whole shape? Outline(agent), f"{agent:outline}"
Are its promises holding? if agent:, Contract.Status(agent), f"{agent:contract}"
What did it look like right after Wizard? agent.Wizard, Wizard[agent]

Reading a Tag

Question Spelling
Who is one, and sound? for a in Wizard, len(Wizard), if Wizard:
Who is one, and broken? for a in ~Wizard, if ~Wizard:
Everyone? Wizard[:]
Either, both, without? Wizard | Fighter, Wizard & Fighter, Wizard - Sworn (the Fields Guide)
What applies with it? Form(Wizard), f"{Wizard:form}"
Take it away del Wizard[agent]
Which Pins does it carry? Wizard in Rare, f"{Wizard:pins}"

Nothing TOP-level lives at Wizard.something. That namespace is yours: put your Reports and Operations there.


Do and don't

Do Don't
Make a Tag for what something is: species, class, role, clearance. Make a Tag for a passing state. Asleep is a Record, asleep = True.
Let independent Tags pile up on one Record with stored. Chain Tags by inheritance just to share a list.
Extend an Action with @Underlay. Call agent.Attack() from inside Attack (that recurses).
Gate with @Pre; promise with @Post; repair through ~Tag. Roll your own validation after the fact.
Mark word-like Tags @Flag and write rules as data. Match every Tag by name. Only Flags are words.
Say things about a Tag with a @Pin. Keep a side table of Tags outside TOP.
Clean up with @Rip, guarantee it with Scope. Rely on del agent for anything that matters.
Reset by Rip and apply again. Reapply an active Tag hoping it resets (it does nothing).
Expect a Rogue Agent to keep its own Actions and lose the Agency's published ones. Check membership by hand inside every published Operation.
Catch the named promise, repair what it names, retry. Catch every failure in one handler and guess.
Keep shared data in a Report, one copy. Copy shared data into every Agent's Record.
Ask agent in Wizard. Ask type(agent) is Character; the type is wrapped.

Where next

Clone this wiki locally