Connect Spice Banker sealed battle spending and saved settlement
Prototype one through three own spice as a separate sealed battle commitment. Reserve funds alongside actual force support, add strength only for the surviving skilled leader, and settle once through existing versioned writes. Apply the documented card/core-rule interpretation: payment survives leader death or loss, except for the sole successful traitor caller. Keep printed disc strength and bounty unchanged. Normal income awaits its deferred-custody ruling; combined modules remain gated. Validation: 54 focused tests; typecheck, lint, 4490 offline tests, production build and 43 HTTP tests passed. Independent rules, privacy, persistence and UI review completed. SQLite tests cover reserved-spice diversion, concurrent settlement, traitor waiver and legacy plans. Browser exercised a three-spice commitment, pending refresh, actual +3 strength and payment, and restored private cards and the next decision. Two genuine four-profile Basic/Advanced samples completed without rejected candidates but did not use Banker spending. Following another reported outage, the server remained healthy. Preserved all 461 opening rooms and original seat/recovery rows (495 rooms after QA/HTTP). SQLite integrity and foreign keys passed. A fresh online backup captured all 495 rooms, and browser refresh restored the QA seat. No restart or reset was needed. Verified source tree: 208bf9324ab994b5e59ed50d78998b4dbf8810b3b6799b1a577ef3b84479335f
This commit is contained in:
1 parent
9381ef140d
commit
1380271bb6
22 files changed
+1166
-25
No files matched your search
+43
-4
@@ -66,6 +66,7 @@ import {
|
||||
} from './shrine';
|
||||
import { reserveShipmentCost } from './shipment-price';
|
||||
import { quoteSmugglerShipment } from './smuggler-shipment';
|
||||
import { spiceBankerBattleMaximum, spiceBankerModeSupported } from './spice-banker';
|
||||
import { nexusGuildSecretAllyAction, nexusGuildSecretAllyCanAct, nexusGuildSecretAllyQuote } from './nexus-guild-secret-ally-options';
|
||||
import { nexusRicheseAction, nexusRicheseQuote } from './nexus-richese-options';
|
||||
import { liveShipmentPromises, matchesShipment } from './shipment-promises';
|
||||
@@ -551,6 +552,9 @@ function ornithopterMoves(g: GameView): Action[] {
|
||||
}
|
||||
function candidatePlan(a: Action): Plan {
|
||||
return {
|
||||
...(Number(a.bankerSpice ?? 0)
|
||||
? { bankerSpice: Number(a.bankerSpice) }
|
||||
: {}),
|
||||
dial: Number(a.dial),
|
||||
support: Number(a.support ?? 0),
|
||||
leader: typeof a.leader === 'string' ? a.leader : null,
|
||||
@@ -676,6 +680,7 @@ function plans(g: GameView): Action[] {
|
||||
(element) => element.field !== field || element.value === value,
|
||||
);
|
||||
const battleSkills = g.leaderSkills?.assignments.filter(a => a.controller === me.id) ?? [];
|
||||
const bankerPrototypeAvailable = spiceBankerModeSupported(g);
|
||||
const specialForLeader = (card: Card | undefined, leader: string | null) =>
|
||||
canUsePlanetologistBattleSpecial({assignments:battleSkills, selectedLeader:leader, card});
|
||||
const weapons = [
|
||||
@@ -818,7 +823,7 @@ function plans(g: GameView): Action[] {
|
||||
const enemySurvivingStrength =
|
||||
effects.stunned || (inspected && effects.defenderDead)
|
||||
? 0
|
||||
: expectedEnemyStrength;
|
||||
: expectedEnemyStrength + (inspected?.plan.bankerSpice ?? 0);
|
||||
const opponentPenalty = stoneBattle ? 0 : bureaucratBattlePenalty(
|
||||
battleSkills, myLeader?.id, !effects.attackerDead,
|
||||
leaderSkillStrongholdCount(g, other),
|
||||
@@ -843,6 +848,7 @@ function plans(g: GameView): Action[] {
|
||||
ideal,
|
||||
...bankSupportedDials,
|
||||
...freeEliteDials,
|
||||
...[1, 2, 3].map((bonus) => Math.max(0, ideal - bonus)),
|
||||
0,
|
||||
...(typedForces ? [0.5] : []),
|
||||
Math.max(0, ownStrength - 1),
|
||||
@@ -866,6 +872,32 @@ function plans(g: GameView): Action[] {
|
||||
: null;
|
||||
if (typedForces && !commitment) continue;
|
||||
if (commitment) action.support = commitment.support;
|
||||
const supportCost = Math.max(
|
||||
0,
|
||||
Number(action.support ?? 0) -
|
||||
(b.strongholdEffects[me.id] === 'arrakeen' ? 2 : 0),
|
||||
);
|
||||
const ordinaryAllyPayment = Math.max(
|
||||
0,
|
||||
supportCost - (me.spice ?? 0),
|
||||
);
|
||||
const ownSupportCost = Math.max(
|
||||
0,
|
||||
supportCost - ordinaryAllyPayment,
|
||||
);
|
||||
const bankerSpice = Math.min(
|
||||
level + 1,
|
||||
3,
|
||||
bankerPrototypeAvailable
|
||||
? spiceBankerBattleMaximum(
|
||||
battleSkills,
|
||||
myLeader?.id,
|
||||
me.spice ?? 0,
|
||||
ownSupportCost,
|
||||
)
|
||||
: 0,
|
||||
);
|
||||
if (bankerSpice) action.bankerSpice = bankerSpice;
|
||||
const stonePlan = isStoneBurner(
|
||||
me.hand?.find((c) => c.id === action.weapon),
|
||||
);
|
||||
@@ -938,18 +970,25 @@ function plans(g: GameView): Action[] {
|
||||
(effectiveDefense ? 2 : 0)
|
||||
: level === 0
|
||||
? variation(g, `${leader}-${weapon}-${defense}-${dial}`) * 100
|
||||
: ownSurvivingStrength * 2 + opponentPenalty * 2 +
|
||||
: (ownSurvivingStrength +
|
||||
(!effects.attackerDead && !effects.stunned
|
||||
? bankerSpice
|
||||
: 0)) * 2 +
|
||||
opponentPenalty * 2 +
|
||||
(inspected && enemySurvivingStrength === 0 ? 10 : 0) +
|
||||
effectiveWeapon +
|
||||
effectiveDefense +
|
||||
(safeLeader && level >= 2 ? 5 : 0) -
|
||||
Math.abs(Number(action.dial) - ideal) * 2 +
|
||||
Math.abs(
|
||||
Number(action.dial) - Math.max(0, ideal - bankerSpice),
|
||||
) * 2 +
|
||||
(correctDefense ? 10 : 0) -
|
||||
(selfExplosion || explosionRisk ? 80 : 0) -
|
||||
(exposedLeader ? 100 : 0);
|
||||
options.push({
|
||||
action,
|
||||
score: score - (commitment?.cost ?? 0) * 0.2,
|
||||
score:
|
||||
score - (commitment?.cost ?? 0) * 0.2 - bankerSpice * 0.2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user