import { Cards } from "../utils/cards" ; import { SegmentedRange } from "../utils/segmented-range" ; import { SupplyCard } from "../dominion/supply-card" ; import { SupplyBan } from "./supply-ban" ; import { SupplyCorrection } from "./supply-correction" ; import { SupplyDivider } from "./supply-divider" ; import { SupplyDivision } from "./supply-division" ; import { SupplyRequirement } from "./supply-requirement" ; import { Supply , Replacements } from "./supply" ; import { getRandomInt , selectRandom } from "../utils/rand" ; import { SupplyDivisions } from "./supply-divisions" ; const NUM_CARDS_IN_KINGDOM = 10 ; const YOUNG_WITCH_ID = "cornucopia_youngwitch" ; const BANE_MIN_COST = 2 ; const BANE_MAX_COST = 3 ; export class SupplyBuilder { private dividers : SupplyDivider [ ] = [ ] ; private requirements : SupplyRequirement [ ] = [ ] ; private bans : SupplyBan [ ] = [ ] ; private corrections : SupplyCorrection [ ] = [ ] ; private forceBaneCard : SupplyCard | null = null ; constructor ( private readonly cards ) { } addDivider ( divider ) { this . dividers . push ( divider ) ; } addRequirement ( requirement ) { this . requirements . push ( requirement ) ; } addBan ( ban ) { this . bans . push ( ban ) ; } addCorrection ( correction ) { this . corrections . push ( correction ) ; } setBaneCard ( baneCard ) { this . forceBaneCard = baneCard ; } createUnfilledDivisions ( existingCards ) { let division = new SupplyDivision ( this . cards , [ ] , [ ] , NUM_CARDS_IN_KINGDOM , new Map ( ) ) ; division = this . prepareDivisionForBanning ( division , existingCards ) ; division = SupplyDivisions . applyBans ( division , this . bans ) ; division = this . addExistingCardsAsAvailable ( division , existingCards ) ; let divisions = SupplyDivisions . applyDividers ( [ division ] , this . dividers ) ; divisions = this . applyExistingCards ( divisions , existingCards ) ; return divisions ; } createSupply ( existingCards ) { let divisions = this . createUnfilledDivisions ( existingCards ) ; divisions = this . applyRequirements ( divisions ) ; divisions = SupplyDivisions . applyCorrections ( divisions , this . corrections ) ; divisions = SupplyDivisions . fillDivisions ( divisions ) ; const baneCard = this . selectBaneCard ( divisions ) ; return this . gatherCardsIntoSupply ( divisions , baneCard ) ; } clone ( ) { const clone = new SupplyBuilder ( this . cards ) ; clone . dividers = this . dividers ; clone . requirements = this . requirements ; clone . bans = this . bans ; clone . corrections = this . corrections ; return clone ; } private prepareDivisionForBanning ( division , existingCards ) { return division . createDivisionWithTotalCount ( division . totalCount - existingCards . length ) ; } private applyRequirements ( divisions ) { divisions = divisions . concat ( ) ; const orderedRequirements = this . orderRequirementsForDivisions ( divisions ) ; for ( let requirement of orderedRequirements ) { if ( requirement . isSatisfied ( divisions ) ) { continue ; } const segmentedRange = this . getSegmentedRangeForRequirement ( requirement , divisions ) ; if ( ! segmentedRange . length ) { throw new Error ( `Unable to satisfy requirement: ${ requirement }.` ) ; } const divisionIndex = segmentedRange . getRandomSegmentIndex ( ) ; const cards = requirement . getSatisfyingCardsFromDivisions ( [ divisions [ divisionIndex ] ] ) ; while ( true ) { const randomIndex = getRandomInt ( 0 , cards . length ) ; const selectedCard = cards [ randomIndex ] ; cards . splice ( randomIndex , 1 ) ; if ( this . allowLockedCard ( divisions , selectedCard ) ) { divisions [ divisionIndex ] = divisions [ divisionIndex ] . createDivisionByLockingCard ( selectedCard . id , cards ) ; break ; } if ( ! cards . length ) { throw new Error ( `Unable to satisfy requirement: ${ requirement }.` ) ; } } } return divisions ; } private addExistingCardsAsAvailable ( division , existingCards ) { const availableCardIds = Cards . extractIds ( division . availableCards ) ; const cardsToAdd = [ ] ; for ( let card of existingCards ) { if ( availableCardIds . indexOf ( card . id ) == - 1 ) { cardsToAdd . push ( card ) ; } } return new SupplyDivision ( division . availableCards . concat ( cardsToAdd ) , division . lockedCards , division . selectedCards , division . totalCount + existingCards . length , division . replacements ) ; } private applyExistingCards ( divisions , existingCards ) { divisions = divisions . concat ( ) ; const cardsForNewDivision = [ ] ; for ( let existingCard of existingCards ) { const divisionIndex = this . findIndexOfDivisionContainingCardId ( divisions , existingCard . id ) ; if ( divisionIndex == - 1 ) { cardsForNewDivision . push ( existingCard ) ; } else { const division = divisions [ divisionIndex ] ; divisions [ divisionIndex ] = division . createDivisionByLockingCard ( existingCard . id ) ; } } if ( ! cardsForNewDivision . length ) { return divisions ; } for ( let i = 0 ; i < cardsForNewDivision . length ; i ++ ) { const divisionIndex = this . getIndexOfDivisionWithMostUnfilledCards ( divisions ) ; const division = divisions [ divisionIndex ] ; divisions [ divisionIndex ] = new SupplyDivision ( division . availableCards , division . lockedCards , division . selectedCards , division . totalCount - 1 , division . replacements ) ; } divisions . push ( new SupplyDivision ( [ ] , cardsForNewDivision , [ ] , cardsForNewDivision . length , new Map ( ) ) ) ; return divisions ; } private selectBaneCard ( divisions ) { if ( ! this . requiresBaneCard ( divisions ) ) { return null ; } if ( this . forceBaneCard ) { return this . forceBaneCard ; } const availableCards = SupplyDivisions . getAvailableCards ( divisions ) . filter ( card => { return card . cost . debt == 0 && card . cost . potion == 0 && card . cost . treasure <= BANE_MAX_COST && card . cost . treasure >= BANE_MIN_COST ; } ) ; if ( ! availableCards . length ) { return null ; } return selectRandom ( availableCards ) ; } private requiresBaneCard ( divisions ) { const supplyCards = SupplyDivisions . getLockedAndSelectedCards ( divisions ) ; return supplyCards . some ( s => s . id == YOUNG_WITCH_ID ) ; } private gatherCardsIntoSupply ( divisions , baneCard ) { const replacements = new Map ( ) ; let cards = [ ] ; for ( let division of divisions ) { for ( let card of division . lockedAndSelectedCards ) { cards . push ( card ) ; replacements . set ( card . id , division . getReplacements ( card . id ) ) ; } } return new Supply ( cards , baneCard , new Replacements ( replacements ) ) ; } private orderRequirementsForDivisions ( divisions ) { const satsifiedRequirements = [ ] ; const requirementAndCountPairs = [ ] ; for ( let requirement of this . requirements ) { if ( requirement . isSatisfied ( divisions ) ) { satsifiedRequirements . push ( requirement ) ; continue ; } requirementAndCountPairs . push ( { requirement : requirement , count : requirement . getSatisfyingCardsFromDivisions ( divisions ) . length } ) ; } requirementAndCountPairs . sort ( ( a , b ) => a . count - b . count ) ; const ordered = [ ] ; for ( let pair of requirementAndCountPairs ) { ordered . push ( pair . requirement ) ; } return satsifiedRequirements . concat ( ordered ) ; } private getSegmentedRangeForRequirement ( requirement , divisions ) { const lengths = [ ] ; for ( let division of divisions ) { if ( division . isFilled ) { lengths . push ( 0 ) ; } else { lengths . push ( requirement . getSatisfyingCardsFromDivisions ( [ division ] ) . length ) ; } } return new SegmentedRange ( 0 , lengths ) ; } private findIndexOfDivisionContainingCardId ( divisions , cardId ) { for ( let i = 0 ; i < divisions . length ; i ++ ) { const division = divisions [ i ] ; if ( ! division . isFilled && Cards . findCardById ( division . availableCards , cardId ) ) { return i ; } } return - 1 ; } private getIndexOfDivisionWithMostUnfilledCards ( divisions ) { let mostUnfilledCards = 0 ; let winningIndex = - 1 ; for ( let i = 0 ; i < divisions . length ; i ++ ) { if ( divisions [ i ] . unfilledCount > mostUnfilledCards ) { mostUnfilledCards = divisions [ i ] . unfilledCount ; winningIndex = i ; } } return winningIndex ; } private allowLockedCard ( divisions , card ) { for ( let correction of this . corrections ) { if ( ! correction . allowLockedCard ( divisions , card ) ) { return false ; } } return true ; } }