import { Addon } from "../dominion/addon" ; import { CardSupplyBan } from "./card-supply-ban" ; import { CostSupplyBan } from "./cost-supply-ban" ; import { CostSupplyDivider } from "./cost-supply-divider" ; import { CardType } from "../dominion/card-type" ; import { Cards } from "../utils/cards" ; import { DominionSet } from "../dominion/dominion-set" ; import { DominionSets } from "../dominion/dominion-sets" ; import { Event } from "../dominion/event" ; import { Kingdom } from "./kingdom" ; import { Landmark } from "../dominion/landmark" ; import { Metadata as KingdomMetadata } from "./kingdom" ; import { Project } from "../dominion/project" ; import { RandomizerOptions } from "./randomizer-options" ; import { SetId } from "../dominion/set-id" ; import { SetSupplyBan } from "./set-supply-ban" ; import { SetSupplyDivider } from "./set-supply-divider" ; import { Supply } from "./supply" ; import { SupplyBuilder } from "./supply-builder" ; import { SupplyCard } from "../dominion/supply-card" ; import { SupplyDivisions } from "./supply-divisions" ; import { TypeSupplyBan } from "./type-supply-ban" ; import { TypeSupplyRequirement } from "./type-supply-requirement" ; import { getRandomInt , getRandomInts , selectRandomN } from "../utils/rand" ; import { Boon } from "../dominion/boon" ; import { Way } from "../dominion/way" ; const SETS_WITH_DUPLICATES = { 'baseset2' : 'baseset' , 'intrigue2' : 'intrigue' } ; const MAX_RETRIES = 3 ; const NUM_CARDS_IN_KINGDOM = 10 ; const MIN_ALCHEMY_CARDS_IN_KINGDOM = 3 ; const MAX_ALCHEMY_CARDS_IN_KINGDOM = 5 ; const HIGH_COST_CUT_OFF = 5 ; const MIN_HIGH_CARDS_IN_KINGDOM = 3 ; const MAX_HIGH_CARDS_IN_KINGDOM = 5 ; const MAX_ADDONS_IN_KINGDOM = 2 ; const NUM_PRIORITIZED_SET = 5 ; export class Randomizer { static createKingdom ( randomizerOptions ) { const supply = this . createSupplyWithRetries ( randomizerOptions ) ; const addons = this . getAddons ( randomizerOptions . setIds ) ; const boons = this . getRandomBoons ( supply , [ ] ) ; const metadata = this . getMetadata ( randomizerOptions . setIds ) ; return new Kingdom ( Date . now ( ) , supply , addons . events , addons . landmarks , addons . projects , addons . ways , boons , metadata ) ; } static createSupplySafe ( randomizerOptions ) { try { return this . createSupplyWithRetries ( randomizerOptions ) ; } catch ( error ) { console . log ( `Failed to create supply: \n${ error . toString ( ) }` ) ; return null ; } } static createSupplyWithRetries ( randomizerOptions ) { let retries = MAX_RETRIES ; while ( retries > 0 ) { try { return this . createSupply ( randomizerOptions ) ; } catch ( error ) { console . log ( `Error when trying to select cards: \n${ error . toString ( ) }` ) ; retries -= 1 ; } } throw new Error ( "Failed to select cards that satisfied all requirements." ) ; } static createSupply ( randomizerOptions ) { const allSupplyCards = Cards . getAllSupplyCards ( Cards . getAllCardsFromSets ( DominionSets . getAllSets ( ) ) ) ; const allSupplyCardsToUse = this . removeDuplicateCards ( allSupplyCards . filter ( Cards . filterByIncludedSetIds ( randomizerOptions . setIds ) ) , [ ] ) ; let supplyBuilder = new SupplyBuilder ( allSupplyCardsToUse ) ; if ( randomizerOptions . baneCardId ) { supplyBuilder . setBaneCard ( DominionSets . getSupplyCardById ( randomizerOptions . baneCardId ) ) ; supplyBuilder . addBan ( new CardSupplyBan ( [ randomizerOptions . baneCardId ] ) ) ; } if ( randomizerOptions . excludeCardIds . length ) { supplyBuilder . addBan ( new CardSupplyBan ( randomizerOptions . excludeCardIds ) ) ; } if ( randomizerOptions . excludeTypes . length ) { supplyBuilder . addBan ( new TypeSupplyBan ( randomizerOptions . excludeTypes ) ) ; } if ( randomizerOptions . excludeCosts . length ) { supplyBuilder . addBan ( new CostSupplyBan ( randomizerOptions . excludeCosts ) ) ; } if ( randomizerOptions . requireSingleCardOfType ) { supplyBuilder . addRequirement ( new TypeSupplyRequirement ( randomizerOptions . requireSingleCardOfType , true ) ) ; } if ( randomizerOptions . requireActionProvider ) { supplyBuilder . addRequirement ( new TypeSupplyRequirement ( CardType . ACTION_SUPPLIER , false ) ) ; } if ( randomizerOptions . requireCardProvider ) { supplyBuilder . addRequirement ( new TypeSupplyRequirement ( CardType . MULTI_DRAWER , false ) ) ; } if ( randomizerOptions . requireBuyProvider ) { supplyBuilder . addRequirement ( new TypeSupplyRequirement ( CardType . BUY_SUPPLIER , false ) ) ; } if ( randomizerOptions . requireTrashing ) { supplyBuilder . addRequirement ( new TypeSupplyRequirement ( CardType . TRASHING , false ) ) ; } let remainingCards = NUM_CARDS_IN_KINGDOM ; if ( randomizerOptions . prioritizeSet && randomizerOptions . prioritizeSet != SetId . ALCHEMY ) { supplyBuilder . addDivider ( new SetSupplyDivider ( randomizerOptions . prioritizeSet , NUM_PRIORITIZED_SET ) ) ; remainingCards -= NUM_PRIORITIZED_SET ; } if ( randomizerOptions . useAlchemyRecommendation ) { if ( this . shouldUseAlchemyDivider ( randomizerOptions ) ) { const alchemyCardsToUse = this . getNumberOfAlchemyCardsToUse ( randomizerOptions , remainingCards ) ; supplyBuilder . addDivider ( new SetSupplyDivider ( SetId . ALCHEMY , alchemyCardsToUse ) ) ; remainingCards -= alchemyCardsToUse ; } else if ( randomizerOptions . setIds . length > 1 ) { supplyBuilder . addBan ( new SetSupplyBan ( [ SetId . ALCHEMY ] ) ) ; } } let highCardsInKingdom = - 1 ; if ( randomizerOptions . distributeCost ) { highCardsInKingdom = getRandomInt ( MIN_HIGH_CARDS_IN_KINGDOM , MAX_HIGH_CARDS_IN_KINGDOM ) ; supplyBuilder . addDivider ( new CostSupplyDivider ( HIGH_COST_CUT_OFF , highCardsInKingdom ) ) ; } const existingCards = randomizerOptions . includeCardIds . map ( ( id ) => DominionSets . getSupplyCardById ( id ) ) ; let supply = this . buildSupplyWithRetries ( supplyBuilder , existingCards ) ; if ( randomizerOptions . requireReactionIfAttacks ) { const correctedSupplyBuilder = this . correctSupplyBuilderForRequiredReaction ( supplyBuilder , existingCards , supply . supplyCards ) ; if ( correctedSupplyBuilder ) { supplyBuilder = correctedSupplyBuilder ; supply = this . buildSupplyWithRetries ( supplyBuilder , existingCards ) ; } } return supply ; } private static getAddons ( setIds ) { const setsToUse = Cards . filterSetsByAllowedSetIds ( DominionSets . getAllSets ( ) , setIds ) ; const cards = Cards . getAllCardsFromSets ( setsToUse ) ; const selectedCards = this . selectRandomCards ( cards , NUM_CARDS_IN_KINGDOM ) ; const selectedEvents = [ ] ; const selectedLandmarks = [ ] ; const selectedProjects = [ ] ; const selectedWays = [ ] ; for ( let card of selectedCards ) { if ( card instanceof Event ) { selectedEvents . push ( card ) ; } else if ( card instanceof Landmark ) { selectedLandmarks . push ( card ) ; } else if ( card instanceof Project ) { selectedProjects . push ( card ) ; } else if ( card instanceof Way ) { selectedWays . push ( card ) ; } const addonCount = selectedEvents . length + selectedLandmarks . length + selectedProjects . length + selectedWays . length ; if ( addonCount >= MAX_ADDONS_IN_KINGDOM ) { break ; } } return { events : selectedEvents , landmarks : selectedLandmarks , projects : selectedProjects , ways : selectedWays , } ; } static getRandomAddons ( setIds , excludeIds , numberOfAddons ) { const addons = this . getAddonsFromSets ( setIds , excludeIds ) ; return this . selectRandomCards ( addons , numberOfAddons ) ; } static getAddonsFromSets ( setIds , excludeIds ) { const setsToUse = Cards . filterSetsByAllowedSetIds ( DominionSets . getAllSets ( ) , setIds ) ; const cards = Cards . getAllCardsFromSets ( setsToUse ) . filter ( Cards . filterByExcludedIds ( excludeIds ) ) ; const events = Cards . getAllEvents ( cards ) as Addon [ ] ; const landmarks = Cards . getAllLandmarks ( cards ) as Addon [ ] ; const projects = Cards . getAllProjects ( cards ) as Addon [ ] ; const ways = Cards . getAllWays ( cards ) as Addon [ ] ; return events . concat ( landmarks , projects , ways ) ; } static getRandomBoons ( supply , keepBoons ) { if ( ! supply . supplyCards . some ( ( s ) => s . id == "nocturne_druid" ) ) { return [ ] ; } const excludeIds = Cards . extractIds ( keepBoons ) ; const cards = Cards . getAllCardsFromSets ( DominionSets . getAllSets ( ) ) ; const boons = Cards . getAllBoons ( cards ) . filter ( Cards . filterByExcludedIds ( excludeIds ) ) ; return selectRandomN ( boons , 3 - excludeIds . length ) . concat ( keepBoons ) ; } static getMetadata ( setIds ) { const setsToUse = Cards . filterSetsByAllowedSetIds ( DominionSets . getAllSets ( ) , setIds ) ; const useColonies = this . shouldUseSpecialtyCardFromSet ( SetId . PROSPERITY , setsToUse ) ; const useShelters = this . shouldUseSpecialtyCardFromSet ( SetId . DARK_AGES , setsToUse ) ; return new KingdomMetadata ( useColonies , useShelters ) ; } private static shouldUseSpecialtyCardFromSet ( setId , setsBeingUsed ) { const index = setsBeingUsed . map ( ( set ) => set . setId ) . indexOf ( setId ) ; if ( index == - 1 ) { return false ; } const numberOfSpecialtySetCards = setsBeingUsed [ index ] . supplyCards . length ; let numberOfCardsBeingUsed = 0 ; for ( let set of setsBeingUsed ) { numberOfCardsBeingUsed += set . supplyCards . length ; } const randomIndex = getRandomInt ( 0 , numberOfCardsBeingUsed ) ; return randomIndex < numberOfSpecialtySetCards ; } private static selectRandomCards < T > ( cards , numberToSelect ) { const randomIndices = getRandomInts ( numberToSelect , cards . length ) ; const selectedCards = [ ] ; for ( const index of randomIndices ) { selectedCards . push ( cards [ index ] ) ; } return selectedCards ; } private static shouldUseAlchemyDivider ( randomizerOptions ) { if ( randomizerOptions . setIds . length == 1 ) { return false ; } if ( randomizerOptions . prioritizeSet == SetId . ALCHEMY ) { return true ; } if ( randomizerOptions . setIds . indexOf ( SetId . ALCHEMY ) == - 1 ) { return false ; } if ( randomizerOptions . setIds . length < 3 ) { return true ; } const useRandomly = ! getRandomInt ( 0 , randomizerOptions . setIds . length ) ; return useRandomly ; } private static correctSupplyBuilderForRequiredReaction ( supplyBuilder , existingCards , selectedCards ) { if ( selectedCards . filter ( Cards . filterByRequiredType ( CardType . REACTION ) ) . length ) { return null ; } if ( ! selectedCards . filter ( Cards . filterByRequiredType ( CardType . ATTACK ) ) . length ) { return null ; } supplyBuilder = supplyBuilder . clone ( ) ; const divisions = supplyBuilder . createUnfilledDivisions ( existingCards ) ; const reactions = SupplyDivisions . getAvailableCardsOfType ( divisions , CardType . REACTION ) ; if ( reactions . length ) { supplyBuilder . addRequirement ( new TypeSupplyRequirement ( CardType . REACTION , false ) ) ; return supplyBuilder ; } supplyBuilder . addBan ( new TypeSupplyBan ( [ CardType . ATTACK ] ) ) ; return supplyBuilder ; } private static buildSupplyWithRetries ( supplyBuilder , existingCards ) { let retries = MAX_RETRIES ; while ( retries > 0 ) { try { return supplyBuilder . createSupply ( existingCards ) ; } catch ( error ) { console . log ( `Error when trying to select cards: \n${ error . toString ( ) }` ) ; retries -= 1 ; } } throw new Error ( "Failed to select cards that satisfied all requirements." ) ; } private static getNumberOfAlchemyCardsToUse ( randomizerOptions , remainingCards ) { let min = MIN_ALCHEMY_CARDS_IN_KINGDOM ; let max = MAX_ALCHEMY_CARDS_IN_KINGDOM ; if ( randomizerOptions . prioritizeSet == SetId . ALCHEMY ) { min = Math . max ( min , NUM_PRIORITIZED_SET ) ; max = Math . max ( max , MAX_ALCHEMY_CARDS_IN_KINGDOM ) ; } return getRandomInt ( min , Math . min ( max , remainingCards ) ) ; } private static removeDuplicateCards ( cards , requiredCardIds ) { const keys = Object . keys ( SETS_WITH_DUPLICATES ) ; for ( let key of keys ) { const setA = key as SetId ; const setB = SETS_WITH_DUPLICATES [ key ] as SetId ; const setACards = cards . filter ( Cards . filterByIncludedSetIds ( [ setA ] ) ) ; const setBCards = cards . filter ( Cards . filterByIncludedSetIds ( [ setB ] ) ) ; const setACardIdsAsSetB = this . replaceSetIdForCards ( setACards , setB ) ; const setBRequiredCards = setBCards . filter ( Cards . filterByIncludedIds ( requiredCardIds ) ) ; const setBCardIdsToExclude = this . removeIds ( setACardIdsAsSetB , Cards . extractIds ( setBRequiredCards ) ) ; const setBCardIdsToInclude = this . removeIds ( Cards . extractIds ( setBCards ) , setBCardIdsToExclude ) ; const setBRequiredCardIdsAsSetA = this . replaceSetIdForCards ( setBRequiredCards , setA ) ; const setACardIdsToInclude = this . removeIds ( Cards . extractIds ( setACards ) , setBRequiredCardIdsAsSetA ) ; const setACardsToExclude = this . removeIds ( Cards . extractIds ( setACards ) , setACardIdsToInclude ) ; const setBCardsToExclude = this . removeIds ( Cards . extractIds ( setBCards ) , setBCardIdsToInclude ) ; cards = cards . filter ( Cards . filterByExcludedIds ( setACardsToExclude ) ) ; cards = cards . filter ( Cards . filterByExcludedIds ( setBCardsToExclude ) ) ; } return cards ; } private static removeIds ( ids , idsToRemove ) { return ids . filter ( ( id ) => idsToRemove . indexOf ( id ) == - 1 ) ; } private static replaceSetIdForCards ( cards , newSetId ) { const cardIds = [ ] ; for ( let card of cards ) { cardIds . push ( this . replaceSetIdInCardId ( card . id , newSetId ) ) ; } return cardIds ; } private static replaceSetIdInCardId ( cardId , newSetId ) { return newSetId + '_' + cardId . split ( '_' ) [ 1 ] ; } }