fix: expansion pack generation limit by adding a withReplacement setting and enabling it for set-based drafts.

This commit is contained in:
2025-12-17 01:45:27 +01:00
parent ca2efb5cd7
commit 97276979bf
5 changed files with 84 additions and 11 deletions

View File

@@ -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. - [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. - [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. - [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.

View File

@@ -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

View File

@@ -214,7 +214,10 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
cards: sourceMode === 'upload' ? rawScryfallData : [], cards: sourceMode === 'upload' ? rawScryfallData : [],
sourceMode, sourceMode,
selectedSets, selectedSets,
settings: genSettings, settings: {
...genSettings,
withReplacement: sourceMode === 'set'
},
numBoxes, numBoxes,
numPacks: sourceMode === 'set' ? (numBoxes * 36) : undefined, numPacks: sourceMode === 'set' ? (numBoxes * 36) : undefined,
filters filters

View File

@@ -76,6 +76,7 @@ export interface SetsMap {
export interface PackGenerationSettings { export interface PackGenerationSettings {
mode: 'mixed' | 'by_set'; mode: 'mixed' | 'by_set';
rarityMode: 'peasant' | 'standard'; // Peasant: 10C/3U, Standard: 10C/3U/1R rarityMode: 'peasant' | 'standard'; // Peasant: 10C/3U, Standard: 10C/3U/1R
withReplacement?: boolean;
} }
export class PackGeneratorService { export class PackGeneratorService {

View File

@@ -49,6 +49,7 @@ export interface SetsMap {
export interface PackGenerationSettings { export interface PackGenerationSettings {
mode: 'mixed' | 'by_set'; mode: 'mixed' | 'by_set';
rarityMode: 'peasant' | 'standard'; // Peasant: 10C/3U, Standard: 10C/3U/1R 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 { export class PackGeneratorService {
@@ -149,7 +150,7 @@ export class PackGeneratorService {
generatePacks(pools: ProcessedPools, sets: SetsMap, settings: PackGenerationSettings, numPacks: number): Pack[] { generatePacks(pools: ProcessedPools, sets: SetsMap, settings: PackGenerationSettings, numPacks: number): Pack[] {
console.time('generatePacks'); 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? // 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. // 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') { if (settings.mode === 'mixed') {
// Mixed Mode (Chaos) // Mixed Mode (Chaos)
const currentPools = { // Initial Shuffle of the master pools
let currentPools = {
commons: this.shuffle([...pools.commons]), commons: this.shuffle([...pools.commons]),
uncommons: this.shuffle([...pools.uncommons]), uncommons: this.shuffle([...pools.uncommons]),
rares: this.shuffle([...pools.rares]), rares: this.shuffle([...pools.rares]),
@@ -177,12 +179,42 @@ export class PackGeneratorService {
}); });
for (let i = 1; i <= numPacks; i++) { for (let i = 1; i <= numPacks; i++) {
const result = this.buildSinglePack(currentPools, i, 'Chaos Pack', settings.rarityMode); // If infinite, we reset the pools for every pack (using a fresh shuffle of original pools)
if (!result) { let packPools = currentPools;
console.warn(`[PackGenerator] Warning: ran out of cards at pack ${i}`); if (settings.withReplacement) {
break; 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...`); if (i % 50 === 0) console.log(`[PackGenerator] Built ${i} packs...`);
} }
@@ -208,7 +240,8 @@ export class PackGeneratorService {
const data = sets[setCode]; const data = sets[setCode];
console.log(`[PackGenerator] Generating ${packsPerSet} packs for set ${data.name}`); console.log(`[PackGenerator] Generating ${packsPerSet} packs for set ${data.name}`);
const currentPools = { // Initial Shuffle
let currentPools = {
commons: this.shuffle([...data.commons]), commons: this.shuffle([...data.commons]),
uncommons: this.shuffle([...data.uncommons]), uncommons: this.shuffle([...data.uncommons]),
rares: this.shuffle([...data.rares]), rares: this.shuffle([...data.rares]),
@@ -220,13 +253,26 @@ export class PackGeneratorService {
for (let i = 0; i < packsPerSet; i++) { for (let i = 0; i < packsPerSet; i++) {
if (packId > numPacks) break; 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) { if (result) {
newPacks.push(result); newPacks.push(result);
packId++; packId++;
} else { } else {
console.warn(`[PackGenerator] Set ${data.name} depleted at pack ${packId}`); console.warn(`[PackGenerator] Set ${data.name} depleted at pack ${packId}`);
break; if (!settings.withReplacement) break;
} }
} }
} }