Connect provisional Basic full-hand Nullentropy Box exchange

This commit is contained in:
Quintine committed 2026-10-08 09:30:34 +11:00
1 parent 8d55b8f3f7
commit f29f14128f
9 files changed
+69 -16

No files matched your search

+5
View File
@@ -5,6 +5,8 @@ import type { Action, GameView } from '@/game/engine';
import { Button } from './ui/button';
import { CardInspector, CardRules } from './card-inspector';
const fullHandPolicy = 'Provisional Basic full-hand exchange: retrieval and the played Box discard settle atomically, leaving your final hand at its limit. The printed intermediate-capacity timing remains unresolved.';
type BoxProps = {
game: GameView;
act: (action: Action) => void;
@@ -21,6 +23,7 @@ export function NullentropyBox({ game, act, busy }: BoxProps) {
<p className="notice">
Your Nullentropy Box search is paid. Complete the private card choice in
the decision panel above.
{box.fullHandExchange && <span className="block">{fullHandPolicy}</span>}
</p>
);
return (
@@ -39,6 +42,7 @@ export function NullentropyBox({ game, act, busy }: BoxProps) {
the cards. If only one card is eligible, the server takes it
automatically.
</p>
{box.fullHandExchange && <p className="notice">{fullHandPolicy}</p>}
<CardRules card={box.card} />
<CardInspector card={box.card} />
{box.blocked && (
@@ -80,6 +84,7 @@ export function NullentropySearch({ game, act, busy }: BoxProps) {
These discard cards are visible only to you during this paid search.
Inspection does not select a card.
</p>
{game.nullentropy?.fullHandExchange && <p className="notice">{fullHandPolicy}</p>}
<div
className="grid min-w-0 gap-4"
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 16rem), 1fr))' }}
+6
View File
@@ -65,6 +65,12 @@ provisional policy is one spice at the rounded one-force Guild rate, entirely
to bank even when Richese pays. Shared quotes connect engine, owned controls
and AI. Two before/after engine scenarios pass, including an actual native
Karama purchase and Easy consent; types pass. Advanced counterparts remain guarded.
**Basic full-hand Nullentropy Box prototype.** A holder at its current limit
can pay for the private search and atomically exchange the Box for one non-Box
card without increasing the final hand. Owned controls label intermediate
capacity as unresolved. One before/after smoke pays two once, uses Easy's
private selection, retains four final cards and puts the Box on top. Types pass;
Advanced full-hand and Guild-refund guards remain.
+27 -2
View File
@@ -1,6 +1,31 @@
# Nullentropy Box: paid search and engine continuation audit
Audit: 2026-09-06. This document proposes an implementation contract; it does not activate the card or certify complete Richese support. Read with [the acquisition audit](RICHESE_ACQUISITION_RULES.md). The user has already been asked about full-hand activation; that answer is pending and this audit does not ask again.
Audit: 2026-09-06, with a Basic runtime amendment on 8 October below. This is a source/continuation contract, not complete Richese certification. Read with [the acquisition audit](RICHESE_ACQUISITION_RULES.md). The full-hand publisher question remains unresolved; the user's Basic-first instruction permits the visibly qualified first version below, not a new timing ruling.
## Basic full-hand exchange prototype — 8 October 2026
Any Basic holder at its current hand limit may now pay for the search.
**Provisional capacity policy:** retrieval and final Box disposal settle
as one atomic exchange, with the final hand no larger than its actual
limit. The Box remains reserved in hand while the owner chooses; it is
still discarded last/on top after the remainder is shuffled. This does
not reverse the printed disposal order or adjudicate intermediate capacity.
The pure resolver accepts the engine's explicit Basic full-hand policy;
its default unknown/Advanced policy still requires a spare slot. Both
the completion preview and real settlement use the same policy. Basic
over-limit hands remain rejected. Payment, private inspection entitlement,
canonical/non-Box identities, search lock, event-bound choice and restored
parent paths are unchanged. Own controls expose `fullHandExchange` with a
provisional notice before payment and during selection.
One before/after direct smoke starts from four real cards including the
Box, pays two once, accepts Easy's private choice, leaves four final
cards and exactly one used Box on top. Chromium observed the actual
SSR panel with the warning and enabled paid-search button. Types pass;
no broad suite, recovery campaign or deployed acceptance was run.
Advanced full-hand and Guild-refund contexts remain guarded; wider
interruption/commitment refinement remains later work.
## Verified authority and remaining boundaries
@@ -15,7 +40,7 @@ The publisher's physical card face was independently read again from `/tmp/dune-
This is an ordinary card effect: no generic faction/alliance Karama cancellation, no recipient consent, no Emperor purchase income, Harkonnen bonus or Ixian purchase replacement. The bank is explicit, even with Emperor in the game. No bidding exclusion is printed on Box; Distrans's separate unresolved-bid guard is not transferable to this card.
Unresolved full-hand activation remains a development guard: adding the retrieved card precedes discarding Box in the literal sequence, and no retrieved authority resolves a temporary full hand. An empty pile or a pile containing only Boxes has no legal result. Reject before payment under an explicit unsupported-empty-search policy; there is no authorized deck fallback or printed permission to waste the fee deliberately. Exclude every Box, not only the activating physical ID. Variant/legacy extra Boxes require semantic Box exclusion as well as strict canonical activation validation.
The full-hand intermediate-capacity question remains unresolved. Basic now uses the explicit atomic-exchange prototype above; Advanced retains the spare-slot guard. Empty or only-Box piles still have no legal result and are rejected before payment under the existing unsupported-empty-search policy. There is no deck fallback or permission to waste the fee; every Box is excluded from retrieval.
A further boundary comes from an existing provisional engine policy, not the Box source. `executeSpecialKaramaIntent` for Guild may refund `g.karamaShipping.card` by taking that exact previously discarded Karama back into its original owner's hand. Box could first recover that same card. Pausing the pile during search does not resolve the later conflicting claim. Do not silently blacklist an otherwise legal non-Box search card as an official rule. Until the provisional refund policy is resolved, guard that particular pending-refund context before payment and label the reason accurately. No broader ban on Box during Shipment is justified.
+5
View File
@@ -45,6 +45,11 @@ Distrans itself as the gifted card is not resolved by the face's give-then-disca
## 4. Nullentropy Box
**8 October Basic runtime amendment:** Nullentropy Box now supports a visibly
provisional full-hand atomic exchange, preserving the final limit and discarding
the used Box last. This does not settle the printed intermediate-capacity
question; Advanced remains guarded. [Current Box contract](NULLENTROPY_BOX_ENGINE_AUDIT.md).
**Exact printed sequence, in original prose:**
1. Pay two spice to the bank.
+1
View File
@@ -18,6 +18,7 @@ They supersede earlier software-absence claims only within their named scope.
| Ordinary mixed No-Field battle | Private pool is physical forces plus reserve-limited token materialization; both plans seal before one reveal. Advanced and combined Occupy remain separate. | [No-Field](RICHESE_NO_FIELD_RULES.md) |
| Guild-recipient allied No-Field | Rounded one-force Guild fee: one spice to bank, even when Richese funds it; no Guild self-shipment income. | [Allied No-Field payment](RICHESE_ALLIED_SHIPMENT_AUDIT.md) |
| Already-active Karama / allied No-Field | Honor the already-played rounded one-force Guild rate and bank routing; no implicit additional card or shipment. | [Allied No-Field payment](RICHESE_ALLIED_SHIPMENT_AUDIT.md) |
| Full-hand Nullentropy Box | Basic permits an atomic retrieval/Box-disposal exchange with final hand within its actual limit; Box remains last/on top. Intermediate capacity is not adjudicated; Advanced remains guarded. | [Box runtime](NULLENTROPY_BOX_ENGINE_AUDIT.md) |
No publication or faction/mode gate is opened. The exact 7 October user
rulings below remain distinct from these prototype choices.
+11 -3
View File
@@ -4098,9 +4098,12 @@ function validateNullentropyBegin(g: Game, owner: Player, cardId: string) {
);
const reserved = transferCardBlock(g, owner, card);
requireRule(!reserved, reserved ?? 'The Box is already committed.');
const limit = handLimit(owner);
requireRule(
owner.hand.length < handLimit(owner),
'Full-hand Nullentropy Box use is unresolved; the current guard requires a pre-existing free hand slot.',
g.advanced ? owner.hand.length < limit : owner.hand.length <= limit,
g.advanced
? 'Full-hand Nullentropy Box use is unresolved; the current guard requires a pre-existing free hand slot.'
: 'The final exchanged hand must not exceed its hand limit.',
);
const guild = byFaction(g, 'guild');
requireRule(
@@ -4168,6 +4171,7 @@ function finishNullentropy(
cardId,
remaining.map((c) => c.id),
handLimit(owner),
!g.advanced,
);
} catch (error) {
throw new RuleError(
@@ -4194,6 +4198,7 @@ function finishNullentropy(
cardId,
order,
handLimit(owner),
!g.advanced,
);
owner.hand = result.ownerHand;
g.discard = result.discard;
@@ -4257,10 +4262,12 @@ function nullentropyView(g: Game, owner: Player) {
(c) => richeseCardDefinition(c)?.card.effect === 'nullentropyBox',
);
if (!card) return null;
const fullHandExchange = !g.advanced && owner.hand.length === handLimit(owner);
if (pending?.player === owner.id) {
const blocked = nullentropyIntegrity(g);
return {
card,
...(fullHandExchange ? { fullHandExchange: true as const } : {}),
blocked,
search: blocked
? null
@@ -4280,7 +4287,8 @@ function nullentropyView(g: Game, owner: Player) {
if (!(error instanceof RuleError)) throw error;
blocked = error.message;
}
return { card, blocked, search: null };
return { card, blocked, search: null,
...(fullHandExchange ? { fullHandExchange: true as const } : {}) };
}
function savedTransferResponses(g: Game) {
const canceled = (pending: Game['pendingKarama']) =>
+6 -4
View File
@@ -37,6 +37,7 @@ export function resolveNullentropyBox(
selectedId: string,
remainingOrderIds: readonly string[],
handLimit: number,
allowFullHandExchange = false,
): NullentropyBoxResult {
if (!Number.isSafeInteger(handLimit) || handLimit < 0)
throw new RangeError('The hand limit must be a nonnegative safe integer.');
@@ -46,10 +47,11 @@ export function resolveNullentropyBox(
throw new Error(
'Use the canonical Nullentropy Box physically in your hand.',
);
if (ownerHand.length >= handLimit)
throw new Error(
'Full-hand Nullentropy Box use is unresolved; a pre-existing free hand slot is required by the current guard.',
);
if (ownerHand.length > handLimit ||
(!allowFullHandExchange && ownerHand.length === handLimit))
throw new Error(allowFullHandExchange
? 'The final exchanged hand must not exceed its hand limit.'
: 'Full-hand Nullentropy Box use is unresolved; a pre-existing free hand slot is required by the current guard.');
const selected = discard.find((card) => card.id === selectedId);
if (!selected || isAnyBox(selected))
throw new Error('Choose a discard card other than any Nullentropy Box.');
+4 -3
View File
@@ -940,6 +940,7 @@ export const RULE_TOPICS: RuleTopic[] = [
title: 'Nullentropy Box: paid private discard search',
category: 'Cards',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary:
'Pay two spice to search privately, recover one non-Box card, shuffle the remainder and discard the Box on top.',
steps: [
@@ -948,10 +949,10 @@ export const RULE_TOPICS: RuleTopic[] = [
'During the paid decision, inspect the cards and use Take on one card to finish. Inspection itself does not select anything. There is no free cancellation after viewing the pile and no second confirmation after selection. If exactly one legal card is available, the server selects it automatically without asking for an identical acknowledgement.',
'Payment is recorded once before inspection. Refresh or reconnect restores an unfinished paid search for its owner without charging again. While it is open, other gameplay actions wait so they cannot change the reserved Box or discard pile. The completed search saves the recovered card and shuffled pile before restoring an interrupted response, decision or phase opening. Recovery does not charge, select or shuffle again, and does not replay the earlier action.',
'The selected card enters your hand privately. Other players do not receive the selected identity or the shuffled discard faces. Temporary search access ends on completion; the normal discard pile remains private, even though the played Box is placed face up on top. Past inspection records do not become a permanent live discard browser.',
'An active Truthtrance question and exact card or transaction commitments must resolve as required. A full hand is currently guarded: the printed sequence adds the recovered card before discarding the Box, and the temporary capacity question is unresolved. The current implementation requires a pre-existing free hand slot. This is not a verified prohibition on every net-zero exchange.',
'An active Truthtrance question and exact card or transaction commitments must resolve as required. Basic full-hand holders now have a visibly provisional atomic exchange: retrieve one non-Box and discard the played Box, leaving the final hand at its current limit. The printed intermediate-capacity question remains unresolved. Advanced still requires a pre-existing spare slot, and Basic over-limit hands are rejected.',
'A pending Guild refund claim on a discarded shipping Karama also blocks starting the search while that provisional refund policy is unresolved. The guard protects the competing claim to that card; it is not a general ban on Box during Shipment. Empty or only-Box searches are rejected before payment under the current unsupported-empty-search policy; there is no fallback deck draw.',
'All four AI profiles start only when their own projected availability and spice reserve allow it. They do not inspect unpaid discard contents. Once paid, they choose from their entitled cards, prefer existing functional effects over unfinished Richese faces and complete a legal selection even if only unfinished effects remain. A paid search takes priority over optional actions.',
'This bounded search, its controls and recovery are integrated in development fixtures. Full-hand activation and the provisional Guild refund interaction remain unresolved. In the explicit Semuta preview, another holder may recover the used Box after a clean completed search without replaying payment or shuffle; other Semuta discard sources remain unfinished. Mirror Weapon has a separate bounded CHOAM/Richese-deck classic-faction battle path, and full Richese starts remain disabled. Complete browser, mobile and full-game acceptance is not claimed here.',
'The paid search and continuations are integrated in development fixtures, with a connected Basic full-hand prototype. Advanced full-hand activation and the provisional Guild refund interaction remain guarded. The explicit Semuta preview can recover the used Box after a clean completed search without replaying payment or shuffle; other discard compositions remain separate. Full Richese starts and complete browser/mobile/full-game acceptance remain gated.',
],
related: [
'card-richese-nullentropy-box',
@@ -965,7 +966,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Implementation',
status: 'Partial',
detail:
'Paid inspection, exact private custody, server-shuffled remainder, Box on top, sole-choice automatic completion and restored parent continuations are integrated. Full-hand and provisional Guild-refund guards remain; future discard reactions are unfinished.',
'Paid inspection, exact private custody, shuffled remainder, Box on top, sole-choice completion and restored parent continuations are integrated. Basic full-hand exchange has a connected provisional final-capacity policy. Advanced full-hand and Guild-refund guards remain; wider discard/commitment refinement is separate.',
},
{
area: 'Player controls',
+4 -4
View File
@@ -26,7 +26,7 @@ void test('all twelve holders can pay and search across nine phases in both mode
});
void test('prepayment projection has no candidates and selection entitlement ends after exact private recovery',()=>{
const g=fixture(), selected=g.discard[0];
const unpaid=viewGame(g,'p').nullentropy!;assert.equal(unpaid.search,null);assert.deepEqual(Object.keys(unpaid).sort(),['blocked','card','search']);
const unpaid=viewGame(g,'p').nullentropy!;assert.equal(unpaid.search,null);
assert.equal(viewGame(g,'q').nullentropy,null);
const paid=begin(g);assert.equal(viewGame(paid,'p').nullentropy!.search!.cards.length,3);
assert.equal(viewGame(paid,'q').nullentropy,null);assert.equal(JSON.stringify(viewGame(paid,'q')).includes(selected.id),false);
@@ -38,9 +38,9 @@ void test('a sole eligible card is taken automatically while every other Box sta
const done=begin(g);assert.equal(done.pendingNullentropy,null);assert.equal(done.decision,null);assert.equal(done.players[0].spice,8);
assert.deepEqual(done.players[0].hand,[selected]);assert.deepEqual(done.discard.map(c=>c.id),['other-box',boxId]);
});
void test('illegal starts do not charge or expose a search, including unresolved full-hand and Guild-refund cases',()=>{
for(const change of ['full','funds','empty','onlyBox','reservedRefund','preselect'] as const){
const g=fixture();if(change==='full')g.players[0].hand.push(...g.deck.splice(0,3));
void test('illegal starts do not charge or expose a search, including Guild-refund cases',()=>{
for(const change of ['funds','empty','onlyBox','reservedRefund','preselect'] as const){
const g=fixture();
if(change==='funds')g.players[0].spice=1;if(change==='empty')g.discard=[];
if(change==='onlyBox')g.discard=[{...g.players[0].hand[0],id:'other-box'}];
if(change==='reservedRefund'){g.players[1].faction='guild';g.karamaShipping={owner:'p',player:'p',card:g.discard[0].id};}