import { Injectable } from '@angular/core' ; import { BgsBattleInfo } from '@firestone-hs/simulate-bgs-battle/dist/bgs-battle-info' ; import { BoardEntity } from '@firestone-hs/simulate-bgs-battle/dist/board-entity' ; import { SimulationResult } from '@firestone-hs/simulate-bgs-battle/dist/simulation-result' ; import Worker from 'worker-loader!../../workers/bgs-multi-sim.worker' ; import { Preferences } from '../../models/preferences' ; import { CardsFacadeService } from '../cards-facade.service' ; import { OverwolfService } from '../overwolf.service' ; import { chunk } from '../utils' ; import { BgsBattleSimulationService } from './bgs-battle-simulation.service' ; @ Injectable ( ) export class BgsBattlePositioningService { private cpuCount : number ; private cancelled : boolean ; private workers : Worker [ ] = [ ] ; constructor ( private readonly ow , private readonly simulationService , private readonly allCards ) { this . init ( ) ; } private async init ( ) { const systemInfo = await this . ow . getSystemInformation ( ) ; this . cpuCount = ( systemInfo ?. PhysicalCPUCount ?? 1 ) - 1 || 1 ; console . log ( 'CPU count' , this . cpuCount ) ; } public findBestPositioning ( battleInfo , prefs ) { const iterator = this . findBestPositioningInternal ( battleInfo , prefs ) ; return iterator ; } public cancel ( ) { console . debug ( 'cancelling?' , this . cancelled ) ; if ( this . cancelled ) { return ; } console . log ( 'cancelling process' ) ; this . cancelled = true ; for ( let i = this . workers . length - 1 ; i >= 0 ; i -- ) { this . workers [ i ] . terminate ( ) ; } this . workers = [ ] ; this . cancelled = false ; console . log ( 'cancelled process' ) ; } private async * findBestPositioningInternal ( battleInfo , prefs ) { this . cancelled = false ; const start = Date . now ( ) ; const initialBoard = battleInfo . playerBoard . board ; const permutations = permutator ( initialBoard ) ; console . debug ( 'permutations' , Date . now ( ) - start , permutations ) ; if ( this . cancelled ) { return [ ProcessingStatus . CANCELLED , null ] ; } yield [ ProcessingStatus . FIRSTPASS , null ] ; const sortedPermutations = await this . prunePermutations ( battleInfo , permutations , 80 , 100 , 50 ) ; console . debug ( 'first step' , Date . now ( ) - start , sortedPermutations . length , sortedPermutations ) ; if ( this . cancelled ) { return [ ProcessingStatus . CANCELLED , null ] ; } yield [ ProcessingStatus . SECONDPASS , null ] ; const sortedPermutations2 = await this . prunePermutations ( battleInfo , sortedPermutations . map ( ( p ) => p . permutation ) , 250 , 400 , 50 ) ; console . debug ( 'second step' , Date . now ( ) - start , sortedPermutations2 . length , sortedPermutations2 ) ; if ( this . cancelled ) { return [ ProcessingStatus . CANCELLED , null ] ; } yield [ ProcessingStatus . FINALRESULT , null ] ; const topPermutationsResults = await this . prunePermutations ( battleInfo , sortedPermutations2 . map ( ( p ) => p . permutation ) , 2500 , 4000 , 1 ) ; console . debug ( 'topPermutationsResults' , Date . now ( ) - start , topPermutationsResults ) ; if ( this . cancelled ) { return [ ProcessingStatus . CANCELLED , null ] ; } const result = { battleInfo : { ... battleInfo , playerBoard : { ... battleInfo . playerBoard , board : topPermutationsResults [ 0 ] . permutation , } , } , result : topPermutationsResults [ 0 ] . result , } ; console . debug ( 'result' , Date . now ( ) - start , result ) ; return [ ProcessingStatus . DONE , result ] ; } private async prunePermutations ( battleInfo , permutations , numberOfSims , maxDuration , minResultsToKeep ) { const chunkSize = Math . ceil ( permutations . length / this . cpuCount ) ; console . debug ( 'chunkSize' , chunkSize ) ; const chunks = chunk ( permutations , chunkSize ) ; const chunkResults = await Promise . all ( chunks . map ( ( chunk ) => this . buildRoughResults ( battleInfo , chunk , numberOfSims , maxDuration ) ) ) ; const permutationResults = chunkResults . reduce ( ( a , b ) => a . concat ( b ) , [ ] ) ; const sortedPermutations = [ ... permutationResults ] . sort ( ( a , b ) => { return ( b . result . wonPercent - a . result . wonPercent || a . result . lostPercent - b . result . lostPercent || b . result . wonLethalPercent - a . result . wonLethalPercent || a . result . lostLethalPercent - b . result . lostLethalPercent ) ; } ) ; const result = sortedPermutations . splice ( 0 , minResultsToKeep ) ; for ( const permutation of sortedPermutations ) { if ( permutation . result . wonPercent >= result [ 0 ] . result . wonPercent ) { if ( permutation . result . wonPercent !== 0 || permutation . result . tiedPercent >= result [ 0 ] . result . tiedPercent ) { result . push ( permutation ) ; } } else { break ; } } return result ; } private async buildRoughResults ( battleInfo , chunk , numberOfSims , maxDuration ) { return new Promise < InternalPermutationResult [ ] > ( ( resolve ) => { const worker = new Worker ( ) ; this . workers . push ( worker ) ; worker . onmessage = ( ev ) => { worker . terminate ( ) ; this . workers . splice ( this . workers . indexOf ( worker ) , 1 ) ; resolve ( JSON . parse ( ev . data ) ) ; } ; const battleMessages = chunk . map ( ( permutation ) => ( { ... battleInfo , playerBoard : { ... battleInfo . playerBoard , board : permutation , } , options : { ... battleInfo . options , numberOfSimulations : numberOfSims , maxAcceptableDuration : maxDuration , skipInfoLogs : true , } , } as BgsBattleInfo ) ) ; worker . postMessage ( { battleMessages : battleMessages , cards : this . allCards . getService ( ) , } ) ; } ) ; } } const permutator = < T > ( inputArr ) => { const result = [ ] ; const permute = ( arr , m = [ ] ) => { if ( arr . length === 0 ) { result . push ( m ) ; } else { for ( let i = 0 ; i < arr . length ; i ++ ) { const curr = arr . slice ( ) ; const next = curr . splice ( i , 1 ) ; permute ( curr . slice ( ) , m . concat ( next ) ) ; } } } ; permute ( inputArr ) ; return result ; } ; type Permutation = BoardEntity [ ] ; type Chunk = Permutation [ ] ; export interface PermutationResult { readonly battleInfo : BgsBattleInfo ; readonly result : SimulationResult ; } export interface InternalPermutationResult { readonly permutation : Permutation ; readonly result : SimulationResult ; } export enum ProcessingStatus { FIRSTPASS , SECONDPASS , FINALRESULT , DONE , CANCELLED }