Skip to content

Releases

0.3.7

Choose a tag to compare

@Sparks-code1 Sparks-code1 released this 05 Sep 16:46
A comeback power is claimed against the copy it is written to.

The pool is exclusive: no two factions may ever hold the same power, and it is
a permanent campaign mark. Both claim paths took the first id THEIR OWN copy
said was free, and that copy is a snapshot.

Two players knocked out by one stroke choose on two machines. Neither has heard
of the other's claim, so both modals offer the same power and both take it --
and the merge cannot help, because nothing about the result looks wrong to it:
one faction key added on each side, kept, and one id added to
claimedComebackPowers twice, deduped. Two factions holding one power and a
claimed list saying it went once. Nothing downstream can tell that happened.

claimComebackPower makes the pick against whatever copy it is handed. As
`reapply` that is the freshly-read row, so a preference still free is honoured,
one taken since is replaced by the next free power, and a faction that already
holds one is returned untouched -- which is what makes it safe to re-run.

RE-PICKING RATHER THAN REFUSING is a decision, not an oversight: refusing
leaves an eliminated player with no comeback power at all, which is worse than
a different one. It is not silent either -- the swap says so on their screen,
because it is a permanent mark on their own faction.

Both paths go through the one rule now, so the AI's automatic pick and the
human's choice cannot drift apart on exclusivity. The rule is in gameLogic,
parameterised by the pool so lib does not import a component, and unit-tested
rather than pinned by source.

Six sabotages: the preference trusted when taken, the already-holds guard
removed, an exhausted pool handing out the last power again, the human path
deciding from a snapshot, and each of the two rebuilds unwired separately.

One note on the tests. Unwiring the AI's rebuild passed at first: both paths
spell it identically, so `.test()` was satisfied by the human's and the pin sat
green over the bug. It counts the occurrences now, and each path fails on its
own. Found by the sabotage meant to prove the pin worked -- which is the
argument for running it.

113/113 unit, 24/24 e2e, build green, edge 16/16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

0.3.6

Choose a tag to compare

@Sparks-code1 Sparks-code1 released this 04 Sep 06:36
v0.3.6

Two-seat online harness: the helpers work, the spec stops one click s…

0.3.5

Choose a tag to compare

@Sparks-code1 Sparks-code1 released this 04 Sep 03:07
The delete refuses, says so, and is no longer offered.

An RLS-refused DELETE is not an error: it matches no rows and returns success.
deleteCampaign checked only `error`, so the picker's ✕ opened a confirmation,
took the press, refreshed the list, and left the campaign sitting there — a
control that appeared to do the one irreversible thing in the app and did
nothing at all.

BOTH HALVES. deleteCampaign counts what it removed and raises on zero, so the
API tells the truth to whatever calls it. And the ✕ is gone: with no DELETE
policy nobody is entitled, not even the campaign's owner, so there is nobody to
offer it to. The confirmation row, the busy state and the handler went with it
rather than being left behind implying a feature that no longer exists.

deleteCampaign is kept, uncalled, because the refusal it now reports is the
useful part — if a policy is ever added, or a script or admin path wants it, it
behaves correctly instead of lying. Its doc says so.

The RLS check gained the assertion behind all of it: a refused delete is silent
but countable — no error, count zero — which is the exact shape the fixed
deleteCampaign reads. Three sabotages on the client pins, all red: dropping the
count option, dropping the raise, and putting a delete call back in the picker.

The migration records the decision where the two options were written down,
and its breakage list no longer warns about a button that is gone.

110 suites, 23 browser tests, 12 RLS checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

0.3.4

Choose a tag to compare

@Sparks-code1 Sparks-code1 released this 03 Sep 21:30
The AI's draft fallback stops being silent cover.

The reinforce step drops the draft on the first territory the player owns when
aiReinforcePlacements offers nothing. It has to: troopsToPlace > 0 blocks the
phase advance, so a troop that never lands is a wedged turn.

BUT IT IS COVER FOR A PLANNER BUG, and it was silent. That is how it was found:
a browser spec stubbing aiReinforcePlacements, aiAttackPlan and aiFortifyMove
all to nothing passed GREEN. The console proved the stubs were called and the
roster proved what happened — territories 1 to 1, so the attack stub really did
bite, and troops 8 to 11, exactly the three-troop draft, placed by this line. A
broken planner would not stall the game. It would quietly draft onto the same
square every turn for the rest of a campaign, playing worse and worse while
looking like it worked.

In unmodified code the fallback never fires: the planner's one empty return is
owned.length === 0 || troops <= 0, this is its only caller, it passes troops: 1
and only asks while troops are owed. So the planner comes back empty only when
the player owns nothing — and then the fallback is empty too. Cover that is dead
in play and alive only when something is wrong is exactly the cover that should
speak up.

It warns now, naming the territory it fell back to. Verified both ways in the
browser: silent across a healthy AI turn, three warnings across a turn with the
planner stubbed. Pinned in aitest so the warn cannot be deleted while the `??`
stays — that is the precise edit that restores the silence — and so it cannot be
made unconditional, which would be noise nobody reads. Three sabotages, three
reds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

0.3.3

Choose a tag to compare

@Sparks-code1 Sparks-code1 released this 03 Sep 19:59
v0.3.3

The note on the basic Bene Gesserit follow stops saying the advanced …