fix: expansion pack generation limit by adding a withReplacement setting and enabling it for set-based drafts.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user