@storylet-studio/runtime 0.4.1 → 0.5.0
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/README.md +2 -1
- package/dist/index.cjs +531 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +215 -23
- package/dist/index.d.ts +215 -23
- package/dist/index.js +531 -80
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -438,7 +438,48 @@ function effectiveGameId(entity) {
|
|
|
438
438
|
const fromTitle = entity.title ? gameIdify(entity.title) : "";
|
|
439
439
|
return fromTitle || entity.id;
|
|
440
440
|
}
|
|
441
|
+
function valueAddresses(bundle) {
|
|
442
|
+
const tags = [];
|
|
443
|
+
for (const box of bundle.boxes) {
|
|
444
|
+
const boxGameId = effectiveGameId(box);
|
|
445
|
+
for (const group of box.tagGroups) {
|
|
446
|
+
for (const tag of group.tags) {
|
|
447
|
+
const gameId = effectiveGameId(tag);
|
|
448
|
+
tags.push({ id: tag.id, gameId, qualified: `${boxGameId}/${gameId}` });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const forms = /* @__PURE__ */ new Map();
|
|
453
|
+
for (const tag of tags) {
|
|
454
|
+
const list = forms.get(tag.gameId) ?? [];
|
|
455
|
+
if (!list.includes(tag.qualified)) list.push(tag.qualified);
|
|
456
|
+
forms.set(tag.gameId, list);
|
|
457
|
+
}
|
|
458
|
+
const print = /* @__PURE__ */ new Map();
|
|
459
|
+
const accept = /* @__PURE__ */ new Map();
|
|
460
|
+
const repeated = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const tag of tags) {
|
|
462
|
+
const candidates = forms.get(tag.gameId) ?? [tag.qualified];
|
|
463
|
+
const ambiguous = candidates.length > 1;
|
|
464
|
+
print.set(tag.id, ambiguous ? tag.qualified : tag.gameId);
|
|
465
|
+
if (!accept.has(tag.qualified)) accept.set(tag.qualified, tag.id);
|
|
466
|
+
if (!ambiguous && !accept.has(tag.gameId)) accept.set(tag.gameId, tag.id);
|
|
467
|
+
if (ambiguous) repeated.set(tag.gameId, candidates);
|
|
468
|
+
}
|
|
469
|
+
return { print, accept, repeated };
|
|
470
|
+
}
|
|
471
|
+
function ambiguousValueAddressMessage(segment, name, candidates) {
|
|
472
|
+
const forms = candidates.map((q) => `"value.${q}.${name}"`);
|
|
473
|
+
const list = forms.length <= 1 ? forms[0] ?? "" : `${forms.slice(0, -1).join(", ")} or ${forms[forms.length - 1]}`;
|
|
474
|
+
return `"value.${segment}.${name}" names a tag in ${candidates.length} boxes; write ${list}`;
|
|
475
|
+
}
|
|
441
476
|
var PLACE_GROUP = "place";
|
|
477
|
+
var HOLE_REF = /^@(hand|world|story)\.([a-z][a-z0-9_-]*)$/;
|
|
478
|
+
var isHoleRef = (value) => value.startsWith("@");
|
|
479
|
+
var parseHoleRef = (value) => {
|
|
480
|
+
const m = HOLE_REF.exec(value);
|
|
481
|
+
return m === null ? void 0 : { scope: m[1], name: m[2] };
|
|
482
|
+
};
|
|
442
483
|
|
|
443
484
|
// ../../../expr/packages/scoperegistry/src/index.ts
|
|
444
485
|
var PropertyBag = class _PropertyBag {
|
|
@@ -473,10 +514,14 @@ var PropertyBag = class _PropertyBag {
|
|
|
473
514
|
}
|
|
474
515
|
/** Write a property. Engine writes (the default) notify subscribers;
|
|
475
516
|
* pass `silent: true` for a host write, which reaches only the audit
|
|
476
|
-
* hook. Throws on a read-only property
|
|
517
|
+
* hook. Throws on a read-only property unless the caller says it is the
|
|
518
|
+
* HOST (`host: true`), for whom `writable: false` was never a rule - it is
|
|
519
|
+
* the story's promise, not the game's. `silent` and `host` are separate on
|
|
520
|
+
* purpose: one is about who hears the write, the other about who may make
|
|
521
|
+
* it. Returns the change. */
|
|
477
522
|
set(name, value, opts) {
|
|
478
523
|
const n = this.norm(name);
|
|
479
|
-
if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
524
|
+
if (!opts?.host && this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
480
525
|
const change = {
|
|
481
526
|
name: n,
|
|
482
527
|
prev: this.values[n],
|
|
@@ -591,27 +636,62 @@ var SCOPE_DEFAULT_SHARED = { story: true, box: false, deck: false, hand: false,
|
|
|
591
636
|
var isShared = (scope, d) => d.shared ?? SCOPE_DEFAULT_SHARED[scope];
|
|
592
637
|
var sharedHalf = (scope, decls) => decls.filter((d) => isShared(scope, d));
|
|
593
638
|
var flowHalf = (scope, decls) => decls.filter((d) => !isShared(scope, d));
|
|
639
|
+
var OWNED_SCOPES = ["box", "deck", "hand", "value"];
|
|
640
|
+
var emptyOwnerIndexes = () => ({
|
|
641
|
+
box: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() },
|
|
642
|
+
deck: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() },
|
|
643
|
+
hand: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() },
|
|
644
|
+
value: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() }
|
|
645
|
+
});
|
|
646
|
+
var indexOwner = (index, entity) => {
|
|
647
|
+
const gameId = effectiveGameId(entity);
|
|
648
|
+
index.gameId.set(entity.id, gameId);
|
|
649
|
+
if (!index.id.has(gameId)) index.id.set(gameId, entity.id);
|
|
650
|
+
};
|
|
651
|
+
var indexValueOwners = (index, bundle) => {
|
|
652
|
+
const addresses = valueAddresses(bundle);
|
|
653
|
+
for (const [id, segment] of addresses.print) index.gameId.set(id, segment);
|
|
654
|
+
for (const [segment, id] of addresses.accept) index.id.set(segment, id);
|
|
655
|
+
for (const [gameId, candidates] of addresses.repeated) index.repeated.set(gameId, candidates);
|
|
656
|
+
};
|
|
594
657
|
var handDeclsOf = (internals, hand) => {
|
|
595
658
|
if (hand.template !== void 0) {
|
|
596
659
|
return internals.templatesById.get(hand.template)?.properties ?? internals.bundle.boxes.flatMap((b) => b.handTemplates).find((t) => t.id === hand.template)?.properties ?? [];
|
|
597
660
|
}
|
|
598
661
|
return hand.properties ?? [];
|
|
599
662
|
};
|
|
663
|
+
var addressOf = (internals, kind, id) => `${kind}.${internals.owners[kind].gameId.get(id) ?? id}`;
|
|
664
|
+
var resolveOwner = (internals, kind, segment) => {
|
|
665
|
+
const candidates = internals.owners[kind].repeated.get(segment);
|
|
666
|
+
if (candidates !== void 0) return { ambiguous: candidates };
|
|
667
|
+
const byGameId = internals.owners[kind].id.get(segment);
|
|
668
|
+
if (byGameId !== void 0) return { id: byGameId, legacy: false };
|
|
669
|
+
if (internals.owners[kind].gameId.has(segment)) return { id: segment, legacy: true };
|
|
670
|
+
return void 0;
|
|
671
|
+
};
|
|
672
|
+
var ownerOrThrow = (internals, kind, segment, name) => {
|
|
673
|
+
const owner = resolveOwner(internals, kind, segment);
|
|
674
|
+
if (owner === void 0) throw new Error(`no ${kind} store "${segment}"`);
|
|
675
|
+
if ("ambiguous" in owner) throw new Error(ambiguousValueAddressMessage(segment, name, owner.ambiguous));
|
|
676
|
+
return owner;
|
|
677
|
+
};
|
|
678
|
+
var legacyAddressMessage = (internals, kind, segment, name) => `"${kind}.${segment}.${name}" names the ${kind} by its internal id; write "${addressOf(internals, kind, segment)}.${name}". The internal-id form is refused after the next release.`;
|
|
600
679
|
var buildPartition = (internals, half) => {
|
|
601
680
|
const b = internals.bundle;
|
|
681
|
+
const at = (kind, id) => `${addressOf(internals, kind, id)}.`;
|
|
602
682
|
return {
|
|
603
683
|
story: bagFromDecls(half("story", b.story.properties), "story."),
|
|
604
|
-
box: new Map(b.boxes.map((box) => [box.id, bagFromDecls(half("box", box.properties),
|
|
684
|
+
box: new Map(b.boxes.map((box) => [box.id, bagFromDecls(half("box", box.properties), at("box", box.id))])),
|
|
605
685
|
deck: new Map(b.boxes.flatMap((box) => box.decks.map(
|
|
606
|
-
(deck) => [deck.id, bagFromDecls(half("deck", deck.properties),
|
|
686
|
+
(deck) => [deck.id, bagFromDecls(half("deck", deck.properties), at("deck", deck.id))]
|
|
607
687
|
))),
|
|
608
688
|
// A template instance inherits the template's property declarations;
|
|
609
689
|
// a standalone hand declares its own (schema 2.6).
|
|
610
690
|
hand: new Map(b.boxes.flatMap((box) => box.hands.map(
|
|
611
|
-
(hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)),
|
|
691
|
+
(hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)), at("hand", hand.id))]
|
|
612
692
|
))),
|
|
613
693
|
value: new Map(b.boxes.flatMap((box) => box.tagGroups.flatMap((group) => group.tags.map(
|
|
614
|
-
(tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []),
|
|
694
|
+
(tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []), at("value", tag.id))]
|
|
615
695
|
))))
|
|
616
696
|
};
|
|
617
697
|
};
|
|
@@ -630,6 +710,105 @@ var loadPartition = (p, values) => {
|
|
|
630
710
|
}
|
|
631
711
|
}
|
|
632
712
|
};
|
|
713
|
+
var emptyDraft = () => ({
|
|
714
|
+
evicted: [],
|
|
715
|
+
droppedCooldowns: [],
|
|
716
|
+
droppedSpent: [],
|
|
717
|
+
droppedProperties: [],
|
|
718
|
+
defaultedProperties: [],
|
|
719
|
+
retypedProperties: []
|
|
720
|
+
});
|
|
721
|
+
var SORT_SEP = "";
|
|
722
|
+
var byKey = (items, key) => [...items].map((item) => ({ item, k: key(item) })).sort((a, b) => a.k < b.k ? -1 : a.k > b.k ? 1 : 0).map((e) => e.item);
|
|
723
|
+
var propKey = (p) => `${p.flow ?? ""}${SORT_SEP}${p.path}`;
|
|
724
|
+
function valueFits(decl, value) {
|
|
725
|
+
switch (decl.type) {
|
|
726
|
+
case "boolean":
|
|
727
|
+
return typeof value === "boolean";
|
|
728
|
+
case "number":
|
|
729
|
+
return typeof value === "number";
|
|
730
|
+
case "string":
|
|
731
|
+
return typeof value === "string";
|
|
732
|
+
case "enum":
|
|
733
|
+
return typeof value === "string" && (decl.values === void 0 || decl.values.includes(value));
|
|
734
|
+
case "quality":
|
|
735
|
+
return typeof value === "string" && (decl.stages === void 0 || decl.stages.includes(value));
|
|
736
|
+
case "flags":
|
|
737
|
+
return Array.isArray(value) && (decl.values === void 0 || value.every((f) => decl.values.includes(f)));
|
|
738
|
+
default:
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function walkScope(decls, saved, path, flow, draft) {
|
|
743
|
+
const at = (name) => ({ ...flow !== void 0 ? { flow } : {}, path: path(name) });
|
|
744
|
+
const byName = new Map((decls ?? []).map((d) => [d.name, d]));
|
|
745
|
+
const values = saved ?? {};
|
|
746
|
+
const clean = {};
|
|
747
|
+
for (const [name, value] of Object.entries(values)) {
|
|
748
|
+
const decl = byName.get(name);
|
|
749
|
+
if (decl === void 0) {
|
|
750
|
+
draft.droppedProperties.push(at(name));
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (!valueFits(decl, value)) {
|
|
754
|
+
draft.retypedProperties.push(at(name));
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
clean[name] = value;
|
|
758
|
+
}
|
|
759
|
+
for (const decl of decls ?? []) {
|
|
760
|
+
if (!(decl.name in values)) draft.defaultedProperties.push(at(decl.name));
|
|
761
|
+
}
|
|
762
|
+
return clean;
|
|
763
|
+
}
|
|
764
|
+
function walkPartition(internals, decls, values, flow, draft) {
|
|
765
|
+
const out = {
|
|
766
|
+
story: walkScope(decls.story, values?.story, (n) => `story.${n}`, flow, draft),
|
|
767
|
+
box: {},
|
|
768
|
+
deck: {},
|
|
769
|
+
hand: {},
|
|
770
|
+
value: {}
|
|
771
|
+
};
|
|
772
|
+
for (const kind of ["box", "deck", "hand", "value"]) {
|
|
773
|
+
const savedKind = values?.[kind] ?? {};
|
|
774
|
+
const ids = [.../* @__PURE__ */ new Set([...decls[kind].keys(), ...Object.keys(savedKind)])].sort();
|
|
775
|
+
for (const id of ids) {
|
|
776
|
+
const owner = addressOf(internals, kind, id);
|
|
777
|
+
out[kind][id] = walkScope(
|
|
778
|
+
decls[kind].get(id),
|
|
779
|
+
savedKind[id],
|
|
780
|
+
(n) => `${owner}.${n}`,
|
|
781
|
+
flow,
|
|
782
|
+
draft
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
788
|
+
function finishReport(bundle, saved, flows, draft) {
|
|
789
|
+
const drift = saved.version !== bundle.version || saved.hash !== bundle.hash;
|
|
790
|
+
const evicted = byKey(draft.evicted, (e) => [e.flow, e.hand, e.card, e.reason].join(SORT_SEP));
|
|
791
|
+
const droppedCooldowns = byKey(draft.droppedCooldowns, (c) => `${c.flow}${SORT_SEP}${c.card}`);
|
|
792
|
+
const droppedSpent = [...draft.droppedSpent].sort();
|
|
793
|
+
const droppedProperties = byKey(draft.droppedProperties, propKey);
|
|
794
|
+
const defaultedProperties = byKey(draft.defaultedProperties, propKey);
|
|
795
|
+
const retypedProperties = byKey(draft.retypedProperties, propKey);
|
|
796
|
+
return {
|
|
797
|
+
// `flows` is what the load restores, not something it had to change, so
|
|
798
|
+
// it never makes a report inexact.
|
|
799
|
+
exact: !drift && evicted.length === 0 && droppedCooldowns.length === 0 && droppedSpent.length === 0 && droppedProperties.length === 0 && defaultedProperties.length === 0 && retypedProperties.length === 0,
|
|
800
|
+
project: bundle.project,
|
|
801
|
+
version: { saved: saved.version, bundle: bundle.version },
|
|
802
|
+
hash: { saved: saved.hash, bundle: bundle.hash },
|
|
803
|
+
flows,
|
|
804
|
+
evicted,
|
|
805
|
+
droppedCooldowns,
|
|
806
|
+
droppedSpent,
|
|
807
|
+
droppedProperties,
|
|
808
|
+
defaultedProperties,
|
|
809
|
+
retypedProperties
|
|
810
|
+
};
|
|
811
|
+
}
|
|
633
812
|
var Engine = class {
|
|
634
813
|
internals;
|
|
635
814
|
seed;
|
|
@@ -653,6 +832,7 @@ var Engine = class {
|
|
|
653
832
|
boxesById: /* @__PURE__ */ new Map(),
|
|
654
833
|
handsById: /* @__PURE__ */ new Map(),
|
|
655
834
|
handsByGameId: /* @__PURE__ */ new Map(),
|
|
835
|
+
owners: emptyOwnerIndexes(),
|
|
656
836
|
templatesById: /* @__PURE__ */ new Map(),
|
|
657
837
|
groupsById: /* @__PURE__ */ new Map(),
|
|
658
838
|
requiredGroups: /* @__PURE__ */ new Set(),
|
|
@@ -661,6 +841,7 @@ var Engine = class {
|
|
|
661
841
|
hasQualities: false,
|
|
662
842
|
hasShared: false,
|
|
663
843
|
flowDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
|
|
844
|
+
sharedDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
|
|
664
845
|
shared: void 0,
|
|
665
846
|
worldResolver: void 0,
|
|
666
847
|
worldReadOnly: /* @__PURE__ */ new Set(),
|
|
@@ -676,14 +857,17 @@ var Engine = class {
|
|
|
676
857
|
engineTracing: () => this.engineTraceHandlers.size > 0
|
|
677
858
|
};
|
|
678
859
|
this.internals = internals;
|
|
860
|
+
indexValueOwners(internals.owners.value, bundle);
|
|
679
861
|
for (const box of bundle.boxes) {
|
|
680
862
|
internals.boxesById.set(box.id, box);
|
|
681
863
|
internals.boxesByGameId.set(effectiveGameId(box), box);
|
|
864
|
+
indexOwner(internals.owners.box, box);
|
|
682
865
|
for (const group of box.tagGroups) {
|
|
683
866
|
internals.groupsById.set(group.id, { group, box });
|
|
684
867
|
if (group.required === true) internals.requiredGroups.add(group.id);
|
|
685
868
|
}
|
|
686
869
|
for (const deck of box.decks) {
|
|
870
|
+
indexOwner(internals.owners.deck, deck);
|
|
687
871
|
if (deck.shared === true) internals.hasShared = true;
|
|
688
872
|
for (const card of deck.cards) {
|
|
689
873
|
const entry = { card, deck, box };
|
|
@@ -698,22 +882,25 @@ var Engine = class {
|
|
|
698
882
|
for (const hand of box.hands) {
|
|
699
883
|
internals.handsById.set(hand.id, { hand, box });
|
|
700
884
|
internals.handsByGameId.set(effectiveGameId(hand), { hand, box });
|
|
885
|
+
indexOwner(internals.owners.hand, hand);
|
|
701
886
|
}
|
|
702
887
|
}
|
|
703
888
|
this.initLadders();
|
|
704
|
-
|
|
705
|
-
story:
|
|
706
|
-
box: new Map(bundle.boxes.map((box) => [box.id,
|
|
889
|
+
const declSet = (half) => ({
|
|
890
|
+
story: half("story", bundle.story.properties),
|
|
891
|
+
box: new Map(bundle.boxes.map((box) => [box.id, half("box", box.properties)])),
|
|
707
892
|
deck: new Map(bundle.boxes.flatMap((box) => box.decks.map(
|
|
708
|
-
(deck) => [deck.id,
|
|
893
|
+
(deck) => [deck.id, half("deck", deck.properties)]
|
|
709
894
|
))),
|
|
710
895
|
hand: new Map(bundle.boxes.flatMap((box) => box.hands.map(
|
|
711
|
-
(hand) => [hand.id,
|
|
896
|
+
(hand) => [hand.id, half("hand", handDeclsOf(internals, hand))]
|
|
712
897
|
))),
|
|
713
898
|
value: new Map(bundle.boxes.flatMap((box) => box.tagGroups.flatMap((group) => group.tags.map(
|
|
714
|
-
(tag) => [tag.id,
|
|
899
|
+
(tag) => [tag.id, half("value", tag.properties ?? [])]
|
|
715
900
|
))))
|
|
716
|
-
};
|
|
901
|
+
});
|
|
902
|
+
internals.flowDecls = declSet(flowHalf);
|
|
903
|
+
internals.sharedDecls = declSet(sharedHalf);
|
|
717
904
|
this.initShared(this.hostWorld);
|
|
718
905
|
}
|
|
719
906
|
/** Build the shared stores and the @world seam. `hostWorld` sticks for the
|
|
@@ -724,14 +911,24 @@ var Engine = class {
|
|
|
724
911
|
internals.worldReadOnly = new Set(internals.bundle.world.properties.filter((d) => d.writable === false).map((d) => d.name));
|
|
725
912
|
if (hostWorld !== void 0) {
|
|
726
913
|
internals.worldResolver = hostWorld;
|
|
914
|
+
const set = hostWorld.set;
|
|
915
|
+
internals.worldSet = set !== void 0 ? (n, v) => {
|
|
916
|
+
set(n, v);
|
|
917
|
+
} : void 0;
|
|
727
918
|
} else {
|
|
728
919
|
const bag = bagFromDecls(internals.bundle.world.properties, "world.");
|
|
729
920
|
internals.worldResolver = {
|
|
921
|
+
// The engine writes through worldSet below, not through this; the `set`
|
|
922
|
+
// is the resolver's SHAPE, so @world still reads as writable to anything
|
|
923
|
+
// inspecting the seam, and it is the story's door: no host flag on it.
|
|
730
924
|
get: (n) => bag.get(n),
|
|
731
925
|
set: (n, v) => {
|
|
732
926
|
bag.set(n, v);
|
|
733
927
|
}
|
|
734
928
|
};
|
|
929
|
+
internals.worldSet = (n, v, host2) => {
|
|
930
|
+
bag.set(n, v, host2 === true ? { host: true } : void 0);
|
|
931
|
+
};
|
|
735
932
|
}
|
|
736
933
|
}
|
|
737
934
|
/** Quality ladders by scope for the eval channel (design/quality.md):
|
|
@@ -765,6 +962,7 @@ var Engine = class {
|
|
|
765
962
|
* state; shared state is untouched. There is no default flow: "main" is
|
|
766
963
|
* a caller convention, not an engine rule. */
|
|
767
964
|
openFlow(id, opts = {}) {
|
|
965
|
+
const otherClaims = opts.restore !== void 0 ? this.sharedClaimsExcept(id) : void 0;
|
|
768
966
|
const existing = this.flowsById.get(id);
|
|
769
967
|
if (existing) {
|
|
770
968
|
const dealt = existing.heldCardIds().length;
|
|
@@ -773,6 +971,13 @@ var Engine = class {
|
|
|
773
971
|
}
|
|
774
972
|
const flow = new Flow(this, this.internals, id, opts.seed ?? this.seed);
|
|
775
973
|
this.flowsById.set(id, flow);
|
|
974
|
+
if (opts.restore !== void 0) {
|
|
975
|
+
const draft = emptyDraft();
|
|
976
|
+
const clean = this.planFlowRestore(id, structuredClone(opts.restore), otherClaims, draft);
|
|
977
|
+
flow.restore(clean);
|
|
978
|
+
const content = this.internals.bundle.content;
|
|
979
|
+
opts.onRestoreReport?.(finishReport(content, content, [id], draft));
|
|
980
|
+
}
|
|
776
981
|
return flow;
|
|
777
982
|
}
|
|
778
983
|
getFlow(id) {
|
|
@@ -846,10 +1051,21 @@ var Engine = class {
|
|
|
846
1051
|
}
|
|
847
1052
|
return counts;
|
|
848
1053
|
}
|
|
1054
|
+
/** The same ledger with one name left out: what the REST of the world
|
|
1055
|
+
* holds, which is the question a resume under that name has to ask. */
|
|
1056
|
+
sharedClaimsExcept(id) {
|
|
1057
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1058
|
+
for (const [flowId, flow] of this.flowsById) {
|
|
1059
|
+
if (flowId === id) continue;
|
|
1060
|
+
for (const cardId of flow.heldCardIds()) counts.set(cardId, (counts.get(cardId) ?? 0) + 1);
|
|
1061
|
+
}
|
|
1062
|
+
return counts;
|
|
1063
|
+
}
|
|
849
1064
|
// --- engine-level state access ----------------------------------------------
|
|
850
1065
|
/**
|
|
851
1066
|
* Read shared state by path: "world.x", "story.gold" (when shared),
|
|
852
|
-
* "box.
|
|
1067
|
+
* "box.village.heat" (when shared) - the owner segment is its GAMEID
|
|
1068
|
+
* (design/engine-server.md 4.4). A ref that resolves PER-FLOW throws,
|
|
853
1069
|
* naming the fix - silently answering with some flow's copy (or a junk
|
|
854
1070
|
* default) was the bug Patter's engine.getProperty guard exists to stop.
|
|
855
1071
|
*/
|
|
@@ -862,13 +1078,13 @@ var Engine = class {
|
|
|
862
1078
|
setProperty(path, value) {
|
|
863
1079
|
const found = this.resolveShared(path);
|
|
864
1080
|
if (found.kind === "world") {
|
|
865
|
-
if (!this.internals.
|
|
1081
|
+
if (!this.internals.worldSet) {
|
|
866
1082
|
throw new Error(`@world is read-only here: the host bound no write`);
|
|
867
1083
|
}
|
|
868
|
-
this.internals.
|
|
1084
|
+
this.internals.worldSet(found.name, value, true);
|
|
869
1085
|
return;
|
|
870
1086
|
}
|
|
871
|
-
found.bag.set(found.name, value, { silent: true, reason: "host setProperty" });
|
|
1087
|
+
found.bag.set(found.name, value, { silent: true, reason: "host setProperty", host: true });
|
|
872
1088
|
}
|
|
873
1089
|
resolveShared(path) {
|
|
874
1090
|
const parts = path.split(".");
|
|
@@ -884,15 +1100,25 @@ var Engine = class {
|
|
|
884
1100
|
}
|
|
885
1101
|
if (parts.length === 3 && (parts[0] === "box" || parts[0] === "deck" || parts[0] === "hand" || parts[0] === "value")) {
|
|
886
1102
|
const kind = parts[0];
|
|
887
|
-
const [,
|
|
1103
|
+
const [, segment, name] = parts;
|
|
1104
|
+
const owner = ownerOrThrow(this.internals, kind, segment, name);
|
|
1105
|
+
if (owner.legacy) this.diagnose(legacyAddressMessage(this.internals, kind, segment, name));
|
|
1106
|
+
const id = owner.id;
|
|
888
1107
|
const bag = this.internals.shared[kind].get(id);
|
|
889
1108
|
if (bag !== void 0 && bag.get(name) !== void 0) return { kind: "bag", bag, name };
|
|
890
1109
|
if (this.internals.flowDecls[kind].get(id)?.some((d) => d.name === name)) perFlow();
|
|
891
|
-
if (bag === void 0 && !this.internals.flowDecls[kind].has(id)) throw new Error(`no ${kind} store "${
|
|
1110
|
+
if (bag === void 0 && !this.internals.flowDecls[kind].has(id)) throw new Error(`no ${kind} store "${segment}"`);
|
|
892
1111
|
throw new Error(`no property at "${path}"`);
|
|
893
1112
|
}
|
|
894
1113
|
throw new Error(`bad property path "${path}"`);
|
|
895
1114
|
}
|
|
1115
|
+
/** The engine's own surface has no flow, so an engine-level diagnostic
|
|
1116
|
+
* carries the empty flow id - the same way a LoadReport's shared half
|
|
1117
|
+
* carries no flow. It reaches the run log and the engine tap; there is
|
|
1118
|
+
* nowhere else for it to go, and it fires only on a legacy address. */
|
|
1119
|
+
diagnose(message) {
|
|
1120
|
+
this.internals.emitEngine("", { type: "diagnostic", where: "property address", message });
|
|
1121
|
+
}
|
|
896
1122
|
/** The shared surface as examiner rows: @world (read through the
|
|
897
1123
|
* resolver) then the shared partitions. Per-flow rows live on each Flow. */
|
|
898
1124
|
listProperties() {
|
|
@@ -908,20 +1134,24 @@ var Engine = class {
|
|
|
908
1134
|
...d.values !== void 0 ? { values: d.values } : {},
|
|
909
1135
|
...d.stages !== void 0 ? { stages: d.stages } : {},
|
|
910
1136
|
// @world is FOREIGN - a host resolver backs it - so writability is whether that
|
|
911
|
-
// resolver can be written at all
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
|
|
1137
|
+
// resolver can be written at all AND what the declaration says, which is the
|
|
1138
|
+
// shared registry's own rule for a foreign scope (its foreignWritable). The
|
|
1139
|
+
// `as PropertyView` cast this replaced was hiding the field's absence: the row
|
|
1140
|
+
// type has always required it, and these rows shipped without one.
|
|
1141
|
+
//
|
|
1142
|
+
// A row is where `writable: false` is meant to SHOW (Reboot.md 10): it tells a
|
|
1143
|
+
// state panel this is the game's value, not the story's. It does not stop the
|
|
1144
|
+
// panel editing it - the host's setProperty passes `{ host: true }`.
|
|
1145
|
+
writable: this.internals.worldSet !== void 0 && !this.internals.worldReadOnly.has(d.name)
|
|
915
1146
|
});
|
|
916
1147
|
}
|
|
917
1148
|
const add = (_prefix, bag) => {
|
|
918
1149
|
for (const row of bag.rows()) out.push(row);
|
|
919
1150
|
};
|
|
920
1151
|
add("story", this.internals.shared.story);
|
|
921
|
-
for (const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
for (const [id, bag] of this.internals.shared.value) add(`value.${id}`, bag);
|
|
1152
|
+
for (const kind of OWNED_SCOPES) {
|
|
1153
|
+
for (const [id, bag] of this.internals.shared[kind]) add(addressOf(this.internals, kind, id), bag);
|
|
1154
|
+
}
|
|
925
1155
|
return out;
|
|
926
1156
|
}
|
|
927
1157
|
/** The SHARED kernel bags with their store path prefixes (the state
|
|
@@ -930,7 +1160,7 @@ var Engine = class {
|
|
|
930
1160
|
listBags() {
|
|
931
1161
|
const mounts = [{ prefix: "story", bag: this.internals.shared.story }];
|
|
932
1162
|
for (const kind of ["box", "deck", "hand", "value"]) {
|
|
933
|
-
for (const [id, bag] of this.internals.shared[kind]) mounts.push({ prefix:
|
|
1163
|
+
for (const [id, bag] of this.internals.shared[kind]) mounts.push({ prefix: addressOf(this.internals, kind, id), bag });
|
|
934
1164
|
}
|
|
935
1165
|
return mounts;
|
|
936
1166
|
}
|
|
@@ -951,20 +1181,137 @@ var Engine = class {
|
|
|
951
1181
|
flows: Object.fromEntries([...this.flowsById].map(([id, flow]) => [id, flow.snapshot()]))
|
|
952
1182
|
});
|
|
953
1183
|
}
|
|
1184
|
+
/** ONE flow's blob, to park a visit that is walking away: the same shape
|
|
1185
|
+
* the envelope carries per flow, and the same shape `openFlow`'s `restore`
|
|
1186
|
+
* option takes back (design/engine-server.md 4.1). Saving the whole
|
|
1187
|
+
* envelope to park one of four hundred players is wrong in cost and in
|
|
1188
|
+
* meaning. Throws for a name that is not open - a closed flow has nothing
|
|
1189
|
+
* left to save. */
|
|
1190
|
+
saveFlow(id) {
|
|
1191
|
+
const flow = this.flowsById.get(id);
|
|
1192
|
+
if (!flow) throw new Error(`unknown flow "${id}"`);
|
|
1193
|
+
return structuredClone(flow.snapshot());
|
|
1194
|
+
}
|
|
1195
|
+
/** What `loadGame(envelope)` would do that is not a plain restore, without
|
|
1196
|
+
* doing any of it (design/engine-server.md 4.9). Pure: nothing on this
|
|
1197
|
+
* engine moves. A project mismatch is refused here exactly as `loadGame`
|
|
1198
|
+
* refuses it - it is the one thing neither call will tolerate. */
|
|
1199
|
+
previewLoad(envelope) {
|
|
1200
|
+
this.assertSameProject(envelope);
|
|
1201
|
+
return this.planLoad(envelope).report;
|
|
1202
|
+
}
|
|
1203
|
+
/** What `openFlow(id, { restore: saved })` would do to a flow of that name,
|
|
1204
|
+
* without doing it: the same report shape, since a visit parked under one
|
|
1205
|
+
* build and resumed under the next raises the same questions. Pure. */
|
|
1206
|
+
previewFlowRestore(id, saved) {
|
|
1207
|
+
const draft = emptyDraft();
|
|
1208
|
+
this.planFlowRestore(id, saved, this.sharedClaimsExcept(id), draft);
|
|
1209
|
+
const content = this.internals.bundle.content;
|
|
1210
|
+
return finishReport(content, content, [id], draft);
|
|
1211
|
+
}
|
|
954
1212
|
/** Restore: shared state once, then every flow REBUILT from its blob.
|
|
955
1213
|
* Handles held from before the load are closed and inert (Patter's
|
|
956
|
-
* rule); take fresh ones from getFlow()/flows().
|
|
1214
|
+
* rule); take fresh ones from getFlow()/flows().
|
|
1215
|
+
*
|
|
1216
|
+
* Returns the report `previewLoad` would have given for this envelope: the
|
|
1217
|
+
* drift tolerance that makes a load forgiving is what hides its cost, so
|
|
1218
|
+
* the cost comes back with the load whether or not anybody looked first. */
|
|
957
1219
|
loadGame(envelope) {
|
|
1220
|
+
this.assertSameProject(envelope);
|
|
1221
|
+
const plan = this.planLoad(structuredClone(envelope));
|
|
1222
|
+
this.reset();
|
|
1223
|
+
loadPartition(this.internals.shared, plan.shared);
|
|
1224
|
+
for (const id of plan.spent) this.spent.add(id);
|
|
1225
|
+
for (const [id, clean] of plan.flows) this.openFlow(id).restore(clean);
|
|
1226
|
+
return plan.report;
|
|
1227
|
+
}
|
|
1228
|
+
assertSameProject(envelope) {
|
|
958
1229
|
if (envelope.content.project !== this.internals.bundle.content.project) {
|
|
959
1230
|
throw new Error(`save is for project "${envelope.content.project}", bundle is "${this.internals.bundle.content.project}"`);
|
|
960
1231
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1232
|
+
}
|
|
1233
|
+
/** The whole-envelope walk: the report, and the cleaned state the apply
|
|
1234
|
+
* half writes. Nothing here touches the engine, which is what lets
|
|
1235
|
+
* previewLoad and loadGame share it. */
|
|
1236
|
+
planLoad(envelope) {
|
|
1237
|
+
const draft = emptyDraft();
|
|
1238
|
+
const shared = walkPartition(
|
|
1239
|
+
this.internals,
|
|
1240
|
+
this.internals.sharedDecls,
|
|
1241
|
+
envelope.shared?.props,
|
|
1242
|
+
void 0,
|
|
1243
|
+
draft
|
|
1244
|
+
);
|
|
1245
|
+
const spent = [];
|
|
1246
|
+
for (const cardId of envelope.shared?.spent ?? []) {
|
|
1247
|
+
if (this.internals.cardsById.has(cardId)) spent.push(cardId);
|
|
1248
|
+
else draft.droppedSpent.push(cardId);
|
|
1249
|
+
}
|
|
1250
|
+
const flows = [];
|
|
1251
|
+
for (const [id, saved] of Object.entries(envelope.flows ?? {})) {
|
|
1252
|
+
flows.push([id, this.planFlowRestore(id, saved, void 0, draft)]);
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
report: finishReport(this.internals.bundle.content, envelope.content, flows.map(([id]) => id), draft),
|
|
1256
|
+
shared,
|
|
1257
|
+
spent,
|
|
1258
|
+
flows
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
/** One flow's walk. `otherClaims` is the rest of the world's shared ledger
|
|
1262
|
+
* and is present only for a SINGLE-flow restore into a live engine: a
|
|
1263
|
+
* whole-envelope load rebuilds every flow from one consistent moment, so
|
|
1264
|
+
* there is nobody else to compete with. */
|
|
1265
|
+
planFlowRestore(id, saved, otherClaims, draft) {
|
|
1266
|
+
const internals = this.internals;
|
|
1267
|
+
const props = walkPartition(internals, internals.flowDecls, saved.props, id, draft);
|
|
1268
|
+
const cooldowns = {};
|
|
1269
|
+
for (const [cardId, turn] of Object.entries(saved.cooldowns ?? {})) {
|
|
1270
|
+
if (internals.cardsById.has(cardId)) cooldowns[cardId] = turn;
|
|
1271
|
+
else draft.droppedCooldowns.push({ flow: id, card: cardId });
|
|
1272
|
+
}
|
|
1273
|
+
const cardName = (cardId) => {
|
|
1274
|
+
const entry = internals.cardsById.get(cardId);
|
|
1275
|
+
return entry ? effectiveGameId(entry.card) : cardId;
|
|
1276
|
+
};
|
|
1277
|
+
const board = {};
|
|
1278
|
+
const restored = /* @__PURE__ */ new Map();
|
|
1279
|
+
for (const [handId, ids] of Object.entries(saved.board ?? {})) {
|
|
1280
|
+
const known = internals.handsById.get(handId);
|
|
1281
|
+
if (known === void 0) {
|
|
1282
|
+
for (const cardId of ids) {
|
|
1283
|
+
draft.evicted.push({ flow: id, hand: handId, card: cardName(cardId), reason: "hand-vanished" });
|
|
1284
|
+
}
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
const hand = effectiveGameId(known.hand);
|
|
1288
|
+
const kept = [];
|
|
1289
|
+
for (const cardId of ids) {
|
|
1290
|
+
const entry = internals.cardsById.get(cardId);
|
|
1291
|
+
if (entry === void 0) {
|
|
1292
|
+
draft.evicted.push({ flow: id, hand, card: cardId, reason: "vanished" });
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (otherClaims !== void 0 && cardIsShared(entry.card, entry.deck.shared ?? false)) {
|
|
1296
|
+
const held = (otherClaims.get(cardId) ?? 0) + (restored.get(cardId) ?? 0);
|
|
1297
|
+
if (held >= sharedCap(entry.card)) {
|
|
1298
|
+
draft.evicted.push({ flow: id, hand, card: effectiveGameId(entry.card), reason: "claimed-elsewhere" });
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
restored.set(cardId, (restored.get(cardId) ?? 0) + 1);
|
|
1302
|
+
}
|
|
1303
|
+
kept.push(cardId);
|
|
1304
|
+
}
|
|
1305
|
+
board[handId] = kept;
|
|
967
1306
|
}
|
|
1307
|
+
return {
|
|
1308
|
+
props,
|
|
1309
|
+
turns: saved.turns ?? {},
|
|
1310
|
+
prng: saved.prng,
|
|
1311
|
+
cooldowns,
|
|
1312
|
+
board,
|
|
1313
|
+
playLog: saved.playLog ?? []
|
|
1314
|
+
};
|
|
968
1315
|
}
|
|
969
1316
|
};
|
|
970
1317
|
var Flow = class {
|
|
@@ -1240,6 +1587,10 @@ var Flow = class {
|
|
|
1240
1587
|
boundTags.set(groupId, tagId);
|
|
1241
1588
|
}
|
|
1242
1589
|
for (const [groupId, tagId] of Object.entries(hand.chosen ?? {})) {
|
|
1590
|
+
if (isHoleRef(tagId)) {
|
|
1591
|
+
this.fillHoleFromProperty(hand, groupId, tagId, boundTags, askNames);
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1243
1594
|
boundTags.set(groupId, tagId);
|
|
1244
1595
|
const found = this.internals.groupsById.get(groupId);
|
|
1245
1596
|
const tag = found?.group.tags.find((t) => t.id === tagId);
|
|
@@ -1248,6 +1599,10 @@ var Flow = class {
|
|
|
1248
1599
|
condition = template.condition;
|
|
1249
1600
|
} else {
|
|
1250
1601
|
for (const [groupId, tagId] of Object.entries(hand.rule?.bindings ?? {})) {
|
|
1602
|
+
if (isHoleRef(tagId)) {
|
|
1603
|
+
this.fillHoleFromProperty(hand, groupId, tagId, boundTags, askNames);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1251
1606
|
boundTags.set(groupId, tagId);
|
|
1252
1607
|
const found = this.internals.groupsById.get(groupId);
|
|
1253
1608
|
const tag = found?.group.tags.find((t) => t.id === tagId);
|
|
@@ -1283,6 +1638,58 @@ var Flow = class {
|
|
|
1283
1638
|
this.bindStateGroups(box, boundTags, askNames);
|
|
1284
1639
|
return { box, boundTags, askNames };
|
|
1285
1640
|
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Fill one hole from the property its value names: the hand that moves
|
|
1643
|
+
* (design/engine-server.md 4.6).
|
|
1644
|
+
*
|
|
1645
|
+
* The semantics are `bindStateGroups`' below, word for word, applied per
|
|
1646
|
+
* HOLE instead of per group: resolved at ask time, and a value naming no tag
|
|
1647
|
+
* leaves the hole UNBOUND (a wildcard) with a diagnostic rather than dealing
|
|
1648
|
+
* a silently empty hand. What is added is the `@hand` scope - the asking
|
|
1649
|
+
* hand's OWN declared state, read here from the flow's merged view (the
|
|
1650
|
+
* shared half under the flow's own, so a `shared: true` declaration moves
|
|
1651
|
+
* the hole for every flow and a per-flow one moves it for this flow alone).
|
|
1652
|
+
*
|
|
1653
|
+
* Read BEFORE tag composition, which is the whole reason it is safe: the
|
|
1654
|
+
* @hand bag a card sees is built from the bound tags, so resolving a hole
|
|
1655
|
+
* from it would be circular. A hand's own declarations are not, so they are.
|
|
1656
|
+
*/
|
|
1657
|
+
fillHoleFromProperty(hand, groupId, ref, boundTags, askNames) {
|
|
1658
|
+
const found = this.internals.groupsById.get(groupId);
|
|
1659
|
+
const groupName = found ? effectiveGameId(found.group) : groupId;
|
|
1660
|
+
const where = `hand ${effectiveGameId(hand)}, tag group ${groupName}`;
|
|
1661
|
+
const parsed = parseHoleRef(ref);
|
|
1662
|
+
if (!parsed) {
|
|
1663
|
+
this.emit({ type: "diagnostic", where, message: `"${ref}" is not a @hand, @world or @story property reference` });
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
if (!found) {
|
|
1667
|
+
this.emit({ type: "diagnostic", where, message: `"${ref}" fills a tag group that is not in this box` });
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
let value;
|
|
1671
|
+
if (parsed.scope === "hand") {
|
|
1672
|
+
value = this.valuesOf("hand", hand.id)[parsed.name];
|
|
1673
|
+
} else {
|
|
1674
|
+
try {
|
|
1675
|
+
value = this.getProperty(`${parsed.scope}.${parsed.name}`);
|
|
1676
|
+
} catch {
|
|
1677
|
+
value = void 0;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (value === void 0) {
|
|
1681
|
+
this.emit({ type: "diagnostic", where, message: `"${ref}" names a property that is not declared` });
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const wanted = typeof value === "string" ? value : String(value);
|
|
1685
|
+
const tag = found.group.tags.find((t) => effectiveGameId(t) === wanted);
|
|
1686
|
+
if (!tag) {
|
|
1687
|
+
this.emit({ type: "diagnostic", where, message: `${ref} is "${wanted}", which is not one of the tags of "${groupName}"` });
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
boundTags.set(groupId, tag.id);
|
|
1691
|
+
askNames[groupName] = effectiveGameId(tag);
|
|
1692
|
+
}
|
|
1286
1693
|
/**
|
|
1287
1694
|
* Bind every `boundBy` group in the box from the property it names.
|
|
1288
1695
|
*
|
|
@@ -1413,8 +1820,8 @@ var Flow = class {
|
|
|
1413
1820
|
runAsk(ask, claimed, trace) {
|
|
1414
1821
|
const { box } = ask;
|
|
1415
1822
|
const handEnv = this.buildHandEnv(ask);
|
|
1416
|
-
const verdict = (
|
|
1417
|
-
trace?.push({ id, verdict: v });
|
|
1823
|
+
const verdict = (card, v) => {
|
|
1824
|
+
trace?.push({ id: effectiveGameId(card), verdict: v });
|
|
1418
1825
|
};
|
|
1419
1826
|
if (!this.passes(ask.condition, this.evalCtx(box, void 0, handEnv), `hand ${ask.hand ? effectiveGameId(ask.hand) : ""} condition`)) {
|
|
1420
1827
|
return { ordered: [], handEnv };
|
|
@@ -1431,19 +1838,19 @@ var Flow = class {
|
|
|
1431
1838
|
for (const card of deck.cards) {
|
|
1432
1839
|
const shared = cardIsShared(card, deckShared);
|
|
1433
1840
|
if (!gateOk.get(deck.id)) {
|
|
1434
|
-
verdict(card
|
|
1841
|
+
verdict(card, "deck-gate");
|
|
1435
1842
|
continue;
|
|
1436
1843
|
}
|
|
1437
1844
|
if (shared && this.engine.isTaken(card.id)) {
|
|
1438
|
-
verdict(card
|
|
1845
|
+
verdict(card, "taken");
|
|
1439
1846
|
continue;
|
|
1440
1847
|
}
|
|
1441
1848
|
if ((this.cooldowns[card.id] ?? 0) > turn) {
|
|
1442
|
-
verdict(card
|
|
1849
|
+
verdict(card, "cooldown");
|
|
1443
1850
|
continue;
|
|
1444
1851
|
}
|
|
1445
1852
|
if (!this.tagsMatch(card, handEnv.boundTags)) {
|
|
1446
|
-
verdict(card
|
|
1853
|
+
verdict(card, "tags");
|
|
1447
1854
|
continue;
|
|
1448
1855
|
}
|
|
1449
1856
|
if (card.condition && !this.passes(
|
|
@@ -1451,12 +1858,12 @@ var Flow = class {
|
|
|
1451
1858
|
deckCtx,
|
|
1452
1859
|
this.tracing ? `card ${card.gameId} condition` : void 0
|
|
1453
1860
|
)) {
|
|
1454
|
-
verdict(card
|
|
1861
|
+
verdict(card, "condition");
|
|
1455
1862
|
continue;
|
|
1456
1863
|
}
|
|
1457
1864
|
const refused = claimed(card, shared);
|
|
1458
1865
|
if (refused) {
|
|
1459
|
-
verdict(card
|
|
1866
|
+
verdict(card, refused);
|
|
1460
1867
|
continue;
|
|
1461
1868
|
}
|
|
1462
1869
|
let priority;
|
|
@@ -1466,7 +1873,7 @@ var Flow = class {
|
|
|
1466
1873
|
try {
|
|
1467
1874
|
const v = this.eval(card.priority, deckCtx);
|
|
1468
1875
|
if (typeof v !== "number") {
|
|
1469
|
-
verdict(card
|
|
1876
|
+
verdict(card, "priority");
|
|
1470
1877
|
continue;
|
|
1471
1878
|
}
|
|
1472
1879
|
priority = v;
|
|
@@ -1474,7 +1881,7 @@ var Flow = class {
|
|
|
1474
1881
|
if (this.tracing) {
|
|
1475
1882
|
this.emit({ type: "diagnostic", where: `card ${card.gameId} priority`, message: e instanceof Error ? e.message : String(e) });
|
|
1476
1883
|
}
|
|
1477
|
-
verdict(card
|
|
1884
|
+
verdict(card, "priority");
|
|
1478
1885
|
continue;
|
|
1479
1886
|
}
|
|
1480
1887
|
}
|
|
@@ -1505,11 +1912,13 @@ var Flow = class {
|
|
|
1505
1912
|
i = j;
|
|
1506
1913
|
}
|
|
1507
1914
|
for (const s of scored) {
|
|
1508
|
-
trace?.push({ id: s.entry.card
|
|
1915
|
+
trace?.push({ id: effectiveGameId(s.entry.card), verdict: "dealt", priority: s.priority, specificity: s.spec });
|
|
1509
1916
|
}
|
|
1510
1917
|
return { ordered: scored.map((s) => s.entry), handEnv };
|
|
1511
1918
|
}
|
|
1512
|
-
/** Flip eligible-but-not-taken trace entries to "capped".
|
|
1919
|
+
/** Flip eligible-but-not-taken trace entries to "capped". `taken` is keyed
|
|
1920
|
+
* by GAMEID, as the trace rows are (4.4): the two must move together or
|
|
1921
|
+
* every dealt card silently reads as capped. */
|
|
1513
1922
|
capTrace(trace, taken) {
|
|
1514
1923
|
for (const entry of trace) {
|
|
1515
1924
|
if (entry.verdict === "dealt" && !taken.has(entry.id)) entry.verdict = "capped";
|
|
@@ -1550,7 +1959,7 @@ var Flow = class {
|
|
|
1550
1959
|
const { ordered } = this.runAsk(ask, (card, shared) => this.claimVerdict(card, shared, claimCounts, worldClaims), trace);
|
|
1551
1960
|
const listed = n === void 0 ? ordered : ordered.slice(0, Math.max(n, 0));
|
|
1552
1961
|
if (trace) {
|
|
1553
|
-
this.capTrace(trace, new Set(listed.map((e) => e.card
|
|
1962
|
+
this.capTrace(trace, new Set(listed.map((e) => effectiveGameId(e.card))));
|
|
1554
1963
|
this.emit({ type: "peek", box: effectiveGameId(box), criteria, cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
1555
1964
|
}
|
|
1556
1965
|
return { box: effectiveGameId(box), cards: listed.map((e) => this.view(e)) };
|
|
@@ -1580,7 +1989,8 @@ var Flow = class {
|
|
|
1580
1989
|
const turn = this.turnCounts.get(box.id) ?? 0;
|
|
1581
1990
|
const evicted = [];
|
|
1582
1991
|
const evict = (cardId, reason) => {
|
|
1583
|
-
|
|
1992
|
+
const known = this.internals.cardsById.get(cardId);
|
|
1993
|
+
evicted.push({ card: known ? effectiveGameId(known.card) : cardId, reason });
|
|
1584
1994
|
return false;
|
|
1585
1995
|
};
|
|
1586
1996
|
const survivors = (this.boardContents.get(hand.id) ?? []).filter((cardId) => {
|
|
@@ -1597,7 +2007,7 @@ var Flow = class {
|
|
|
1597
2007
|
});
|
|
1598
2008
|
this.boardContents.set(hand.id, survivors);
|
|
1599
2009
|
if (this.tracing) {
|
|
1600
|
-
for (const e of evicted) this.emit({ type: "evict", hand: hand
|
|
2010
|
+
for (const e of evicted) this.emit({ type: "evict", hand: effectiveGameId(hand), card: e.card, reason: e.reason }, turn);
|
|
1601
2011
|
}
|
|
1602
2012
|
}
|
|
1603
2013
|
const claimCounts = this.claims();
|
|
@@ -1614,14 +2024,15 @@ var Flow = class {
|
|
|
1614
2024
|
(card, shared) => own.has(card.id) ? "claimed" : this.claimVerdict(card, shared, claimCounts, worldClaims),
|
|
1615
2025
|
trace
|
|
1616
2026
|
);
|
|
1617
|
-
const
|
|
2027
|
+
const taking = ordered.slice(0, free);
|
|
2028
|
+
const added = taking.map((e) => e.card.id);
|
|
1618
2029
|
this.boardContents.set(hand.id, [...contents, ...added]);
|
|
1619
2030
|
for (const id of added) {
|
|
1620
2031
|
claimCounts.set(id, (claimCounts.get(id) ?? 0) + 1);
|
|
1621
2032
|
worldClaims.set(id, (worldClaims.get(id) ?? 0) + 1);
|
|
1622
2033
|
}
|
|
1623
2034
|
if (trace) {
|
|
1624
|
-
this.capTrace(trace, new Set(
|
|
2035
|
+
this.capTrace(trace, new Set(taking.map((e) => effectiveGameId(e.card))));
|
|
1625
2036
|
this.emit({ type: "deal", hand: effectiveGameId(hand), cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
1626
2037
|
}
|
|
1627
2038
|
}
|
|
@@ -1689,7 +2100,8 @@ var Flow = class {
|
|
|
1689
2100
|
if (!this.passes(outcome.condition, ctx)) {
|
|
1690
2101
|
throw new Error(`outcome "${outcomeGameId}" on "${effectiveGameId(entry.card)}" is gated shut`);
|
|
1691
2102
|
}
|
|
1692
|
-
const
|
|
2103
|
+
const perPlay = entry.box.turn !== void 0 ? 0 : this.internals.bundle.settings.playAdvancesTurns;
|
|
2104
|
+
const newTurn = (this.turnCounts.get(entry.box.id) ?? 0) + (opts.advanceTurns ?? perPlay);
|
|
1693
2105
|
const writes = [];
|
|
1694
2106
|
for (const [target, expr] of Object.entries(outcome.changes)) {
|
|
1695
2107
|
writes.push({ target, value: this.eval(expr, ctx) });
|
|
@@ -1713,7 +2125,11 @@ var Flow = class {
|
|
|
1713
2125
|
(this.boardContents.get(handId) ?? []).filter((id) => id !== entry.card.id)
|
|
1714
2126
|
);
|
|
1715
2127
|
this.turnCounts.set(entry.box.id, newTurn);
|
|
1716
|
-
if (this.tracing) this.emit({ type: "play", card: entry.card
|
|
2128
|
+
if (this.tracing) this.emit({ type: "play", card: effectiveGameId(entry.card), outcome: effectiveGameId(outcome), turn: newTurn }, newTurn);
|
|
2129
|
+
}
|
|
2130
|
+
/** One owned property's address, owner segment and all (4.4). */
|
|
2131
|
+
address(kind, id) {
|
|
2132
|
+
return addressOf(this.internals, kind, id);
|
|
1717
2133
|
}
|
|
1718
2134
|
/** Land one change in whichever partition declares the name: the flow's
|
|
1719
2135
|
* bag when the property is per-flow, the shared bag when it is shared -
|
|
@@ -1734,24 +2150,24 @@ var Flow = class {
|
|
|
1734
2150
|
const [, scope, name] = match;
|
|
1735
2151
|
switch (scope) {
|
|
1736
2152
|
case "world": {
|
|
1737
|
-
const
|
|
1738
|
-
if (!
|
|
2153
|
+
const worldSet = this.internals.worldSet;
|
|
2154
|
+
if (!worldSet) throw new Error(`@world.${name} cannot be written: the host bound @world read-only`);
|
|
1739
2155
|
if (this.internals.worldReadOnly.has(name)) throw new Error(`'@world.${name}' is read-only (writable: false)`);
|
|
1740
|
-
const prev =
|
|
1741
|
-
|
|
2156
|
+
const prev = this.internals.worldResolver.get(name);
|
|
2157
|
+
worldSet(name, value);
|
|
1742
2158
|
return { path: `world.${name}`, ...prev !== void 0 ? { prev } : {} };
|
|
1743
2159
|
}
|
|
1744
2160
|
case "story":
|
|
1745
2161
|
return this.landIn("story", void 0, name, value, `story.${name}`);
|
|
1746
2162
|
case "box":
|
|
1747
|
-
return this.landIn("box", entry.box.id, name, value,
|
|
2163
|
+
return this.landIn("box", entry.box.id, name, value, `${this.address("box", entry.box.id)}.${name}`);
|
|
1748
2164
|
case "deck":
|
|
1749
|
-
return this.landIn("deck", entry.deck.id, name, value,
|
|
2165
|
+
return this.landIn("deck", entry.deck.id, name, value, `${this.address("deck", entry.deck.id)}.${name}`);
|
|
1750
2166
|
case "hand": {
|
|
1751
2167
|
const source = handEnv.sources.get(name);
|
|
1752
2168
|
if (!source) throw new Error(`@hand.${name} is not composed in this ask`);
|
|
1753
2169
|
if (source.kind === "criteria") throw new Error(`@hand.${name} is a chosen tag / criteria name and cannot be written`);
|
|
1754
|
-
return this.landIn(source.kind, source.id, name, value, `${source.kind
|
|
2170
|
+
return this.landIn(source.kind, source.id, name, value, `${this.address(source.kind, source.id)}.${name}`);
|
|
1755
2171
|
}
|
|
1756
2172
|
default:
|
|
1757
2173
|
throw new Error(`bad change target scope "@${scope}"`);
|
|
@@ -1787,7 +2203,7 @@ var Flow = class {
|
|
|
1787
2203
|
this.assertOpen();
|
|
1788
2204
|
const mounts = [{ prefix: "story", bag: this.stores.story }];
|
|
1789
2205
|
for (const kind of ["box", "deck", "hand", "value"]) {
|
|
1790
|
-
for (const [id, bag] of this.stores[kind]) mounts.push({ prefix:
|
|
2206
|
+
for (const [id, bag] of this.stores[kind]) mounts.push({ prefix: addressOf(this.internals, kind, id), bag });
|
|
1791
2207
|
}
|
|
1792
2208
|
return mounts;
|
|
1793
2209
|
}
|
|
@@ -1810,10 +2226,15 @@ var Flow = class {
|
|
|
1810
2226
|
...d.values !== void 0 ? { values: d.values } : {},
|
|
1811
2227
|
...d.stages !== void 0 ? { stages: d.stages } : {},
|
|
1812
2228
|
// @world is FOREIGN - a host resolver backs it - so writability is whether that
|
|
1813
|
-
// resolver can be written at all
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
|
|
2229
|
+
// resolver can be written at all AND what the declaration says, which is the
|
|
2230
|
+
// shared registry's own rule for a foreign scope (its foreignWritable). The
|
|
2231
|
+
// `as PropertyView` cast this replaced was hiding the field's absence: the row
|
|
2232
|
+
// type has always required it, and these rows shipped without one.
|
|
2233
|
+
//
|
|
2234
|
+
// A row is where `writable: false` is meant to SHOW (Reboot.md 10): it tells a
|
|
2235
|
+
// state panel this is the game's value, not the story's. It does not stop the
|
|
2236
|
+
// panel editing it - the host's setProperty passes `{ host: true }`.
|
|
2237
|
+
writable: this.internals.worldSet !== void 0 && !this.internals.worldReadOnly.has(d.name)
|
|
1817
2238
|
});
|
|
1818
2239
|
}
|
|
1819
2240
|
const add = (_prefix, shared, own) => {
|
|
@@ -1823,15 +2244,22 @@ var Flow = class {
|
|
|
1823
2244
|
}
|
|
1824
2245
|
};
|
|
1825
2246
|
add("story", this.internals.shared.story, this.stores.story);
|
|
1826
|
-
for (const kind of
|
|
2247
|
+
for (const kind of OWNED_SCOPES) {
|
|
1827
2248
|
const ids = /* @__PURE__ */ new Set([...this.internals.shared[kind].keys(), ...this.stores[kind].keys()]);
|
|
1828
|
-
for (const id of ids)
|
|
2249
|
+
for (const id of ids) {
|
|
2250
|
+
add(addressOf(this.internals, kind, id), this.internals.shared[kind].get(id), this.stores[kind].get(id));
|
|
2251
|
+
}
|
|
1829
2252
|
}
|
|
1830
2253
|
return out;
|
|
1831
2254
|
}
|
|
1832
|
-
/** Read by path: "world.x", "story.gold", "value.
|
|
1833
|
-
* "box.
|
|
1834
|
-
* merged view, routed by the declaration's sharing.
|
|
2255
|
+
/** Read by path: "world.x", "story.gold", "value.docks.danger",
|
|
2256
|
+
* "box.village.heat", "deck.wares.n", "hand.the-elder.zone" - the flow's
|
|
2257
|
+
* merged view, routed by the declaration's sharing.
|
|
2258
|
+
*
|
|
2259
|
+
* The owner segment is the entity's GAMEID, the name it is called by
|
|
2260
|
+
* everywhere else (4.4). Its internal id is accepted for this release and
|
|
2261
|
+
* earns a `diagnostic` naming the address to move to; the next lockstep
|
|
2262
|
+
* release refuses it. */
|
|
1835
2263
|
getProperty(path) {
|
|
1836
2264
|
this.assertOpen();
|
|
1837
2265
|
const found = this.resolvePath(path);
|
|
@@ -1843,13 +2271,13 @@ var Flow = class {
|
|
|
1843
2271
|
this.assertOpen();
|
|
1844
2272
|
const found = this.resolvePath(path);
|
|
1845
2273
|
if (found.kind === "world") {
|
|
1846
|
-
if (!this.internals.
|
|
1847
|
-
this.internals.
|
|
2274
|
+
if (!this.internals.worldSet) throw new Error(`@world is read-only here: the host bound no write`);
|
|
2275
|
+
this.internals.worldSet(found.name, value, true);
|
|
1848
2276
|
return;
|
|
1849
2277
|
}
|
|
1850
2278
|
const bag = found.own !== void 0 && found.own.get(found.name) !== void 0 ? found.own : found.shared !== void 0 && found.shared.get(found.name) !== void 0 ? found.shared : void 0;
|
|
1851
2279
|
if (bag === void 0) throw new Error(`no property at "${path}"`);
|
|
1852
|
-
bag.set(found.name, value, { silent: true, reason: "host setProperty" });
|
|
2280
|
+
bag.set(found.name, value, { silent: true, reason: "host setProperty", host: true });
|
|
1853
2281
|
}
|
|
1854
2282
|
resolvePath(path) {
|
|
1855
2283
|
const parts = path.split(".");
|
|
@@ -1859,10 +2287,15 @@ var Flow = class {
|
|
|
1859
2287
|
}
|
|
1860
2288
|
if (parts.length === 3 && (parts[0] === "box" || parts[0] === "deck" || parts[0] === "hand" || parts[0] === "value")) {
|
|
1861
2289
|
const kind = parts[0];
|
|
1862
|
-
const
|
|
1863
|
-
const
|
|
1864
|
-
if (
|
|
1865
|
-
|
|
2290
|
+
const [, segment, name] = parts;
|
|
2291
|
+
const owner = ownerOrThrow(this.internals, kind, segment, name);
|
|
2292
|
+
if (owner.legacy && this.tracing) {
|
|
2293
|
+
this.emit({ type: "diagnostic", where: "property address", message: legacyAddressMessage(this.internals, kind, segment, name) });
|
|
2294
|
+
}
|
|
2295
|
+
const own = this.stores[kind].get(owner.id);
|
|
2296
|
+
const shared = this.internals.shared[kind].get(owner.id);
|
|
2297
|
+
if (own === void 0 && shared === void 0) throw new Error(`no ${kind} store "${segment}"`);
|
|
2298
|
+
return { kind: "bag", ...own !== void 0 ? { own } : {}, ...shared !== void 0 ? { shared } : {}, name };
|
|
1866
2299
|
}
|
|
1867
2300
|
throw new Error(`bad property path "${path}"`);
|
|
1868
2301
|
}
|
|
@@ -1903,14 +2336,27 @@ var summarise = (decls) => decls.map((d) => ({
|
|
|
1903
2336
|
type: d.type,
|
|
1904
2337
|
default: d.default,
|
|
1905
2338
|
...d.values !== void 0 ? { values: d.values } : {},
|
|
2339
|
+
...d.durable === true ? { durable: true } : {},
|
|
1906
2340
|
...d.purpose !== void 0 ? { purpose: d.purpose } : {}
|
|
1907
2341
|
}));
|
|
2342
|
+
var durableCardCount = (box) => box.decks.reduce((n, deck) => n + deck.cards.filter((card) => (card.durable ?? deck.durable) === true).length, 0);
|
|
1908
2343
|
var handDecls = (hand, box) => {
|
|
1909
2344
|
if (hand.template !== void 0) {
|
|
1910
2345
|
return box.handTemplates.find((t) => t.id === hand.template)?.properties ?? [];
|
|
1911
2346
|
}
|
|
1912
2347
|
return hand.properties ?? [];
|
|
1913
2348
|
};
|
|
2349
|
+
var movableHoles = (hand, box) => {
|
|
2350
|
+
const filled = hand.template !== void 0 ? hand.chosen : hand.rule?.bindings;
|
|
2351
|
+
const out = [];
|
|
2352
|
+
for (const [groupId, value] of Object.entries(filled ?? {})) {
|
|
2353
|
+
if (!isHoleRef(value)) continue;
|
|
2354
|
+
const group = box.tagGroups.find((g) => g.id === groupId);
|
|
2355
|
+
if (group === void 0) continue;
|
|
2356
|
+
out.push({ group: effectiveGameId(group), from: value });
|
|
2357
|
+
}
|
|
2358
|
+
return out;
|
|
2359
|
+
};
|
|
1914
2360
|
var handSlots = (hand, box) => {
|
|
1915
2361
|
if (hand.slots !== void 0) return hand.slots;
|
|
1916
2362
|
const declared = hand.template !== void 0 ? box.handTemplates.find((t) => t.id === hand.template)?.slots : hand.rule?.slots;
|
|
@@ -1931,6 +2377,8 @@ function describeBundle(bundle) {
|
|
|
1931
2377
|
gameId: boxGameId,
|
|
1932
2378
|
...box.title !== void 0 ? { title: box.title } : {},
|
|
1933
2379
|
ranking: { specificity: box.ranking.specificity },
|
|
2380
|
+
...box.turn !== void 0 ? { turn: { seconds: box.turn.seconds } } : {},
|
|
2381
|
+
...durableCardCount(box) > 0 ? { durableCards: durableCardCount(box) } : {},
|
|
1934
2382
|
tagGroups: box.tagGroups.map((group) => ({
|
|
1935
2383
|
gameId: effectiveGameId(group),
|
|
1936
2384
|
tags: group.tags.map((tag) => effectiveGameId(tag))
|
|
@@ -1951,12 +2399,14 @@ function describeBundle(bundle) {
|
|
|
1951
2399
|
totals.tagGroups += box.tagGroups.length;
|
|
1952
2400
|
for (const hand of box.hands) {
|
|
1953
2401
|
const template = hand.template !== void 0 ? box.handTemplates.find((t) => t.id === hand.template) : void 0;
|
|
2402
|
+
const movable = movableHoles(hand, box);
|
|
1954
2403
|
hands.push({
|
|
1955
2404
|
gameId: effectiveGameId(hand),
|
|
1956
2405
|
...hand.title !== void 0 ? { title: hand.title } : {},
|
|
1957
2406
|
box: boxGameId,
|
|
1958
2407
|
slots: handSlots(hand, box),
|
|
1959
|
-
...template !== void 0 ? { template: effectiveGameId(template) } : {}
|
|
2408
|
+
...template !== void 0 ? { template: effectiveGameId(template) } : {},
|
|
2409
|
+
...movable.length > 0 ? { movable } : {}
|
|
1960
2410
|
});
|
|
1961
2411
|
}
|
|
1962
2412
|
const push = (scope, owner, decls, group) => {
|
|
@@ -1994,7 +2444,8 @@ function describeBundle(bundle) {
|
|
|
1994
2444
|
box: map.box,
|
|
1995
2445
|
group: map.group,
|
|
1996
2446
|
zones: map.zones.length,
|
|
1997
|
-
backgrounds: map.backgrounds?.length ?? 0
|
|
2447
|
+
backgrounds: map.backgrounds?.length ?? 0,
|
|
2448
|
+
sites: map.sites?.length ?? 0
|
|
1998
2449
|
}))
|
|
1999
2450
|
};
|
|
2000
2451
|
}
|