fix: strictly enforce 13/14 card pack limits and remove rarity fallback logic in pack generation.
This commit is contained in:
@@ -66,3 +66,4 @@
|
||||
- [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.
|
||||
- [Change Default Filter Flags](./devlog/2025-12-17-025500_change_default_flags.md): Completed. Updated client and server defaults for "Ignore Basic Lands", "Ignore Commander Sets", and "Ignore Tokens" to be unchecked (false).
|
||||
- [Sidebar Max Width](./devlog/2025-12-17-031000_sidebar_max_width.md): Completed. Constrained the left sidebar in Cube Manager to a maximum width of 400px on large screens to improve layout balance.
|
||||
- [Strict Pack Generation Logic](./devlog/2025-12-17-030600_fix_strict_pack_generation.md): Succeeded. Enforced strict 14-card (Standard) and 13-card (Peasant) limits, removing fallback logic to prevent invalid pack generation.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Strict Pack Generation Logic Fix
|
||||
|
||||
## Objective
|
||||
Fix the pack generation algorithm to strictly enforce pack sizes and composition rules. Ensure that "Standard" packs have exactly 14 cards and "Peasant" packs have exactly 13 cards. Any generation attempt that fails to meet these criteria due to card depletion must result in the termination of the generation process for that set, rather than producing partial/invalid packs.
|
||||
|
||||
## Current Issues
|
||||
1. **Partial Packs**: The current generator continues to produce packs even when specific rarity pools (like Rares for Standard) are depleted, resulting in packs with fewer than the required number of cards.
|
||||
2. **Fallbacks**: The logic currently "falls back" to lower rarities (e.g., Common instead of List/Wildcard) to fill slots, which might violate "Strict" adherence if not desired.
|
||||
3. **Size limit**: Packs are not strictly truncated or validated against the target size (14 or 13).
|
||||
|
||||
## Proposed Changes
|
||||
### `PackGeneratorService.ts`
|
||||
1. **Refactor `buildSinglePack`**:
|
||||
- define `targetSize` based on `rarityMode` (14 for Standard, 13 for Peasant).
|
||||
- **Slot 7 (List)**: Remove fallback to Common if Uncommon/List pool is empty. If the strict RNG calls for a List card and none are available, the slot remains empty (causing validation failure).
|
||||
- **Wildcards**: Remove fallback to Common if the rolled rarity pool is empty.
|
||||
- **Peasant Isolation**: Explicitly restrict Peasant Wildcards to Common/Uncommon only (No Rares/Mythics under any RNG circumstance).
|
||||
- **Strict Validation**: At the end of pack construction, check if `packCards.length` is less than `targetSize`. If so, return `null`.
|
||||
- **Truncation**: Slice the `packCards` array to `targetSize` to ensure no "exceeding" cards (like Tokens or extra Wildcards) are included beyond the strict limit.
|
||||
|
||||
2. **Algorithm Details**:
|
||||
- **Standard**: 14 Cards.
|
||||
- Slots 1-6: Commons
|
||||
- Slot 7: Common/List
|
||||
- Slots 8-10: Uncommons
|
||||
- Slot 11: Rare/Mythic
|
||||
- Slot 12: Land
|
||||
- Slot 13: Wildcard
|
||||
- Slot 14: Foil Wildcard
|
||||
- (Slot 15 Token ignored/truncated)
|
||||
- **Peasant**: 13 Cards.
|
||||
- Slots 1-6: Commons
|
||||
- Slot 7: Common/List
|
||||
- Slots 8-11: Uncommons
|
||||
- Slot 12: Land
|
||||
- Slot 13: Wildcard (Common/Uncommon ONLY)
|
||||
- (Slot 14 Foil WC ignored/truncated)
|
||||
- (Slot 15 Token ignored/truncated)
|
||||
|
||||
3. **Behavior**:
|
||||
- If `buildSinglePack` returns `null`, the `generatePacks` loop will `break` (stop), preventing the creation of any further illegal packs from that pool.
|
||||
|
||||
## Verification
|
||||
- Run `npm run build` to ensure compilation.
|
||||
- (Manual) Verify in Draft App that generating from a small pool stops correctly when Rares run out.
|
||||
@@ -287,17 +287,18 @@ export class PackGeneratorService {
|
||||
const packCards: DraftCard[] = [];
|
||||
const namesInPack = new Set<string>();
|
||||
|
||||
// Standard: 14 cards exactly. Peasant: 13 cards exactly.
|
||||
const targetSize = rarityMode === 'peasant' ? 13 : 14;
|
||||
|
||||
// 1. Commons (6)
|
||||
const drawC = this.drawUniqueCards(pools.commons, 6, namesInPack);
|
||||
if (!drawC.success && pools.commons.length < 6) return null; // Hard fail if really empty
|
||||
// Accept partial if just duplication is unavoidable?
|
||||
// "Strict" mode would return null. Let's be lenient but log?
|
||||
packCards.push(...drawC.selected);
|
||||
pools.commons = drawC.remainingPool; // Update ref
|
||||
drawC.selected.forEach(c => namesInPack.add(c.name));
|
||||
if (drawC.selected.length > 0) {
|
||||
packCards.push(...drawC.selected);
|
||||
pools.commons = drawC.remainingPool; // Update ref
|
||||
drawC.selected.forEach(c => namesInPack.add(c.name));
|
||||
}
|
||||
|
||||
// 2. Slot 7 (Common or List)
|
||||
// Quick implementation of logic from memo
|
||||
let slot7: DraftCard | undefined;
|
||||
const roll7 = Math.random() * 100;
|
||||
if (roll7 < 87) {
|
||||
@@ -305,14 +306,10 @@ export class PackGeneratorService {
|
||||
const r = this.drawUniqueCards(pools.commons, 1, namesInPack);
|
||||
if (r.selected.length) { slot7 = r.selected[0]; pools.commons = r.remainingPool; }
|
||||
} else {
|
||||
// Uncommon/List (Simplification: pick uncommon)
|
||||
// Uncommon/List
|
||||
// Strict Mode: If List/Uncommon unavailable, DO NOT fallback to Common.
|
||||
const r = this.drawUniqueCards(pools.uncommons, 1, namesInPack);
|
||||
if (r.selected.length) { slot7 = r.selected[0]; pools.uncommons = r.remainingPool; }
|
||||
else {
|
||||
// Fallback to common
|
||||
const rc = this.drawUniqueCards(pools.commons, 1, namesInPack);
|
||||
if (rc.selected.length) { slot7 = rc.selected[0]; pools.commons = rc.remainingPool; }
|
||||
}
|
||||
}
|
||||
if (slot7) { packCards.push(slot7); namesInPack.add(slot7.name); }
|
||||
|
||||
@@ -320,9 +317,11 @@ export class PackGeneratorService {
|
||||
// Memo says: PEASANT slots 8-11 (4 uncommons). STANDARD slots 8-10 (3 uncommons).
|
||||
const uNeeded = rarityMode === 'peasant' ? 4 : 3;
|
||||
const drawU = this.drawUniqueCards(pools.uncommons, uNeeded, namesInPack);
|
||||
packCards.push(...drawU.selected);
|
||||
pools.uncommons = drawU.remainingPool;
|
||||
drawU.selected.forEach(c => namesInPack.add(c.name));
|
||||
if (drawU.selected.length > 0) {
|
||||
packCards.push(...drawU.selected);
|
||||
pools.uncommons = drawU.remainingPool;
|
||||
drawU.selected.forEach(c => namesInPack.add(c.name));
|
||||
}
|
||||
|
||||
// 4. Rare/Mythic (Standard Only)
|
||||
if (rarityMode === 'standard') {
|
||||
@@ -363,23 +362,28 @@ export class PackGeneratorService {
|
||||
}
|
||||
|
||||
// 6. Wildcards (2 slots) + Foil Wildcard
|
||||
|
||||
// Re-implement Wildcard simply:
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const isFoil = i === 1; // 2nd is foil
|
||||
const wRoll = Math.random() * 100;
|
||||
let targetPool = pools.commons;
|
||||
let targetKey: keyof ProcessedPools = 'commons';
|
||||
|
||||
if (wRoll > 87) { targetPool = pools.mythics; targetKey = 'mythics'; }
|
||||
else if (wRoll > 74) { targetPool = pools.rares; targetKey = 'rares'; }
|
||||
else if (wRoll > 50) { targetPool = pools.uncommons; targetKey = 'uncommons'; }
|
||||
|
||||
if (targetPool.length === 0) {
|
||||
targetPool = pools.commons;
|
||||
targetKey = 'commons';
|
||||
if (rarityMode === 'peasant') {
|
||||
// Peasant Wildcard: Strictly Common or Uncommon. No Rare/Mythic.
|
||||
// Adjusted probability: 40% Uncommon, 60% Common (arbitrary but peasant-friendly)
|
||||
if (wRoll > 60) { targetPool = pools.uncommons; targetKey = 'uncommons'; }
|
||||
else { targetPool = pools.commons; targetKey = 'commons'; }
|
||||
} else {
|
||||
// Standard Wildcard: Can be anything.
|
||||
if (wRoll > 87) { targetPool = pools.mythics; targetKey = 'mythics'; }
|
||||
else if (wRoll > 74) { targetPool = pools.rares; targetKey = 'rares'; }
|
||||
else if (wRoll > 50) { targetPool = pools.uncommons; targetKey = 'uncommons'; }
|
||||
}
|
||||
|
||||
// Strict Mode: NO Fallback if target pool empty.
|
||||
// If targetPool is empty, we simply cannot fill this wildcard slot with the selected rarity.
|
||||
// The slot remains empty, potentially causing valid pack failure.
|
||||
|
||||
const res = this.drawUniqueCards(targetPool, 1, namesInPack);
|
||||
if (res.selected.length) {
|
||||
const c = { ...res.selected[0] };
|
||||
@@ -414,10 +418,19 @@ export class PackGeneratorService {
|
||||
|
||||
packCards.sort((a, b) => getWeight(b) - getWeight(a));
|
||||
|
||||
// ENFORCE SIZE STRICTLY
|
||||
// Truncate to target size (ignoring exceeding tokens/extra)
|
||||
const finalCards = packCards.slice(0, targetSize);
|
||||
|
||||
// Strict Validation: If we don't have enough cards, FAIL.
|
||||
if (finalCards.length < targetSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: packId,
|
||||
setName: setName,
|
||||
cards: packCards
|
||||
cards: finalCards
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user