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.
|
||||
- [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.
|
||||
|
||||
@@ -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 : [],
|
||||
sourceMode,
|
||||
selectedSets,
|
||||
settings: genSettings,
|
||||
settings: {
|
||||
...genSettings,
|
||||
withReplacement: sourceMode === 'set'
|
||||
},
|
||||
numBoxes,
|
||||
numPacks: sourceMode === 'set' ? (numBoxes * 36) : undefined,
|
||||
filters
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user