diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 4909317..5dc4b02 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -63,3 +63,4 @@ - [Explicit Preview Suppression](./devlog/2025-12-17-025500_explicit_preview_suppression.md): Completed. Implemented strict `preventPreview` prop to enforce suppression logic reliably regardless of card overlap or DOM state. - [Synchronized Display Boundaries](./devlog/2025-12-17-030000_synchronized_boundaries.md): Completed. Aligned "Art Crop" and "Preview Suppression" thresholds to 200px (50% slider value) for a unified UI behavior. - [Squared Art Crops](./devlog/2025-12-17-030500_squared_art_crops.md): Completed. Enforced square aspect ratio for art-crop thumbnails to optimize visual density and stacking. +- [Fix Expansion Generation Limit](./devlog/2025-12-17-024500_fix_expansion_generation_limit.md): Completed. Implemented "Unlimited Pool" mode for expansion drafts to allow generating large numbers of packs from singleton set data. diff --git a/docs/development/devlog/2025-12-17-024500_fix_expansion_generation_limit.md b/docs/development/devlog/2025-12-17-024500_fix_expansion_generation_limit.md new file mode 100644 index 0000000..8c03c72 --- /dev/null +++ b/docs/development/devlog/2025-12-17-024500_fix_expansion_generation_limit.md @@ -0,0 +1,22 @@ +# Bug Fix: Pack Generation Limits in From Expansion Mode + +## Issue +The user reported that when generating "1 Box" (36 packs) in "From Expansion" mode, only about 10 packs were generated. +This was caused by the pack generation algorithm treating the card pool as finite (consuming cards as they are picked). Since Scryfall data usually provides a singleton list (1 copy of each card), the pool of Commons would deplete rapidly (e.g., 10 packs * 10 commons = 100 commons), halting generation when unique commons ran out. + +## Solution +Implemented a "Unlimited Pool" / "With Replacement" mode for pack generation. +- **Server (`PackGeneratorService.ts`)**: Added `withReplacement` flag to `PackGenerationSettings`. + - When enabled, the generator creates a FRESH copy of the shuffled pool for EACH pack. + - This simulates a "Retail Draft" or "Print Run" scenario where packs are independent samples from a large supply, rather than drawing from a fixed, finite Cube. + - Uniqueness is still enforced WITHIN each pack (no duplicate cards in the same pack). +- **Client (`CubeManager.tsx`)**: updated the payload to strictly enable `withReplacement: true` whenever `sourceMode` is set to "From Expansion" ("set"). + +## Files Modified +- `src/server/services/PackGeneratorService.ts`: Implemented replacement logic. +- `src/client/src/modules/cube/CubeManager.tsx`: Updated API call payload. +- `src/client/src/services/PackGeneratorService.ts`: Updated interface definitions. + +## Status +- [x] Fix Implemented +- [x] Verified Logic diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index af0d614..f57aaa6 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -214,7 +214,10 @@ export const CubeManager: React.FC = ({ packs, setPacks, onGoT cards: sourceMode === 'upload' ? rawScryfallData : [], sourceMode, selectedSets, - settings: genSettings, + settings: { + ...genSettings, + withReplacement: sourceMode === 'set' + }, numBoxes, numPacks: sourceMode === 'set' ? (numBoxes * 36) : undefined, filters diff --git a/src/client/src/services/PackGeneratorService.ts b/src/client/src/services/PackGeneratorService.ts index 34f99c9..aa400d1 100644 --- a/src/client/src/services/PackGeneratorService.ts +++ b/src/client/src/services/PackGeneratorService.ts @@ -76,6 +76,7 @@ export interface SetsMap { export interface PackGenerationSettings { mode: 'mixed' | 'by_set'; rarityMode: 'peasant' | 'standard'; // Peasant: 10C/3U, Standard: 10C/3U/1R + withReplacement?: boolean; } export class PackGeneratorService { diff --git a/src/server/services/PackGeneratorService.ts b/src/server/services/PackGeneratorService.ts index 1833234..f29f450 100644 --- a/src/server/services/PackGeneratorService.ts +++ b/src/server/services/PackGeneratorService.ts @@ -49,6 +49,7 @@ export interface SetsMap { export interface PackGenerationSettings { mode: 'mixed' | 'by_set'; rarityMode: 'peasant' | 'standard'; // Peasant: 10C/3U, Standard: 10C/3U/1R + withReplacement?: boolean; // If true, pools are refilled/reshuffled for each pack (unlimited generation) } export class PackGeneratorService { @@ -149,7 +150,7 @@ export class PackGeneratorService { generatePacks(pools: ProcessedPools, sets: SetsMap, settings: PackGenerationSettings, numPacks: number): Pack[] { console.time('generatePacks'); - console.log('[PackGenerator] Starting generation:', { mode: settings.mode, rarity: settings.rarityMode, count: numPacks }); + console.log('[PackGenerator] Starting generation:', { mode: settings.mode, rarity: settings.rarityMode, count: numPacks, infinite: settings.withReplacement }); // Optimize: Deep clone only what's needed? // Actually, we destructively modify lists in the algo (shifting/drawing), so we must clone the arrays of specific pools we use. @@ -159,7 +160,8 @@ export class PackGeneratorService { if (settings.mode === 'mixed') { // Mixed Mode (Chaos) - const currentPools = { + // Initial Shuffle of the master pools + let currentPools = { commons: this.shuffle([...pools.commons]), uncommons: this.shuffle([...pools.uncommons]), rares: this.shuffle([...pools.rares]), @@ -177,12 +179,42 @@ export class PackGeneratorService { }); for (let i = 1; i <= numPacks; i++) { - const result = this.buildSinglePack(currentPools, i, 'Chaos Pack', settings.rarityMode); - if (!result) { - console.warn(`[PackGenerator] Warning: ran out of cards at pack ${i}`); - break; + // If infinite, we reset the pools for every pack (using a fresh shuffle of original pools) + let packPools = currentPools; + if (settings.withReplacement) { + packPools = { + commons: this.shuffle([...pools.commons]), + uncommons: this.shuffle([...pools.uncommons]), + rares: this.shuffle([...pools.rares]), + mythics: this.shuffle([...pools.mythics]), + lands: this.shuffle([...pools.lands]), + tokens: this.shuffle([...pools.tokens]) + }; + } + + const result = this.buildSinglePack(packPools, i, 'Chaos Pack', settings.rarityMode); + + if (result) { + newPacks.push(result); + if (!settings.withReplacement) { + // If not infinite, we must persist the depleting state + // This assumes buildSinglePack MODIFIED packPools in place (via reassigning properties). + // However, packPools is a shallow clone of currentPools if (settings.infinite) was false? + // Wait. 'let packPools = currentPools' is a reference copy. + // buildSinglePack reassigns properties of packPools. + // e.g. packPools.commons = ... + // This mutates the object 'packPools'. + // If 'packPools' IS 'currentPools', then 'currentPools' is mutated. Correct. + } + } else { + if (!settings.withReplacement) { + console.warn(`[PackGenerator] Warning: ran out of cards at pack ${i}`); + break; + } else { + // Should not happen with replacement unless pools are intrinsically empty + console.warn(`[PackGenerator] Infinite mode but failed to generate pack ${i} (empty source?)`); + } } - newPacks.push(result); if (i % 50 === 0) console.log(`[PackGenerator] Built ${i} packs...`); } @@ -208,7 +240,8 @@ export class PackGeneratorService { const data = sets[setCode]; console.log(`[PackGenerator] Generating ${packsPerSet} packs for set ${data.name}`); - const currentPools = { + // Initial Shuffle + let currentPools = { commons: this.shuffle([...data.commons]), uncommons: this.shuffle([...data.uncommons]), rares: this.shuffle([...data.rares]), @@ -220,13 +253,26 @@ export class PackGeneratorService { for (let i = 0; i < packsPerSet; i++) { if (packId > numPacks) break; - const result = this.buildSinglePack(currentPools, packId, data.name, settings.rarityMode); + let packPools = currentPools; + if (settings.withReplacement) { + // Refresh pools for every pack from the source data + packPools = { + commons: this.shuffle([...data.commons]), + uncommons: this.shuffle([...data.uncommons]), + rares: this.shuffle([...data.rares]), + mythics: this.shuffle([...data.mythics]), + lands: this.shuffle([...data.lands]), + tokens: this.shuffle([...data.tokens]) + }; + } + + const result = this.buildSinglePack(packPools, packId, data.name, settings.rarityMode); if (result) { newPacks.push(result); packId++; } else { console.warn(`[PackGenerator] Set ${data.name} depleted at pack ${packId}`); - break; + if (!settings.withReplacement) break; } } }