Add verified Truthtrance hand inventory questions
Players can ask total-hand and primary-role card-count comparisons using the existing count controls and compound facts. Component presentation shares the same default-role classification, including canonical Richese cards, Ix alternate uses, and Ecaz slot-only specials. Only the respondent receives the verified aggregate answer; current facts do not impose future custody promises. Eight new tests cover all 60 catalog cards, real engine actions, malformed input, private worlds, all four bots, JSON continuation, and a legal custody transfer. Expanded production-room SQLite cases cover restored total/category/compound facts, suspended auction state, and one winning concurrent answer/discard. Independent review's two copy issues were fixed before broad verification. Validation: types, lint and 4,200 offline tests passed; production build passed; 40 HTTP tests passed. One stable broad run (check 123036 ms, build 6505 ms, integration 23467 ms). Browser room LC3E55GY verified combined role counts, total hand size, invalid-input feedback and refresh at 390px phone width. All 33 pre-integration rooms preserved unchanged; HTTP checks added 30 rooms. No complete-mode, full-game or publication gate opened. Private report: 20260913-hand-inventory-final/report.json Source fingerprint: c6a69a9e69941cdd1e5734d92afedbf10d31fda36c91d26ca7e15f4fb7c59b18 Efficiency pilot milestone 3: aggregate counter 58,695,053 to 59,144,653 at completed verification = 449,600 tokens, including context/coordination overhead. Two bounded workhorse-model helpers, reused UI and SQLite fixtures, one browser room. Counter is not monetary cost and this differently sized batch does not establish a savings percentage. Final Git/push overhead follows this sample.
This commit is contained in:
1 parent
32791ed5e2
commit
7454456755
14 files changed
+980
-151
No files matched your search
@@ -450,12 +450,45 @@ void test('an existing structured hand fact remains answerable after room-module
|
||||
}
|
||||
});
|
||||
|
||||
void test('hand-count questions restore physical duplicate counts and expose the definite answer only to the target', async () => {
|
||||
for (const [compare, value, answer] of [
|
||||
['eq', 2, 'yes'],
|
||||
['gte', 3, 'no'],
|
||||
['lte', 2, 'yes'],
|
||||
] as const) {
|
||||
void test('hand-count and inventory questions restore physical counts and expose the definite answer only to the target', async () => {
|
||||
const cases: [TruthFact, 'yes' | 'no', 'yes' | 'no'][] = [
|
||||
[
|
||||
{ kind: 'handCount', name: 'Shield', compare: 'eq', value: 2 },
|
||||
'yes',
|
||||
'no',
|
||||
],
|
||||
[
|
||||
{ kind: 'handCount', name: 'Shield', compare: 'gte', value: 3 },
|
||||
'no',
|
||||
'no',
|
||||
],
|
||||
[
|
||||
{ kind: 'handCount', name: 'Shield', compare: 'lte', value: 2 },
|
||||
'yes',
|
||||
'yes',
|
||||
],
|
||||
[
|
||||
{ kind: 'handInventory', category: 'all', compare: 'eq', value: 3 },
|
||||
'yes',
|
||||
'yes',
|
||||
],
|
||||
[
|
||||
{ kind: 'handInventory', category: 'defense', compare: 'eq', value: 2 },
|
||||
'yes',
|
||||
'no',
|
||||
],
|
||||
[
|
||||
{ kind: 'handInventory', category: 'weapon', compare: 'gte', value: 1 },
|
||||
'no',
|
||||
'yes',
|
||||
],
|
||||
[
|
||||
{ kind: 'handInventory', category: 'special', compare: 'lte', value: 1 },
|
||||
'yes',
|
||||
'yes',
|
||||
],
|
||||
];
|
||||
for (const [fact, answer, alternateAnswer] of cases) {
|
||||
const f = await fixture();
|
||||
try {
|
||||
const secondShield = f.initial.deck.findIndex(
|
||||
@@ -466,12 +499,6 @@ void test('hand-count questions restore physical duplicate counts and expose the
|
||||
f.sqlite
|
||||
.prepare('UPDATE rooms SET state=? WHERE code=?')
|
||||
.run(JSON.stringify(f.initial), f.code);
|
||||
const fact: TruthFact = {
|
||||
kind: 'handCount',
|
||||
name: 'Shield',
|
||||
compare,
|
||||
value,
|
||||
};
|
||||
await ask(f, fact);
|
||||
const pending = await restored(f);
|
||||
assertSuspended(f, pending);
|
||||
@@ -502,11 +529,10 @@ void test('hand-count questions restore physical duplicate counts and expose the
|
||||
engine.viewGame(alternative, f.seats[index].playerId),
|
||||
engine.viewGame(pending, f.seats[index].playerId),
|
||||
);
|
||||
if (compare === 'eq')
|
||||
assert.equal(
|
||||
engine.viewGame(alternative, f.seats[1].playerId).truthAnswer,
|
||||
'no',
|
||||
);
|
||||
assert.equal(
|
||||
engine.viewGame(alternative, f.seats[1].playerId).truthAnswer,
|
||||
alternateAnswer,
|
||||
);
|
||||
|
||||
const done = await act(f, 1, { type: 'truthAnswer', answer });
|
||||
assert.equal(done.truthtrance, null);
|
||||
@@ -533,99 +559,112 @@ void test('hand-count questions restore physical duplicate counts and expose the
|
||||
}
|
||||
});
|
||||
|
||||
void test('recovered hand-count answers reject dishonest or wrong-seat requests and concurrent truthful answers commit exactly once', async () => {
|
||||
const f = await fixture();
|
||||
try {
|
||||
await ask(f, {
|
||||
kind: 'handCount',
|
||||
name: 'Shield',
|
||||
compare: 'eq',
|
||||
value: 1,
|
||||
});
|
||||
const before = await restored(f);
|
||||
await assertPrivate(f, 'yes');
|
||||
const writeCount = f.writes.length;
|
||||
for (const index of [0, 2])
|
||||
await assert.rejects(
|
||||
f
|
||||
.restart()
|
||||
.act(
|
||||
f.code,
|
||||
f.seats[index],
|
||||
before.version,
|
||||
{ type: 'truthAnswer', answer: 'yes' },
|
||||
clock,
|
||||
),
|
||||
/questioned player/,
|
||||
);
|
||||
for (const answer of ['no', 'unknown'])
|
||||
await assert.rejects(
|
||||
f
|
||||
.restart()
|
||||
.act(
|
||||
f.code,
|
||||
f.seats[1],
|
||||
before.version,
|
||||
{ type: 'truthAnswer', answer },
|
||||
clock,
|
||||
),
|
||||
/truthfully/,
|
||||
);
|
||||
assert.equal(f.writes.length, writeCount);
|
||||
assert.deepEqual(await f.restart().readRoom(f.code), before);
|
||||
void test('recovered hand-count and compound inventory answers reject dishonest or wrong-seat requests and concurrent truthful answers commit exactly once', async () => {
|
||||
const facts: TruthFact[] = [
|
||||
{ kind: 'handCount', name: 'Shield', compare: 'eq', value: 1 },
|
||||
{ kind: 'handInventory', category: 'all', compare: 'eq', value: 2 },
|
||||
{
|
||||
kind: 'and',
|
||||
terms: [
|
||||
{
|
||||
kind: 'handInventory',
|
||||
category: 'defense',
|
||||
compare: 'gte',
|
||||
value: 1,
|
||||
},
|
||||
{ kind: 'handInventory', category: 'special', compare: 'eq', value: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
for (const fact of facts) {
|
||||
const f = await fixture();
|
||||
try {
|
||||
await ask(f, fact);
|
||||
const before = await restored(f);
|
||||
await assertPrivate(f, 'yes');
|
||||
const writeCount = f.writes.length;
|
||||
for (const index of [0, 2])
|
||||
await assert.rejects(
|
||||
f
|
||||
.restart()
|
||||
.act(
|
||||
f.code,
|
||||
f.seats[index],
|
||||
before.version,
|
||||
{ type: 'truthAnswer', answer: 'yes' },
|
||||
clock,
|
||||
),
|
||||
/questioned player/,
|
||||
);
|
||||
for (const answer of ['no', 'unknown'])
|
||||
await assert.rejects(
|
||||
f
|
||||
.restart()
|
||||
.act(
|
||||
f.code,
|
||||
f.seats[1],
|
||||
before.version,
|
||||
{ type: 'truthAnswer', answer },
|
||||
clock,
|
||||
),
|
||||
/truthfully/,
|
||||
);
|
||||
assert.equal(f.writes.length, writeCount);
|
||||
assert.deepEqual(await f.restart().readRoom(f.code), before);
|
||||
|
||||
f.writes.length = 0;
|
||||
f.hooks.beforeWrite = barrier();
|
||||
const action = { type: 'truthAnswer', answer: 'yes' };
|
||||
const outcomes = await Promise.allSettled([
|
||||
f.rooms.act(f.code, f.seats[1], before.version, action, clock),
|
||||
f.restart().act(f.code, f.seats[1], before.version, action, clock),
|
||||
]);
|
||||
f.hooks.beforeWrite = undefined;
|
||||
assert.equal(
|
||||
outcomes.filter((result) => result.status === 'fulfilled').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
outcomes.filter((result) => result.status === 'rejected').length,
|
||||
1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
f.writes.map((write) => write.changes).sort((a, b) => a - b),
|
||||
[0, 1],
|
||||
);
|
||||
const after = await f.restart().readRoom(f.code);
|
||||
assert.equal(after.version, before.version + 1);
|
||||
assert.equal(after.truthtrance, null);
|
||||
assert.deepEqual(after.truthHistory, [
|
||||
{
|
||||
turn: before.turn,
|
||||
phase: before.phase,
|
||||
asker: f.seats[0].playerId,
|
||||
question: before.truthtrance!.question!,
|
||||
answer: 'yes',
|
||||
},
|
||||
]);
|
||||
assert.equal(
|
||||
after.discard.filter((card) => card.id === f.card.id).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
after.players[0].hand.some((card) => card.id === f.card.id),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(inventory(after), inventory(before));
|
||||
assertSuspended(f, after);
|
||||
const committedWrites = f.writes.length;
|
||||
for (const version of [before.version, after.version])
|
||||
await assert.rejects(
|
||||
f.restart().act(f.code, f.seats[1], version, action, clock),
|
||||
f.writes.length = 0;
|
||||
f.hooks.beforeWrite = barrier();
|
||||
const action = { type: 'truthAnswer', answer: 'yes' };
|
||||
const outcomes = await Promise.allSettled([
|
||||
f.rooms.act(f.code, f.seats[1], before.version, action, clock),
|
||||
f.restart().act(f.code, f.seats[1], before.version, action, clock),
|
||||
]);
|
||||
f.hooks.beforeWrite = undefined;
|
||||
assert.equal(
|
||||
outcomes.filter((result) => result.status === 'fulfilled').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(f.writes.length, committedWrites);
|
||||
assert.deepEqual(await restored(f), after);
|
||||
await assertPrivate(f, null);
|
||||
} finally {
|
||||
f.hooks.beforeWrite = undefined;
|
||||
f.sqlite.close();
|
||||
assert.equal(
|
||||
outcomes.filter((result) => result.status === 'rejected').length,
|
||||
1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
f.writes.map((write) => write.changes).sort((a, b) => a - b),
|
||||
[0, 1],
|
||||
);
|
||||
const after = await f.restart().readRoom(f.code);
|
||||
assert.equal(after.version, before.version + 1);
|
||||
assert.equal(after.truthtrance, null);
|
||||
assert.deepEqual(after.truthHistory, [
|
||||
{
|
||||
turn: before.turn,
|
||||
phase: before.phase,
|
||||
asker: f.seats[0].playerId,
|
||||
question: before.truthtrance!.question!,
|
||||
answer: 'yes',
|
||||
},
|
||||
]);
|
||||
assert.equal(
|
||||
after.discard.filter((card) => card.id === f.card.id).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
after.players[0].hand.some((card) => card.id === f.card.id),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(inventory(after), inventory(before));
|
||||
assertSuspended(f, after);
|
||||
const committedWrites = f.writes.length;
|
||||
for (const version of [before.version, after.version])
|
||||
await assert.rejects(
|
||||
f.restart().act(f.code, f.seats[1], version, action, clock),
|
||||
);
|
||||
assert.equal(f.writes.length, committedWrites);
|
||||
assert.deepEqual(await restored(f), after);
|
||||
await assertPrivate(f, null);
|
||||
} finally {
|
||||
f.hooks.beforeWrite = undefined;
|
||||
f.sqlite.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user