The Tollkeeper's Rulebook
The tollkeeper of bitwick prices every crossing out of a single rulebook nailed inside her hut. A traveller is described by a handful of attributes, the whole book is consulted at once, and stronger rules override weaker ones.
Arguments
rules— a list of strings: the rulebook, in order.traveller— a mapping from attribute name (string) to attribute value (string).
Rule format. Every rule is exactly "<conditions> => <action>". There is no whitespace beyond the single spaces shown.
The conditions are either the single character * (no conditions — the rule matches every traveller) or one or more terms key=value joined by &. A term matches when the traveller has that attribute and either value is * or the traveller's value for that attribute equals value. A rule matches when all of its terms match. No key appears twice within one rule.
The action is one of deny, allow, base N, add N, cap N, where N is an integer and may be negative.
Strength. Compare any two rules by, in this order: the number of terms (more wins), then the number of terms whose value is not * (more wins), then position in rules (later wins). This ranks every pair of rules.
Result. Considering only the rules that match the traveller:
1. Among the matching deny and allow rules, the strongest one wins. If it is a deny, return -1. If no deny or allow rule matches, the traveller passes. 2. The strongest matching base rule fixes the base fee. If no base rule matches, the base fee is 0. 3. Take the sum of the amounts of every matching add rule that is strictly stronger than the winning base rule — or, if no base rule matched, of every matching add rule. Add that sum to the base fee. 4. If any cap rule matches, the strongest one's N is an upper limit: the fee becomes the smaller of the fee and N. 5. Finally, a fee below 0 becomes 0.
Return the fee as an integer.
examples
- case 1
rules of ["* => base 10", "guild=smith => add -4", "cart=* => add 2", "guild=smith&cart=ox => base 3"], traveller of {guild: "smith", cart: "hand"}8 — the two-term rule does not match, so the base is 10, and both one-term adds are stronger than the zero-term base rule: 10 - 4 + 2 - case 2
the same rules, traveller of {guild: "smith", cart: "ox"}3 — the two-term base rule is the strongest base, and both adds are weaker than it, so they are discarded - case 3
rules of ["guild=smith => allow", "town=* => deny"], traveller of {guild: "smith", town: "north"}0 — both have one term, but the allow's term is exact and the deny's is a wildcard, so the traveller passes; no base or add rule matches, so the fee is 0 - case 4
rules of ["* => base 10", "guild=smith => add -30", "* => cap 8"], traveller of {guild: "smith"}0 — 10 - 30 = -20, the cap of 8 does not lower it, and the floor raises it to 0
- reward
- 540 XP
- signature
tollFee
TypeScript, run as ES2022 — types are stripped, not checked. If a JS engine would run it, so will we.
- hand cart at the smith's rate
- a two-term base silences the discounts
- exact term outranks wildcard before position does
- a narrow allow overrules the broad deny
- turned back at the bridge
- wildcard deny loses to exact allow
- a narrower deny takes the allow back
- empty rulebook
- traveller with no attributes at all
- cap does not bite, floor does
- the cap bites
- strongest cap wins and adds stack above the base