Skip to content

Commit

Permalink
Update documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
homothetyhk committed Dec 17, 2023
1 parent 3913de0 commit 9822d21
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 14 deletions.
55 changes: 55 additions & 0 deletions articles/item_strings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Item Strings

Item strings provide a simple grammar for defining items that act on the terms which represent the player's permanent abilities and progression. Item strings consist of primitive effects, which can be chained together and controlled via conditionals depending on current progression. The primitive effects are:
- Addition assignment `+=` (`Term += Int`) and increment (`Term++`, equivalent to `Term += 1`), which add the specified amount to the term.
- Max assignment `=/` (`Term =/ Int`), which raises the term to the specified value if it is below that value.

Effects can be combined via
- Conditional `=>` (`Bool => Effect`), which performs the effect if the bool test evaluates true. The bool test should be standard logic, wrapped in backticks.
- Chaining (`Effect >> Effect`), which performs the effects in sequence.
- Short-circuit chaining (`Effect >|> Effect`), which attempts to perform the first effect, and performs the second effect only if the first failed (e.g. if the first effect were a conditional and the bool test evaluated false).

There are also a few miscellaneous meta-symbols.
- Negation: a logic statement can be preceded by `!` to invert the test.
- Reference expressions: `*` followed by text resolves to the effect of the item with that name. This can be used to introduce effects which cannot otherwise be expressed in terms of item strings.
- Coalescing expressions: In expressions which expect a term, a `?` can be added after the term name. When the item string is converted to an item, if the term with that name was not defined, the expression directly containing the term is replaced with an empty (no-op) effect. This can be useful for compatibility between third party logic edits.

## Order of Operations

Item effects are always applied left to right. Parenthesization is important however for understanding how conditional and chaining expressions interact. Operators are bound to their arguments in the following order:

- Addition assignment, increment, max assignment, negation, reference, and coalescing expressions
- Conditionals
- Short-circuit chains
- Chains

Also, it should be noted that the chaining operators have higher right binding power (thus, are left associative). In particular, the order of operations ensures that
```
A++ >> `A>0` => B++
```
is well-formed without parentheses, and can be interpreted as "increment A, then if A is now positive, increment B".

We give a few examples of parenthesizations (equivalence denoted with `===`):
```
`A` => B++ >|> C++ >> D++ === (((`A` => B++) >|> C++) >> D++)
`A` => B++ >|> C++ >|> D++ === (((`A` => B++) >|> C++) >|> D++)
```
For a more complex example:
```
`A` => (`B` => (C++ >> D++) >|> `E` => F += 2 >|> G++)
```
is interpreted as
```
`A` => (((`B` => (C++ >> D++)) >|> (`E` => F += 2)) >|> G++)
```
or rather
```
`A` => (`B` => (C++ >> D++)
>|> `E` => F += 2
>|> G++)
```
where the indentation matches short-circuit operators with the corresponding conditionals (in other words, the `E` branch is reached if `A` succeeds and `B` fails, while the final branch `G++` is reached only if `A` succeeds and `B` and `E` both fail). Note that the parentheses around `(C++ >> D++)` are needed or else it would bind as ``(`B` => C++) >> D++``. The outermost parentheses are also needed, or else we would get
```
((`A` => (`B` => (C++ >> D++))) >|> (`E` => F += 2)) >|> G++
```
In this case, `G` is reached only if `E` fails and one of `A` or `B` fails.
8 changes: 6 additions & 2 deletions articles/safe_naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ The following symbols are reserved for use in infix logic:
- Less than `<`, equals `=`, and greater than `>` (used for comparison operations)
- Asterisk `*` (used to create reference tokens)
- Question mark `?` (used to create coallescing tokens)
- Some symbols are not currently used, but are reserved for future use: `!`, `~`, `` ` ``, `/`, `\`, `@`, `#`, `%`, `^`, `&`, `.`
- Forward slash `/` (used for projecting state providers to bool values)
- Additionally, `!` and `` ` `` are used in item strings
- Some symbols are not currently used, but are reserved for future use: `!`, `~`, `\`, `@`, `#`, `%`, `^`, `&`, `.`



The following symbols are safe to include in logic:
- Underscore `_`
Expand All @@ -16,6 +20,6 @@ The following symbols are safe to include in logic:
- Colon `:`
- Dollar sign `$`

Also, it should be noted that leading and trailing white space is stripped from tokens when an infix expression is tokenized. Internal white space is safe to use.
Also, it should be noted that leading and trailing white space is stripped from tokens when an infix expression is tokenized. Internal white space should be avoided.

Names which contain illegal symbols can be used to create tokens and logic in code, but will result in infix expressions which do not correctly deserialize. Thus, names of terms, macros, and logic variables should not use illegal symbols. Names of logic defs are encouraged to avoid illegal symbols, or else they will be unusable in reference tokens.
120 changes: 115 additions & 5 deletions articles/state.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
## Introduction

This article assumes some prior familiarity with the randomizer.
At a very high level, the randomizer consists of locations which have certain requirements and items which combine to fulfill those requirements. In RandomizerCore, these requirements are expressed through terms that appear in both logic and item definitions. Terms must represent permanent abilities or events; one of the core assumptions of the randomizer is that items only expand (and never restrict) the class of reachable locations, so items cannot have a temporary effect on terms.

When Hollow Knight's Randomizer 2 was first released, state logic would have been completely unnecessary. Back then, logic for every location was written painstakingly in terms of every manner to reach that part of the world from the start of the game in King's Pass. As Randomizer 2 developed into Randomizer 3, and start locations and room transitions became randomizable, more and more indirection had to be used in logic to make things still work. The result is that the logic became *relative*--instead of just listing combinations of items to unlock a location, it started to list combinations of items, along with points of the world that had to be "reachable". A location in King's Pass which previously had logic which always evaluated true, now had logic which evaluated true when King's Pass was "reachable", which might be always if the game started in King's Pass, but might mean something very different if the game started elsewhere.
The purpose of state logic is to allow representation of temporary effects (whether from items or pathing) in logic. Instead of partitioning locations into a binary reachable/unreachable, state logic allows a location to produce a list of alternative states that characterize its reachability. States may contain information regarding consumable resources, equipment which can't be swapped out in certain locations, and other types of decisions which might be made on a path-dependent basis. In what follows, some of the basic concepts of state logic are explored, including what exactly RandomizerCore allows in a state and how they can be created and modified using logic.

## Background

One problem with this system is that "reachable" may not have a single clear definition. In Hollow Knight, different paths to the same location may require equipping different charms, spending soul (MP), or taking damage. To get around this originally, we wrote the randomizer logic with very stringent restrictions on "nonterminal" logic: basically, we required transitions and waypoints to never make those kinds of decisions. Only "terminal" logic, meaning logic for actual locations, could require charms or soul or anything along those lines. In practice, these guidelines were overly limiting and had to be repeatedly broken.
When Hollow Knight's Randomizer 2 was first released, state logic would have been completely unnecessary. Back then, logic for every location was written painstakingly in terms of every manner to reach that part of the world from the start of the game in King's Pass. As Randomizer 2 developed into Randomizer 3, and start locations and room transitions became randomizable, more and more indirection had to be used in logic to make things still work. The result is that the logic became *relative*--instead of just listing combinations of items to unlock a location, it started to list combinations of items, along with points of the world that had to be "reachable". A location in King's Pass which previously had logic which always evaluated true, now had logic which evaluated true when King's Pass was "reachable", which might be always if the game started in King's Pass, but might mean something very different if the game started elsewhere.

The purpose of state logic is to give a way to represent conditional reachability, rather than a single true/false value. States can represent consumable resources, equipment which can't be swapped out in certain locations, and other types of decisions which might be made on a path-dependent basis. In what follows, some of the basic concepts of state logic are explored, including what exactly RandomizerCore allows in a state and how they can be created and modified using logic.
One problem with this system is that "reachable" may not have a single clear definition. In Hollow Knight, different paths to the same location may require equipping different charms, spending soul (MP), or taking damage. To get around this originally, we wrote the randomizer logic with very stringent restrictions on "nonterminal" logic: basically, we required transitions and waypoints to never make those kinds of decisions. Only "terminal" logic, meaning logic for actual locations, could require charms or soul or anything along those lines. In practice, these guidelines were overly limiting and had to be repeatedly broken. State logic was introduced to handle path-dependent information in an automatic, consistent, and decentralized fashion.

## State Mechanics

Expand All @@ -20,4 +22,112 @@ The StateUnion class represents a collection of states which have been reduced b

The StateLogicDef class is the base class for logic defs which support state calculations. DNFLogicDef is the RandomizerCore implementation of StateLogicDef.

Before evaluating state logic, the logic has to be put in disjunctive normal form. What this means is that all nested "or" operations are expanded out, so that the expression becomes an "or" of subexpressions which only contain "and" (a disjunction of conjunctions). For example, the disjunctive normal form of "A + (B | (C + D) | E)" is "(A + B) | (A + C + D) | (A + E)". The state modifiers in each conjunction then act sequentially left-to-right to perform state modifications. For more information, see the article on [implementing state logic](state_adv.md).
Before evaluating state logic, the logic has to be put in disjunctive normal form. What this means is that all nested "or" operations are expanded out, so that the expression becomes an "or" of subexpressions which only contain "and" (a disjunction of conjunctions). For example, the disjunctive normal form of "A + (B | (C + D) | E)" is "(A + B) | (A + C + D) | (A + E)". The state modifiers in each conjunction then act sequentially left-to-right to perform state modifications. For more information, see the article on [implementing state logic](state_adv.md).

## Basic State Model

We examine a toy model to illustrate how state logic works. We suppose we have a LogicManager with the following terms:
- `Item1` of type Int
- `Item2` of type Int
- `Room[left]` of type State (representing a doorway, transition, etc into the room)

and a StateManager with the following fields:
- `SBool1` of type Bool
- `SBool2` of type Bool
- `SInt1` of type Int

We also assume that we have defined a custom VariableResolver to provide the following StateModifiers:
- `$TryIncrement[INTNAME,CAP]` (increments the named StateInt, fails if the result exceeds the cap)
- `$TrySetBool[BOOLNAME]` (sets the named bool true, fails if the bool is already true)

(Note that the `$` in front of variables in logic is only a convention; the `$` does not have any special role here.)

Then, given the above, a fragment of state logic might look like:

`Room[left] + Item1 + Item2 + $TryIncrement[SInt1,2] + $TrySetBool[SBool1]`

This fragment breaks down into
- a state provider (the state-valued term `Room[left]`)
- a stateless requirement (the non-state terms `Item1` and `Item2`)
- a requirement on the provided state (given by the statemodifiers `$TryIncrement[SInt1,2]` and `$TrySetBool[SBool1]`)

The entire fragment can be interpreted as: "Require `Item1` and `Item2`, and also require that `Room[left]` can be reached with a state with `SInt1 < 2` and `SBool1 = false`."

For a more complicated example, we could add another state-valued term `Room[right]` and then consider the following logic:

`(Room[left] | Room[right]) + (Item1 | Item2) + ($TrySetBool[SBool1] | $TryIncrement[SInt1,1]) + ($TrySetBool[SBool1] | $TryIncrement[SInt1,2])`

The disjunctive normal form of this expression, given by distributing all of the operations, results in 16 clauses:
```
Room[left] + Item1 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
| Room[left] + Item1 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
| Room[left] + Item1 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
| Room[left] + Item1 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
| Room[left] + Item2 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
| Room[left] + Item2 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
| Room[left] + Item2 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
| Room[left] + Item2 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
| Room[right] + Item1 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
| Room[right] + Item1 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
| Room[right] + Item1 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
| Room[right] + Item1 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
| Room[right] + Item2 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
| Room[right] + Item2 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
| Room[right] + Item2 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
| Room[right] + Item2 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
```
In each clause, the state-valued term `Room[left]` or `Room[right]` provides a state; there are then requirements from nonstate terms which are independent of the provided state, and requirements from the state modifiers which act on the provided state sequentially. A few remarks:
- Some of the clauses above are impossible to satisfy. For example, anything with `$TrySetBool[SBool1] + $TrySetBool[SBool1]` can never be satisfied, since the first `$TrySetBool[SBool1]` sets `SBool1` to `true`, and the second `$TrySetBool[SBool1]` subsequently fails.
- The left-to-right application of state modifiers is essential: `$TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]` succeeds on a state which initially has `SInt1 = 0`, since neither increment causes `SInt1` to exceed either cap. On the other hand, `$TryIncrement[SInt1,2] + $TryIncrement[SInt1,1]` fails, since after 2 increments we have `SInt1 = 2`, which exceeds the cap for the second increment.

The location represented by the logic is reachable if any of the 16 clauses is satisfied. A state union representing the states that can reach the location could be created by accumulating the state-valued results of the satisfied clauses, and then reducing according to the state partial order. For example, suppose we evaluate with the following term values:
```
Item1: 1
Item2: 0
Room[left]: [{ SBool1: false, SBool2: true, SInt1: 0 }]
Room[right]: [{ SBool1: false, SBool2: false, SInt1: 1 }, { SBool1: true, SBool2: false, SInt1: 0 }]
```

Then clause-by-clause evaluation yields:
```
Room[left] + Item1 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
fails
Room[left] + Item1 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
produces [{ SBool1: true, SBool2: true, SInt1: 1 }]
Room[left] + Item1 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
produces [{ SBool1: true, SBool2: true, SInt1: 1 }]
Room[left] + Item1 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
produces [{ SBool1: false, SBool2: true, SInt1: 2 }]
Room[left] + Item2 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
fails
Room[left] + Item2 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
fails
Room[left] + Item2 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
fails
Room[left] + Item2 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
fails
Room[right] + Item1 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
fails
Room[right] + Item1 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
[{ SBool1: true, SBool2: false, SInt1: 2}]
Room[right] + Item1 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
fails
Room[right] + Item1 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
fails
Room[right] + Item2 + $TrySetBool[SBool1] + $TrySetBool[SBool1]
fails
Room[right] + Item2 + $TrySetBool[SBool1] + $TryIncrement[SInt1,2]
fails
Room[right] + Item2 + $TryIncrement[SInt1,1] + $TrySetBool[SBool1]
fails
Room[right] + Item2 + $TryIncrement[SInt1,1] + $TryIncrement[SInt1,2]
fails
```
The list of states produced is
```
[{ SBool1: true, SBool2: true, SInt1: 1 }, { SBool1: true, SBool2: true, SInt1: 1 }, { SBool1: false, SBool2: true, SInt1: 2 }, { SBool1: true, SBool2: false, SInt1: 2}]
```
Reducing along the partial order, the resulting StateUnion is
```
[{ SBool1: true, SBool2: true, SInt1: 1 }, { SBool1: false, SBool2: true, SInt1: 2 }, { SBool1: true, SBool2: false, SInt1: 2}]
```
Loading

0 comments on commit 9822d21

Please sign in to comment.