-
Notifications
You must be signed in to change notification settings - Fork 44
/
Copy pathbasicAI.coffee
1459 lines (1297 loc) · 48.5 KB
/
basicAI.coffee
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
{c} = require './cards' if exports?
# The Basic AI
# ------------
# This class defines a rule-based AI of the kind that is popular
# for evaluating Dominion strategies. It can be subclassed -- or simply
# have its methods overwritten on an instance -- to play new strategies.
#
# Every time the player needs to make a meaningful decision, a method called
# `chooseX` (for some X) will be called on the AI, which can examine the game
# state and make a decision accordingly.
#
# In any case that is not a simple yes/no decision, the method will be
# given a list of choices. It will first check a method called `xPriority`,
# which takes in the state and returns an ordered list of choices.
# The player will make the first valid choice in that list. Choices are
# skipped when they have an "if" clause that fails.
#
# If the priority list doesn't choose anything, or if there is no priority
# function, it will consult the `xValue` method instead, which takes in
# a specific choice and assigns it a numerical value.
#
# Priority functions are usually easier to define than value functions, but
# value functions can easily cover every possible case.
#
# The BasicAI has a default decision function for every decision, so
# every AI that derives from it will have *some* way to decide what to do in
# any situation. However, when defining an AI, you will often want to override
# some of these decision functions.
class BasicAI
name: 'Basic AI'
author: 'rspeer'
# Referring to `state.current` to find information about one's own state is
# not always safe! Some of these decisions may be made during other players'
# turns. In those cases, what we want is `this.myPlayer(state)`.
#
# This is passed in as an argument `my` to the decision functions, because
# it's convenient and it creates nice idioms such as `my.hand`.
myPlayer: (state) ->
for player in state.players
if player.ai is this
return player
throw new Error("#{this} is being asked to make a decision, but isn't playing the game...?")
# Decision-making machinery
# -------------------------
# Make the AI's preferred choice, first by checking its explicit priority
# list. If no valid choices are on the list, ask the value function instead.
#
# The priority function returns an ordered list of choices it will want to
# make when they are available. If 'null' is on the priority list, that
# represents an explicit preference to choose "none of the above" when it's
# an option.
#
# The value list assigns a numerical value to every possible choice. 'null'
# automatically has a value of 0. Here you can represent actions you will
# only take when forced to, by giving them negative values.
#
# If a choice should be made entirely using the value function, make the
# priority function return an empty list.
choose: (type, state, choices) ->
my = this.myPlayer(state)
# Are there no choices? We follow the rule that makes the null choice
# available in that situation, and choose it.
if choices.length == 0
return null
# First, try the priority function. If the priority function reaches
# the end of its list, it is treated as "none of the above".
priorityfunc = this[type+'Priority']
if priorityfunc?
# Construct an object with the choices as keys, so we can look them
# up quickly.
choiceSet = {}
for choice in choices
choiceSet[choice] = choice
nullable = null in choices
# Get the priority list.
priority = priorityfunc.call(this, state, my)
# Now look up all the preferences in that list. The moment we encounter
# a valid choice, we can return it.
for preference in priority
if preference is null and nullable
return null
if choiceSet[preference]?
return choiceSet[preference]
# The priority list doesn't want any of these choices (perhaps because
# it doesn't exist). Now try the value function.
bestChoice = null
bestValue = -Infinity
for choice in choices
value = this.getChoiceValue(type, state, choice, my)
if value > bestValue
bestValue = value
bestChoice = choice
# If we got a valid choice, return it.
if bestChoice in choices
return bestChoice
# If we get here, the AI probably wants to choose none of the above.
if null in choices
return null
throw new Error("#{this} somehow failed to make a choice")
getChoiceValue: (type, state, choice, my) ->
if choice is null or choice is no
return 0
specificValueFunc = this[type+'Value']
if specificValueFunc?
result = specificValueFunc.call(this, state, choice, my)
if result is undefined
throw new Error("#{this} has an undefined #{type} value for #{choice}")
if result isnt null
return result
defaultValueFunc = choice['ai_'+type+'Value']
if defaultValueFunc?
result = defaultValueFunc.call(choice, state, my)
if result is undefined
throw new Error("#{this} has an undefined #{type} value for #{choice}")
if result isnt null
return result
state.warn("#{this} doesn't know how to make a #{type} decision for #{choice}")
return -1000
# Sometimes we need to compare choices in a strictly numeric way. This takes
# a particular choice for a particular choice type, and gets its numeric value.
# If the value comes from a priority list, it will be 100 * (distance from end
# of list).
#
# So, for example, the default choiceToValue of discarding a Colony is 999, while
# the choiceToValue of discarding an extra terminal is 1.
choiceToValue: (type, state, choice) ->
return 0 if choice is null or choice is no
my = this.myPlayer(state)
priorityfunc = this[type+'Priority']
if priorityfunc?
priority = priorityfunc.bind(this)(state, my)
else
priority = []
index = priority.indexOf(stringify(choice))
if index != -1
return (priority.length - index) * 100
else
return this.getChoiceValue(type, state, choice, my)
# Originally implemented in the `Rebuild.coffee` strategy, this method gets
# the difference in score if the game were to end now.
getScoreDifference: (state, my) ->
for status in state.getFinalStatus()
[name, score, turns] = status
if name == my.ai.toString()
myScore = score
else
opponentScore = score
return myScore - opponentScore
# More utilities from the Rebuild strategy.
countNotInHand: (my, card) ->
return my.countInDeck(card) - my.countInHand(card)
countInDraw: (my, card) ->
return my.countInDeck(card) - my.countInHand(card) - my.countInDiscard(card)
#### Backwards-compatible choices
#
# To avoid having to rewrite all the code at once, we support these functions
# that pass `chooseAction` onto `choose('action')`, and so on.
chooseAction: (state, choices) -> this.choose('play', state, choices)
chooseTreasure: (state, choices) -> this.choose('play', state, choices)
chooseGain: (state, choices) -> this.choose('gain', state, choices)
chooseDiscard: (state, choices) -> this.choose('discard', state, choices)
chooseTrash: (state, choices) -> this.choose('trash', state, choices)
# Default strategies
# ------------------
# The default buying strategy is a form of Big Money that has, by now,
# been beaten by the newer one in BigMoney.coffee.
gainPriority: (state, my) -> [
"Colony" if my.countInDeck("Platinum") > 0
"Province" if state.countInSupply("Colony") <= 6
"Duchy" if 0 < state.gainsToEndGame() <= 5
"Estate" if 0 < state.gainsToEndGame() <= 2
"Platinum"
"Gold"
"Silver"
"Copper" if state.gainsToEndGame() <= 3
]
# gainValue covers cases where a strategy has to gain a card that isn't in
# its priority list. The default is to favor more expensive cards,
# particularly action and treasure cards.
#
# It is important for all these values to be negative, to avoid giving defined
# strategies cards they don't actually want.
gainValue: (state, card, my) ->
card.cost + 2*card.costPotion + card.isTreasure + card.isAction - 20
# This used to be the default action-playing priority. Now the value of playing
# a card is defined on the "ai_playValue" function of each card.
old_actionPriority: (state, my, skipMultipliers = false) ->
wantsToTrash = this.wantsToTrash(state)
countInHandCopper = my.countInHand("Copper")
currentAction = my.getCurrentAction()
multiplier = 1
if currentAction?.isMultiplier
multiplier = currentAction.multiplier
wantsToPlayMultiplier = false
okayToPlayMultiplier = false
unless skipMultipliers
mults = (card for card in my.hand when card.isMultiplier)
if mults.length > 0
# We've got a multiplier in hand. Figure out if we want to play it.
mult = mults[0]
choices = my.hand.slice(0)
choices.remove(mult)
choices.push(null)
# Determine if it's better than nothing.
choice1 = this.choose('multipliedAction', state, choices)
if choice1 isnt null
okayToPlayMultiplier = true
# Now add the "wait" option and see if we want to multiply an action
# *right now*.
if choices.length > 1
choices.push("wait")
choice = this.choose('multipliedAction', state, choices)
if choice != "wait"
wantsToPlayMultiplier = true
# Priority 1: cards that succeed if we play them now, and might
# not if we play them later. (950-999)
["Menagerie" if my.menagerieDraws() == 3
"Shanty Town" if my.shantyTownDraws(true) == 2
"Tournament" if my.countInHand("Province") > 0
"Library" if my.hand.length <= 3 and my.actions > 1
# 2: Multipliers that do something sufficiently cool. (900-949)
"Throne Room" if wantsToPlayMultiplier
"King's Court" if wantsToPlayMultiplier
# 3: cards that stack the deck. (850-899)
"Lookout" if state.gainsToEndGame() >= 5 or state.cardInfo.Curse in my.draw
"Cartographer"
"Bag of Gold"
"Apothecary"
"Scout"
"Scrying Pool"
"Spy"
# 4: cards that give +2 actions. (800-849)
"Trusty Steed"
"Festival"
"University"
"Farming Village"
"Bazaar"
"Worker's Village"
"City"
"Walled Village"
"Fishing Village"
"Village"
"Border Village"
"Mining Village"
# 5: cards that give +1 action and are almost always good. (700-800)
"Grand Market"
"Hunting Party"
"Alchemist"
"Laboratory"
"Caravan"
"Market"
"Peddler"
"Treasury"
"Conspirator" if my.inPlay.length >= 2 or multiplier > 1
"Familiar"
"Highway"
"Scheme"
"Wishing Well"
"Golem" # seems to be reasonable to expect +1 action from Golem
"Great Hall" if state.cardInfo.Crossroads not in my.hand
"Spice Merchant" if state.cardInfo.Copper in my.hand
"Stables" if this.choose('stablesDiscard', state, my.hand.concat([null]))
"Apprentice"
"Pearl Diver"
"Hamlet"
"Lighthouse"
"Haven"
"Minion"
# 6: terminal card-drawers, if we have actions to spare. (600-699)
"Library" if my.actions > 1 and my.hand.length <= 4 # 695
"Torturer" if my.actions > 1
"Margrave" if my.actions > 1
"Rabble" if my.actions > 1
"Witch" if my.actions > 1
"Ghost Ship" if my.actions > 1
"Smithy" if my.actions > 1
"Embassy" if my.actions > 1
"Watchtower" if my.actions > 1 and my.hand.length <= 4
"Library" if my.actions > 1 and my.hand.length <= 5 # 620
"Council Room" if my.actions > 1
"Courtyard" if my.actions > 1 and (my.discard.length + my.draw.length) <= 3
"Oracle" if my.actions > 1
# 7: Let's insert here an overly simplistic idea of how to play Crossroads.
# Or if we don't have a Crossroads, play a Great Hall that we might otherwise
# have played in priority level 5. (500-599)
"Crossroads" unless my.countInPlay(state.cardInfo.Crossroads) > 0
"Great Hall"
# 8: card-cycling that might improve the hand. (400-499)
"Upgrade" if wantsToTrash >= multiplier
"Oasis"
"Pawn"
"Warehouse"
"Cellar"
"Library" if my.actions > 1 and my.hand.length <= 6
"Spice Merchant" if this.choose('spiceMerchantTrash', state, my.hand.concat([null]))
# 9: non-terminal cards that don't succeed but at least give us something. (300-399)
"King's Court"
"Throne Room" if okayToPlayMultiplier
"Tournament"
"Menagerie"
"Shanty Town" if my.actions < 2
# 10: terminals. Of course, Nobles might be a non-terminal
# if we decide we need the actions more than the cards. (100-299)
"Crossroads"
"Nobles"
"Treasure Map" if my.countInHand("Treasure Map") >= 2
"Followers"
"Mountebank" # 290
"Witch"
"Sea Hag"
"Torturer"
"Young Witch"
"Tribute" # after Cursers but before other terminals, there is probably a better spot for it
"Margrave" # 280
"Goons"
"Wharf"
# Tactician needs a play condition, but I don't know what it would be.
"Tactician"
"Masquerade" # 270
"Vault"
"Ghost Ship"
"Princess"
"Explorer" if my.countInHand("Province") >= 1
"Library" if my.hand.length <= 3 # 260
"Jester"
"Militia"
"Cutpurse" # 250
"Bridge"
"Bishop"
"Horse Traders" # 240
"Jack of All Trades"
"Steward"
"Moneylender" if countInHandCopper >= 1 # 230
"Expand"
"Remodel"
"Salvager" # 220
"Mine"
"Coppersmith" if countInHandCopper >= 3
"Library" if my.hand.length <= 4 # 210
"Rabble"
"Envoy"
"Smithy" # 200
"Embassy"
"Watchtower" if my.hand.length <= 3
"Council Room"
"Library" if my.hand.length <= 5
"Watchtower" if my.hand.length <= 4 # 190
"Courtyard" if (my.discard.length + my.draw.length) > 0
"Merchant Ship"
"Baron" if my.countInHand("Estate") >= 1
"Monument"
"Oracle" # 180
"Remake" if wantsToTrash >= multiplier * 2 # has a low priority so it'll mostly be played early in the game
"Adventurer"
"Harvest"
"Haggler" # probably needs to make sure the gained card will be wanted; 170
"Mandarin"
"Explorer"
"Woodcutter"
"Nomad Camp"
"Chancellor" # 160
"Counting House"
"Coppersmith" if countInHandCopper >= 2
"Outpost" if state.extraturn == false
"Ambassador" if wantsToTrash # 150
"Trading Post" if wantsToTrash + my.countInHand("Silver") >= 2 * multiplier
"Chapel" if wantsToTrash
"Trader" if wantsToTrash >= multiplier
"Trade Route" if wantsToTrash >= multiplier
"Mint" if my.ai.choose('mint', state, my.hand) # 140
"Secret Chamber"
"Pirate Ship"
"Noble Brigand"
"Thief"
"Island" # could be moved
"Fortune Teller" # 130
"Bureaucrat"
"Navigator"
"Conspirator" if my.actions < 2
"Herbalist"
"Moat" # 120
"Library" if my.hand.length <= 6
"Ironworks" # should have higher priority if condition can see it will gain an Action card
"Workshop"
"Smugglers" if state.smugglerChoices().length > 1 # 110
"Feast"
"Transmute" if wantsToTrash >= multiplier
"Coppersmith"
"Saboteur"
"Poor House"
"Duchess"
"Library" if my.hand.length <= 7
"Thief" # 100
# 11: cards that have become useless. Maybe they'll decrease
# the cost of Peddler, trigger Conspirator, or something. (20-99)
"Treasure Map" if my.countInDeck("Gold") >= 4 and state.current.countInDeck("Treasure Map") == 1
"Spice Merchant"
"Shanty Town"
"Stables" # 50
"Chapel"
"Library"
# 12: Conspirator when +actions remain. (10)
"Conspirator"
# "Baron"
# At this point, we take no action if that choice is available.
null
# Nope, something is forcing us to take an action.
#
# Last priority: cards that are actively harmful to play at this point,
# in order of increasing badness.
"Baron"
"Mint"
"Watchtower"
"Outpost"
"Ambassador" # -20
"Trader"
"Transmute"
"Trade Route"
"Upgrade" # -30
"Remake"
"Trading Post"
"Treasure Map" # -40
"Throne Room"
]
# `multipliedActionPriority` is similar to `actionPriority`, but is used when
# we have played a Throne Room or King's Court.
#
# This list emphasizes cards that are really good when multiplied, especially
# terminals when there are +actions left. At the end, it falls back on the
# usual actionPriority list.
old_multipliedActionPriority: (state, my) ->
[
"King's Court" # 2000
"Throne Room" # 1900
"Followers" if my.actions > 0
"Grand Market"
"Mountebank" if my.actions > 0
"Witch" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Sea Hag" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Torturer" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Young Witch" if my.actions > 0 and state.countInSupply("Curse") >= 2
"Crossroads" if my.actions > 0 or my.countInPlay(state.cardInfo.Crossroads) == 0 # 1800
"Scheme" if my.countInDeck("King's Court") >= 2
# Scrying Pool was here once, but I think you'd rather use it to *draw*
# actions for your KC
"Wharf" if my.actions > 0
"Bridge" if my.actions > 0
"Minion" # 1700
"Ghost Ship" if my.actions > 0
"Jester" if my.actions > 0
"Horse Traders" if my.actions > 0
"Mandarin" if my.actions > 0
"Rabble" if my.actions > 0 # 1600
"Council Room" if my.actions > 0
"Margrave" if my.actions > 0
"Smithy" if my.actions > 0
"Embassy" if my.actions > 0
"Merchant Ship" if my.actions > 0 # 1500
"Pirate Ship" if my.actions > 0
"Saboteur" if my.actions > 0
"Noble Brigand" if my.actions > 0
"Thief" if my.actions > 0
"Monument" if my.actions > 0 # 1400
"Feast" if my.actions > 0
"Conspirator"
"Nobles"
"Tribute"
"Steward" if my.actions > 0 # 1300
"Goons" if my.actions > 0
"Mine" if my.actions > 0
"Masquerade" if my.actions > 0
"Vault" if my.actions > 0
"Oracle" if my.actions > 0 # 1200
"Cutpurse" if my.actions > 0
"Coppersmith" if my.actions > 0 and my.countInHand("Copper") >= 2
"Ambassador" if my.actions > 0 and this.wantsToTrash(state) # 1100
"wait"
# We could add here some more cards that would be nice to play with a
# multiplier. Nicer than Lookout, let's say, which appears pretty high
# on the regular action priority list.
#
# But at this point, just fall back on that priority list.
].concat(this.old_actionPriority(state, my, skipMultipliers=true))
# `treasurePriority` determines what order to play treasures in.
# Most of the order has no effect on gameplay. The
# important part is that Bank and Horn of Plenty are last.
treasurePriority: (state, my) -> [
"Platinum"
"Diadem"
"Philosopher's Stone"
"Gold"
"Cache"
"Hoard"
"Royal Seal"
"Harem"
"Silver"
"Fool's Gold"
"Quarry"
"Talisman"
"Copper"
"Masterpiece"
"Potion" # 100 from here up
"Loan" # 90
"Venture" # 80
"Ill-Gotten Gains"
"Bank"
"Horn of Plenty" if my.numUniqueCardsInPlay() >= 2
"Spoils" if this.wantsToPlaySpoils(state)
null
]
# The default `discardPriority` is tuned for Big Money where the decisions
# are obvious. But many strategies would probably prefer a different
# priority list, especially one that knows about action cards.
#
# It doesn't understand
# discarding cards to make Shanty Town or Menagerie work, for example.
discardPriority: (state, my) -> [
"Tunnel"
"Vineyard"
"Colony"
"Duke"
"Duchy"
"Fairgrounds"
"Gardens"
"Province" # Provinces are occasionally useful in hand
"Curse"
"Estate"
]
discardValue: (state, card, my) ->
# If we can discard excess actions, do so. Otherwise, discard the cheapest
# cards. Victory cards would already have been discarded by discardPriority,
# but if Tunnel fell through somehow we discard it here.
#
# First, check to see if it's our turn. That changes whether we want to discard
# actions.
myTurn = (state.current == my)
if card.name == 'Tunnel'
25
else if card.isAction and myTurn and \
((card.actions == 0 and my.actionBalance() <= 0) or (my.actions == 0))
20 - card.cost
else
0 - card.cost
trashPriority: (state, my) -> [
"Curse"
"Estate" if state.gainsToEndGame() > 4
"Copper" if my.getTotalMoney() > 4
"Potion" if my.turnsTaken >= 10
"Estate" if state.gainsToEndGame() > 2
]
# If we have to trash a card we don't want to, assign a value to each card.
# By default, we want to trash the card with the lowest (cost + VP).
trashValue: (state, card, my) ->
0 - card.vp - card.cost
#developPriority: (state, my) =>
# trashPriority(state, my)
#developValue: (state, card, my) =>
# this.trashValue(state, card, my)
# Some cards give you a choice to discard an opponent's deck. These are
# evaluated with `discardFromOpponentDeckValue`.
discardFromOpponentDeckValue: (state, card, my) ->
if card.name == 'Tunnel'
return -2000
else if not (card.isAction) and not (card.isTreasure)
return -10
else
return card.coins + card.cost + 2*card.isAttack
# `discardHandValue` decides whether to discard an entire hand of cards.
discardHandValue: (state, hand, my, nCards = 5) ->
return 0 if hand is null
deck = my.discard.concat(my.draw)
total = 0
for i in [0...5]
shuffle(deck)
randomHand = deck[0...nCards]
# If a random hand from this deck is better, discard this hand.
total += my.ai.compareByDiscarding(state, randomHand, hand)
return total
# Prefer to gain action and treasure cards on the deck, assuming we want
# them at all. Give other cards a value of -1 so that `null` is a better
# choice.
gainOnDeckValue: (state, card, my) ->
if (card.isAction or card.isTreasure)
this.getChoiceValue('gain', state, card, my)
else
-1
# Changed Priorities for putting cards back on deck. Only works well for putting back 1 card, and for 1 buy.
#
putOnDeckPriority: (state, my) ->
# Make a priority order of:
#
# 1. Actions we can't or don't intend to play, from best to worst
# 2. Treasures we can afford to put back
# 3. Junk cards
# 1) Actions
actions = (card for card in my.hand when card.isAction)
getChoiceValue = this.getChoiceValue
byPlayValue = (x, y) ->
getChoiceValue('play', state, y, my) - getChoiceValue('play', state, x, my)
actions.sort(byPlayValue)
putBack = actions[my.countPlayableTerminals(state) ...]
# 2) Put back as much money as you can
if putBack.length == 0
# Get a list of all distinct treasures in hand, in order.
treasures = []
for card in my.hand
if (card.isTreasure) and (not (card in treasures))
treasures.push card
treasures.sort( (x, y) -> y.coins - x.coins )
# Get the margin of how much money we're willing to discard.
margin = my.ai.coinLossMargin(state)
# Find the treasure cards worth less than that.
for card in treasures
if my.ai.coinsDueToCard(state, card) <= margin
putBack.push(card)
# Don't put back last Potion if Alchemists are in play
if my.countInPlay(state.cardInfo["Alchemist"])>0
if "Potion" in putBack
putBack.remove(state.cardInfo["Potion"])
# 3) Put back the worst card (take priority for discard)
if putBack.length == 0
putBack = [my.ai.choose('discard', state, my.hand)]
putBack
putOnDeckValue: (state, card, my) =>
this.discardValue(state, card, my)
# How much does the AI want to discard its deck right now (for Chancellor)?
# Here, we decide to reshuffle (returning a reshuffleValue over 0) when most
# of the non-Action, non-Treasure cards are in the draw pile, or when there
# are no such cards in the deck.
reshuffleValue: (state, choice, my) ->
junkToDraw = 0
totalJunk = 0
for card in my.draw
if not (card.isAction or card.isTreasure)
junkToDraw++
for card in my.getDeck()
if not (card.isAction or card.isTreasure)
totalJunk++
return 1 if (totalJunk == 0)
proportion = junkToDraw/totalJunk
return (proportion - 0.5)
# Choose opponent treasure to trash; go by the card's base cost.
# Diadems are comparable to the cost-5 treasures.
trashOppTreasureValue: (state, card, my) =>
if card is 'Diadem'
return 5
return card.cost
#### Decisions for particular cards
# `ambassadorPriority` chooses a card to Ambassador and how many of it to
# return.
#
# These choices may look odd: remember that choices are evaluated as strings.
# So if we return lists, they won't match any of the choices. We need to
# return their joined string versions.
#
# This is a moderately acceptable way to deal with the fact that, in
# JavaScript, lists are never "equal" to other lists anyway.
ambassadorPriority: (state, my) ->
[
"[Curse, 2]"
"[Curse, 1]"
"[Curse, 0]"
# Handle a silly case:
"[Ambassador, 2]"
"[Estate, 2]"
"[Estate, 1]"
# Make sure we have at least $5 in the deck, including if we buy a Silver.
"[Copper, 2]" if my.getTreasureInHand() < 3 and my.getTotalMoney() >= 5
"[Copper, 2]" if my.getTreasureInHand() >= 5
"[Copper, 2]" if my.getTreasureInHand() == 3 and my.getTotalMoney() >= 7
"[Copper, 1]" if my.getTreasureInHand() < 3 and my.getTotalMoney() >= 4
"[Copper, 1]" if my.getTreasureInHand() >= 4
"[Estate, 0]"
"[Copper, 0]"
"[Potion, 2]"
"[Potion, 1]"
null
].concat ("[#{card}, 1]" for card in my.ai.trashPriority(state, my) when card?)\
.concat ("[#{card}, 0]" for card in my.hand)
apprenticeTrashPriority: (state, my) ->
"Border Village"
"Mandarin"
"Ill-Gotten Gains" if this.coinLossMargin(state) > 0
"Feodum"
"Estate"
"Curse"
"Apprentice"
apprenticeTrashValue: (state, card, my) ->
vp = card.getVP(my)
[coins, potions] = card.getCost(state)
drawn = Math.min(my.draw.length + my.discard.length, coins+2*potions)
return this.choiceToValue('trash', state, card) + 2*drawn - vp
# The question here is: do you want to discard an Estate using a Baron?
# And the answer is yes.
baronDiscardPriority: (state, my) -> [yes]
# `bishopTrashPriority` lists cards that are especially good to trash.
bishopTrashPriority: (state, my) -> [
"Farmland"
"Duchy" if this.goingGreen(state) < 3
"Border Village"
"Mandarin"
"Feodum"
"Bishop"
"Ill-Gotten Gains" if this.coinLossMargin(state) > 0
"Curse"
]
bishopTrashValue: (state, card, my) ->
[coins, potions] = card.getCost(state)
value = Math.floor(coins/2) - card.getVP(my)
# if we're going for victory points, that's all we care about.
if this.goingGreen(state) >= 3
return value
# otherwise, focus on what we want to trash
else
if card in this.trashPriority(state, my)
value += 1
if card.isAction and ((card.actions == 0 and my.actionBalance() <= 0) or (my.actions == 0))
value += 1
if card.isTreasure and card.coins > (this.coinLossMargin(state) + 1)
value -= 10
return value
envoyValue: (state, card, my) ->
# Choose a card to discard from your opponent's hand when it's their turn.
opp = state.current
if card.name == 'Tunnel'
return -25
else if not (card.isAction) and not (card.isTreasure)
return -10
else if opp.actions == 0 and card.isAction
return -5
else if opp.actions >= 2
return card.cards + card.coins + card.cost + 2*card.isAttack
else
return card.coins + card.cost + 2*card.isAttack
# `foolsGoldTrashPriority` will trash a Fool's Gold for a real Gold if
# it's nearing the endgame (5 gains or less), there is one FG in hand,
# and losing it will not change its buy.
foolsGoldTrashPriority: (state, my) ->
if my.countInHand(state.cardInfo["Fool's Gold"]) == 1 and my.ai.coinLossMargin(state) >= 1
[yes]
else
[no]
# Do you want to gain a copper from Ill-Gotten Gains? Yes, we want if that improves our buy
gainCopperPriority: (state, my) ->
if my.ai.coinGainMargin(state) <= my.countInHand("Ill-Gotten Gains")+1
[yes]
else
[no]
# The `herbalist` decision puts a treasure card back on the deck. It sounds
# the same as `putOnDeck`, but it's for a different
# situation -- the card is coming from in play, not from your hand. So
# actually we use the `mintValue` by default.
herbalistValue: (state, card, my) =>
this.mintValue(state, card, my)
huntingGroundsGainPriority: (state, my) -> [
"Duchy"
"Estates"
]
# islandPriority chooses which card to set aside with Island. At present this
# list is incomplete, but covers just about everything that we would want to set aside
# with an Island.
islandPriority: (state, my) ->
[
"Colony"
"Province"
"Fairgrounds"
"Duchy"
"Duke"
"Gardens"
"Vineyard"
"Estate"
"Copper"
"Curse"
"Island"
"Tunnel"
]
islandValue: (state, card, my) -> this.discardValue(state, card, my)
librarySetAsideValue: (state, card, my) -> [
if my.actions == 0 and card.isAction
1
else
-1
]
miningVillageTrashValue: (state, choice, my) ->
if this.goingGreen(state) and this.coinGainMargin(state) <= 2
1
else
-1
minionDiscardValue: (state, choice, my) ->
if choice == yes
# Find out how valuable it would be to discard these cards and draw 4.
value = this.discardHandValue(state, my.hand, my, 4)
opponent = state.players[state.players.length - 1]
# If the attack would decrease an opponent's hand size, it's more valuable.
if opponent.hand.length > 4
value += 2
return value
else
return 0
# Mint anything but Copper and Diadem. Otherwise, go mostly by the card's base cost.
# There is only 1 Diadem, never any available to gain, so never Mint it.
mintValue: (state, card, my) ->
return card.cost - 1
# Choose whether we want these cards or two random cards.
oracleDiscardValue: (state, cards, my) ->
deck = my.discard.concat(my.draw)
shuffle(deck)
randomCards = deck[0...cards.length]
return my.ai.compareByDiscarding(state, my.hand.concat(randomCards), my.hand.concat(cards))
# Choose to attack or use available coins when playing Pirate Ship.
# Current strategy is basically Geronimoo's attackUntil5Coins play strategy,
# but only with Provinces--or technically, cards costing 8 or more.
pirateShipPriority: (state, my) -> [
'coins' if state.current.mats.pirateShip >= 5 and state.current.getAvailableMoney()+state.current.mats.pirateShip >= 8
'attack'
]
# might want to think about something more clever, but for first, just discard Coppers
plazaDiscardPriority: (state, my) -> [
"Copper"
null
]
rogueGainValue: (state, card, my) ->
[coins, potions] = card.getCost(state)
return coins
rogueTrashValue: (state, card, my) ->
[coins, potions] = card.getCost(state)
return coins
salvagerTrashPriority: (state, card, my) -> [
"Border Village"
"Mandarin"
"Ill-Gotten Gains" if this.coinLossMargin(state) > 0
"Feodum"
"Salvager"
]
# To calculate the salvagerTrashValue, we simulate trashing each card, determine
# the best card we would buy as a result, and evaluate it as if we were
# upgrading the trashed card into the bought one.
salvagerTrashValue: (state, card, my) ->
[hypothesis, hypothetically_my] = state.hypothetical(this)
hypothetically_my.hand.remove(card)
[coins, potions] = card.getCost(hypothesis)
hypothetically_my.coins += coins
hypothetically_my.buys += 1
buyState = this.fastForwardToBuy(hypothesis, hypothetically_my)
gained = buyState.getSingleBuyDecision()
return this.upgradeValue(state, [card, gained], my)
# Scheme uses the same priority function as multiplied actions. Good actions
# to multiply this turn are typically good actions to have around next turn.
schemeValue: (state, card, my) ->
# Project a little of what the state will look like at the beginning of the
# next turn. This keeps multipliedActionPriority from evaluating a card
# as though it will be used in the current (finished) turn.
myNext = {}
myNext[key] = value for key, value of my
myNext.actions = 1
myNext.buys = 1
myNext.coins = 0
return this.getChoiceValue('multiplied', state, card, myNext)
# `scryingPoolDiscardValue` is like `discardValue`, except it strongly
# prefers to discard non-actions.
scryingPoolDiscardValue: (state, card, my) ->
if not card.isAction
2000
else
this.choiceToValue('discard', state, card)
spiceMerchantTrashPriority: (state, my) -> [
"Copper",
"Potion",
"Loan",
"Ill-Gotten Gains",
"Fool's Gold" if my.countInDeck("Fool's Gold") == 1,
"Silver" if my.getTotalMoney() >= 8,
null,
"Silver",
"Venture",
"Cache",
"Gold",
"Harem",
"Platinum"
]
# Which treasure, if any, should be discarded to feed Stables? Defaults
# to a list of generally crappy treasures. Doesn't include $1 Fool's Gold
# because you presumably have another one you're trying to draw.
stablesDiscardPriority: (state, my) -> [
"Copper"
"Potion" if my.countInPlay(state.cardInfo["Alchemist"]) == 0
"Ill-Gotten Gains"
"Silver"
"Horn of Plenty"
null
"Potion"
"Venture"
"Cache"
"Gold"
"Platinum"
]