@bct-app/game-engine 0.1.14 → 0.1.16-beta.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.
Files changed (3) hide show
  1. package/dist/index.js +185 -124
  2. package/dist/index.mjs +185 -124
  3. package/package.json +11 -10
package/dist/index.js CHANGED
@@ -1078,13 +1078,7 @@ var isDeferredOperation = (operation, abilityMap) => {
1078
1078
  const ability = abilityMap.get(operation.abilityId);
1079
1079
  return ability?.executionTiming === "DEFER_TO_END";
1080
1080
  };
1081
- var hasGatedLifetime = (allAbilities) => {
1082
- return allAbilities.some((ability) => ability.effects.some((effect) => {
1083
- if (!("lifetime" in effect)) return false;
1084
- const lifetime = effect.lifetime;
1085
- return Boolean(lifetime) && lifetime?.kind !== "PERMANENT";
1086
- }));
1087
- };
1081
+ var FIXED_POINT_MAX_ITERATIONS = 32;
1088
1082
  var applyOperationToPlayers = ({
1089
1083
  players,
1090
1084
  operations,
@@ -1110,120 +1104,81 @@ var applyOperationToPlayers = ({
1110
1104
  }
1111
1105
  operationsByTimeline.get(timelineIndex)?.push(operation);
1112
1106
  });
1113
- const runReplay = (lifetimeSnapshotSeatMap) => {
1114
- const playersWithStatus = copyPlayers(players);
1115
- const applyOpsSequence = (ops) => {
1116
- ops.forEach((operation) => {
1117
- const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
1118
- const ability = abilityMap.get(operation.abilityId);
1119
- if (!ability) {
1120
- console.warn("Ability not found for operation:", operation);
1121
- return;
1122
- }
1123
- const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1124
- const effector = playerSeatMap.get(operation.effector);
1125
- const payloads = operation.payloads || [];
1126
- const writableInputs = ability.inputs.filter((part) => !part.readonly);
1127
- let snapshotSeatMap = null;
1128
- const getSnapshotSeatMap = () => {
1129
- if (!snapshotSeatMap) {
1130
- snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
1131
- }
1132
- return snapshotSeatMap;
1133
- };
1134
- ability.effects.forEach((effect) => {
1135
- const resolvedTargets = resolveEffectTargets({
1136
- effect,
1137
- operation,
1138
- payloads,
1139
- effector,
1140
- playerSeatMap,
1141
- characterMap
1142
- });
1143
- if (!resolvedTargets) {
1144
- return;
1145
- }
1146
- const precondition = "precondition" in effect ? effect.precondition : void 0;
1147
- const gatedTargets = precondition ? evaluatePrecondition({
1148
- expr: precondition,
1149
- operationTimelineIdx: operationInTimelineIdx,
1150
- nowTimelineIndex,
1151
- timelines,
1152
- effector,
1153
- targets: resolvedTargets,
1154
- snapshotSeatMap: playerSeatMap,
1155
- characterMap,
1156
- payloads,
1157
- customResolver
1158
- }) : resolvedTargets;
1159
- if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1160
- return;
1161
- }
1162
- const makePlayersEffect = evaluateLifetime({
1163
- effect,
1164
- operationTimelineIdx: operationInTimelineIdx,
1165
- nowTimelineIndex,
1166
- timelines,
1167
- effector,
1168
- targets: gatedTargets,
1169
- snapshotSeatMap: lifetimeSnapshotSeatMap,
1170
- characterMap,
1171
- payloads,
1172
- customResolver
1173
- });
1174
- const handler = effectHandlers[effect.type];
1175
- if (!handler) {
1176
- console.warn("Unknown effect type:", effect.type);
1177
- return;
1178
- }
1179
- const context = {
1180
- effect,
1181
- operation,
1182
- payloads,
1183
- effector,
1184
- writableInputs,
1185
- playerSeatMap,
1186
- getSnapshotSeatMap,
1187
- characterMap,
1188
- abilityMap,
1189
- nowTimelineIndex,
1190
- operationInTimelineIdx,
1191
- makePlayersEffect
1192
- };
1193
- handler(context);
1194
- });
1195
- });
1196
- };
1197
- const applyOps = (ops) => {
1198
- const normalOps = [];
1199
- const deferredOps = [];
1200
- ops.forEach((operation) => {
1201
- if (isDeferredOperation(operation, abilityMap)) {
1202
- deferredOps.push(operation);
1203
- return;
1204
- }
1205
- normalOps.push(operation);
1206
- });
1207
- applyOpsSequence(normalOps);
1208
- applyOpsSequence(deferredOps);
1107
+ const orderedOpsForTimeline = (timelineIdx) => {
1108
+ const ops = operationsByTimeline.get(timelineIdx) ?? [];
1109
+ const normal = [];
1110
+ const deferred = [];
1111
+ ops.forEach((op) => {
1112
+ if (isDeferredOperation(op, abilityMap)) deferred.push(op);
1113
+ else normal.push(op);
1114
+ });
1115
+ return [...normal, ...deferred];
1116
+ };
1117
+ const allOpsInOrder = [];
1118
+ allOpsInOrder.push(...orderedOpsForTimeline(-1));
1119
+ for (let i = 0; i <= nowTimelineIndex; i += 1) {
1120
+ if (timelines[i]) allOpsInOrder.push(...orderedOpsForTimeline(i));
1121
+ }
1122
+ const processedOps = /* @__PURE__ */ new Set();
1123
+ const records = [];
1124
+ const recordsByOp = /* @__PURE__ */ new Map();
1125
+ const applyRecordsForOp = (op, playersWithStatus) => {
1126
+ const opRecords = recordsByOp.get(op);
1127
+ if (!opRecords || opRecords.length === 0) return;
1128
+ const ability = abilityMap.get(op.abilityId);
1129
+ if (!ability) return;
1130
+ const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1131
+ const effector = playerSeatMap.get(op.effector);
1132
+ const payloads = op.payloads || [];
1133
+ const writableInputs = ability.inputs.filter((part) => !part.readonly);
1134
+ let snapshotSeatMap = null;
1135
+ const getSnapshotSeatMap = () => {
1136
+ if (!snapshotSeatMap) {
1137
+ snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
1138
+ }
1139
+ return snapshotSeatMap;
1209
1140
  };
1210
- const settingsOps = operationsByTimeline.get(-1);
1211
- if (settingsOps) {
1212
- applyOps(settingsOps);
1213
- }
1141
+ opRecords.forEach((record) => {
1142
+ const effect = ability.effects[record.effectIdx];
1143
+ if (!effect) return;
1144
+ const handler = effectHandlers[effect.type];
1145
+ if (!handler) {
1146
+ console.warn("Unknown effect type:", effect.type);
1147
+ return;
1148
+ }
1149
+ const makePlayersEffect = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
1150
+ const context = {
1151
+ effect,
1152
+ operation: op,
1153
+ payloads,
1154
+ effector,
1155
+ writableInputs,
1156
+ playerSeatMap,
1157
+ getSnapshotSeatMap,
1158
+ characterMap,
1159
+ abilityMap,
1160
+ nowTimelineIndex,
1161
+ operationInTimelineIdx: record.operationInTimelineIdx,
1162
+ makePlayersEffect
1163
+ };
1164
+ handler(context);
1165
+ });
1166
+ };
1167
+ const rebuildState = () => {
1168
+ const playersWithStatus = copyPlayers(players);
1169
+ const settingsOps = orderedOpsForTimeline(-1);
1170
+ settingsOps.forEach((op) => {
1171
+ if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1172
+ });
1214
1173
  for (let i = 0; i <= nowTimelineIndex; i += 1) {
1215
1174
  const timeline = timelines[i];
1216
- if (!timeline) {
1217
- continue;
1218
- }
1175
+ if (!timeline) continue;
1219
1176
  const nominations = timeline.nominations;
1220
1177
  if (nominations && nominations.length > 0) {
1221
1178
  const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1222
1179
  nominations.forEach((nomination) => {
1223
1180
  const voterSeats = nomination.voterSeats;
1224
- if (!voterSeats || voterSeats.length === 0) {
1225
- return;
1226
- }
1181
+ if (!voterSeats || voterSeats.length === 0) return;
1227
1182
  voterSeats.forEach((seat) => {
1228
1183
  const player = playerSeatMap.get(seat);
1229
1184
  if (player && player.isDead && !player.hasUsedDeadVote) {
@@ -1232,20 +1187,126 @@ var applyOperationToPlayers = ({
1232
1187
  });
1233
1188
  });
1234
1189
  }
1235
- const timelineOps = operationsByTimeline.get(i);
1236
- if (timelineOps) {
1237
- applyOps(timelineOps);
1238
- }
1190
+ orderedOpsForTimeline(i).forEach((op) => {
1191
+ if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1192
+ });
1239
1193
  }
1240
1194
  return playersWithStatus;
1241
1195
  };
1242
- if (!hasGatedLifetime(allAbilities)) {
1243
- return transformEmptyArray(runReplay(null));
1244
- }
1245
- const groundTruth = runReplay(null);
1246
- const groundTruthSeatMap = buildPlayerSeatMap(groundTruth);
1247
- const final = runReplay(groundTruthSeatMap);
1248
- return transformEmptyArray(final);
1196
+ const computeRecordsForOp = (op, state) => {
1197
+ const ability = abilityMap.get(op.abilityId);
1198
+ if (!ability) {
1199
+ console.warn("Ability not found for operation:", op);
1200
+ return [];
1201
+ }
1202
+ const playerSeatMap = buildPlayerSeatMap(state);
1203
+ const effector = playerSeatMap.get(op.effector);
1204
+ const payloads = op.payloads || [];
1205
+ const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
1206
+ const newRecords = [];
1207
+ ability.effects.forEach((effect, effectIdx) => {
1208
+ const resolvedTargets = resolveEffectTargets({
1209
+ effect,
1210
+ operation: op,
1211
+ payloads,
1212
+ effector,
1213
+ playerSeatMap,
1214
+ characterMap
1215
+ });
1216
+ if (!resolvedTargets) return;
1217
+ const precondition = "precondition" in effect ? effect.precondition : void 0;
1218
+ const gatedTargets = precondition ? evaluatePrecondition({
1219
+ expr: precondition,
1220
+ operationTimelineIdx: operationInTimelineIdx,
1221
+ nowTimelineIndex,
1222
+ timelines,
1223
+ effector,
1224
+ targets: resolvedTargets,
1225
+ snapshotSeatMap: playerSeatMap,
1226
+ characterMap,
1227
+ payloads,
1228
+ customResolver
1229
+ }) : resolvedTargets;
1230
+ if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1231
+ return;
1232
+ }
1233
+ const lifeTargets = evaluateLifetime({
1234
+ effect,
1235
+ operationTimelineIdx: operationInTimelineIdx,
1236
+ nowTimelineIndex,
1237
+ timelines,
1238
+ effector,
1239
+ targets: gatedTargets,
1240
+ snapshotSeatMap: playerSeatMap,
1241
+ characterMap,
1242
+ payloads,
1243
+ customResolver
1244
+ });
1245
+ newRecords.push({
1246
+ operation: op,
1247
+ effectIdx,
1248
+ operationInTimelineIdx,
1249
+ activeTargetSeats: lifeTargets.map((t) => t.seat)
1250
+ });
1251
+ });
1252
+ return newRecords;
1253
+ };
1254
+ const reEvaluateLifetimes = (state) => {
1255
+ const playerSeatMap = buildPlayerSeatMap(state);
1256
+ let anyShrunk = false;
1257
+ records.forEach((record) => {
1258
+ if (record.activeTargetSeats.length === 0) return;
1259
+ const ability = abilityMap.get(record.operation.abilityId);
1260
+ if (!ability) return;
1261
+ const effect = ability.effects[record.effectIdx];
1262
+ if (!effect) return;
1263
+ const effector = playerSeatMap.get(record.operation.effector);
1264
+ const payloads = record.operation.payloads || [];
1265
+ const targets = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
1266
+ const liveTargets = evaluateLifetime({
1267
+ effect,
1268
+ operationTimelineIdx: record.operationInTimelineIdx,
1269
+ nowTimelineIndex,
1270
+ timelines,
1271
+ effector,
1272
+ targets,
1273
+ snapshotSeatMap: playerSeatMap,
1274
+ characterMap,
1275
+ payloads,
1276
+ customResolver
1277
+ });
1278
+ const newSeats = liveTargets.map((t) => t.seat);
1279
+ if (newSeats.length < record.activeTargetSeats.length) {
1280
+ record.activeTargetSeats = newSeats;
1281
+ anyShrunk = true;
1282
+ }
1283
+ });
1284
+ return anyShrunk;
1285
+ };
1286
+ let lastState = copyPlayers(players);
1287
+ allOpsInOrder.forEach((op) => {
1288
+ const newRecords = computeRecordsForOp(op, lastState);
1289
+ if (newRecords.length > 0) {
1290
+ records.push(...newRecords);
1291
+ const arr = recordsByOp.get(op) ?? [];
1292
+ arr.push(...newRecords);
1293
+ recordsByOp.set(op, arr);
1294
+ }
1295
+ processedOps.add(op);
1296
+ let state = rebuildState();
1297
+ let iter = 0;
1298
+ while (iter < FIXED_POINT_MAX_ITERATIONS) {
1299
+ const shrunk = reEvaluateLifetimes(state);
1300
+ if (!shrunk) break;
1301
+ state = rebuildState();
1302
+ iter += 1;
1303
+ }
1304
+ if (iter >= FIXED_POINT_MAX_ITERATIONS) {
1305
+ console.warn("applyOperationToPlayers: lifetime fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
1306
+ }
1307
+ lastState = state;
1308
+ });
1309
+ return transformEmptyArray(lastState);
1249
1310
  };
1250
1311
 
1251
1312
  // src/can-invoke-ability.ts
package/dist/index.mjs CHANGED
@@ -1038,13 +1038,7 @@ var isDeferredOperation = (operation, abilityMap) => {
1038
1038
  const ability = abilityMap.get(operation.abilityId);
1039
1039
  return ability?.executionTiming === "DEFER_TO_END";
1040
1040
  };
1041
- var hasGatedLifetime = (allAbilities) => {
1042
- return allAbilities.some((ability) => ability.effects.some((effect) => {
1043
- if (!("lifetime" in effect)) return false;
1044
- const lifetime = effect.lifetime;
1045
- return Boolean(lifetime) && lifetime?.kind !== "PERMANENT";
1046
- }));
1047
- };
1041
+ var FIXED_POINT_MAX_ITERATIONS = 32;
1048
1042
  var applyOperationToPlayers = ({
1049
1043
  players,
1050
1044
  operations,
@@ -1070,120 +1064,81 @@ var applyOperationToPlayers = ({
1070
1064
  }
1071
1065
  operationsByTimeline.get(timelineIndex)?.push(operation);
1072
1066
  });
1073
- const runReplay = (lifetimeSnapshotSeatMap) => {
1074
- const playersWithStatus = copyPlayers(players);
1075
- const applyOpsSequence = (ops) => {
1076
- ops.forEach((operation) => {
1077
- const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
1078
- const ability = abilityMap.get(operation.abilityId);
1079
- if (!ability) {
1080
- console.warn("Ability not found for operation:", operation);
1081
- return;
1082
- }
1083
- const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1084
- const effector = playerSeatMap.get(operation.effector);
1085
- const payloads = operation.payloads || [];
1086
- const writableInputs = ability.inputs.filter((part) => !part.readonly);
1087
- let snapshotSeatMap = null;
1088
- const getSnapshotSeatMap = () => {
1089
- if (!snapshotSeatMap) {
1090
- snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
1091
- }
1092
- return snapshotSeatMap;
1093
- };
1094
- ability.effects.forEach((effect) => {
1095
- const resolvedTargets = resolveEffectTargets({
1096
- effect,
1097
- operation,
1098
- payloads,
1099
- effector,
1100
- playerSeatMap,
1101
- characterMap
1102
- });
1103
- if (!resolvedTargets) {
1104
- return;
1105
- }
1106
- const precondition = "precondition" in effect ? effect.precondition : void 0;
1107
- const gatedTargets = precondition ? evaluatePrecondition({
1108
- expr: precondition,
1109
- operationTimelineIdx: operationInTimelineIdx,
1110
- nowTimelineIndex,
1111
- timelines,
1112
- effector,
1113
- targets: resolvedTargets,
1114
- snapshotSeatMap: playerSeatMap,
1115
- characterMap,
1116
- payloads,
1117
- customResolver
1118
- }) : resolvedTargets;
1119
- if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1120
- return;
1121
- }
1122
- const makePlayersEffect = evaluateLifetime({
1123
- effect,
1124
- operationTimelineIdx: operationInTimelineIdx,
1125
- nowTimelineIndex,
1126
- timelines,
1127
- effector,
1128
- targets: gatedTargets,
1129
- snapshotSeatMap: lifetimeSnapshotSeatMap,
1130
- characterMap,
1131
- payloads,
1132
- customResolver
1133
- });
1134
- const handler = effectHandlers[effect.type];
1135
- if (!handler) {
1136
- console.warn("Unknown effect type:", effect.type);
1137
- return;
1138
- }
1139
- const context = {
1140
- effect,
1141
- operation,
1142
- payloads,
1143
- effector,
1144
- writableInputs,
1145
- playerSeatMap,
1146
- getSnapshotSeatMap,
1147
- characterMap,
1148
- abilityMap,
1149
- nowTimelineIndex,
1150
- operationInTimelineIdx,
1151
- makePlayersEffect
1152
- };
1153
- handler(context);
1154
- });
1155
- });
1156
- };
1157
- const applyOps = (ops) => {
1158
- const normalOps = [];
1159
- const deferredOps = [];
1160
- ops.forEach((operation) => {
1161
- if (isDeferredOperation(operation, abilityMap)) {
1162
- deferredOps.push(operation);
1163
- return;
1164
- }
1165
- normalOps.push(operation);
1166
- });
1167
- applyOpsSequence(normalOps);
1168
- applyOpsSequence(deferredOps);
1067
+ const orderedOpsForTimeline = (timelineIdx) => {
1068
+ const ops = operationsByTimeline.get(timelineIdx) ?? [];
1069
+ const normal = [];
1070
+ const deferred = [];
1071
+ ops.forEach((op) => {
1072
+ if (isDeferredOperation(op, abilityMap)) deferred.push(op);
1073
+ else normal.push(op);
1074
+ });
1075
+ return [...normal, ...deferred];
1076
+ };
1077
+ const allOpsInOrder = [];
1078
+ allOpsInOrder.push(...orderedOpsForTimeline(-1));
1079
+ for (let i = 0; i <= nowTimelineIndex; i += 1) {
1080
+ if (timelines[i]) allOpsInOrder.push(...orderedOpsForTimeline(i));
1081
+ }
1082
+ const processedOps = /* @__PURE__ */ new Set();
1083
+ const records = [];
1084
+ const recordsByOp = /* @__PURE__ */ new Map();
1085
+ const applyRecordsForOp = (op, playersWithStatus) => {
1086
+ const opRecords = recordsByOp.get(op);
1087
+ if (!opRecords || opRecords.length === 0) return;
1088
+ const ability = abilityMap.get(op.abilityId);
1089
+ if (!ability) return;
1090
+ const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1091
+ const effector = playerSeatMap.get(op.effector);
1092
+ const payloads = op.payloads || [];
1093
+ const writableInputs = ability.inputs.filter((part) => !part.readonly);
1094
+ let snapshotSeatMap = null;
1095
+ const getSnapshotSeatMap = () => {
1096
+ if (!snapshotSeatMap) {
1097
+ snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
1098
+ }
1099
+ return snapshotSeatMap;
1169
1100
  };
1170
- const settingsOps = operationsByTimeline.get(-1);
1171
- if (settingsOps) {
1172
- applyOps(settingsOps);
1173
- }
1101
+ opRecords.forEach((record) => {
1102
+ const effect = ability.effects[record.effectIdx];
1103
+ if (!effect) return;
1104
+ const handler = effectHandlers[effect.type];
1105
+ if (!handler) {
1106
+ console.warn("Unknown effect type:", effect.type);
1107
+ return;
1108
+ }
1109
+ const makePlayersEffect = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
1110
+ const context = {
1111
+ effect,
1112
+ operation: op,
1113
+ payloads,
1114
+ effector,
1115
+ writableInputs,
1116
+ playerSeatMap,
1117
+ getSnapshotSeatMap,
1118
+ characterMap,
1119
+ abilityMap,
1120
+ nowTimelineIndex,
1121
+ operationInTimelineIdx: record.operationInTimelineIdx,
1122
+ makePlayersEffect
1123
+ };
1124
+ handler(context);
1125
+ });
1126
+ };
1127
+ const rebuildState = () => {
1128
+ const playersWithStatus = copyPlayers(players);
1129
+ const settingsOps = orderedOpsForTimeline(-1);
1130
+ settingsOps.forEach((op) => {
1131
+ if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1132
+ });
1174
1133
  for (let i = 0; i <= nowTimelineIndex; i += 1) {
1175
1134
  const timeline = timelines[i];
1176
- if (!timeline) {
1177
- continue;
1178
- }
1135
+ if (!timeline) continue;
1179
1136
  const nominations = timeline.nominations;
1180
1137
  if (nominations && nominations.length > 0) {
1181
1138
  const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1182
1139
  nominations.forEach((nomination) => {
1183
1140
  const voterSeats = nomination.voterSeats;
1184
- if (!voterSeats || voterSeats.length === 0) {
1185
- return;
1186
- }
1141
+ if (!voterSeats || voterSeats.length === 0) return;
1187
1142
  voterSeats.forEach((seat) => {
1188
1143
  const player = playerSeatMap.get(seat);
1189
1144
  if (player && player.isDead && !player.hasUsedDeadVote) {
@@ -1192,20 +1147,126 @@ var applyOperationToPlayers = ({
1192
1147
  });
1193
1148
  });
1194
1149
  }
1195
- const timelineOps = operationsByTimeline.get(i);
1196
- if (timelineOps) {
1197
- applyOps(timelineOps);
1198
- }
1150
+ orderedOpsForTimeline(i).forEach((op) => {
1151
+ if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1152
+ });
1199
1153
  }
1200
1154
  return playersWithStatus;
1201
1155
  };
1202
- if (!hasGatedLifetime(allAbilities)) {
1203
- return transformEmptyArray(runReplay(null));
1204
- }
1205
- const groundTruth = runReplay(null);
1206
- const groundTruthSeatMap = buildPlayerSeatMap(groundTruth);
1207
- const final = runReplay(groundTruthSeatMap);
1208
- return transformEmptyArray(final);
1156
+ const computeRecordsForOp = (op, state) => {
1157
+ const ability = abilityMap.get(op.abilityId);
1158
+ if (!ability) {
1159
+ console.warn("Ability not found for operation:", op);
1160
+ return [];
1161
+ }
1162
+ const playerSeatMap = buildPlayerSeatMap(state);
1163
+ const effector = playerSeatMap.get(op.effector);
1164
+ const payloads = op.payloads || [];
1165
+ const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
1166
+ const newRecords = [];
1167
+ ability.effects.forEach((effect, effectIdx) => {
1168
+ const resolvedTargets = resolveEffectTargets({
1169
+ effect,
1170
+ operation: op,
1171
+ payloads,
1172
+ effector,
1173
+ playerSeatMap,
1174
+ characterMap
1175
+ });
1176
+ if (!resolvedTargets) return;
1177
+ const precondition = "precondition" in effect ? effect.precondition : void 0;
1178
+ const gatedTargets = precondition ? evaluatePrecondition({
1179
+ expr: precondition,
1180
+ operationTimelineIdx: operationInTimelineIdx,
1181
+ nowTimelineIndex,
1182
+ timelines,
1183
+ effector,
1184
+ targets: resolvedTargets,
1185
+ snapshotSeatMap: playerSeatMap,
1186
+ characterMap,
1187
+ payloads,
1188
+ customResolver
1189
+ }) : resolvedTargets;
1190
+ if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1191
+ return;
1192
+ }
1193
+ const lifeTargets = evaluateLifetime({
1194
+ effect,
1195
+ operationTimelineIdx: operationInTimelineIdx,
1196
+ nowTimelineIndex,
1197
+ timelines,
1198
+ effector,
1199
+ targets: gatedTargets,
1200
+ snapshotSeatMap: playerSeatMap,
1201
+ characterMap,
1202
+ payloads,
1203
+ customResolver
1204
+ });
1205
+ newRecords.push({
1206
+ operation: op,
1207
+ effectIdx,
1208
+ operationInTimelineIdx,
1209
+ activeTargetSeats: lifeTargets.map((t) => t.seat)
1210
+ });
1211
+ });
1212
+ return newRecords;
1213
+ };
1214
+ const reEvaluateLifetimes = (state) => {
1215
+ const playerSeatMap = buildPlayerSeatMap(state);
1216
+ let anyShrunk = false;
1217
+ records.forEach((record) => {
1218
+ if (record.activeTargetSeats.length === 0) return;
1219
+ const ability = abilityMap.get(record.operation.abilityId);
1220
+ if (!ability) return;
1221
+ const effect = ability.effects[record.effectIdx];
1222
+ if (!effect) return;
1223
+ const effector = playerSeatMap.get(record.operation.effector);
1224
+ const payloads = record.operation.payloads || [];
1225
+ const targets = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
1226
+ const liveTargets = evaluateLifetime({
1227
+ effect,
1228
+ operationTimelineIdx: record.operationInTimelineIdx,
1229
+ nowTimelineIndex,
1230
+ timelines,
1231
+ effector,
1232
+ targets,
1233
+ snapshotSeatMap: playerSeatMap,
1234
+ characterMap,
1235
+ payloads,
1236
+ customResolver
1237
+ });
1238
+ const newSeats = liveTargets.map((t) => t.seat);
1239
+ if (newSeats.length < record.activeTargetSeats.length) {
1240
+ record.activeTargetSeats = newSeats;
1241
+ anyShrunk = true;
1242
+ }
1243
+ });
1244
+ return anyShrunk;
1245
+ };
1246
+ let lastState = copyPlayers(players);
1247
+ allOpsInOrder.forEach((op) => {
1248
+ const newRecords = computeRecordsForOp(op, lastState);
1249
+ if (newRecords.length > 0) {
1250
+ records.push(...newRecords);
1251
+ const arr = recordsByOp.get(op) ?? [];
1252
+ arr.push(...newRecords);
1253
+ recordsByOp.set(op, arr);
1254
+ }
1255
+ processedOps.add(op);
1256
+ let state = rebuildState();
1257
+ let iter = 0;
1258
+ while (iter < FIXED_POINT_MAX_ITERATIONS) {
1259
+ const shrunk = reEvaluateLifetimes(state);
1260
+ if (!shrunk) break;
1261
+ state = rebuildState();
1262
+ iter += 1;
1263
+ }
1264
+ if (iter >= FIXED_POINT_MAX_ITERATIONS) {
1265
+ console.warn("applyOperationToPlayers: lifetime fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
1266
+ }
1267
+ lastState = state;
1268
+ });
1269
+ return transformEmptyArray(lastState);
1209
1270
  };
1210
1271
 
1211
1272
  // src/can-invoke-ability.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bct-app/game-engine",
3
- "version": "0.1.14",
3
+ "version": "0.1.16-beta.0",
4
4
  "description": "Game engine utilities for BCT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,6 +15,14 @@
15
15
  "dist",
16
16
  "README.md"
17
17
  ],
18
+ "scripts": {
19
+ "clean": "rm -rf dist",
20
+ "build": "tsup src/index.ts --dts --format cjs,esm",
21
+ "dev": "tsup src/index.ts --dts --format cjs,esm --watch",
22
+ "test": "vitest",
23
+ "test:run": "vitest run",
24
+ "publish:beta": "npm publish --tag beta"
25
+ },
18
26
  "keywords": [
19
27
  "game-engine",
20
28
  "bct"
@@ -30,19 +38,12 @@
30
38
  "access": "public"
31
39
  },
32
40
  "dependencies": {
33
- "@bct-app/game-model": "0.1.9"
41
+ "@bct-app/game-model": "workspace:*"
34
42
  },
35
43
  "devDependencies": {
36
44
  "@vitest/ui": "^4.1.3",
37
45
  "tsup": "^8.0.2",
38
46
  "typescript": "^5.9.3",
39
47
  "vitest": "^4.1.3"
40
- },
41
- "scripts": {
42
- "clean": "rm -rf dist",
43
- "build": "tsup src/index.ts --dts --format cjs,esm",
44
- "dev": "tsup src/index.ts --dts --format cjs,esm --watch",
45
- "test": "vitest",
46
- "test:run": "vitest run"
47
48
  }
48
- }
49
+ }