@bct-app/game-engine 0.1.17 → 0.1.18

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/dist/index.js CHANGED
@@ -572,11 +572,6 @@ var resolveDynamicValue = (value, payloads, isExpected) => {
572
572
  var getSortedPlayers = (playerSeatMap) => {
573
573
  return [...playerSeatMap.values()].sort((left, right) => left.seat - right.seat);
574
574
  };
575
- var getCircularDistance = (anchorIdx, targetIdx, total) => {
576
- const clockwise = (targetIdx - anchorIdx + total) % total;
577
- const anticlockwise = (anchorIdx - targetIdx + total) % total;
578
- return Math.min(clockwise, anticlockwise);
579
- };
580
575
  var getAnchorSeat = (dynamicTarget, payloads, effector) => {
581
576
  const anchor = dynamicTarget.anchor;
582
577
  if (!anchor || anchor.from === "EFFECTOR") {
@@ -595,42 +590,6 @@ var resolveSelectorScope = (dynamicTarget, payloads) => {
595
590
  (value) => value === "BOTH_SIDES" || value === "LEFT_SIDE" || value === "RIGHT_SIDE"
596
591
  ) ?? "BOTH_SIDES";
597
592
  };
598
- var getCandidatesBySelector = (dynamicTarget, sortedPlayers, anchorSeat, payloads) => {
599
- const selector = dynamicTarget.selector;
600
- if (!selector) {
601
- return sortedPlayers;
602
- }
603
- const scope = resolveSelectorScope(dynamicTarget, payloads);
604
- if (!anchorSeat) {
605
- return [];
606
- }
607
- const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
608
- if (anchorIdx < 0) {
609
- return [];
610
- }
611
- const total = sortedPlayers.length;
612
- if (scope === "LEFT_SIDE") {
613
- const left2 = [];
614
- for (let step = 1; step < total; step += 1) {
615
- left2.push(sortedPlayers[(anchorIdx + step) % total]);
616
- }
617
- return left2;
618
- }
619
- if (scope === "RIGHT_SIDE") {
620
- const right2 = [];
621
- for (let step = 1; step < total; step += 1) {
622
- right2.push(sortedPlayers[(anchorIdx - step + total) % total]);
623
- }
624
- return right2;
625
- }
626
- const left = [];
627
- const right = [];
628
- for (let step = 1; step < total; step += 1) {
629
- left.push(sortedPlayers[(anchorIdx + step) % total]);
630
- right.push(sortedPlayers[(anchorIdx - step + total) % total]);
631
- }
632
- return [...left, ...right];
633
- };
634
593
  var matchesCondition = (player, condition, payloads, characterMap) => {
635
594
  if (condition.field === "IS_DEAD") {
636
595
  const expected = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "boolean");
@@ -659,55 +618,72 @@ var matchesCondition = (player, condition, payloads, characterMap) => {
659
618
  const expectedKind = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "string");
660
619
  return typeof expectedKind === "string" && kind === expectedKind;
661
620
  };
662
- var filterByWhere = (candidates, dynamicTarget, payloads, characterMap) => {
621
+ var passesWhere = (player, dynamicTarget, payloads, characterMap) => {
663
622
  const where = dynamicTarget.where;
664
623
  const conditions = where?.conditions ?? [];
665
- if (conditions.length === 0) {
666
- return candidates;
667
- }
624
+ if (conditions.length === 0) return true;
668
625
  const isAllMode = where?.mode !== "ANY";
669
- return candidates.filter((player) => {
670
- const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
671
- return isAllMode ? results.every(Boolean) : results.some(Boolean);
672
- });
626
+ const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
627
+ return isAllMode ? results.every(Boolean) : results.some(Boolean);
673
628
  };
674
- var applySort = (candidates, dynamicTarget, payloads, sortedPlayers, anchorSeat) => {
629
+ var walkSidesWithFilter = (dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap) => {
675
630
  const scope = resolveSelectorScope(dynamicTarget, payloads);
676
- if (scope !== "BOTH_SIDES") {
677
- return [...candidates];
678
- }
679
- if (!anchorSeat || sortedPlayers.length <= 1) {
680
- return [...candidates].sort((left, right) => left.seat - right.seat);
681
- }
682
- const seatIndexMap = new Map(sortedPlayers.map((player, idx) => [player.seat, idx]));
683
- const anchorIdx = seatIndexMap.get(anchorSeat);
684
- if (typeof anchorIdx !== "number") {
685
- return [...candidates].sort((left, right) => left.seat - right.seat);
686
- }
687
- return [...candidates].sort((left, right) => {
688
- const leftIdx = seatIndexMap.get(left.seat);
689
- const rightIdx = seatIndexMap.get(right.seat);
690
- if (typeof leftIdx !== "number" || typeof rightIdx !== "number") {
691
- return left.seat - right.seat;
631
+ if (!anchorSeat) return { left: [], right: [] };
632
+ const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
633
+ if (anchorIdx < 0) return { left: [], right: [] };
634
+ const total = sortedPlayers.length;
635
+ const left = [];
636
+ const right = [];
637
+ if (scope === "LEFT_SIDE" || scope === "BOTH_SIDES") {
638
+ for (let step = 1; step < total; step += 1) {
639
+ const player = sortedPlayers[(anchorIdx + step) % total];
640
+ if (passesWhere(player, dynamicTarget, payloads, characterMap)) left.push(player);
692
641
  }
693
- const leftDistance = getCircularDistance(anchorIdx, leftIdx, sortedPlayers.length);
694
- const rightDistance = getCircularDistance(anchorIdx, rightIdx, sortedPlayers.length);
695
- if (leftDistance !== rightDistance) {
696
- return leftDistance - rightDistance;
642
+ }
643
+ if (scope === "RIGHT_SIDE" || scope === "BOTH_SIDES") {
644
+ for (let step = 1; step < total; step += 1) {
645
+ const player = sortedPlayers[(anchorIdx - step + total) % total];
646
+ if (passesWhere(player, dynamicTarget, payloads, characterMap)) right.push(player);
697
647
  }
698
- return left.seat - right.seat;
699
- });
648
+ }
649
+ return { left, right };
700
650
  };
701
- var pickTargets = (sortedCandidates, dynamicTarget, payloads) => {
702
- const dedupedCandidates = [...new Map(sortedCandidates.map((player) => [player.seat, player])).values()];
651
+ var pickBalanced = (sides, dynamicTarget, payloads) => {
703
652
  const configuredLimit = resolveDynamicValue(dynamicTarget.limit, payloads, (value) => typeof value === "number");
653
+ const seen = /* @__PURE__ */ new Set();
654
+ const out = [];
655
+ const pushUnique = (player) => {
656
+ if (!player || seen.has(player.seat)) return false;
657
+ seen.add(player.seat);
658
+ out.push(player);
659
+ return true;
660
+ };
704
661
  if (typeof configuredLimit === "undefined") {
705
- return dedupedCandidates;
706
- }
707
- if (configuredLimit <= 0) {
708
- return [];
662
+ sides.left.forEach(pushUnique);
663
+ sides.right.forEach(pushUnique);
664
+ return out;
665
+ }
666
+ if (configuredLimit <= 0) return [];
667
+ let li = 0;
668
+ let ri = 0;
669
+ let preferLeft = true;
670
+ while (out.length < configuredLimit && (li < sides.left.length || ri < sides.right.length)) {
671
+ if (preferLeft && li < sides.left.length) {
672
+ if (pushUnique(sides.left[li])) li += 1;
673
+ else li += 1;
674
+ } else if (!preferLeft && ri < sides.right.length) {
675
+ if (pushUnique(sides.right[ri])) ri += 1;
676
+ else ri += 1;
677
+ } else if (li < sides.left.length) {
678
+ if (pushUnique(sides.left[li])) li += 1;
679
+ else li += 1;
680
+ } else if (ri < sides.right.length) {
681
+ if (pushUnique(sides.right[ri])) ri += 1;
682
+ else ri += 1;
683
+ }
684
+ preferLeft = !preferLeft;
709
685
  }
710
- return dedupedCandidates.slice(0, configuredLimit);
686
+ return out;
711
687
  };
712
688
  var resolveEffectTargets = ({
713
689
  effect,
@@ -740,10 +716,12 @@ var resolveEffectTargets = ({
740
716
  const dynamicTarget = effect.target;
741
717
  const sortedPlayers = getSortedPlayers(playerSeatMap);
742
718
  const anchorSeat = getAnchorSeat(dynamicTarget, payloads, effector);
743
- const candidates = getCandidatesBySelector(dynamicTarget, sortedPlayers, anchorSeat, payloads);
744
- const filtered = filterByWhere(candidates, dynamicTarget, payloads, characterMap);
745
- const ordered = applySort(filtered, dynamicTarget, payloads, sortedPlayers, anchorSeat);
746
- return pickTargets(ordered, dynamicTarget, payloads);
719
+ if (!dynamicTarget.selector) {
720
+ const matches = sortedPlayers.filter((player) => passesWhere(player, dynamicTarget, payloads, characterMap));
721
+ return pickBalanced({ left: matches, right: [] }, dynamicTarget, payloads);
722
+ }
723
+ const sides = walkSidesWithFilter(dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap);
724
+ return pickBalanced(sides, dynamicTarget, payloads);
747
725
  }
748
726
  return [];
749
727
  };
package/dist/index.mjs CHANGED
@@ -532,11 +532,6 @@ var resolveDynamicValue = (value, payloads, isExpected) => {
532
532
  var getSortedPlayers = (playerSeatMap) => {
533
533
  return [...playerSeatMap.values()].sort((left, right) => left.seat - right.seat);
534
534
  };
535
- var getCircularDistance = (anchorIdx, targetIdx, total) => {
536
- const clockwise = (targetIdx - anchorIdx + total) % total;
537
- const anticlockwise = (anchorIdx - targetIdx + total) % total;
538
- return Math.min(clockwise, anticlockwise);
539
- };
540
535
  var getAnchorSeat = (dynamicTarget, payloads, effector) => {
541
536
  const anchor = dynamicTarget.anchor;
542
537
  if (!anchor || anchor.from === "EFFECTOR") {
@@ -555,42 +550,6 @@ var resolveSelectorScope = (dynamicTarget, payloads) => {
555
550
  (value) => value === "BOTH_SIDES" || value === "LEFT_SIDE" || value === "RIGHT_SIDE"
556
551
  ) ?? "BOTH_SIDES";
557
552
  };
558
- var getCandidatesBySelector = (dynamicTarget, sortedPlayers, anchorSeat, payloads) => {
559
- const selector = dynamicTarget.selector;
560
- if (!selector) {
561
- return sortedPlayers;
562
- }
563
- const scope = resolveSelectorScope(dynamicTarget, payloads);
564
- if (!anchorSeat) {
565
- return [];
566
- }
567
- const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
568
- if (anchorIdx < 0) {
569
- return [];
570
- }
571
- const total = sortedPlayers.length;
572
- if (scope === "LEFT_SIDE") {
573
- const left2 = [];
574
- for (let step = 1; step < total; step += 1) {
575
- left2.push(sortedPlayers[(anchorIdx + step) % total]);
576
- }
577
- return left2;
578
- }
579
- if (scope === "RIGHT_SIDE") {
580
- const right2 = [];
581
- for (let step = 1; step < total; step += 1) {
582
- right2.push(sortedPlayers[(anchorIdx - step + total) % total]);
583
- }
584
- return right2;
585
- }
586
- const left = [];
587
- const right = [];
588
- for (let step = 1; step < total; step += 1) {
589
- left.push(sortedPlayers[(anchorIdx + step) % total]);
590
- right.push(sortedPlayers[(anchorIdx - step + total) % total]);
591
- }
592
- return [...left, ...right];
593
- };
594
553
  var matchesCondition = (player, condition, payloads, characterMap) => {
595
554
  if (condition.field === "IS_DEAD") {
596
555
  const expected = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "boolean");
@@ -619,55 +578,72 @@ var matchesCondition = (player, condition, payloads, characterMap) => {
619
578
  const expectedKind = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "string");
620
579
  return typeof expectedKind === "string" && kind === expectedKind;
621
580
  };
622
- var filterByWhere = (candidates, dynamicTarget, payloads, characterMap) => {
581
+ var passesWhere = (player, dynamicTarget, payloads, characterMap) => {
623
582
  const where = dynamicTarget.where;
624
583
  const conditions = where?.conditions ?? [];
625
- if (conditions.length === 0) {
626
- return candidates;
627
- }
584
+ if (conditions.length === 0) return true;
628
585
  const isAllMode = where?.mode !== "ANY";
629
- return candidates.filter((player) => {
630
- const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
631
- return isAllMode ? results.every(Boolean) : results.some(Boolean);
632
- });
586
+ const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
587
+ return isAllMode ? results.every(Boolean) : results.some(Boolean);
633
588
  };
634
- var applySort = (candidates, dynamicTarget, payloads, sortedPlayers, anchorSeat) => {
589
+ var walkSidesWithFilter = (dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap) => {
635
590
  const scope = resolveSelectorScope(dynamicTarget, payloads);
636
- if (scope !== "BOTH_SIDES") {
637
- return [...candidates];
638
- }
639
- if (!anchorSeat || sortedPlayers.length <= 1) {
640
- return [...candidates].sort((left, right) => left.seat - right.seat);
641
- }
642
- const seatIndexMap = new Map(sortedPlayers.map((player, idx) => [player.seat, idx]));
643
- const anchorIdx = seatIndexMap.get(anchorSeat);
644
- if (typeof anchorIdx !== "number") {
645
- return [...candidates].sort((left, right) => left.seat - right.seat);
646
- }
647
- return [...candidates].sort((left, right) => {
648
- const leftIdx = seatIndexMap.get(left.seat);
649
- const rightIdx = seatIndexMap.get(right.seat);
650
- if (typeof leftIdx !== "number" || typeof rightIdx !== "number") {
651
- return left.seat - right.seat;
591
+ if (!anchorSeat) return { left: [], right: [] };
592
+ const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
593
+ if (anchorIdx < 0) return { left: [], right: [] };
594
+ const total = sortedPlayers.length;
595
+ const left = [];
596
+ const right = [];
597
+ if (scope === "LEFT_SIDE" || scope === "BOTH_SIDES") {
598
+ for (let step = 1; step < total; step += 1) {
599
+ const player = sortedPlayers[(anchorIdx + step) % total];
600
+ if (passesWhere(player, dynamicTarget, payloads, characterMap)) left.push(player);
652
601
  }
653
- const leftDistance = getCircularDistance(anchorIdx, leftIdx, sortedPlayers.length);
654
- const rightDistance = getCircularDistance(anchorIdx, rightIdx, sortedPlayers.length);
655
- if (leftDistance !== rightDistance) {
656
- return leftDistance - rightDistance;
602
+ }
603
+ if (scope === "RIGHT_SIDE" || scope === "BOTH_SIDES") {
604
+ for (let step = 1; step < total; step += 1) {
605
+ const player = sortedPlayers[(anchorIdx - step + total) % total];
606
+ if (passesWhere(player, dynamicTarget, payloads, characterMap)) right.push(player);
657
607
  }
658
- return left.seat - right.seat;
659
- });
608
+ }
609
+ return { left, right };
660
610
  };
661
- var pickTargets = (sortedCandidates, dynamicTarget, payloads) => {
662
- const dedupedCandidates = [...new Map(sortedCandidates.map((player) => [player.seat, player])).values()];
611
+ var pickBalanced = (sides, dynamicTarget, payloads) => {
663
612
  const configuredLimit = resolveDynamicValue(dynamicTarget.limit, payloads, (value) => typeof value === "number");
613
+ const seen = /* @__PURE__ */ new Set();
614
+ const out = [];
615
+ const pushUnique = (player) => {
616
+ if (!player || seen.has(player.seat)) return false;
617
+ seen.add(player.seat);
618
+ out.push(player);
619
+ return true;
620
+ };
664
621
  if (typeof configuredLimit === "undefined") {
665
- return dedupedCandidates;
666
- }
667
- if (configuredLimit <= 0) {
668
- return [];
622
+ sides.left.forEach(pushUnique);
623
+ sides.right.forEach(pushUnique);
624
+ return out;
625
+ }
626
+ if (configuredLimit <= 0) return [];
627
+ let li = 0;
628
+ let ri = 0;
629
+ let preferLeft = true;
630
+ while (out.length < configuredLimit && (li < sides.left.length || ri < sides.right.length)) {
631
+ if (preferLeft && li < sides.left.length) {
632
+ if (pushUnique(sides.left[li])) li += 1;
633
+ else li += 1;
634
+ } else if (!preferLeft && ri < sides.right.length) {
635
+ if (pushUnique(sides.right[ri])) ri += 1;
636
+ else ri += 1;
637
+ } else if (li < sides.left.length) {
638
+ if (pushUnique(sides.left[li])) li += 1;
639
+ else li += 1;
640
+ } else if (ri < sides.right.length) {
641
+ if (pushUnique(sides.right[ri])) ri += 1;
642
+ else ri += 1;
643
+ }
644
+ preferLeft = !preferLeft;
669
645
  }
670
- return dedupedCandidates.slice(0, configuredLimit);
646
+ return out;
671
647
  };
672
648
  var resolveEffectTargets = ({
673
649
  effect,
@@ -700,10 +676,12 @@ var resolveEffectTargets = ({
700
676
  const dynamicTarget = effect.target;
701
677
  const sortedPlayers = getSortedPlayers(playerSeatMap);
702
678
  const anchorSeat = getAnchorSeat(dynamicTarget, payloads, effector);
703
- const candidates = getCandidatesBySelector(dynamicTarget, sortedPlayers, anchorSeat, payloads);
704
- const filtered = filterByWhere(candidates, dynamicTarget, payloads, characterMap);
705
- const ordered = applySort(filtered, dynamicTarget, payloads, sortedPlayers, anchorSeat);
706
- return pickTargets(ordered, dynamicTarget, payloads);
679
+ if (!dynamicTarget.selector) {
680
+ const matches = sortedPlayers.filter((player) => passesWhere(player, dynamicTarget, payloads, characterMap));
681
+ return pickBalanced({ left: matches, right: [] }, dynamicTarget, payloads);
682
+ }
683
+ const sides = walkSidesWithFilter(dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap);
684
+ return pickBalanced(sides, dynamicTarget, payloads);
707
685
  }
708
686
  return [];
709
687
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bct-app/game-engine",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Game engine utilities for BCT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",