Repository navigation
Expand file tree
/
Copy pathai_corp.js
More file actions
3399 lines (3222 loc) · 134 KB
/
Copy pathai_corp.js
File metadata and controls
3399 lines (3222 loc) · 134 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//AI decisionmaking
class CorpAI {
//**CORP UTILITY FUNCTIONS**
_log(message) {
//just comment this line to suppress AI log
console.log("AI: " + message);
}
_iceIsDisabled(iceCard) {
//for now assume hosted cards disable it
if (typeof iceCard.hostedCards !== "undefined") {
if (iceCard.hostedCards.length > 0) return true;
}
//or if chosen by Femme
var femmes = this._copiesOfCardIn("Femme Fatale", runner.rig.programs);
for (var i=0; i<femmes.length; i++) {
if (femmes[i].chosenCard == iceCard) return true;
}
//not disabled
return false;
}
//this returns either a list of affordable ice or a list or not-affordable ice
//'affordable' isn't necessarily monetary e.g. it may not be tactically 'affordable' right now
_iceInCardsCommon(
serverToInstallTo,
affordable,
cards //utility used by affordable/not ice listers (affordable is a boolean)
) {
if (typeof cards == "undefined") cards = corp.HQ.cards; //usually installing from hand but this makes other options possible
var extraCost = 0;
if (serverToInstallTo != null) {
//take into account install cost if not first ice in server
extraCost += serverToInstallTo.ice.length;
//and unrezzed ice already in the server
var uri = this._unrezzedIce(serverToInstallTo);
for (var i=0; i<uri.length; i++) {
extraCost += RezCost(uri[i]);
}
}
var relevantIceInCards = [];
for (var i = 0; i < cards.length; i++) {
if (cards[i].cardType == "ice") {
if (extraCost < corp.creditPool) { //even not-affordable ice is excluded if it can't be installed
var yesToThisIce = true;
if (typeof cards[i].AIWorthwhileIce == 'function') {
yesToThisIce = cards[i].AIWorthwhileIce.call(cards[i],serverToInstallTo,"install");
}
var isAffordable = (cards[i].rezCost + extraCost <= corp.creditPool) && yesToThisIce;
if (affordable == isAffordable) relevantIceInCards.push(cards[i]);
}
}
}
return relevantIceInCards;
}
_affordableIce(
serverToInstallTo,
cards //returns all ice in cards (or hand, if cards not specified) with cost equal to or less than credit pool
) {
if (typeof cards == "undefined") cards = corp.HQ.cards; //usually installing from hand but this makes other options possible
return this._iceInCardsCommon(serverToInstallTo, true, cards);
}
_notAffordableIce(
serverToInstallTo,
cards //returns all ice in hand with cost greater than credit pool
) {
if (typeof cards == "undefined") cards = corp.HQ.cards; //usually installing from hand but this makes other options possible
return this._iceInCardsCommon(serverToInstallTo, false, cards);
}
//array of unrezzed ice in server
_unrezzedIce(server) {
var ret = [];
if (server == null) return ret;
for (var i = 0; i < server.ice.length; i++) {
if (!server.ice[i].rezzed) ret.push(server.ice[i]);
}
return ret;
}
//array of rezzed ice in server
_rezzedIce(server) {
var ret = [];
if (server == null) return ret;
for (var i = 0; i < server.ice.length; i++) {
if (server.ice[i].rezzed) ret.push(server.ice[i]);
}
return ret;
}
_copyAlreadyInstalled(
card //returns true if a copy already installed (rezzed or unrezzed)
) {
var installedCards = InstalledCards(card.player);
for (var i = 0; i < installedCards.length; i++) {
if (
installedCards[i] !== card &&
GetTitle(installedCards[i]) == GetTitle(card)
)
return true;
}
return false;
}
_uniqueCopyAlreadyInstalled(
card //returns true if is unique and a copy already installed (rezzed or unrezzed)
) {
if (!card.unique) return false; //i.e. .unique == false or undefined
return this._copyAlreadyInstalled(card);
}
_iceInstallScore(ice, serverToInstallTo) {
//higher is better
var ret = 0;
//put a temporary copy of this card into the server to test for effects (if it's null we'll need a pretend remote)
if (serverToInstallTo == null) corp.remoteServers.push({ice:[ice], root:[]});
else serverToInstallTo.ice.push(ice);
//start with strength as a base
ret = Strength(ice);
//include rez cost as an assumption of value
ret += RezCost(ice);
//special specifics
if (ice.title == "Palisade" && serverToInstallTo != null && typeof(serverToInstallTo.cards) !== 'undefined') ret -= 3; //arbitrary, encourage to save Palisade for remotes
//weaker if threatened
if (this._aCompatibleBreakerIsInstalled(ice)) ret *= 0.5;
//remove the temporary copy
if (serverToInstallTo == null) corp.remoteServers.splice(corp.remoteServers.length-1,1); //by removing the pretend server
else serverToInstallTo.ice.splice(serverToInstallTo.ice.length-1,1); //by removing the temporary ice copy
return ret;
}
//returns the ice (assumes list is all ice and at least one present)
_bestIceToInstall(iceToChooseFrom, serverToInstallTo) {
if (iceToChooseFrom.length < 2) return iceToChooseFrom[0];
//calculate an install score for each (higher is better)
for (var i=0; i<iceToChooseFrom.length; i++) {
iceToChooseFrom[i].AIIceInstallScore = this._iceInstallScore(iceToChooseFrom[i], serverToInstallTo);
}
//now sort
iceToChooseFrom.sort(function(a,b) {
return b.AIIceInstallScore - a.AIIceInstallScore;
});
//console.log(iceToChooseFrom);
return iceToChooseFrom[0]; //just random for now
}
//special case e.g Sneakdoor Beta
_archivesIsBackdoorToHQ() {
if (this._copyOfCardExistsIn("Sneakdoor Beta",runner.rig.programs)) {
return true;
}
return false;
}
//special case e.g Sneakdoor Beta
//returns a protection value to be subtracted
_extraThreatOnRnD() {
var conduit = this._copyOfCardExistsIn("Conduit",runner.rig.programs);
if (conduit) {
//the numbers are arbitrary but basically a bit for each counter and some for existing at all
//multiplier was 0.5 and constant was 2.5. AI still seemed to underappreciate it so I increased these.
return 1.0 * Counters(conduit,"virus") + 3.0;
}
return 0;
}
//returns the preferred server. input is the upgrade to install
_bestServerToUpgrade(upgrade) {
var preferredServer = null; //install into new if necessary
var nonEmptyProtectedRemotes = this._nonEmptyProtectedRemotes();
if (nonEmptyProtectedRemotes.length > 0)
preferredServer = nonEmptyProtectedRemotes[0];
if (!preferredServer || this._protectionScore(corp.RnD, {}) < this._protectionScore(preferredServer, {})) {
if (this._protectionScore(corp.HQ, {returnArchivesLowerScoreForHQIfBackdoor:true}) < this._protectionScore(corp.RnD, {})) {
if (this._archivesIsBackdoorToHQ() && this._protectionScore(corp.archives, {}) < this._protectionScore(corp.HQ, {})) preferredServer = corp.archives;
else preferredServer = corp.HQ;
}
else preferredServer = corp.RnD;
}
return preferredServer;
}
_isAScoringServer(
server //note that this can includes servers with and without non-upgrade cards installed (so be careful what you would need to trash to install)
) {
if (server == null) return false; //protection is too weak
if (typeof server.cards == "undefined") {
//is remote
//yes if it has a scoring upgrade, an agenda or an ambush installed
//this code was originally after the protection check but this lead to AI installing random assets in scoring servers
for (var j = 0; j < server.root.length; j++) {
if (server.root[j].AIIsScoringUpgrade) return true;
if (CheckCardType(server.root[j], ["agenda"])) return true;
if (CheckSubType(server.root[j], "Ambush")) return true;
}
//no if its protection is too weak
var protScore = this._protectionScore(server, {});
var minProt = this._protectionScore(corp.HQ, {returnArchivesLowerScoreForHQIfBackdoor:true}); //new method: just needs to be at least as strong as HQ
//var minProt = RandomRange(3, 4); //arbitrary: tweak as needed (old method)
if (this._agendasInHand() > MaxHandSize(corp) - 1) {
//if it's going to be thrown out, it just has to be better protection than Archives
minProt = this._protectionScore(corp.archives, {ignoreBackdoorFromArchives:true});
}
if (protScore < minProt) return false;
//no if there is a better server available (empty except for a scoring upgrade, or stronger protection score)
var emptyProtectedRemotes = this._emptyProtectedRemotes();
for (var i = 0; i < emptyProtectedRemotes.length; i++) {
if (server !== emptyProtectedRemotes[i]) {
for (var j = 0; j < emptyProtectedRemotes[i].root.length; j++) {
if (emptyProtectedRemotes[i].root[j].AIIsScoringUpgrade)
return false;
}
}
}
//otherwise yes if it is the empty remote server with the most protection
if (emptyProtectedRemotes.length > 0) {
if (server == emptyProtectedRemotes[0]) return true;
}
}
return false;
}
//returns all scoring servers (in order from strongest to weakest, if input is in that order
_scoringServers(emptyProtectedRemotes) {
var ret = [];
//loop through possible scoring servers
for (var j = 0; j < emptyProtectedRemotes.length; j++) {
var emptyProtectedRemote = emptyProtectedRemotes[j];
if (this._isAScoringServer(emptyProtectedRemote))
ret.push(emptyProtectedRemote);
}
return ret;
}
//returns value of current scoring window if using the given server (zero if no window)
_scoringWindow(server) {
var ret = 0;
ret = this._protectionScore(server, {}) - this._protectionScore(corp.HQ, {returnArchivesLowerScoreForHQIfBackdoor:true}) + (0.3*this._clicksLeft()) + (Credits(corp)/(Credits(runner)+1.0)) - 1.0;
return ret;
}
//if inhibit is false, more willing installs are permitted (use this for free install&rez)
_shouldUpgradeServerWithCard(server, card, inhibit=true) {
if (CheckCardType(card, ["upgrade"])) {
//although we could install more than one copy of a unique card, let's not
if (this._uniqueCopyAlreadyInstalled(card)) return false;
if (server) {
//limit 1 region per server
if (CheckSubType(card, "Region")) {
for (var i=0; i<server.root.length; i++) {
if (CheckSubType(server.root[i], "Region")) return false;
};
}
//or some other limit, if defined
if (typeof card.AILimitPerServer == 'function') {
var numberThatWouldBeInServer = 1; //for this one we're installing
server.root.forEach(function(item) {
if (item.title == card.title) numberThatWouldBeInServer++;
});
if (numberThatWouldBeInServer > card.AILimitPerServer.call(card,server)) return false;
}
}
if (!inhibit) return true;
if (card.AIIsScoringUpgrade) {
if (!this._isAScoringServer(server)) return false;
}
if (typeof card.AIDefensiveValue == 'function') {
if (card.AIDefensiveValue.call(card, server) < 1) return false;
}
return true; //should be ok to install
}
return false;
}
_upgradeInstallPreferences(
server,
cards, //if server is not specified, the best server for each card will be chosen
inhibit=true //if inhibit is false, more willing installs are permitted (use this for free install&rez)
) {
var ret = [];
if (typeof cards == "undefined") cards = corp.HQ.cards; //usually installing from hand but this makes other options possible
for (var i = 0; i < cards.length; i++) {
var serverToInstallTo = this._bestServerToUpgrade(cards[i]);
if (typeof server !== "undefined") serverToInstallTo = server;
if (this._shouldUpgradeServerWithCard(serverToInstallTo, cards[i], inhibit))
ret.push({
cardToInstall: cards[i],
serverToInstallTo: serverToInstallTo,
});
}
return ret;
}
//this modifies the input array
_reducedDiscardList(
optionList,
minCount = 1, //reduces optionList (only include ones we are ok to discard) but keeps list size at or above minCount
maxCount //and at or below maxCount
) {
if (typeof maxCount == "undefined") maxCount = minCount;
//don't discard agendas
for (
var i = 0;
i < optionList.length && optionList.length > minCount;
i++
) {
if (typeof optionList[i].card !== "undefined") {
//in case there are non-card options present
if (CheckCardType(optionList[i].card, ["agenda"])) {
optionList.splice(i, 1);
i--;
}
}
}
//other than that, use common what's-the-best-card-to-keep code to remove cards from discard list
while (optionList.length > maxCount) optionList.splice(optionList.indexOf(this._bestNonAgendaTutorOption(optionList)), 1); //the 1 means remove 1 card
//the worst-cards-to-keep are left now, return them
return optionList;
}
//this may modify the input array but accept the return value to guarantee modification
//reduces optionList to the one we want most to return to RnD
_reduceOptionsToBestCardToReturnToRnD(
optionList
) {
var logStart = "Best card to return to R&D from "+JSON.stringify(CardsInOptionList(optionList));
//if HQ is not protected or there is more than one agenda in hand, return agendas to R&D
var returnAgendasToRnD = false;
if ( this._agendasInHand() > 1 || this._serverToProtect() == corp.HQ ) returnAgendasToRnD = true;
if (returnAgendasToRnD) {
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card.cardType == 'agenda') optionList = [optionList[i]];
}
}
if (optionList.length > 1) optionList = this._reducedDiscardList(optionList, 1, 1); //values here are min and max number of cards to choose (i.e. exactly 1)
this._log(logStart+" is "+optionList[0].card.title);
return optionList;
}
_bestDiscardOption(optionList) {
this._log("considering discard options...");
//importantly, this modifies the input array to length 1
//so that the choice of index 0 is the best option (ideally the only one left)
optionList = this._reducedDiscardList(optionList, 1, 1); //values here are min and max number of cards to choose (i.e. exactly 1)
return 0; //just arbitrary for now
}
_bestSabotageOption(optionList) {
//importantly, this modifies the input array to length 1 (recreates the desired option)
var minSabotageFromHQ = optionList[optionList.length-1].minSabotageFromHQ;
this._log("considering sabotage options, need to trash at least "+minSabotageFromHQ+" from HQ");
var selectCards = optionList[optionList.length-1].cards;
//make a faux option list for discard helper to use
var fauxOptionList = ChoicesHandCards(corp);
fauxOptionList = this._reducedDiscardList(fauxOptionList, minSabotageFromHQ, selectCards.length); //min is minSabotageFromHQ, max is selectCards.length
//convert back to multi-select
for (var i=0; i<fauxOptionList.length; i++) {
selectCards[i]=fauxOptionList[i].card;
}
optionList.splice(0, optionList.length - 1); //remove all but the last element
return 0; //there is only one option
}
_bestForfeitOption(optionList) {
this._log("considering forfeit options...");
var ret = 0;
var forfAg = null;
for (var i=0; i<corp.scoreArea.length; i++) {
if (!forfAg) {
ret = i;
forfAg = corp.scoreArea[i];
}
else if (corp.scoreArea[i].agendaPoints < forfAg.agendaPoints) {
ret = i;
forfAg = corp.scoreArea[i];
}
else if (corp.scoreArea[i].agendaPoints == forfAg.agendaPoints && Counters(corp.scoreArea[i],"agenda") < Counters(forfAg,"agenda")) {
ret = i;
forfAg = corp.scoreArea[i];
}
}
return ret;
}
//get the desired advancement limit (may exceed advancement requirement)
_advancementLimit(card,server) {
if (typeof server == 'undefined') server = GetServer(card);
var ret = Infinity;
if (typeof card.AIAdvancementLimit == "function") {
ret = card.AIAdvancementLimit.call(card);
}
else if (typeof card.advancementRequirement !== "undefined") {
ret = AdvancementRequirement(card);
}
//rezzed/unrezzed SanSan (except overadvanced, or already applied by AdvancementRequirement)
if (server && typeof card.advancementRequirement !== "undefined" && ret == card.advancementRequirement) {
var sanSan = this._copyOfCardExistsIn("SanSan City Grid", server.root);
if (sanSan) {
//ignoring other discounts for now
//this is approximate - for proper calculation would probably need some kind of combined use of _potentialAdvancement
if (sanSan.rezzed || AvailableCredits(corp) >= RezCost(sanSan) + ret - Counters(card,"advancement")) ret--;
}
}
return ret;
}
//amount of advancement that still needs to be added to reach limit
_advancementStillRequired(card, server) {
if (typeof server == 'undefined') server = GetServer(card);
return this._advancementLimit(card) - Counters(card,"advancement");
}
_bestAdvanceOption(optionList) {
//currently just chooses first reasonable option (TODO need to do more work here to include/exclude/rank them)
//e.g. using _cardShouldBeFastAdvanced and/or _isFullyAdvanceableAgenda or similar
for (var i = 0; i < optionList.length; i++) {
if (
typeof optionList[i].card.advancement === "undefined" ||
optionList[i].card.advancement < this._advancementLimit(optionList[i].card) ||
optionList[i].card.AIOverAdvance
)
return i;
}
return 0; //just arbitrary
}
_highestELO(cards) {
if (cards.length < 1) {
console.error("Empty cards array?");
return;
}
var ret = cards[0];
var highestElo = cards[0].elo;
for (var i=1; i<cards.length; i++) {
if (cards[i].elo > highestElo) {
highestElo = cards[i].elo;
ret = cards[i];
}
}
return ret;
}
//return option of best economy card in source option list
//or null if no economy option found
_bestEconomyCardOption(optionList) {
var economyCards = this._economyCards();
for (var i=0; i<economyCards.length; i++) {
for (var j=0; j<optionList.length; j++) {
if (optionList[j].card) {
if (economyCards[i] == optionList[j].card.title) return optionList[j];
}
}
}
return null;
}
//returns the option or null if not found
_firstOptionWithCard(optionList, card) {
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (optionList[i].card == card) return optionList[i];
}
}
return null;
}
//returns null if no agendas found in options
_bestAgendaTutorOption(optionList) {
//just random for now
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (CheckCardType(optionList[i].card, ["agenda"])) return optionList[i];
}
}
return null;
}
_bestNonAgendaTutorOption(optionList,onlyTheBest=false) {
//fast advance
var fastAdvanceCards = [];
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (optionList[i].card.AIFastAdvance) fastAdvanceCards.push(optionList[i].card);
}
}
if (fastAdvanceCards.length > 0) return this._firstOptionWithCard(optionList,this._highestELO(fastAdvanceCards));
//opportunity (e.g. tag punishment/damage)
var opportunityCards = [];
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card && !optionList[i].card.AIIsRecurOrTutor) {
if ( optionList[i].card.AITagPunishment || optionList[i].card.AIDamageOperation || ( typeof optionList[i].card.AIWouldPlay == 'function' && optionList[i].card.AIWouldPlay.call(optionList[i].card) ) ) opportunityCards.push(optionList[i].card);
}
}
if (opportunityCards.length > 0) return this._firstOptionWithCard(optionList,this._highestELO(opportunityCards));
if (onlyTheBest) return null;
//if economy isn't looking too good, choose an econ card
if (!this._sufficientEconomy()) {
var bestEconomyOption = this._bestEconomyCardOption(optionList);
if (bestEconomyOption) return bestEconomyOption;
}
//if no affordable ice in hand, choose ice
var cardList = [];
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card && !optionList[i].card.AIIsRecurOrTutor) cardList.push(optionList[i].card);
}
if (!this._affordableIce(null).length == 0) {
var affordableIceInList = this._affordableIce(null,cardList);
if (affordableIceInList.length > 0) return this._firstOptionWithCard(optionList,this._highestELO(affordableIceInList));
}
//choose highest elo card in option list
cardList = [];
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) cardList.push(optionList[i].card);
}
return this._firstOptionWithCard(optionList,this._highestELO(cardList));
}
_bestRecurToHQOption(optionList,serverUnderThreat,useNowIfPossible=false) {
if (optionList.length < 1) return null;
var ret = null;
if (typeof serverUnderThreat == 'undefined') serverUnderThreat = null; //no server under threat
//if archives is under threat, only recur agendas (rank by highest points)
if (serverUnderThreat == corp.archives) {
var hap = 0;
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (optionList[i].card.agendaPoints) {
if (optionList[i].card.agendaPoints > hap) {
hap = optionList[i].card.agendaPoints;
ret = optionList[i];
}
}
}
}
return ret;
}
//if HQ is under threat and contains agendas, seek to pad with non agenda cards
if (serverUnderThreat == corp.HQ && this._agendasInHand() > 0) {
useNowIfPossible = true;
//Snare! to make HQ more dangerous (if have snare credits)
if (CheckCredits(corp,4)) {
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (optionList[i].card.title == "Snare!") return optionList[i];
}
}
}
//or just any non-agenda card
return this._bestNonAgendaTutorOption(optionList);
}
//TODO don't include multiple copies of cards in archives since only one can be recurred
//TODO also take into account whether there will be the credits/clicks after whatever is causing this recur
var faai = this._fullyAdvanceableAgendaInstalled(corp.HQ.cards.concat(corp.archives.cards));
//look for fast advance cards (if could use to finish an agenda using cards from both HQ and archives)
if (useNowIfPossible || faai) {
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (optionList[i].card.AIFastAdvance) return optionList[i];
}
}
}
//damage cards (for now, just if useNowIfPossible, later could check here for kill combos)
if (useNowIfPossible) {
for (var i=0; i<optionList.length; i++) {
if (optionList[i].card) {
if (optionList[i].card.AIDamageOperation) return optionList[i];
}
}
}
//save the ability for later, if possible
if (!useNowIfPossible) return null;
//for now it's just arbitrary but could potentially add logic here
return optionList[0];
}
//this damage is potential because the run could jack out/prevent, not because we don't know the cards
//this function assumes the runner is mid-run on this server
_potentialDamageOnBreach(server) {
if (!server) return 0;
var ret = 0;
//1 if it is contains an agenda and identity is Jinteki:PE (unless Runner wins before PE fires)
if (this._agendaPointsInServer(server) > 0 && corp.identityCard.title=="Jinteki: Personal Evolution") {
if (!this._runnerMayWinIfServerBreached(server)) ret++;
}
//2+advancement if it is an active Urtica
var urtica = this._copyOfCardExistsIn("Urtica Cipher", server.root);
if (urtica) ret+=2+urtica.advancement;
//1 for each scored House of Knives that has at least 1 counter and hasn't been used this run
for (var i=0; i<corp.scoreArea.length; i++) {
if (corp.scoreArea[i].title == "House of Knives" && !corp.scoreArea[i].usedThisRun) ret++;
}
//3 for Snare! if affordable (for now, assumes exactly one will be accessed)
var snare = null;
if (this._copyOfCardExistsIn("Snare!", server.root)) snare = true;
else if (typeof server.cards != 'undefined') {
if (this._copyOfCardExistsIn("Snare!", server.cards)) snare = true;
}
var creditsLeft = AvailableCredits(corp);
if (snare && creditsLeft > 3) {
creditsLeft -= 4;
ret+=3;
}
//1 for Hokusai Grid if affordable (is region so limited to 1)
if (this._copyOfCardExistsIn("Hokusai Grid", server.root) && creditsLeft > 1) {
creditsLeft -= 2;
ret++;
}
this._log("Could do "+ret+" damage on breach");
return ret;
}
//NOTE: a copy of this exists in ai_runner so duplicate changes there
_breachWouldBePrevented(activeCards, server) {
var ret = false;
for (var j = 0; j < activeCards.length; j++) {
if (typeof activeCards[j].AIPreventBreach == 'function') {
if (activeCards[j].AIPreventBreach.call(activeCards[j],server)) {
return true;
}
}
}
return ret;
}
_serverContainsUnknownCards(server) {
if (!server) return false;
for (var i=0; i<server.root.length; i++) {
if (!PlayerCanLook(runner,server.root[i])) return true;
}
if (typeof server.cards != 'undefined') {
for (var i=0; i<server.cards.length; i++) {
if (!PlayerCanLook(runner,server.cards[i])) return true;
}
}
return false;
}
_agendaPointsInServer(
server //returns int
) {
if (!server) return 0;
var ret = 0;
for (var i = 0; i < server.root.length; i++) {
if (CheckCardType(server.root[i], ["agenda"])) ret+=server.root[i].agendaPoints;
}
if (typeof server.cards !== "undefined") {
for (var i = 0; i < server.cards.length; i++) {
if (CheckCardType(server.cards[i], ["agenda"])) ret+= server.cards[i].agendaPoints;
}
}
return ret;
}
//optionally, specific an array of cards to ignore
//returns int
_agendasInServer(
server,
ignoreCards=[]
) {
if (!server) return 0;
var ret = 0;
for (var i = 0; i < server.root.length; i++) {
if (CheckCardType(server.root[i], ["agenda"]) && !ignoreCards.includes(server.root[i])) ret++;
}
if (typeof server.cards !== "undefined") {
for (var i = 0; i < server.cards.length; i++) {
if (CheckCardType(server.cards[i], ["agenda"]) && !ignoreCards.includes(server.cards[i])) ret++;
}
}
return ret;
}
_agendasInRemoteServers() {
var ret = 0;
for (var i=0; i<corp.remoteServers.length; i++) {
ret += this._agendasInServer(corp.remoteServers[i]);
}
return ret;
}
_isHVT(card) { //agendas, ambushes, hostiles.
//obselete bluffs are no longer HVTs
if (this._obsoleteBluff(card)) return false;
//otherwise...
return (
CheckCardType(card, ["agenda"]) ||
CheckSubType(card, "Ambush") ||
CheckSubType(card, "Hostile")
);
}
_serverValue(server) {
var ret=0;
//just simple for now
for (var i=0; i<server.root.length; i++) {
if (this._isHVT(server.root[i])) ret++;
}
return ret;
}
_HVTsInServer(server) { //returns int
if (!server) return 0;
var ret=0;
for (var j=0; j<server.root.length; j++) {
if (this._isHVT(server.root[j])) ret++;
}
return ret;
}
_HVTsInstalled() { //returns int
var ret=0;
for (var i=0; i<corp.remoteServers.length; i++) {
ret += this._HVTsInServer(corp.remoteServers[i]);
}
return ret;
}
_HVTserver() { //returns first one found
for (var i=0; i<corp.remoteServers.length; i++) {
for (var j=0; j<corp.remoteServers[i].root.length; j++) {
if (this._isHVT(corp.remoteServers[i].root[j])) return corp.remoteServers[i];
}
}
return null;
}
//optionally, specific an array of cards to ignore
_agendasInHand(ignoreCards=[]) { //returns int
return this._agendasInServer(corp.HQ,ignoreCards);
}
//optionally, specific an array of cards to ignore
_HVTsInHand(ignoreCards=[]) { //returns int
var ret = 0;
for (var i=0; i<corp.HQ.cards.length; i++) {
if (!ignoreCards.includes(corp.HQ.cards[i])) {
if (this._isHVT(corp.HQ.cards[i])) ret++;
}
}
return ret;
}
_faceDownCardsOrAgendasExistInArchives() {
for (var i = 0; i < corp.archives.cards.length; i++) {
if (!IsFaceUp(corp.archives.cards[i])) return true;
if (CheckCardType(corp.archives.cards[i], ["agenda"])) return true;
}
}
//highest amount to advance bluff to
_bluffAdvanceLimit(card) {
var advlim = 4; //advancing an ambush more that this is not a good bluff (most agendas are only 5 or less to score)
if (typeof(card.AIAdvancementLimit) == 'function') advlim = card.AIAdvancementLimit.call(card);
var hostileCards = corp.RnD.cards.concat(corp.HQ.cards).concat(corp.archives.cards);
for (var k=0; k<hostileCards.length; k++) {
if (CheckSubType(hostileCards[k],"Hostile")) {
if (typeof(hostileCards[k].AIAdvancementLimit) == 'function') {
var hostadvlim = hostileCards[k].AIAdvancementLimit.call(hostileCards[k]);
if (hostadvlim > advlim) advlim = hostadvlim;
}
}
}
return advlim;
}
_remoteServerWithHighestIceAndRootProtection() {
var ret = null;
var highestIARP = 0;
for (var i=0; i<corp.remoteServers.length; i++) {
var thisIARP = this._iceAndRootProtection(corp.remoteServers[i]);
if (thisIARP > highestIARP) {
highestIARP = thisIARP;
ret = corp.remoteServers[i];
}
}
return ret;
}
_obsoleteBluff(card) {
//if it's an ambush then no need to keep it if it is known or no longer a meaningful bluff
if (CheckSubType(card, "Ambush") && CheckCardType(card, ["asset"])) {
var advlim = this._bluffAdvanceLimit(card);
if (card.knownToRunner || card.advancement >= advlim) return true;
}
//other assets are worth less if they have been around while or there are lots of agendas in hand
if (typeof(card.AITurnsInstalled) !== 'undefined') {
var agendasInHand = this._agendasInHand();
var agingFactor = card.AITurnsInstalled - 2; //arbitrary, test and tweak
if (card.AIAvoidInstallingOverThis) agingFactor -= 4; //arbitrary, test and tweak
if ( (agendasInHand > 0)&&(corp.HQ.cards.length - this._agendasInHand() < agingFactor) ) {
if (!CheckSubType(card, "Hostile") && CheckCardType(card, ["asset"])) {
//most assets will still be overwritten if critical
//(i.e. stop clogging up the strongest scoring server
if (this._remoteServerWithHighestIceAndRootProtection() == GetServer(card)) return true;
//check if it has AIAvoidInstallingOverThis: true
if (card.AIAvoidInstallingOverThis) return false;
//check if it has credits left
if (!CheckCounters(card, "credits", 1)) {
return true;
}
}
}
}
return false;
}
_obsoleteBluffInstalledInServer(server) {
if (!server) return false;
for (var i=0; i<server.root.length; i++) {
if (this._obsoleteBluff(server.root[i])) return true;
};
return false;
}
_aCompatibleBreakerIsInstalled(
iceCard //ignores strength and credit requirements
) {
var installedRunnerCards = InstalledCards(runner);
for (var i = 0; i < installedRunnerCards.length; i++) {
if (CheckSubType(installedRunnerCards[i], "Icebreaker")) {
if (
CheckSubType(installedRunnerCards[i], "AI") &&
!iceCard.cannotBreakUsingAIPrograms
)
return true;
if (
CheckSubType(installedRunnerCards[i], "Killer") &&
CheckSubType(iceCard, "Sentry")
)
return true;
if (
CheckSubType(installedRunnerCards[i], "Decoder") &&
CheckSubType(iceCard, "Code Gate")
)
return true;
if (
CheckSubType(installedRunnerCards[i], "Fracter") &&
CheckSubType(iceCard, "Barrier")
)
return true;
}
}
return false;
}
_numCompatibleIceInstalled(
breakerCard //ignores strength and credit requirements
) {
var ret = 0;
var installedCorpCards = InstalledCards(corp);
for (var i = 0; i < installedCorpCards.length; i++) {
if (CheckCardType(installedCorpCards[i], ["ice"])) {
if (
CheckSubType(breakerCard, "AI") &&
!installedCorpCards[i].cannotBreakUsingAIPrograms
)
ret++;
if (
CheckSubType(breakerCard, "Killer") &&
CheckSubType(installedCorpCards[i], "Sentry")
)
ret++;
if (
CheckSubType(breakerCard, "Decoder") &&
CheckSubType(installedCorpCards[i], "Code Gate")
)
ret++;
if (
CheckSubType(breakerCard, "Fracter") &&
CheckSubType(installedCorpCards[i], "Barrier")
)
ret++;
}
}
return ret;
}
_cardProtectionValue(
card //from 0 (completely pointless) to 2+ (depending on rez cost etc)
) {
var ret = 0;
if (CheckCardType(card, ["ice"])) {
//if unrezzed and can't afford to rez, consider to be no protection (just a simple check ignoring cost of other ice in server)
if ( card.rezzed || Credits(corp) >= RezCost(card)) {
ret++; //1 point for any ice
if (card.rezCost > 4 || Strength(card) > 3) {
ret++; //plus bonus point for high rez cost (based on printed value) or strong
}
//special case: ice wall gets extra strength for advancement tokens (constant is arbitrary)
if (card.title == "Ice Wall") ret += 0.5 * card.advancement;
//ice on centrals is essentially better due to the random 'mystery' nature of central servers
var server = GetServer(card); //returns null if not installed
if (server != null) {
if (typeof server.cards !== "undefined") ret += 2;
//stronger for each other ice in server
ret += 0.1*Math.sqrt(server.ice.length-1);
}
if (!card.rezzed) ret *= 1.5; //for being unrezzed (tweaked down from 2 because corp was overvaluing unrezzed ice)
//weaker if threatened
if (this._aCompatibleBreakerIsInstalled(card)) ret *= 0.5;
//and modify value based on hosted cards and virus counters
if (typeof card.hostedCards !== "undefined" && !card.AIDisablesHostedPrograms) {
for (var i = 0; i < card.hostedCards.length; i++) {
if (card.hostedCards[i].player == corp) ret++;
else {
ret--;
if (Counters(card.hostedCards[i], "virus") > 2) ret--;
}
}
}
}
}
else if (CheckCardType(card, ["upgrade"])) {
if (typeof card.AIDefensiveValue == 'function') {
var defensiveValue = card.AIDefensiveValue.call(card, GetServer(card));
//if unrezzed and can't afford to rez, consider to be no protection (just a simple check ignoring cost of other ice in server)
if ( card.rezzed || Credits(corp) >= RezCost(card)) {
ret += defensiveValue;
}
}
}
return ret;
}
_iceAndRootProtection(server) {
var ret = 0;
for (var i = 0; i < server.ice.length; i++) {
ret += this._cardProtectionValue(server.ice[i]);
}
for (var i = 0; i < server.root.length; i++) {
ret += this._cardProtectionValue(server.root[i]);
}
return ret;
}
_protectionScore(
server, //higher number = more protected
options //ignoreSuccessfulRuns, ignoreBackdoorFromArchives, returnArchivesLowerScoreForHQIfBackdoor
) {
if (typeof options == 'undefined') {
console.error("options not defined for call to corp.AI._protectionScore");
options={};
}
//new servers have a score of 1 (because they are empty)
if (server == null) return 1;
var archivesIsBackdoorToHQ = this._archivesIsBackdoorToHQ();
if (options.ignoreBackdoorFromArchives) archivesIsBackdoorToHQ = false;
//for 'protecting' check we build a score considering how much ice and how effective the ice is
var ret = 0;
//servers with obsolete bluffs get bonus protection score (they don't really need protecting)
if (this._obsoleteBluffInstalledInServer(server)) ret += 5; //the value is arbitrary, test and tweak
//protection score depends on ice and upgrades protecting
ret += this._iceAndRootProtection(server);
if (!options.ignoreSuccessfulRuns) {
//if it is being run successfully a lot, need extra protection
var successfulRuns = 0;
if (typeof server.AISuccessfulRuns !== "undefined") successfulRuns = server.AISuccessfulRuns;
//if archives is a backdoor to HQ, consider successful runs on HQ
if (server == corp.archives && archivesIsBackdoorToHQ && corp.HQ.AISuccessfulRuns !== "undefined") {
if (corp.HQ.AISuccessfulRuns > successfulRuns) successfulRuns = corp.HQ.AISuccessfulRuns;
}
if (successfulRuns > 0) ret -= Math.round(Math.sqrt(successfulRuns));
}
//if it is archives we will deprioritise protection (if it is not a backdoor into HQ)
if (server == corp.archives && !archivesIsBackdoorToHQ) ret += 3; //archives (the 3 is arbitrary)
//if it is HQ (or backdoor), increase or decrease priorisation based on agenda points compared to cards in hand
if (server == corp.HQ || (server == corp.archives && archivesIsBackdoorToHQ) ) ret += corp.HQ.cards.length - this._agendaPointsInServer(corp.HQ) - 2.5; //the subtracted value is arbitrary (with 2 the AI was underprotective of HQ)
//if it is R&D and there is extra threat e.g. a Conduit, need more protection
if (server == corp.RnD) {
ret -= this._extraThreatOnRnD();
}
if (options.returnArchivesLowerScoreForHQIfBackdoor) {
//if it is HQ we will return the lowest protection of either HQ or Archives
if (server == corp.HQ && archivesIsBackdoorToHQ) {
var archivesProt = this._protectionScore(corp.archives,options);
if (archivesProt < ret) ret = archivesProt;
}
}
return ret;
}
//returns true if we don't want to add more protection to this server
_NoMoreProtectionForThisServer(server) {
if (!server) return false;
//for now we'll just use this check to limit AIAvoidInstallingOverThis asset protection to 1 ice
for (var i=0; i<server.root.length; i++) {
if ( server.root[i].cardType == "asset" && server.root[i].AIAvoidInstallingOverThis && server.ice.length > 0 ) {
return true;
}
}
return false; //usually we are ok with adding more ice
}
_serverToProtect(
ignoreArchives = false, //returns the server that most needs increased protection (does not return null, will be HQ by default or R&D against shapers)