@storylet-studio/runtime 0.4.0 → 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.js
CHANGED
|
@@ -408,7 +408,48 @@ function effectiveGameId(entity) {
|
|
|
408
408
|
const fromTitle = entity.title ? gameIdify(entity.title) : "";
|
|
409
409
|
return fromTitle || entity.id;
|
|
410
410
|
}
|
|
411
|
+
function valueAddresses(bundle) {
|
|
412
|
+
const tags = [];
|
|
413
|
+
for (const box of bundle.boxes) {
|
|
414
|
+
const boxGameId = effectiveGameId(box);
|
|
415
|
+
for (const group of box.tagGroups) {
|
|
416
|
+
for (const tag of group.tags) {
|
|
417
|
+
const gameId = effectiveGameId(tag);
|
|
418
|
+
tags.push({ id: tag.id, gameId, qualified: `${boxGameId}/${gameId}` });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const forms = /* @__PURE__ */ new Map();
|
|
423
|
+
for (const tag of tags) {
|
|
424
|
+
const list = forms.get(tag.gameId) ?? [];
|
|
425
|
+
if (!list.includes(tag.qualified)) list.push(tag.qualified);
|
|
426
|
+
forms.set(tag.gameId, list);
|
|
427
|
+
}
|
|
428
|
+
const print = /* @__PURE__ */ new Map();
|
|
429
|
+
const accept = /* @__PURE__ */ new Map();
|
|
430
|
+
const repeated = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const tag of tags) {
|
|
432
|
+
const candidates = forms.get(tag.gameId) ?? [tag.qualified];
|
|
433
|
+
const ambiguous = candidates.length > 1;
|
|
434
|
+
print.set(tag.id, ambiguous ? tag.qualified : tag.gameId);
|
|
435
|
+
if (!accept.has(tag.qualified)) accept.set(tag.qualified, tag.id);
|
|
436
|
+
if (!ambiguous && !accept.has(tag.gameId)) accept.set(tag.gameId, tag.id);
|
|
437
|
+
if (ambiguous) repeated.set(tag.gameId, candidates);
|
|
438
|
+
}
|
|
439
|
+
return { print, accept, repeated };
|
|
440
|
+
}
|
|
441
|
+
function ambiguousValueAddressMessage(segment, name, candidates) {
|
|
442
|
+
const forms = candidates.map((q) => `"value.${q}.${name}"`);
|
|
443
|
+
const list = forms.length <= 1 ? forms[0] ?? "" : `${forms.slice(0, -1).join(", ")} or ${forms[forms.length - 1]}`;
|
|
444
|
+
return `"value.${segment}.${name}" names a tag in ${candidates.length} boxes; write ${list}`;
|
|
445
|
+
}
|
|
411
446
|
var PLACE_GROUP = "place";
|
|
447
|
+
var HOLE_REF = /^@(hand|world|story)\.([a-z][a-z0-9_-]*)$/;
|
|
448
|
+
var isHoleRef = (value) => value.startsWith("@");
|
|
449
|
+
var parseHoleRef = (value) => {
|
|
450
|
+
const m = HOLE_REF.exec(value);
|
|
451
|
+
return m === null ? void 0 : { scope: m[1], name: m[2] };
|
|
452
|
+
};
|
|
412
453
|
|
|
413
454
|
// ../../../expr/packages/scoperegistry/src/index.ts
|
|
414
455
|
var PropertyBag = class _PropertyBag {
|
|
@@ -443,10 +484,14 @@ var PropertyBag = class _PropertyBag {
|
|
|
443
484
|
}
|
|
444
485
|
/** Write a property. Engine writes (the default) notify subscribers;
|
|
445
486
|
* pass `silent: true` for a host write, which reaches only the audit
|
|
446
|
-
* hook. Throws on a read-only property
|
|
487
|
+
* hook. Throws on a read-only property unless the caller says it is the
|
|
488
|
+
* HOST (`host: true`), for whom `writable: false` was never a rule - it is
|
|
489
|
+
* the story's promise, not the game's. `silent` and `host` are separate on
|
|
490
|
+
* purpose: one is about who hears the write, the other about who may make
|
|
491
|
+
* it. Returns the change. */
|
|
447
492
|
set(name, value, opts) {
|
|
448
493
|
const n = this.norm(name);
|
|
449
|
-
if (this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
494
|
+
if (!opts?.host && this.decls.get(n)?.writable === false) throw new Error(`'${name}' is read-only`);
|
|
450
495
|
const change = {
|
|
451
496
|
name: n,
|
|
452
497
|
prev: this.values[n],
|
|
@@ -561,27 +606,62 @@ var SCOPE_DEFAULT_SHARED = { story: true, box: false, deck: false, hand: false,
|
|
|
561
606
|
var isShared = (scope, d) => d.shared ?? SCOPE_DEFAULT_SHARED[scope];
|
|
562
607
|
var sharedHalf = (scope, decls) => decls.filter((d) => isShared(scope, d));
|
|
563
608
|
var flowHalf = (scope, decls) => decls.filter((d) => !isShared(scope, d));
|
|
609
|
+
var OWNED_SCOPES = ["box", "deck", "hand", "value"];
|
|
610
|
+
var emptyOwnerIndexes = () => ({
|
|
611
|
+
box: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() },
|
|
612
|
+
deck: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() },
|
|
613
|
+
hand: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() },
|
|
614
|
+
value: { gameId: /* @__PURE__ */ new Map(), id: /* @__PURE__ */ new Map(), repeated: /* @__PURE__ */ new Map() }
|
|
615
|
+
});
|
|
616
|
+
var indexOwner = (index, entity) => {
|
|
617
|
+
const gameId = effectiveGameId(entity);
|
|
618
|
+
index.gameId.set(entity.id, gameId);
|
|
619
|
+
if (!index.id.has(gameId)) index.id.set(gameId, entity.id);
|
|
620
|
+
};
|
|
621
|
+
var indexValueOwners = (index, bundle) => {
|
|
622
|
+
const addresses = valueAddresses(bundle);
|
|
623
|
+
for (const [id, segment] of addresses.print) index.gameId.set(id, segment);
|
|
624
|
+
for (const [segment, id] of addresses.accept) index.id.set(segment, id);
|
|
625
|
+
for (const [gameId, candidates] of addresses.repeated) index.repeated.set(gameId, candidates);
|
|
626
|
+
};
|
|
564
627
|
var handDeclsOf = (internals, hand) => {
|
|
565
628
|
if (hand.template !== void 0) {
|
|
566
629
|
return internals.templatesById.get(hand.template)?.properties ?? internals.bundle.boxes.flatMap((b) => b.handTemplates).find((t) => t.id === hand.template)?.properties ?? [];
|
|
567
630
|
}
|
|
568
631
|
return hand.properties ?? [];
|
|
569
632
|
};
|
|
633
|
+
var addressOf = (internals, kind, id) => `${kind}.${internals.owners[kind].gameId.get(id) ?? id}`;
|
|
634
|
+
var resolveOwner = (internals, kind, segment) => {
|
|
635
|
+
const candidates = internals.owners[kind].repeated.get(segment);
|
|
636
|
+
if (candidates !== void 0) return { ambiguous: candidates };
|
|
637
|
+
const byGameId = internals.owners[kind].id.get(segment);
|
|
638
|
+
if (byGameId !== void 0) return { id: byGameId, legacy: false };
|
|
639
|
+
if (internals.owners[kind].gameId.has(segment)) return { id: segment, legacy: true };
|
|
640
|
+
return void 0;
|
|
641
|
+
};
|
|
642
|
+
var ownerOrThrow = (internals, kind, segment, name) => {
|
|
643
|
+
const owner = resolveOwner(internals, kind, segment);
|
|
644
|
+
if (owner === void 0) throw new Error(`no ${kind} store "${segment}"`);
|
|
645
|
+
if ("ambiguous" in owner) throw new Error(ambiguousValueAddressMessage(segment, name, owner.ambiguous));
|
|
646
|
+
return owner;
|
|
647
|
+
};
|
|
648
|
+
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.`;
|
|
570
649
|
var buildPartition = (internals, half) => {
|
|
571
650
|
const b = internals.bundle;
|
|
651
|
+
const at = (kind, id) => `${addressOf(internals, kind, id)}.`;
|
|
572
652
|
return {
|
|
573
653
|
story: bagFromDecls(half("story", b.story.properties), "story."),
|
|
574
|
-
box: new Map(b.boxes.map((box) => [box.id, bagFromDecls(half("box", box.properties),
|
|
654
|
+
box: new Map(b.boxes.map((box) => [box.id, bagFromDecls(half("box", box.properties), at("box", box.id))])),
|
|
575
655
|
deck: new Map(b.boxes.flatMap((box) => box.decks.map(
|
|
576
|
-
(deck) => [deck.id, bagFromDecls(half("deck", deck.properties),
|
|
656
|
+
(deck) => [deck.id, bagFromDecls(half("deck", deck.properties), at("deck", deck.id))]
|
|
577
657
|
))),
|
|
578
658
|
// A template instance inherits the template's property declarations;
|
|
579
659
|
// a standalone hand declares its own (schema 2.6).
|
|
580
660
|
hand: new Map(b.boxes.flatMap((box) => box.hands.map(
|
|
581
|
-
(hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)),
|
|
661
|
+
(hand) => [hand.id, bagFromDecls(half("hand", handDeclsOf(internals, hand)), at("hand", hand.id))]
|
|
582
662
|
))),
|
|
583
663
|
value: new Map(b.boxes.flatMap((box) => box.tagGroups.flatMap((group) => group.tags.map(
|
|
584
|
-
(tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []),
|
|
664
|
+
(tag) => [tag.id, bagFromDecls(half("value", tag.properties ?? []), at("value", tag.id))]
|
|
585
665
|
))))
|
|
586
666
|
};
|
|
587
667
|
};
|
|
@@ -600,6 +680,105 @@ var loadPartition = (p, values) => {
|
|
|
600
680
|
}
|
|
601
681
|
}
|
|
602
682
|
};
|
|
683
|
+
var emptyDraft = () => ({
|
|
684
|
+
evicted: [],
|
|
685
|
+
droppedCooldowns: [],
|
|
686
|
+
droppedSpent: [],
|
|
687
|
+
droppedProperties: [],
|
|
688
|
+
defaultedProperties: [],
|
|
689
|
+
retypedProperties: []
|
|
690
|
+
});
|
|
691
|
+
var SORT_SEP = "";
|
|
692
|
+
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);
|
|
693
|
+
var propKey = (p) => `${p.flow ?? ""}${SORT_SEP}${p.path}`;
|
|
694
|
+
function valueFits(decl, value) {
|
|
695
|
+
switch (decl.type) {
|
|
696
|
+
case "boolean":
|
|
697
|
+
return typeof value === "boolean";
|
|
698
|
+
case "number":
|
|
699
|
+
return typeof value === "number";
|
|
700
|
+
case "string":
|
|
701
|
+
return typeof value === "string";
|
|
702
|
+
case "enum":
|
|
703
|
+
return typeof value === "string" && (decl.values === void 0 || decl.values.includes(value));
|
|
704
|
+
case "quality":
|
|
705
|
+
return typeof value === "string" && (decl.stages === void 0 || decl.stages.includes(value));
|
|
706
|
+
case "flags":
|
|
707
|
+
return Array.isArray(value) && (decl.values === void 0 || value.every((f) => decl.values.includes(f)));
|
|
708
|
+
default:
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function walkScope(decls, saved, path, flow, draft) {
|
|
713
|
+
const at = (name) => ({ ...flow !== void 0 ? { flow } : {}, path: path(name) });
|
|
714
|
+
const byName = new Map((decls ?? []).map((d) => [d.name, d]));
|
|
715
|
+
const values = saved ?? {};
|
|
716
|
+
const clean = {};
|
|
717
|
+
for (const [name, value] of Object.entries(values)) {
|
|
718
|
+
const decl = byName.get(name);
|
|
719
|
+
if (decl === void 0) {
|
|
720
|
+
draft.droppedProperties.push(at(name));
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (!valueFits(decl, value)) {
|
|
724
|
+
draft.retypedProperties.push(at(name));
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
clean[name] = value;
|
|
728
|
+
}
|
|
729
|
+
for (const decl of decls ?? []) {
|
|
730
|
+
if (!(decl.name in values)) draft.defaultedProperties.push(at(decl.name));
|
|
731
|
+
}
|
|
732
|
+
return clean;
|
|
733
|
+
}
|
|
734
|
+
function walkPartition(internals, decls, values, flow, draft) {
|
|
735
|
+
const out = {
|
|
736
|
+
story: walkScope(decls.story, values?.story, (n) => `story.${n}`, flow, draft),
|
|
737
|
+
box: {},
|
|
738
|
+
deck: {},
|
|
739
|
+
hand: {},
|
|
740
|
+
value: {}
|
|
741
|
+
};
|
|
742
|
+
for (const kind of ["box", "deck", "hand", "value"]) {
|
|
743
|
+
const savedKind = values?.[kind] ?? {};
|
|
744
|
+
const ids = [.../* @__PURE__ */ new Set([...decls[kind].keys(), ...Object.keys(savedKind)])].sort();
|
|
745
|
+
for (const id of ids) {
|
|
746
|
+
const owner = addressOf(internals, kind, id);
|
|
747
|
+
out[kind][id] = walkScope(
|
|
748
|
+
decls[kind].get(id),
|
|
749
|
+
savedKind[id],
|
|
750
|
+
(n) => `${owner}.${n}`,
|
|
751
|
+
flow,
|
|
752
|
+
draft
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return out;
|
|
757
|
+
}
|
|
758
|
+
function finishReport(bundle, saved, flows, draft) {
|
|
759
|
+
const drift = saved.version !== bundle.version || saved.hash !== bundle.hash;
|
|
760
|
+
const evicted = byKey(draft.evicted, (e) => [e.flow, e.hand, e.card, e.reason].join(SORT_SEP));
|
|
761
|
+
const droppedCooldowns = byKey(draft.droppedCooldowns, (c) => `${c.flow}${SORT_SEP}${c.card}`);
|
|
762
|
+
const droppedSpent = [...draft.droppedSpent].sort();
|
|
763
|
+
const droppedProperties = byKey(draft.droppedProperties, propKey);
|
|
764
|
+
const defaultedProperties = byKey(draft.defaultedProperties, propKey);
|
|
765
|
+
const retypedProperties = byKey(draft.retypedProperties, propKey);
|
|
766
|
+
return {
|
|
767
|
+
// `flows` is what the load restores, not something it had to change, so
|
|
768
|
+
// it never makes a report inexact.
|
|
769
|
+
exact: !drift && evicted.length === 0 && droppedCooldowns.length === 0 && droppedSpent.length === 0 && droppedProperties.length === 0 && defaultedProperties.length === 0 && retypedProperties.length === 0,
|
|
770
|
+
project: bundle.project,
|
|
771
|
+
version: { saved: saved.version, bundle: bundle.version },
|
|
772
|
+
hash: { saved: saved.hash, bundle: bundle.hash },
|
|
773
|
+
flows,
|
|
774
|
+
evicted,
|
|
775
|
+
droppedCooldowns,
|
|
776
|
+
droppedSpent,
|
|
777
|
+
droppedProperties,
|
|
778
|
+
defaultedProperties,
|
|
779
|
+
retypedProperties
|
|
780
|
+
};
|
|
781
|
+
}
|
|
603
782
|
var Engine = class {
|
|
604
783
|
internals;
|
|
605
784
|
seed;
|
|
@@ -623,6 +802,7 @@ var Engine = class {
|
|
|
623
802
|
boxesById: /* @__PURE__ */ new Map(),
|
|
624
803
|
handsById: /* @__PURE__ */ new Map(),
|
|
625
804
|
handsByGameId: /* @__PURE__ */ new Map(),
|
|
805
|
+
owners: emptyOwnerIndexes(),
|
|
626
806
|
templatesById: /* @__PURE__ */ new Map(),
|
|
627
807
|
groupsById: /* @__PURE__ */ new Map(),
|
|
628
808
|
requiredGroups: /* @__PURE__ */ new Set(),
|
|
@@ -631,6 +811,7 @@ var Engine = class {
|
|
|
631
811
|
hasQualities: false,
|
|
632
812
|
hasShared: false,
|
|
633
813
|
flowDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
|
|
814
|
+
sharedDecls: { story: [], box: /* @__PURE__ */ new Map(), deck: /* @__PURE__ */ new Map(), hand: /* @__PURE__ */ new Map(), value: /* @__PURE__ */ new Map() },
|
|
634
815
|
shared: void 0,
|
|
635
816
|
worldResolver: void 0,
|
|
636
817
|
worldReadOnly: /* @__PURE__ */ new Set(),
|
|
@@ -646,14 +827,17 @@ var Engine = class {
|
|
|
646
827
|
engineTracing: () => this.engineTraceHandlers.size > 0
|
|
647
828
|
};
|
|
648
829
|
this.internals = internals;
|
|
830
|
+
indexValueOwners(internals.owners.value, bundle);
|
|
649
831
|
for (const box of bundle.boxes) {
|
|
650
832
|
internals.boxesById.set(box.id, box);
|
|
651
833
|
internals.boxesByGameId.set(effectiveGameId(box), box);
|
|
834
|
+
indexOwner(internals.owners.box, box);
|
|
652
835
|
for (const group of box.tagGroups) {
|
|
653
836
|
internals.groupsById.set(group.id, { group, box });
|
|
654
837
|
if (group.required === true) internals.requiredGroups.add(group.id);
|
|
655
838
|
}
|
|
656
839
|
for (const deck of box.decks) {
|
|
840
|
+
indexOwner(internals.owners.deck, deck);
|
|
657
841
|
if (deck.shared === true) internals.hasShared = true;
|
|
658
842
|
for (const card of deck.cards) {
|
|
659
843
|
const entry = { card, deck, box };
|
|
@@ -668,22 +852,25 @@ var Engine = class {
|
|
|
668
852
|
for (const hand of box.hands) {
|
|
669
853
|
internals.handsById.set(hand.id, { hand, box });
|
|
670
854
|
internals.handsByGameId.set(effectiveGameId(hand), { hand, box });
|
|
855
|
+
indexOwner(internals.owners.hand, hand);
|
|
671
856
|
}
|
|
672
857
|
}
|
|
673
858
|
this.initLadders();
|
|
674
|
-
|
|
675
|
-
story:
|
|
676
|
-
box: new Map(bundle.boxes.map((box) => [box.id,
|
|
859
|
+
const declSet = (half) => ({
|
|
860
|
+
story: half("story", bundle.story.properties),
|
|
861
|
+
box: new Map(bundle.boxes.map((box) => [box.id, half("box", box.properties)])),
|
|
677
862
|
deck: new Map(bundle.boxes.flatMap((box) => box.decks.map(
|
|
678
|
-
(deck) => [deck.id,
|
|
863
|
+
(deck) => [deck.id, half("deck", deck.properties)]
|
|
679
864
|
))),
|
|
680
865
|
hand: new Map(bundle.boxes.flatMap((box) => box.hands.map(
|
|
681
|
-
(hand) => [hand.id,
|
|
866
|
+
(hand) => [hand.id, half("hand", handDeclsOf(internals, hand))]
|
|
682
867
|
))),
|
|
683
868
|
value: new Map(bundle.boxes.flatMap((box) => box.tagGroups.flatMap((group) => group.tags.map(
|
|
684
|
-
(tag) => [tag.id,
|
|
869
|
+
(tag) => [tag.id, half("value", tag.properties ?? [])]
|
|
685
870
|
))))
|
|
686
|
-
};
|
|
871
|
+
});
|
|
872
|
+
internals.flowDecls = declSet(flowHalf);
|
|
873
|
+
internals.sharedDecls = declSet(sharedHalf);
|
|
687
874
|
this.initShared(this.hostWorld);
|
|
688
875
|
}
|
|
689
876
|
/** Build the shared stores and the @world seam. `hostWorld` sticks for the
|
|
@@ -694,14 +881,24 @@ var Engine = class {
|
|
|
694
881
|
internals.worldReadOnly = new Set(internals.bundle.world.properties.filter((d) => d.writable === false).map((d) => d.name));
|
|
695
882
|
if (hostWorld !== void 0) {
|
|
696
883
|
internals.worldResolver = hostWorld;
|
|
884
|
+
const set = hostWorld.set;
|
|
885
|
+
internals.worldSet = set !== void 0 ? (n, v) => {
|
|
886
|
+
set(n, v);
|
|
887
|
+
} : void 0;
|
|
697
888
|
} else {
|
|
698
889
|
const bag = bagFromDecls(internals.bundle.world.properties, "world.");
|
|
699
890
|
internals.worldResolver = {
|
|
891
|
+
// The engine writes through worldSet below, not through this; the `set`
|
|
892
|
+
// is the resolver's SHAPE, so @world still reads as writable to anything
|
|
893
|
+
// inspecting the seam, and it is the story's door: no host flag on it.
|
|
700
894
|
get: (n) => bag.get(n),
|
|
701
895
|
set: (n, v) => {
|
|
702
896
|
bag.set(n, v);
|
|
703
897
|
}
|
|
704
898
|
};
|
|
899
|
+
internals.worldSet = (n, v, host2) => {
|
|
900
|
+
bag.set(n, v, host2 === true ? { host: true } : void 0);
|
|
901
|
+
};
|
|
705
902
|
}
|
|
706
903
|
}
|
|
707
904
|
/** Quality ladders by scope for the eval channel (design/quality.md):
|
|
@@ -735,6 +932,7 @@ var Engine = class {
|
|
|
735
932
|
* state; shared state is untouched. There is no default flow: "main" is
|
|
736
933
|
* a caller convention, not an engine rule. */
|
|
737
934
|
openFlow(id, opts = {}) {
|
|
935
|
+
const otherClaims = opts.restore !== void 0 ? this.sharedClaimsExcept(id) : void 0;
|
|
738
936
|
const existing = this.flowsById.get(id);
|
|
739
937
|
if (existing) {
|
|
740
938
|
const dealt = existing.heldCardIds().length;
|
|
@@ -743,6 +941,13 @@ var Engine = class {
|
|
|
743
941
|
}
|
|
744
942
|
const flow = new Flow(this, this.internals, id, opts.seed ?? this.seed);
|
|
745
943
|
this.flowsById.set(id, flow);
|
|
944
|
+
if (opts.restore !== void 0) {
|
|
945
|
+
const draft = emptyDraft();
|
|
946
|
+
const clean = this.planFlowRestore(id, structuredClone(opts.restore), otherClaims, draft);
|
|
947
|
+
flow.restore(clean);
|
|
948
|
+
const content = this.internals.bundle.content;
|
|
949
|
+
opts.onRestoreReport?.(finishReport(content, content, [id], draft));
|
|
950
|
+
}
|
|
746
951
|
return flow;
|
|
747
952
|
}
|
|
748
953
|
getFlow(id) {
|
|
@@ -816,10 +1021,21 @@ var Engine = class {
|
|
|
816
1021
|
}
|
|
817
1022
|
return counts;
|
|
818
1023
|
}
|
|
1024
|
+
/** The same ledger with one name left out: what the REST of the world
|
|
1025
|
+
* holds, which is the question a resume under that name has to ask. */
|
|
1026
|
+
sharedClaimsExcept(id) {
|
|
1027
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1028
|
+
for (const [flowId, flow] of this.flowsById) {
|
|
1029
|
+
if (flowId === id) continue;
|
|
1030
|
+
for (const cardId of flow.heldCardIds()) counts.set(cardId, (counts.get(cardId) ?? 0) + 1);
|
|
1031
|
+
}
|
|
1032
|
+
return counts;
|
|
1033
|
+
}
|
|
819
1034
|
// --- engine-level state access ----------------------------------------------
|
|
820
1035
|
/**
|
|
821
1036
|
* Read shared state by path: "world.x", "story.gold" (when shared),
|
|
822
|
-
* "box.
|
|
1037
|
+
* "box.village.heat" (when shared) - the owner segment is its GAMEID
|
|
1038
|
+
* (design/engine-server.md 4.4). A ref that resolves PER-FLOW throws,
|
|
823
1039
|
* naming the fix - silently answering with some flow's copy (or a junk
|
|
824
1040
|
* default) was the bug Patter's engine.getProperty guard exists to stop.
|
|
825
1041
|
*/
|
|
@@ -832,13 +1048,13 @@ var Engine = class {
|
|
|
832
1048
|
setProperty(path, value) {
|
|
833
1049
|
const found = this.resolveShared(path);
|
|
834
1050
|
if (found.kind === "world") {
|
|
835
|
-
if (!this.internals.
|
|
1051
|
+
if (!this.internals.worldSet) {
|
|
836
1052
|
throw new Error(`@world is read-only here: the host bound no write`);
|
|
837
1053
|
}
|
|
838
|
-
this.internals.
|
|
1054
|
+
this.internals.worldSet(found.name, value, true);
|
|
839
1055
|
return;
|
|
840
1056
|
}
|
|
841
|
-
found.bag.set(found.name, value, { silent: true, reason: "host setProperty" });
|
|
1057
|
+
found.bag.set(found.name, value, { silent: true, reason: "host setProperty", host: true });
|
|
842
1058
|
}
|
|
843
1059
|
resolveShared(path) {
|
|
844
1060
|
const parts = path.split(".");
|
|
@@ -854,15 +1070,25 @@ var Engine = class {
|
|
|
854
1070
|
}
|
|
855
1071
|
if (parts.length === 3 && (parts[0] === "box" || parts[0] === "deck" || parts[0] === "hand" || parts[0] === "value")) {
|
|
856
1072
|
const kind = parts[0];
|
|
857
|
-
const [,
|
|
1073
|
+
const [, segment, name] = parts;
|
|
1074
|
+
const owner = ownerOrThrow(this.internals, kind, segment, name);
|
|
1075
|
+
if (owner.legacy) this.diagnose(legacyAddressMessage(this.internals, kind, segment, name));
|
|
1076
|
+
const id = owner.id;
|
|
858
1077
|
const bag = this.internals.shared[kind].get(id);
|
|
859
1078
|
if (bag !== void 0 && bag.get(name) !== void 0) return { kind: "bag", bag, name };
|
|
860
1079
|
if (this.internals.flowDecls[kind].get(id)?.some((d) => d.name === name)) perFlow();
|
|
861
|
-
if (bag === void 0 && !this.internals.flowDecls[kind].has(id)) throw new Error(`no ${kind} store "${
|
|
1080
|
+
if (bag === void 0 && !this.internals.flowDecls[kind].has(id)) throw new Error(`no ${kind} store "${segment}"`);
|
|
862
1081
|
throw new Error(`no property at "${path}"`);
|
|
863
1082
|
}
|
|
864
1083
|
throw new Error(`bad property path "${path}"`);
|
|
865
1084
|
}
|
|
1085
|
+
/** The engine's own surface has no flow, so an engine-level diagnostic
|
|
1086
|
+
* carries the empty flow id - the same way a LoadReport's shared half
|
|
1087
|
+
* carries no flow. It reaches the run log and the engine tap; there is
|
|
1088
|
+
* nowhere else for it to go, and it fires only on a legacy address. */
|
|
1089
|
+
diagnose(message) {
|
|
1090
|
+
this.internals.emitEngine("", { type: "diagnostic", where: "property address", message });
|
|
1091
|
+
}
|
|
866
1092
|
/** The shared surface as examiner rows: @world (read through the
|
|
867
1093
|
* resolver) then the shared partitions. Per-flow rows live on each Flow. */
|
|
868
1094
|
listProperties() {
|
|
@@ -878,20 +1104,24 @@ var Engine = class {
|
|
|
878
1104
|
...d.values !== void 0 ? { values: d.values } : {},
|
|
879
1105
|
...d.stages !== void 0 ? { stages: d.stages } : {},
|
|
880
1106
|
// @world is FOREIGN - a host resolver backs it - so writability is whether that
|
|
881
|
-
// resolver can be written at all
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
1107
|
+
// resolver can be written at all AND what the declaration says, which is the
|
|
1108
|
+
// shared registry's own rule for a foreign scope (its foreignWritable). The
|
|
1109
|
+
// `as PropertyView` cast this replaced was hiding the field's absence: the row
|
|
1110
|
+
// type has always required it, and these rows shipped without one.
|
|
1111
|
+
//
|
|
1112
|
+
// A row is where `writable: false` is meant to SHOW (Reboot.md 10): it tells a
|
|
1113
|
+
// state panel this is the game's value, not the story's. It does not stop the
|
|
1114
|
+
// panel editing it - the host's setProperty passes `{ host: true }`.
|
|
1115
|
+
writable: this.internals.worldSet !== void 0 && !this.internals.worldReadOnly.has(d.name)
|
|
885
1116
|
});
|
|
886
1117
|
}
|
|
887
1118
|
const add = (_prefix, bag) => {
|
|
888
1119
|
for (const row of bag.rows()) out.push(row);
|
|
889
1120
|
};
|
|
890
1121
|
add("story", this.internals.shared.story);
|
|
891
|
-
for (const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
for (const [id, bag] of this.internals.shared.value) add(`value.${id}`, bag);
|
|
1122
|
+
for (const kind of OWNED_SCOPES) {
|
|
1123
|
+
for (const [id, bag] of this.internals.shared[kind]) add(addressOf(this.internals, kind, id), bag);
|
|
1124
|
+
}
|
|
895
1125
|
return out;
|
|
896
1126
|
}
|
|
897
1127
|
/** The SHARED kernel bags with their store path prefixes (the state
|
|
@@ -900,7 +1130,7 @@ var Engine = class {
|
|
|
900
1130
|
listBags() {
|
|
901
1131
|
const mounts = [{ prefix: "story", bag: this.internals.shared.story }];
|
|
902
1132
|
for (const kind of ["box", "deck", "hand", "value"]) {
|
|
903
|
-
for (const [id, bag] of this.internals.shared[kind]) mounts.push({ prefix:
|
|
1133
|
+
for (const [id, bag] of this.internals.shared[kind]) mounts.push({ prefix: addressOf(this.internals, kind, id), bag });
|
|
904
1134
|
}
|
|
905
1135
|
return mounts;
|
|
906
1136
|
}
|
|
@@ -921,20 +1151,137 @@ var Engine = class {
|
|
|
921
1151
|
flows: Object.fromEntries([...this.flowsById].map(([id, flow]) => [id, flow.snapshot()]))
|
|
922
1152
|
});
|
|
923
1153
|
}
|
|
1154
|
+
/** ONE flow's blob, to park a visit that is walking away: the same shape
|
|
1155
|
+
* the envelope carries per flow, and the same shape `openFlow`'s `restore`
|
|
1156
|
+
* option takes back (design/engine-server.md 4.1). Saving the whole
|
|
1157
|
+
* envelope to park one of four hundred players is wrong in cost and in
|
|
1158
|
+
* meaning. Throws for a name that is not open - a closed flow has nothing
|
|
1159
|
+
* left to save. */
|
|
1160
|
+
saveFlow(id) {
|
|
1161
|
+
const flow = this.flowsById.get(id);
|
|
1162
|
+
if (!flow) throw new Error(`unknown flow "${id}"`);
|
|
1163
|
+
return structuredClone(flow.snapshot());
|
|
1164
|
+
}
|
|
1165
|
+
/** What `loadGame(envelope)` would do that is not a plain restore, without
|
|
1166
|
+
* doing any of it (design/engine-server.md 4.9). Pure: nothing on this
|
|
1167
|
+
* engine moves. A project mismatch is refused here exactly as `loadGame`
|
|
1168
|
+
* refuses it - it is the one thing neither call will tolerate. */
|
|
1169
|
+
previewLoad(envelope) {
|
|
1170
|
+
this.assertSameProject(envelope);
|
|
1171
|
+
return this.planLoad(envelope).report;
|
|
1172
|
+
}
|
|
1173
|
+
/** What `openFlow(id, { restore: saved })` would do to a flow of that name,
|
|
1174
|
+
* without doing it: the same report shape, since a visit parked under one
|
|
1175
|
+
* build and resumed under the next raises the same questions. Pure. */
|
|
1176
|
+
previewFlowRestore(id, saved) {
|
|
1177
|
+
const draft = emptyDraft();
|
|
1178
|
+
this.planFlowRestore(id, saved, this.sharedClaimsExcept(id), draft);
|
|
1179
|
+
const content = this.internals.bundle.content;
|
|
1180
|
+
return finishReport(content, content, [id], draft);
|
|
1181
|
+
}
|
|
924
1182
|
/** Restore: shared state once, then every flow REBUILT from its blob.
|
|
925
1183
|
* Handles held from before the load are closed and inert (Patter's
|
|
926
|
-
* rule); take fresh ones from getFlow()/flows().
|
|
1184
|
+
* rule); take fresh ones from getFlow()/flows().
|
|
1185
|
+
*
|
|
1186
|
+
* Returns the report `previewLoad` would have given for this envelope: the
|
|
1187
|
+
* drift tolerance that makes a load forgiving is what hides its cost, so
|
|
1188
|
+
* the cost comes back with the load whether or not anybody looked first. */
|
|
927
1189
|
loadGame(envelope) {
|
|
1190
|
+
this.assertSameProject(envelope);
|
|
1191
|
+
const plan = this.planLoad(structuredClone(envelope));
|
|
1192
|
+
this.reset();
|
|
1193
|
+
loadPartition(this.internals.shared, plan.shared);
|
|
1194
|
+
for (const id of plan.spent) this.spent.add(id);
|
|
1195
|
+
for (const [id, clean] of plan.flows) this.openFlow(id).restore(clean);
|
|
1196
|
+
return plan.report;
|
|
1197
|
+
}
|
|
1198
|
+
assertSameProject(envelope) {
|
|
928
1199
|
if (envelope.content.project !== this.internals.bundle.content.project) {
|
|
929
1200
|
throw new Error(`save is for project "${envelope.content.project}", bundle is "${this.internals.bundle.content.project}"`);
|
|
930
1201
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1202
|
+
}
|
|
1203
|
+
/** The whole-envelope walk: the report, and the cleaned state the apply
|
|
1204
|
+
* half writes. Nothing here touches the engine, which is what lets
|
|
1205
|
+
* previewLoad and loadGame share it. */
|
|
1206
|
+
planLoad(envelope) {
|
|
1207
|
+
const draft = emptyDraft();
|
|
1208
|
+
const shared = walkPartition(
|
|
1209
|
+
this.internals,
|
|
1210
|
+
this.internals.sharedDecls,
|
|
1211
|
+
envelope.shared?.props,
|
|
1212
|
+
void 0,
|
|
1213
|
+
draft
|
|
1214
|
+
);
|
|
1215
|
+
const spent = [];
|
|
1216
|
+
for (const cardId of envelope.shared?.spent ?? []) {
|
|
1217
|
+
if (this.internals.cardsById.has(cardId)) spent.push(cardId);
|
|
1218
|
+
else draft.droppedSpent.push(cardId);
|
|
1219
|
+
}
|
|
1220
|
+
const flows = [];
|
|
1221
|
+
for (const [id, saved] of Object.entries(envelope.flows ?? {})) {
|
|
1222
|
+
flows.push([id, this.planFlowRestore(id, saved, void 0, draft)]);
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
report: finishReport(this.internals.bundle.content, envelope.content, flows.map(([id]) => id), draft),
|
|
1226
|
+
shared,
|
|
1227
|
+
spent,
|
|
1228
|
+
flows
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/** One flow's walk. `otherClaims` is the rest of the world's shared ledger
|
|
1232
|
+
* and is present only for a SINGLE-flow restore into a live engine: a
|
|
1233
|
+
* whole-envelope load rebuilds every flow from one consistent moment, so
|
|
1234
|
+
* there is nobody else to compete with. */
|
|
1235
|
+
planFlowRestore(id, saved, otherClaims, draft) {
|
|
1236
|
+
const internals = this.internals;
|
|
1237
|
+
const props = walkPartition(internals, internals.flowDecls, saved.props, id, draft);
|
|
1238
|
+
const cooldowns = {};
|
|
1239
|
+
for (const [cardId, turn] of Object.entries(saved.cooldowns ?? {})) {
|
|
1240
|
+
if (internals.cardsById.has(cardId)) cooldowns[cardId] = turn;
|
|
1241
|
+
else draft.droppedCooldowns.push({ flow: id, card: cardId });
|
|
1242
|
+
}
|
|
1243
|
+
const cardName = (cardId) => {
|
|
1244
|
+
const entry = internals.cardsById.get(cardId);
|
|
1245
|
+
return entry ? effectiveGameId(entry.card) : cardId;
|
|
1246
|
+
};
|
|
1247
|
+
const board = {};
|
|
1248
|
+
const restored = /* @__PURE__ */ new Map();
|
|
1249
|
+
for (const [handId, ids] of Object.entries(saved.board ?? {})) {
|
|
1250
|
+
const known = internals.handsById.get(handId);
|
|
1251
|
+
if (known === void 0) {
|
|
1252
|
+
for (const cardId of ids) {
|
|
1253
|
+
draft.evicted.push({ flow: id, hand: handId, card: cardName(cardId), reason: "hand-vanished" });
|
|
1254
|
+
}
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
const hand = effectiveGameId(known.hand);
|
|
1258
|
+
const kept = [];
|
|
1259
|
+
for (const cardId of ids) {
|
|
1260
|
+
const entry = internals.cardsById.get(cardId);
|
|
1261
|
+
if (entry === void 0) {
|
|
1262
|
+
draft.evicted.push({ flow: id, hand, card: cardId, reason: "vanished" });
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
if (otherClaims !== void 0 && cardIsShared(entry.card, entry.deck.shared ?? false)) {
|
|
1266
|
+
const held = (otherClaims.get(cardId) ?? 0) + (restored.get(cardId) ?? 0);
|
|
1267
|
+
if (held >= sharedCap(entry.card)) {
|
|
1268
|
+
draft.evicted.push({ flow: id, hand, card: effectiveGameId(entry.card), reason: "claimed-elsewhere" });
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
restored.set(cardId, (restored.get(cardId) ?? 0) + 1);
|
|
1272
|
+
}
|
|
1273
|
+
kept.push(cardId);
|
|
1274
|
+
}
|
|
1275
|
+
board[handId] = kept;
|
|
937
1276
|
}
|
|
1277
|
+
return {
|
|
1278
|
+
props,
|
|
1279
|
+
turns: saved.turns ?? {},
|
|
1280
|
+
prng: saved.prng,
|
|
1281
|
+
cooldowns,
|
|
1282
|
+
board,
|
|
1283
|
+
playLog: saved.playLog ?? []
|
|
1284
|
+
};
|
|
938
1285
|
}
|
|
939
1286
|
};
|
|
940
1287
|
var Flow = class {
|
|
@@ -1210,6 +1557,10 @@ var Flow = class {
|
|
|
1210
1557
|
boundTags.set(groupId, tagId);
|
|
1211
1558
|
}
|
|
1212
1559
|
for (const [groupId, tagId] of Object.entries(hand.chosen ?? {})) {
|
|
1560
|
+
if (isHoleRef(tagId)) {
|
|
1561
|
+
this.fillHoleFromProperty(hand, groupId, tagId, boundTags, askNames);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1213
1564
|
boundTags.set(groupId, tagId);
|
|
1214
1565
|
const found = this.internals.groupsById.get(groupId);
|
|
1215
1566
|
const tag = found?.group.tags.find((t) => t.id === tagId);
|
|
@@ -1218,6 +1569,10 @@ var Flow = class {
|
|
|
1218
1569
|
condition = template.condition;
|
|
1219
1570
|
} else {
|
|
1220
1571
|
for (const [groupId, tagId] of Object.entries(hand.rule?.bindings ?? {})) {
|
|
1572
|
+
if (isHoleRef(tagId)) {
|
|
1573
|
+
this.fillHoleFromProperty(hand, groupId, tagId, boundTags, askNames);
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1221
1576
|
boundTags.set(groupId, tagId);
|
|
1222
1577
|
const found = this.internals.groupsById.get(groupId);
|
|
1223
1578
|
const tag = found?.group.tags.find((t) => t.id === tagId);
|
|
@@ -1253,6 +1608,58 @@ var Flow = class {
|
|
|
1253
1608
|
this.bindStateGroups(box, boundTags, askNames);
|
|
1254
1609
|
return { box, boundTags, askNames };
|
|
1255
1610
|
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Fill one hole from the property its value names: the hand that moves
|
|
1613
|
+
* (design/engine-server.md 4.6).
|
|
1614
|
+
*
|
|
1615
|
+
* The semantics are `bindStateGroups`' below, word for word, applied per
|
|
1616
|
+
* HOLE instead of per group: resolved at ask time, and a value naming no tag
|
|
1617
|
+
* leaves the hole UNBOUND (a wildcard) with a diagnostic rather than dealing
|
|
1618
|
+
* a silently empty hand. What is added is the `@hand` scope - the asking
|
|
1619
|
+
* hand's OWN declared state, read here from the flow's merged view (the
|
|
1620
|
+
* shared half under the flow's own, so a `shared: true` declaration moves
|
|
1621
|
+
* the hole for every flow and a per-flow one moves it for this flow alone).
|
|
1622
|
+
*
|
|
1623
|
+
* Read BEFORE tag composition, which is the whole reason it is safe: the
|
|
1624
|
+
* @hand bag a card sees is built from the bound tags, so resolving a hole
|
|
1625
|
+
* from it would be circular. A hand's own declarations are not, so they are.
|
|
1626
|
+
*/
|
|
1627
|
+
fillHoleFromProperty(hand, groupId, ref, boundTags, askNames) {
|
|
1628
|
+
const found = this.internals.groupsById.get(groupId);
|
|
1629
|
+
const groupName = found ? effectiveGameId(found.group) : groupId;
|
|
1630
|
+
const where = `hand ${effectiveGameId(hand)}, tag group ${groupName}`;
|
|
1631
|
+
const parsed = parseHoleRef(ref);
|
|
1632
|
+
if (!parsed) {
|
|
1633
|
+
this.emit({ type: "diagnostic", where, message: `"${ref}" is not a @hand, @world or @story property reference` });
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (!found) {
|
|
1637
|
+
this.emit({ type: "diagnostic", where, message: `"${ref}" fills a tag group that is not in this box` });
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
let value;
|
|
1641
|
+
if (parsed.scope === "hand") {
|
|
1642
|
+
value = this.valuesOf("hand", hand.id)[parsed.name];
|
|
1643
|
+
} else {
|
|
1644
|
+
try {
|
|
1645
|
+
value = this.getProperty(`${parsed.scope}.${parsed.name}`);
|
|
1646
|
+
} catch {
|
|
1647
|
+
value = void 0;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (value === void 0) {
|
|
1651
|
+
this.emit({ type: "diagnostic", where, message: `"${ref}" names a property that is not declared` });
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const wanted = typeof value === "string" ? value : String(value);
|
|
1655
|
+
const tag = found.group.tags.find((t) => effectiveGameId(t) === wanted);
|
|
1656
|
+
if (!tag) {
|
|
1657
|
+
this.emit({ type: "diagnostic", where, message: `${ref} is "${wanted}", which is not one of the tags of "${groupName}"` });
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
boundTags.set(groupId, tag.id);
|
|
1661
|
+
askNames[groupName] = effectiveGameId(tag);
|
|
1662
|
+
}
|
|
1256
1663
|
/**
|
|
1257
1664
|
* Bind every `boundBy` group in the box from the property it names.
|
|
1258
1665
|
*
|
|
@@ -1383,8 +1790,8 @@ var Flow = class {
|
|
|
1383
1790
|
runAsk(ask, claimed, trace) {
|
|
1384
1791
|
const { box } = ask;
|
|
1385
1792
|
const handEnv = this.buildHandEnv(ask);
|
|
1386
|
-
const verdict = (
|
|
1387
|
-
trace?.push({ id, verdict: v });
|
|
1793
|
+
const verdict = (card, v) => {
|
|
1794
|
+
trace?.push({ id: effectiveGameId(card), verdict: v });
|
|
1388
1795
|
};
|
|
1389
1796
|
if (!this.passes(ask.condition, this.evalCtx(box, void 0, handEnv), `hand ${ask.hand ? effectiveGameId(ask.hand) : ""} condition`)) {
|
|
1390
1797
|
return { ordered: [], handEnv };
|
|
@@ -1401,19 +1808,19 @@ var Flow = class {
|
|
|
1401
1808
|
for (const card of deck.cards) {
|
|
1402
1809
|
const shared = cardIsShared(card, deckShared);
|
|
1403
1810
|
if (!gateOk.get(deck.id)) {
|
|
1404
|
-
verdict(card
|
|
1811
|
+
verdict(card, "deck-gate");
|
|
1405
1812
|
continue;
|
|
1406
1813
|
}
|
|
1407
1814
|
if (shared && this.engine.isTaken(card.id)) {
|
|
1408
|
-
verdict(card
|
|
1815
|
+
verdict(card, "taken");
|
|
1409
1816
|
continue;
|
|
1410
1817
|
}
|
|
1411
1818
|
if ((this.cooldowns[card.id] ?? 0) > turn) {
|
|
1412
|
-
verdict(card
|
|
1819
|
+
verdict(card, "cooldown");
|
|
1413
1820
|
continue;
|
|
1414
1821
|
}
|
|
1415
1822
|
if (!this.tagsMatch(card, handEnv.boundTags)) {
|
|
1416
|
-
verdict(card
|
|
1823
|
+
verdict(card, "tags");
|
|
1417
1824
|
continue;
|
|
1418
1825
|
}
|
|
1419
1826
|
if (card.condition && !this.passes(
|
|
@@ -1421,12 +1828,12 @@ var Flow = class {
|
|
|
1421
1828
|
deckCtx,
|
|
1422
1829
|
this.tracing ? `card ${card.gameId} condition` : void 0
|
|
1423
1830
|
)) {
|
|
1424
|
-
verdict(card
|
|
1831
|
+
verdict(card, "condition");
|
|
1425
1832
|
continue;
|
|
1426
1833
|
}
|
|
1427
1834
|
const refused = claimed(card, shared);
|
|
1428
1835
|
if (refused) {
|
|
1429
|
-
verdict(card
|
|
1836
|
+
verdict(card, refused);
|
|
1430
1837
|
continue;
|
|
1431
1838
|
}
|
|
1432
1839
|
let priority;
|
|
@@ -1436,7 +1843,7 @@ var Flow = class {
|
|
|
1436
1843
|
try {
|
|
1437
1844
|
const v = this.eval(card.priority, deckCtx);
|
|
1438
1845
|
if (typeof v !== "number") {
|
|
1439
|
-
verdict(card
|
|
1846
|
+
verdict(card, "priority");
|
|
1440
1847
|
continue;
|
|
1441
1848
|
}
|
|
1442
1849
|
priority = v;
|
|
@@ -1444,7 +1851,7 @@ var Flow = class {
|
|
|
1444
1851
|
if (this.tracing) {
|
|
1445
1852
|
this.emit({ type: "diagnostic", where: `card ${card.gameId} priority`, message: e instanceof Error ? e.message : String(e) });
|
|
1446
1853
|
}
|
|
1447
|
-
verdict(card
|
|
1854
|
+
verdict(card, "priority");
|
|
1448
1855
|
continue;
|
|
1449
1856
|
}
|
|
1450
1857
|
}
|
|
@@ -1475,11 +1882,13 @@ var Flow = class {
|
|
|
1475
1882
|
i = j;
|
|
1476
1883
|
}
|
|
1477
1884
|
for (const s of scored) {
|
|
1478
|
-
trace?.push({ id: s.entry.card
|
|
1885
|
+
trace?.push({ id: effectiveGameId(s.entry.card), verdict: "dealt", priority: s.priority, specificity: s.spec });
|
|
1479
1886
|
}
|
|
1480
1887
|
return { ordered: scored.map((s) => s.entry), handEnv };
|
|
1481
1888
|
}
|
|
1482
|
-
/** Flip eligible-but-not-taken trace entries to "capped".
|
|
1889
|
+
/** Flip eligible-but-not-taken trace entries to "capped". `taken` is keyed
|
|
1890
|
+
* by GAMEID, as the trace rows are (4.4): the two must move together or
|
|
1891
|
+
* every dealt card silently reads as capped. */
|
|
1483
1892
|
capTrace(trace, taken) {
|
|
1484
1893
|
for (const entry of trace) {
|
|
1485
1894
|
if (entry.verdict === "dealt" && !taken.has(entry.id)) entry.verdict = "capped";
|
|
@@ -1520,7 +1929,7 @@ var Flow = class {
|
|
|
1520
1929
|
const { ordered } = this.runAsk(ask, (card, shared) => this.claimVerdict(card, shared, claimCounts, worldClaims), trace);
|
|
1521
1930
|
const listed = n === void 0 ? ordered : ordered.slice(0, Math.max(n, 0));
|
|
1522
1931
|
if (trace) {
|
|
1523
|
-
this.capTrace(trace, new Set(listed.map((e) => e.card
|
|
1932
|
+
this.capTrace(trace, new Set(listed.map((e) => effectiveGameId(e.card))));
|
|
1524
1933
|
this.emit({ type: "peek", box: effectiveGameId(box), criteria, cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
1525
1934
|
}
|
|
1526
1935
|
return { box: effectiveGameId(box), cards: listed.map((e) => this.view(e)) };
|
|
@@ -1550,7 +1959,8 @@ var Flow = class {
|
|
|
1550
1959
|
const turn = this.turnCounts.get(box.id) ?? 0;
|
|
1551
1960
|
const evicted = [];
|
|
1552
1961
|
const evict = (cardId, reason) => {
|
|
1553
|
-
|
|
1962
|
+
const known = this.internals.cardsById.get(cardId);
|
|
1963
|
+
evicted.push({ card: known ? effectiveGameId(known.card) : cardId, reason });
|
|
1554
1964
|
return false;
|
|
1555
1965
|
};
|
|
1556
1966
|
const survivors = (this.boardContents.get(hand.id) ?? []).filter((cardId) => {
|
|
@@ -1567,7 +1977,7 @@ var Flow = class {
|
|
|
1567
1977
|
});
|
|
1568
1978
|
this.boardContents.set(hand.id, survivors);
|
|
1569
1979
|
if (this.tracing) {
|
|
1570
|
-
for (const e of evicted) this.emit({ type: "evict", hand: hand
|
|
1980
|
+
for (const e of evicted) this.emit({ type: "evict", hand: effectiveGameId(hand), card: e.card, reason: e.reason }, turn);
|
|
1571
1981
|
}
|
|
1572
1982
|
}
|
|
1573
1983
|
const claimCounts = this.claims();
|
|
@@ -1584,14 +1994,15 @@ var Flow = class {
|
|
|
1584
1994
|
(card, shared) => own.has(card.id) ? "claimed" : this.claimVerdict(card, shared, claimCounts, worldClaims),
|
|
1585
1995
|
trace
|
|
1586
1996
|
);
|
|
1587
|
-
const
|
|
1997
|
+
const taking = ordered.slice(0, free);
|
|
1998
|
+
const added = taking.map((e) => e.card.id);
|
|
1588
1999
|
this.boardContents.set(hand.id, [...contents, ...added]);
|
|
1589
2000
|
for (const id of added) {
|
|
1590
2001
|
claimCounts.set(id, (claimCounts.get(id) ?? 0) + 1);
|
|
1591
2002
|
worldClaims.set(id, (worldClaims.get(id) ?? 0) + 1);
|
|
1592
2003
|
}
|
|
1593
2004
|
if (trace) {
|
|
1594
|
-
this.capTrace(trace, new Set(
|
|
2005
|
+
this.capTrace(trace, new Set(taking.map((e) => effectiveGameId(e.card))));
|
|
1595
2006
|
this.emit({ type: "deal", hand: effectiveGameId(hand), cards: trace }, this.turnCounts.get(box.id) ?? 0);
|
|
1596
2007
|
}
|
|
1597
2008
|
}
|
|
@@ -1659,7 +2070,8 @@ var Flow = class {
|
|
|
1659
2070
|
if (!this.passes(outcome.condition, ctx)) {
|
|
1660
2071
|
throw new Error(`outcome "${outcomeGameId}" on "${effectiveGameId(entry.card)}" is gated shut`);
|
|
1661
2072
|
}
|
|
1662
|
-
const
|
|
2073
|
+
const perPlay = entry.box.turn !== void 0 ? 0 : this.internals.bundle.settings.playAdvancesTurns;
|
|
2074
|
+
const newTurn = (this.turnCounts.get(entry.box.id) ?? 0) + (opts.advanceTurns ?? perPlay);
|
|
1663
2075
|
const writes = [];
|
|
1664
2076
|
for (const [target, expr] of Object.entries(outcome.changes)) {
|
|
1665
2077
|
writes.push({ target, value: this.eval(expr, ctx) });
|
|
@@ -1683,7 +2095,11 @@ var Flow = class {
|
|
|
1683
2095
|
(this.boardContents.get(handId) ?? []).filter((id) => id !== entry.card.id)
|
|
1684
2096
|
);
|
|
1685
2097
|
this.turnCounts.set(entry.box.id, newTurn);
|
|
1686
|
-
if (this.tracing) this.emit({ type: "play", card: entry.card
|
|
2098
|
+
if (this.tracing) this.emit({ type: "play", card: effectiveGameId(entry.card), outcome: effectiveGameId(outcome), turn: newTurn }, newTurn);
|
|
2099
|
+
}
|
|
2100
|
+
/** One owned property's address, owner segment and all (4.4). */
|
|
2101
|
+
address(kind, id) {
|
|
2102
|
+
return addressOf(this.internals, kind, id);
|
|
1687
2103
|
}
|
|
1688
2104
|
/** Land one change in whichever partition declares the name: the flow's
|
|
1689
2105
|
* bag when the property is per-flow, the shared bag when it is shared -
|
|
@@ -1704,24 +2120,24 @@ var Flow = class {
|
|
|
1704
2120
|
const [, scope, name] = match;
|
|
1705
2121
|
switch (scope) {
|
|
1706
2122
|
case "world": {
|
|
1707
|
-
const
|
|
1708
|
-
if (!
|
|
2123
|
+
const worldSet = this.internals.worldSet;
|
|
2124
|
+
if (!worldSet) throw new Error(`@world.${name} cannot be written: the host bound @world read-only`);
|
|
1709
2125
|
if (this.internals.worldReadOnly.has(name)) throw new Error(`'@world.${name}' is read-only (writable: false)`);
|
|
1710
|
-
const prev =
|
|
1711
|
-
|
|
2126
|
+
const prev = this.internals.worldResolver.get(name);
|
|
2127
|
+
worldSet(name, value);
|
|
1712
2128
|
return { path: `world.${name}`, ...prev !== void 0 ? { prev } : {} };
|
|
1713
2129
|
}
|
|
1714
2130
|
case "story":
|
|
1715
2131
|
return this.landIn("story", void 0, name, value, `story.${name}`);
|
|
1716
2132
|
case "box":
|
|
1717
|
-
return this.landIn("box", entry.box.id, name, value,
|
|
2133
|
+
return this.landIn("box", entry.box.id, name, value, `${this.address("box", entry.box.id)}.${name}`);
|
|
1718
2134
|
case "deck":
|
|
1719
|
-
return this.landIn("deck", entry.deck.id, name, value,
|
|
2135
|
+
return this.landIn("deck", entry.deck.id, name, value, `${this.address("deck", entry.deck.id)}.${name}`);
|
|
1720
2136
|
case "hand": {
|
|
1721
2137
|
const source = handEnv.sources.get(name);
|
|
1722
2138
|
if (!source) throw new Error(`@hand.${name} is not composed in this ask`);
|
|
1723
2139
|
if (source.kind === "criteria") throw new Error(`@hand.${name} is a chosen tag / criteria name and cannot be written`);
|
|
1724
|
-
return this.landIn(source.kind, source.id, name, value, `${source.kind
|
|
2140
|
+
return this.landIn(source.kind, source.id, name, value, `${this.address(source.kind, source.id)}.${name}`);
|
|
1725
2141
|
}
|
|
1726
2142
|
default:
|
|
1727
2143
|
throw new Error(`bad change target scope "@${scope}"`);
|
|
@@ -1757,7 +2173,7 @@ var Flow = class {
|
|
|
1757
2173
|
this.assertOpen();
|
|
1758
2174
|
const mounts = [{ prefix: "story", bag: this.stores.story }];
|
|
1759
2175
|
for (const kind of ["box", "deck", "hand", "value"]) {
|
|
1760
|
-
for (const [id, bag] of this.stores[kind]) mounts.push({ prefix:
|
|
2176
|
+
for (const [id, bag] of this.stores[kind]) mounts.push({ prefix: addressOf(this.internals, kind, id), bag });
|
|
1761
2177
|
}
|
|
1762
2178
|
return mounts;
|
|
1763
2179
|
}
|
|
@@ -1780,10 +2196,15 @@ var Flow = class {
|
|
|
1780
2196
|
...d.values !== void 0 ? { values: d.values } : {},
|
|
1781
2197
|
...d.stages !== void 0 ? { stages: d.stages } : {},
|
|
1782
2198
|
// @world is FOREIGN - a host resolver backs it - so writability is whether that
|
|
1783
|
-
// resolver can be written at all
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
|
|
2199
|
+
// resolver can be written at all AND what the declaration says, which is the
|
|
2200
|
+
// shared registry's own rule for a foreign scope (its foreignWritable). The
|
|
2201
|
+
// `as PropertyView` cast this replaced was hiding the field's absence: the row
|
|
2202
|
+
// type has always required it, and these rows shipped without one.
|
|
2203
|
+
//
|
|
2204
|
+
// A row is where `writable: false` is meant to SHOW (Reboot.md 10): it tells a
|
|
2205
|
+
// state panel this is the game's value, not the story's. It does not stop the
|
|
2206
|
+
// panel editing it - the host's setProperty passes `{ host: true }`.
|
|
2207
|
+
writable: this.internals.worldSet !== void 0 && !this.internals.worldReadOnly.has(d.name)
|
|
1787
2208
|
});
|
|
1788
2209
|
}
|
|
1789
2210
|
const add = (_prefix, shared, own) => {
|
|
@@ -1793,15 +2214,22 @@ var Flow = class {
|
|
|
1793
2214
|
}
|
|
1794
2215
|
};
|
|
1795
2216
|
add("story", this.internals.shared.story, this.stores.story);
|
|
1796
|
-
for (const kind of
|
|
2217
|
+
for (const kind of OWNED_SCOPES) {
|
|
1797
2218
|
const ids = /* @__PURE__ */ new Set([...this.internals.shared[kind].keys(), ...this.stores[kind].keys()]);
|
|
1798
|
-
for (const id of ids)
|
|
2219
|
+
for (const id of ids) {
|
|
2220
|
+
add(addressOf(this.internals, kind, id), this.internals.shared[kind].get(id), this.stores[kind].get(id));
|
|
2221
|
+
}
|
|
1799
2222
|
}
|
|
1800
2223
|
return out;
|
|
1801
2224
|
}
|
|
1802
|
-
/** Read by path: "world.x", "story.gold", "value.
|
|
1803
|
-
* "box.
|
|
1804
|
-
* merged view, routed by the declaration's sharing.
|
|
2225
|
+
/** Read by path: "world.x", "story.gold", "value.docks.danger",
|
|
2226
|
+
* "box.village.heat", "deck.wares.n", "hand.the-elder.zone" - the flow's
|
|
2227
|
+
* merged view, routed by the declaration's sharing.
|
|
2228
|
+
*
|
|
2229
|
+
* The owner segment is the entity's GAMEID, the name it is called by
|
|
2230
|
+
* everywhere else (4.4). Its internal id is accepted for this release and
|
|
2231
|
+
* earns a `diagnostic` naming the address to move to; the next lockstep
|
|
2232
|
+
* release refuses it. */
|
|
1805
2233
|
getProperty(path) {
|
|
1806
2234
|
this.assertOpen();
|
|
1807
2235
|
const found = this.resolvePath(path);
|
|
@@ -1813,13 +2241,13 @@ var Flow = class {
|
|
|
1813
2241
|
this.assertOpen();
|
|
1814
2242
|
const found = this.resolvePath(path);
|
|
1815
2243
|
if (found.kind === "world") {
|
|
1816
|
-
if (!this.internals.
|
|
1817
|
-
this.internals.
|
|
2244
|
+
if (!this.internals.worldSet) throw new Error(`@world is read-only here: the host bound no write`);
|
|
2245
|
+
this.internals.worldSet(found.name, value, true);
|
|
1818
2246
|
return;
|
|
1819
2247
|
}
|
|
1820
2248
|
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;
|
|
1821
2249
|
if (bag === void 0) throw new Error(`no property at "${path}"`);
|
|
1822
|
-
bag.set(found.name, value, { silent: true, reason: "host setProperty" });
|
|
2250
|
+
bag.set(found.name, value, { silent: true, reason: "host setProperty", host: true });
|
|
1823
2251
|
}
|
|
1824
2252
|
resolvePath(path) {
|
|
1825
2253
|
const parts = path.split(".");
|
|
@@ -1829,10 +2257,15 @@ var Flow = class {
|
|
|
1829
2257
|
}
|
|
1830
2258
|
if (parts.length === 3 && (parts[0] === "box" || parts[0] === "deck" || parts[0] === "hand" || parts[0] === "value")) {
|
|
1831
2259
|
const kind = parts[0];
|
|
1832
|
-
const
|
|
1833
|
-
const
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
2260
|
+
const [, segment, name] = parts;
|
|
2261
|
+
const owner = ownerOrThrow(this.internals, kind, segment, name);
|
|
2262
|
+
if (owner.legacy && this.tracing) {
|
|
2263
|
+
this.emit({ type: "diagnostic", where: "property address", message: legacyAddressMessage(this.internals, kind, segment, name) });
|
|
2264
|
+
}
|
|
2265
|
+
const own = this.stores[kind].get(owner.id);
|
|
2266
|
+
const shared = this.internals.shared[kind].get(owner.id);
|
|
2267
|
+
if (own === void 0 && shared === void 0) throw new Error(`no ${kind} store "${segment}"`);
|
|
2268
|
+
return { kind: "bag", ...own !== void 0 ? { own } : {}, ...shared !== void 0 ? { shared } : {}, name };
|
|
1836
2269
|
}
|
|
1837
2270
|
throw new Error(`bad property path "${path}"`);
|
|
1838
2271
|
}
|
|
@@ -1873,14 +2306,27 @@ var summarise = (decls) => decls.map((d) => ({
|
|
|
1873
2306
|
type: d.type,
|
|
1874
2307
|
default: d.default,
|
|
1875
2308
|
...d.values !== void 0 ? { values: d.values } : {},
|
|
2309
|
+
...d.durable === true ? { durable: true } : {},
|
|
1876
2310
|
...d.purpose !== void 0 ? { purpose: d.purpose } : {}
|
|
1877
2311
|
}));
|
|
2312
|
+
var durableCardCount = (box) => box.decks.reduce((n, deck) => n + deck.cards.filter((card) => (card.durable ?? deck.durable) === true).length, 0);
|
|
1878
2313
|
var handDecls = (hand, box) => {
|
|
1879
2314
|
if (hand.template !== void 0) {
|
|
1880
2315
|
return box.handTemplates.find((t) => t.id === hand.template)?.properties ?? [];
|
|
1881
2316
|
}
|
|
1882
2317
|
return hand.properties ?? [];
|
|
1883
2318
|
};
|
|
2319
|
+
var movableHoles = (hand, box) => {
|
|
2320
|
+
const filled = hand.template !== void 0 ? hand.chosen : hand.rule?.bindings;
|
|
2321
|
+
const out = [];
|
|
2322
|
+
for (const [groupId, value] of Object.entries(filled ?? {})) {
|
|
2323
|
+
if (!isHoleRef(value)) continue;
|
|
2324
|
+
const group = box.tagGroups.find((g) => g.id === groupId);
|
|
2325
|
+
if (group === void 0) continue;
|
|
2326
|
+
out.push({ group: effectiveGameId(group), from: value });
|
|
2327
|
+
}
|
|
2328
|
+
return out;
|
|
2329
|
+
};
|
|
1884
2330
|
var handSlots = (hand, box) => {
|
|
1885
2331
|
if (hand.slots !== void 0) return hand.slots;
|
|
1886
2332
|
const declared = hand.template !== void 0 ? box.handTemplates.find((t) => t.id === hand.template)?.slots : hand.rule?.slots;
|
|
@@ -1901,6 +2347,8 @@ function describeBundle(bundle) {
|
|
|
1901
2347
|
gameId: boxGameId,
|
|
1902
2348
|
...box.title !== void 0 ? { title: box.title } : {},
|
|
1903
2349
|
ranking: { specificity: box.ranking.specificity },
|
|
2350
|
+
...box.turn !== void 0 ? { turn: { seconds: box.turn.seconds } } : {},
|
|
2351
|
+
...durableCardCount(box) > 0 ? { durableCards: durableCardCount(box) } : {},
|
|
1904
2352
|
tagGroups: box.tagGroups.map((group) => ({
|
|
1905
2353
|
gameId: effectiveGameId(group),
|
|
1906
2354
|
tags: group.tags.map((tag) => effectiveGameId(tag))
|
|
@@ -1921,12 +2369,14 @@ function describeBundle(bundle) {
|
|
|
1921
2369
|
totals.tagGroups += box.tagGroups.length;
|
|
1922
2370
|
for (const hand of box.hands) {
|
|
1923
2371
|
const template = hand.template !== void 0 ? box.handTemplates.find((t) => t.id === hand.template) : void 0;
|
|
2372
|
+
const movable = movableHoles(hand, box);
|
|
1924
2373
|
hands.push({
|
|
1925
2374
|
gameId: effectiveGameId(hand),
|
|
1926
2375
|
...hand.title !== void 0 ? { title: hand.title } : {},
|
|
1927
2376
|
box: boxGameId,
|
|
1928
2377
|
slots: handSlots(hand, box),
|
|
1929
|
-
...template !== void 0 ? { template: effectiveGameId(template) } : {}
|
|
2378
|
+
...template !== void 0 ? { template: effectiveGameId(template) } : {},
|
|
2379
|
+
...movable.length > 0 ? { movable } : {}
|
|
1930
2380
|
});
|
|
1931
2381
|
}
|
|
1932
2382
|
const push = (scope, owner, decls, group) => {
|
|
@@ -1964,7 +2414,8 @@ function describeBundle(bundle) {
|
|
|
1964
2414
|
box: map.box,
|
|
1965
2415
|
group: map.group,
|
|
1966
2416
|
zones: map.zones.length,
|
|
1967
|
-
backgrounds: map.backgrounds?.length ?? 0
|
|
2417
|
+
backgrounds: map.backgrounds?.length ?? 0,
|
|
2418
|
+
sites: map.sites?.length ?? 0
|
|
1968
2419
|
}))
|
|
1969
2420
|
};
|
|
1970
2421
|
}
|