Add private-hand search, category filters and display sorting

Wrap existing physical hand cards with local name search, printed-category
filters and saved/name/category display order. Preserve card IDs, actions and
inspectors, keep required decisions outside filtering, and recompute results
from current owned cards. Provide count, clear filters, keyboard scrolling,
separate empty/no-match states and internal guidance. Filters reset on leaving
the hand tab, changing seats or refreshing; saved order is never modified.
Correct the stale Harass availability note to reflect connected allocation.

Six focused tests cover normalized literal search, duplicate physical IDs,
canonical expansion categories, stable immutable sorting, current holdings,
JSON restoration, full-table per-seat privacy and empty setup messaging.
Independent review found no material issue. Types, lint, all 4996 offline
tests and production build pass on stable source. No HTTP, auth, schema or
persistence implementation changed, so HTTP tests were not repeated.

Browser QA used a new two-player Basic Harkonnen/Easy Atreides game created
through genuine setup. Search/category/reset, named inspector and all three
sort orders worked. An active no-match filter updated from 0 of 2 to 0 of 4
when a real auction purchase and Harkonnen bonus arrived. Leaving the hand tab
and refreshing restored all four cards in their original saved order while
the second auction remained at a human decision. Enter in search performed
no game action. The internal guide worked by keyboard. At 390px, controls
were 44px high with 16px text and 14px labels, form width/scroll width 339px;
the focused overflowing card row responded to ArrowRight. Normal viewport
restored. Screenshot capture timed out; full visual/screen-reader acceptance
and complete expansion/card-effect combinations remain open.

All 1538 opening rooms, 3258 seats and auxiliary rows
remain unchanged. SQLite integrity and foreign keys pass. Only one isolated
QA game, its human seat and entry receipt were added; the AI remains in game state. Healthy server reused without restart,
migration, reset or automation. No AI strategy or mode/publication gate changed.

Private evidence: 20260921-hand-browsing.
Verified source tree: 39dce28efa52dd66baf019baaf8610e6795601b3b569ac4ac0b6f405bffc1d96
This commit is contained in:
Quintine committed 2026-09-21 12:00:34 +10:00
1 parent 14a7800fe9
commit e3cfd82540
13 files changed
+328 -27

No files matched your search

+1 -1
View File
@@ -112,7 +112,7 @@ export function cardPresentation(card: VisibleCard): CardPresentation {
availability: ecaz.card.effect === 'recruits'
? 'Recruits has development controls during Revival. Earlier paid revivals, pending transactions, later free-rate changes and repeated use this turn remain guarded pending their rulings.'
: ecaz.card.effect === 'harassWithdraw'
? 'Harass & Withdraw has development controls in either battle-card slot. Ambiguous physical allocations, Richese card combinations and additional optional modules remain guarded.'
? 'Harass & Withdraw has development controls in either battle-card slot, with physical allocation choices after reveal. Richese card combinations and additional optional modules remain guarded.'
: 'This battle effect is still being implemented.',
topics: RULE_TOPICS.filter((topic) => topic.id === (ecaz.card.effect === 'harassWithdraw' ? 'card-harass-withdraw' : 'card-recruits')),
};
+23
View File
@@ -0,0 +1,23 @@
import type { Card } from './cards';
import { CARD_CATEGORIES, printedCardCategory, type CardCategory } from './card-category';
export type HandCategory = 'all' | CardCategory;
export type HandSort = 'hand' | 'name' | 'category';
const normalized = (name: string) => name.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase();
/** Select already-visible physical cards without changing their saved order or identity.
* Printed categories organize the display; they never decide action legality. */
export function browseHand<T extends Card>(cards: readonly T[], query: string, category: HandCategory, sort: HandSort): T[] {
const needle = normalized(query.trim());
const result = cards.filter(card =>
(!needle || normalized(card.name).includes(needle)) &&
(category === 'all' || printedCardCategory(card) === category),
);
if (sort !== 'hand') result.sort((a, b) =>
(sort === 'category'
? CARD_CATEGORIES.indexOf(printedCardCategory(a)) - CARD_CATEGORIES.indexOf(printedCardCategory(b))
: 0) || a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }),
);
return result;
}
+25
View File
@@ -205,6 +205,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'interactive-introduction',
'storm-cards',
'draw-piles',
'hand-browsing',
'kwisatz-haderach',
'truthtrance-spice',
'ecaz-ambassadors',
@@ -2108,6 +2109,30 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Verification', status: 'Partial', detail: 'Full-table rendering, JSON continuation, pending-window rejection and SQLite acceptance/withdrawal races are covered. All-configuration and full visual acceptance remain open.', evidence: ['tests/nexus-offer-controls.test.tsx', 'tests/nexus-offer-recovery.test.ts', 'tests/nexus-alliance.test.ts'] },
],
},
{
id: 'hand-browsing',
title: 'Browse your private hand',
category: 'Getting started',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Find cards by name or printed category and arrange their display while keeping every physical card and its controls.',
steps: [
'Open Your private hand and type part of a card name in Search your hand. Search ignores capitalization, surrounding spaces and accents; punctuation is literal. The result count shows how many of your current cards are visible.',
'Printed category filters weapons, defenses, Worthless cards, Cheap Heroes or Heroines, and Special cards. These are the cards’ primary printed categories. Flexible battle roles and faction powers still use their normal choices and rules.',
'Display order offers your saved hand order, Name A–Z or printed category with names within each category. Identical names remain separate physical cards. Choosing Hand order restores their saved sequence.',
'Show all cards clears the name and category filters while keeping your selected display order. An empty hand has a different message from a search with no matches.',
'Inspect and play the displayed cards using their existing controls. Required decisions and dedicated effect panels remain outside this filter. Browsing does not play, discard, rearrange the saved hand or change a Battle Plan.',
'Incoming table updates recalculate the results from the cards you currently own. Filters and display sorting are local to the open hand panel; leaving the tab, changing seats or refreshing resets them. Saved cards and pending game decisions remain intact.',
],
related: ['draw-piles', 'battle-cards', 'privacy', 'nullentropy-search'],
checklist: [
{ area: 'Implementation', status: 'Implemented', detail: 'Search, printed-category filtering and stable display sorting select only the current owned cards without changing their physical IDs or saved order.', evidence: ['game/hand-browsing.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'Native search/category/order controls, result count, clear filters, keyboard scrolling and existing per-card inspectors/actions are connected.', evidence: ['components/hand-browser.tsx', 'components/game-table.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'This local display adds no game action. Existing legal AI participation is unchanged; strategic refinement remains deferred.' },
{ area: 'Documentation', status: 'Implemented', detail: 'Filtering, printed roles, sort order, identical cards, empty states, live changes and refresh behavior are explained.', evidence: ['docs/HAND_BROWSING.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused selection and live table rendering cover expansion categories, physical identity, immutable sorting, current-card updates and viewer privacy. Browser and full visual acceptance have separate evidence and limits.', evidence: ['tests/hand-browsing.test.ts', 'tests/hand-browser-controls.test.tsx'] },
],
},
{
id: 'draw-piles',
title: 'Cards and draw piles',