@bct-app/game-engine 0.1.15 → 0.1.16-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +185 -138
  2. package/dist/index.mjs +185 -138
  3. package/package.json +3 -2
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,134 +1104,81 @@ var applyOperationToPlayers = ({
1110
1104
  }
1111
1105
  operationsByTimeline.get(timelineIndex)?.push(operation);
1112
1106
  });
1113
- const preconditionCache = /* @__PURE__ */ new Map();
1114
- const runReplay = (lifetimeSnapshotSeatMap) => {
1115
- const playersWithStatus = copyPlayers(players);
1116
- const applyOpsSequence = (ops) => {
1117
- ops.forEach((operation) => {
1118
- const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
1119
- const ability = abilityMap.get(operation.abilityId);
1120
- if (!ability) {
1121
- console.warn("Ability not found for operation:", operation);
1122
- return;
1123
- }
1124
- const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1125
- const effector = playerSeatMap.get(operation.effector);
1126
- const payloads = operation.payloads || [];
1127
- const writableInputs = ability.inputs.filter((part) => !part.readonly);
1128
- let snapshotSeatMap = null;
1129
- const getSnapshotSeatMap = () => {
1130
- if (!snapshotSeatMap) {
1131
- snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
1132
- }
1133
- return snapshotSeatMap;
1134
- };
1135
- ability.effects.forEach((effect, effectIdx) => {
1136
- const resolvedTargets = resolveEffectTargets({
1137
- effect,
1138
- operation,
1139
- payloads,
1140
- effector,
1141
- playerSeatMap,
1142
- characterMap
1143
- });
1144
- if (!resolvedTargets) {
1145
- return;
1146
- }
1147
- const precondition = "precondition" in effect ? effect.precondition : void 0;
1148
- let gatedTargets;
1149
- if (precondition) {
1150
- const cachedSeats = preconditionCache.get(operation)?.get(effectIdx);
1151
- if (cachedSeats) {
1152
- gatedTargets = resolvedTargets.filter((t) => cachedSeats.has(t.seat));
1153
- } else {
1154
- gatedTargets = evaluatePrecondition({
1155
- expr: precondition,
1156
- operationTimelineIdx: operationInTimelineIdx,
1157
- nowTimelineIndex,
1158
- timelines,
1159
- effector,
1160
- targets: resolvedTargets,
1161
- snapshotSeatMap: playerSeatMap,
1162
- characterMap,
1163
- payloads,
1164
- customResolver
1165
- });
1166
- const opCache = preconditionCache.get(operation) ?? /* @__PURE__ */ new Map();
1167
- opCache.set(effectIdx, new Set(gatedTargets.map((t) => t.seat)));
1168
- preconditionCache.set(operation, opCache);
1169
- }
1170
- } else {
1171
- gatedTargets = resolvedTargets;
1172
- }
1173
- if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1174
- return;
1175
- }
1176
- const makePlayersEffect = evaluateLifetime({
1177
- effect,
1178
- operationTimelineIdx: operationInTimelineIdx,
1179
- nowTimelineIndex,
1180
- timelines,
1181
- effector,
1182
- targets: gatedTargets,
1183
- snapshotSeatMap: lifetimeSnapshotSeatMap,
1184
- characterMap,
1185
- payloads,
1186
- customResolver
1187
- });
1188
- const handler = effectHandlers[effect.type];
1189
- if (!handler) {
1190
- console.warn("Unknown effect type:", effect.type);
1191
- return;
1192
- }
1193
- const context = {
1194
- effect,
1195
- operation,
1196
- payloads,
1197
- effector,
1198
- writableInputs,
1199
- playerSeatMap,
1200
- getSnapshotSeatMap,
1201
- characterMap,
1202
- abilityMap,
1203
- nowTimelineIndex,
1204
- operationInTimelineIdx,
1205
- makePlayersEffect
1206
- };
1207
- handler(context);
1208
- });
1209
- });
1210
- };
1211
- const applyOps = (ops) => {
1212
- const normalOps = [];
1213
- const deferredOps = [];
1214
- ops.forEach((operation) => {
1215
- if (isDeferredOperation(operation, abilityMap)) {
1216
- deferredOps.push(operation);
1217
- return;
1218
- }
1219
- normalOps.push(operation);
1220
- });
1221
- applyOpsSequence(normalOps);
1222
- 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;
1223
1140
  };
1224
- const settingsOps = operationsByTimeline.get(-1);
1225
- if (settingsOps) {
1226
- applyOps(settingsOps);
1227
- }
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
+ });
1228
1173
  for (let i = 0; i <= nowTimelineIndex; i += 1) {
1229
1174
  const timeline = timelines[i];
1230
- if (!timeline) {
1231
- continue;
1232
- }
1175
+ if (!timeline) continue;
1233
1176
  const nominations = timeline.nominations;
1234
1177
  if (nominations && nominations.length > 0) {
1235
1178
  const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1236
1179
  nominations.forEach((nomination) => {
1237
1180
  const voterSeats = nomination.voterSeats;
1238
- if (!voterSeats || voterSeats.length === 0) {
1239
- return;
1240
- }
1181
+ if (!voterSeats || voterSeats.length === 0) return;
1241
1182
  voterSeats.forEach((seat) => {
1242
1183
  const player = playerSeatMap.get(seat);
1243
1184
  if (player && player.isDead && !player.hasUsedDeadVote) {
@@ -1246,20 +1187,126 @@ var applyOperationToPlayers = ({
1246
1187
  });
1247
1188
  });
1248
1189
  }
1249
- const timelineOps = operationsByTimeline.get(i);
1250
- if (timelineOps) {
1251
- applyOps(timelineOps);
1252
- }
1190
+ orderedOpsForTimeline(i).forEach((op) => {
1191
+ if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1192
+ });
1253
1193
  }
1254
1194
  return playersWithStatus;
1255
1195
  };
1256
- if (!hasGatedLifetime(allAbilities)) {
1257
- return transformEmptyArray(runReplay(null));
1258
- }
1259
- const groundTruth = runReplay(null);
1260
- const groundTruthSeatMap = buildPlayerSeatMap(groundTruth);
1261
- const final = runReplay(groundTruthSeatMap);
1262
- 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);
1263
1310
  };
1264
1311
 
1265
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,134 +1064,81 @@ var applyOperationToPlayers = ({
1070
1064
  }
1071
1065
  operationsByTimeline.get(timelineIndex)?.push(operation);
1072
1066
  });
1073
- const preconditionCache = /* @__PURE__ */ new Map();
1074
- const runReplay = (lifetimeSnapshotSeatMap) => {
1075
- const playersWithStatus = copyPlayers(players);
1076
- const applyOpsSequence = (ops) => {
1077
- ops.forEach((operation) => {
1078
- const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
1079
- const ability = abilityMap.get(operation.abilityId);
1080
- if (!ability) {
1081
- console.warn("Ability not found for operation:", operation);
1082
- return;
1083
- }
1084
- const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1085
- const effector = playerSeatMap.get(operation.effector);
1086
- const payloads = operation.payloads || [];
1087
- const writableInputs = ability.inputs.filter((part) => !part.readonly);
1088
- let snapshotSeatMap = null;
1089
- const getSnapshotSeatMap = () => {
1090
- if (!snapshotSeatMap) {
1091
- snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
1092
- }
1093
- return snapshotSeatMap;
1094
- };
1095
- ability.effects.forEach((effect, effectIdx) => {
1096
- const resolvedTargets = resolveEffectTargets({
1097
- effect,
1098
- operation,
1099
- payloads,
1100
- effector,
1101
- playerSeatMap,
1102
- characterMap
1103
- });
1104
- if (!resolvedTargets) {
1105
- return;
1106
- }
1107
- const precondition = "precondition" in effect ? effect.precondition : void 0;
1108
- let gatedTargets;
1109
- if (precondition) {
1110
- const cachedSeats = preconditionCache.get(operation)?.get(effectIdx);
1111
- if (cachedSeats) {
1112
- gatedTargets = resolvedTargets.filter((t) => cachedSeats.has(t.seat));
1113
- } else {
1114
- gatedTargets = evaluatePrecondition({
1115
- expr: precondition,
1116
- operationTimelineIdx: operationInTimelineIdx,
1117
- nowTimelineIndex,
1118
- timelines,
1119
- effector,
1120
- targets: resolvedTargets,
1121
- snapshotSeatMap: playerSeatMap,
1122
- characterMap,
1123
- payloads,
1124
- customResolver
1125
- });
1126
- const opCache = preconditionCache.get(operation) ?? /* @__PURE__ */ new Map();
1127
- opCache.set(effectIdx, new Set(gatedTargets.map((t) => t.seat)));
1128
- preconditionCache.set(operation, opCache);
1129
- }
1130
- } else {
1131
- gatedTargets = resolvedTargets;
1132
- }
1133
- if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1134
- return;
1135
- }
1136
- const makePlayersEffect = evaluateLifetime({
1137
- effect,
1138
- operationTimelineIdx: operationInTimelineIdx,
1139
- nowTimelineIndex,
1140
- timelines,
1141
- effector,
1142
- targets: gatedTargets,
1143
- snapshotSeatMap: lifetimeSnapshotSeatMap,
1144
- characterMap,
1145
- payloads,
1146
- customResolver
1147
- });
1148
- const handler = effectHandlers[effect.type];
1149
- if (!handler) {
1150
- console.warn("Unknown effect type:", effect.type);
1151
- return;
1152
- }
1153
- const context = {
1154
- effect,
1155
- operation,
1156
- payloads,
1157
- effector,
1158
- writableInputs,
1159
- playerSeatMap,
1160
- getSnapshotSeatMap,
1161
- characterMap,
1162
- abilityMap,
1163
- nowTimelineIndex,
1164
- operationInTimelineIdx,
1165
- makePlayersEffect
1166
- };
1167
- handler(context);
1168
- });
1169
- });
1170
- };
1171
- const applyOps = (ops) => {
1172
- const normalOps = [];
1173
- const deferredOps = [];
1174
- ops.forEach((operation) => {
1175
- if (isDeferredOperation(operation, abilityMap)) {
1176
- deferredOps.push(operation);
1177
- return;
1178
- }
1179
- normalOps.push(operation);
1180
- });
1181
- applyOpsSequence(normalOps);
1182
- 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;
1183
1100
  };
1184
- const settingsOps = operationsByTimeline.get(-1);
1185
- if (settingsOps) {
1186
- applyOps(settingsOps);
1187
- }
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
+ });
1188
1133
  for (let i = 0; i <= nowTimelineIndex; i += 1) {
1189
1134
  const timeline = timelines[i];
1190
- if (!timeline) {
1191
- continue;
1192
- }
1135
+ if (!timeline) continue;
1193
1136
  const nominations = timeline.nominations;
1194
1137
  if (nominations && nominations.length > 0) {
1195
1138
  const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
1196
1139
  nominations.forEach((nomination) => {
1197
1140
  const voterSeats = nomination.voterSeats;
1198
- if (!voterSeats || voterSeats.length === 0) {
1199
- return;
1200
- }
1141
+ if (!voterSeats || voterSeats.length === 0) return;
1201
1142
  voterSeats.forEach((seat) => {
1202
1143
  const player = playerSeatMap.get(seat);
1203
1144
  if (player && player.isDead && !player.hasUsedDeadVote) {
@@ -1206,20 +1147,126 @@ var applyOperationToPlayers = ({
1206
1147
  });
1207
1148
  });
1208
1149
  }
1209
- const timelineOps = operationsByTimeline.get(i);
1210
- if (timelineOps) {
1211
- applyOps(timelineOps);
1212
- }
1150
+ orderedOpsForTimeline(i).forEach((op) => {
1151
+ if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1152
+ });
1213
1153
  }
1214
1154
  return playersWithStatus;
1215
1155
  };
1216
- if (!hasGatedLifetime(allAbilities)) {
1217
- return transformEmptyArray(runReplay(null));
1218
- }
1219
- const groundTruth = runReplay(null);
1220
- const groundTruthSeatMap = buildPlayerSeatMap(groundTruth);
1221
- const final = runReplay(groundTruthSeatMap);
1222
- 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);
1223
1270
  };
1224
1271
 
1225
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.15",
3
+ "version": "0.1.16-beta.1",
4
4
  "description": "Game engine utilities for BCT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -43,6 +43,7 @@
43
43
  "build": "tsup src/index.ts --dts --format cjs,esm",
44
44
  "dev": "tsup src/index.ts --dts --format cjs,esm --watch",
45
45
  "test": "vitest",
46
- "test:run": "vitest run"
46
+ "test:run": "vitest run",
47
+ "publish:beta": "pnpm publish --tag beta --no-git-checks"
47
48
  }
48
49
  }