@patterkit/runtime 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/dist/index.cjs +67 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +67 -2
- package/dist/index.js.map +1 -1
- package/dist/patterplay.min.js +2 -2
- package/dist/patterplay.min.js.map +1 -1
- package/package.json +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [34429f0]
|
|
8
|
+
- Updated dependencies [34429f0]
|
|
9
|
+
- Updated dependencies [c61c146]
|
|
10
|
+
- @patterkit/model@0.2.0
|
|
11
|
+
- @patterkit/dialect@0.1.3
|
|
12
|
+
|
|
3
13
|
All notable changes to `@patterkit/runtime` (Patterplay JS) are documented here. The
|
|
4
14
|
Patterplay runtimes - JS, Unity, Unreal, and Godot - are versioned in lockstep: the same
|
|
5
15
|
version number always means the same runtime behaviour. This package is versioned by
|
|
@@ -7,9 +17,20 @@ version number always means the same runtime behaviour. This package is versione
|
|
|
7
17
|
|
|
8
18
|
## [Unreleased]
|
|
9
19
|
|
|
10
|
-
## [0.
|
|
20
|
+
## [0.2.0] - 2026-07-07
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Best match** selection (a new `sequence` order, `specificity`): among the eligible children,
|
|
25
|
+
play the one whose condition most specifically fits the current state; equally-specific ties break
|
|
26
|
+
by the seeded shuffle, and a condition-less child is the filler that wins only when nothing more
|
|
27
|
+
specific applies. Composes with the exhaust axis (re-pickable, or graceful degradation to the
|
|
28
|
+
filler). Locked by the conformance corpus, so all four runtimes agree.
|
|
29
|
+
|
|
30
|
+
## [0.1.0] - 2026-07-04
|
|
11
31
|
|
|
12
32
|
### Added
|
|
33
|
+
|
|
13
34
|
- The Patter runtime in JS/TS: `Engine` + `Flow` over a compiled `.patterc` bundle - scenes,
|
|
14
35
|
blocks, run/choice/branch/sequence selectors, sticky/fallback options, call-return jumps,
|
|
15
36
|
conditions + effects, visit counts, `{@ref}` interpolation, game events, tags, gameData
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
31
31
|
|
|
32
32
|
// src/engine.ts
|
|
33
33
|
var import_expr = require("@wildwinter/expr");
|
|
34
|
+
var import_expr_specificity = require("@wildwinter/expr-specificity");
|
|
34
35
|
var import_scoperegistry = require("@wildwinter/scoperegistry");
|
|
35
36
|
var import_dialect = require("@patterkit/dialect");
|
|
36
37
|
var import_model = require("@patterkit/model");
|
|
@@ -933,7 +934,7 @@ var Flow = class {
|
|
|
933
934
|
case "sequence": {
|
|
934
935
|
const order = group.options?.order ?? "sequential";
|
|
935
936
|
const exhaust = group.options?.exhaust ?? "once";
|
|
936
|
-
return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
|
|
937
|
+
return order === "shuffle" ? this.pickShuffle(eligible, exhaust, st) : order === "specificity" ? this.pickSpecificity(eligible, exhaust, st) : this.pickSequential(eligible, exhaust, st);
|
|
937
938
|
}
|
|
938
939
|
case "run":
|
|
939
940
|
case "choice":
|
|
@@ -980,6 +981,65 @@ var Flow = class {
|
|
|
980
981
|
st.last = id;
|
|
981
982
|
return eligible.find((c) => c.id === id);
|
|
982
983
|
}
|
|
984
|
+
/**
|
|
985
|
+
* `sequence` with `order: "specificity"` - **Best match**: score every eligible child by how
|
|
986
|
+
* specifically its condition fits the CURRENT state (`matchedSpec`), keep the top-scoring tier,
|
|
987
|
+
* and break ties with the seeded shuffle (no immediate repeat). A child with no condition scores
|
|
988
|
+
* 0, so it is the filler that wins only when nothing more specific is eligible.
|
|
989
|
+
*
|
|
990
|
+
* `exhaust` composes as it does for the other orders: `repeat` re-scores the full eligible set
|
|
991
|
+
* every draw (re-pickable - the character keeps preferring the on-topic line); `once` uses each
|
|
992
|
+
* pick up (a bag of remaining ids), so as specific lines are consumed the group slides down to
|
|
993
|
+
* less-specific ones and finally the filler, then yields null; `stick` degrades like `once` but
|
|
994
|
+
* holds the final pick forever instead of drying up.
|
|
995
|
+
*/
|
|
996
|
+
pickSpecificity(eligible, exhaust, st) {
|
|
997
|
+
let pool = eligible;
|
|
998
|
+
if (exhaust !== "repeat") {
|
|
999
|
+
if (st.bag === void 0) st.bag = eligible.map((c) => c.id);
|
|
1000
|
+
const remaining = new Set(st.bag);
|
|
1001
|
+
pool = eligible.filter((c) => remaining.has(c.id));
|
|
1002
|
+
if (pool.length === 0) {
|
|
1003
|
+
return exhaust === "stick" && st.last !== void 0 ? eligible.find((c) => c.id === st.last) ?? null : null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
let best = -1;
|
|
1007
|
+
const scored = pool.map((c) => {
|
|
1008
|
+
const s = this.specScore(c);
|
|
1009
|
+
if (s > best) best = s;
|
|
1010
|
+
return { c, s };
|
|
1011
|
+
});
|
|
1012
|
+
const tier = scored.filter((x) => x.s === best).map((x) => x.c);
|
|
1013
|
+
let pick;
|
|
1014
|
+
if (tier.length === 1) {
|
|
1015
|
+
pick = tier[0];
|
|
1016
|
+
} else {
|
|
1017
|
+
const p = st.last !== void 0 ? tier.findIndex((c) => c.id === st.last) : -1;
|
|
1018
|
+
let i = Math.floor(this.rng() * (p >= 0 ? tier.length - 1 : tier.length));
|
|
1019
|
+
if (p >= 0 && i >= p) i++;
|
|
1020
|
+
pick = tier[i];
|
|
1021
|
+
}
|
|
1022
|
+
if (exhaust !== "repeat") st.bag = st.bag.filter((id) => id !== pick.id);
|
|
1023
|
+
st.last = pick.id;
|
|
1024
|
+
return pick;
|
|
1025
|
+
}
|
|
1026
|
+
/** A child's Best-match score against the current state: 0 when it has no condition (the filler
|
|
1027
|
+
* tier), else the specificity of its (already-passing) condition. */
|
|
1028
|
+
specScore(node) {
|
|
1029
|
+
return node.condition ? this.matchedSpec(this.conditionAst(node.condition), true) : 0;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The **matched-specificity** metric (parity contract): how many atomic constraints are actively
|
|
1033
|
+
* holding this condition TRUE against the live state. Evaluation-aware, not a static clause count -
|
|
1034
|
+
* it walks the tree with a De-Morgan polarity flag so `or` and `not` score the branch that is
|
|
1035
|
+
* actually carrying the truth. `want` = "does this subtree need to be true for the whole condition
|
|
1036
|
+
* to hold?" (true at the root). Only `and`/`or`/`not`/`check_flags` are structural; every other
|
|
1037
|
+
* node (comparisons, scoped vars, literals, other calls) is an atom, evaluated whole.
|
|
1038
|
+
*/
|
|
1039
|
+
matchedSpec(node, want) {
|
|
1040
|
+
const evalTruthy = (n) => truthy((0, import_expr.evaluate)(n, this.evalCtx, import_dialect.patterDialect));
|
|
1041
|
+
return (0, import_expr_specificity.matchedSpecificity)(node, evalTruthy, { want });
|
|
1042
|
+
}
|
|
983
1043
|
/** A selector's cursor state - shared across flows (`group.shared`) or this flow's own. */
|
|
984
1044
|
selectorState(group) {
|
|
985
1045
|
const map = group.shared ? this.host.sharedSelectors : this.selectors;
|
|
@@ -1001,12 +1061,17 @@ var Flow = class {
|
|
|
1001
1061
|
return truthy(this.evalExpr(node.condition));
|
|
1002
1062
|
}
|
|
1003
1063
|
evalExpr(expr) {
|
|
1064
|
+
return (0, import_expr.evaluate)(this.conditionAst(expr), this.evalCtx, import_dialect.patterDialect);
|
|
1065
|
+
}
|
|
1066
|
+
/** The deserialised (in-memory) AST for an expression, cached per Expression. Shared by the
|
|
1067
|
+
* evaluator and the Best-match specificity walker so both work off one parse. */
|
|
1068
|
+
conditionAst(expr) {
|
|
1004
1069
|
let ast = astCache.get(expr);
|
|
1005
1070
|
if (!ast) {
|
|
1006
1071
|
ast = (0, import_expr.deserialiseAst)(expr.ast);
|
|
1007
1072
|
astCache.set(expr, ast);
|
|
1008
1073
|
}
|
|
1009
|
-
return
|
|
1074
|
+
return ast;
|
|
1010
1075
|
}
|
|
1011
1076
|
/** Record an entry of a node (entered-only; spec §7): bumps the flow + world counts. */
|
|
1012
1077
|
enter(id) {
|