mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 19:28:01 +00:00
Compare commits
2474 Commits
forge-1.6.
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35706d71f7 | ||
|
|
c38f0900d1 | ||
|
|
ea4b82f11e | ||
|
|
8cfff4299a | ||
|
|
d4d376c8c6 | ||
|
|
0146357eed | ||
|
|
9759264aec | ||
|
|
c17465a2a7 | ||
|
|
dc3795d1cd | ||
|
|
fdf2b26fdf | ||
|
|
eff0012ea1 | ||
|
|
d74186e93c | ||
|
|
fe108bb354 | ||
|
|
9fb556f180 | ||
|
|
f1db204e0d | ||
|
|
fd35c8d862 | ||
|
|
4d134e5087 | ||
|
|
1e3b4e34e5 | ||
|
|
95a02bece7 | ||
|
|
62ffb786b2 | ||
|
|
4d41ed60c9 | ||
|
|
fc8624c93b | ||
|
|
800a77b2e0 | ||
|
|
361443d845 | ||
|
|
7db9a6730a | ||
|
|
5a784e6c9b | ||
|
|
91c16a319c | ||
|
|
06bc5e074a | ||
|
|
85adc466b3 | ||
|
|
afc02a856d | ||
|
|
21d6b02410 | ||
|
|
5b0b719ce9 | ||
|
|
9ce0315c3e | ||
|
|
d7bca1c232 | ||
|
|
0a034f9acb | ||
|
|
98774c408c | ||
|
|
802036f693 | ||
|
|
5bb9ac6fcd | ||
|
|
fcc02ed600 | ||
|
|
1e45d14ec3 | ||
|
|
97027e657e | ||
|
|
487fec0258 | ||
|
|
b25bc72f3e | ||
|
|
d086e4d692 | ||
|
|
8d2d66abb9 | ||
|
|
83242e1fa9 | ||
|
|
35fa4e7d8f | ||
|
|
5022e96a48 | ||
|
|
1779c8f84e | ||
|
|
74d9791f03 | ||
|
|
a6cb96c758 | ||
|
|
b05463ad6c | ||
|
|
f229fbc4b1 | ||
|
|
1d470f87d4 | ||
|
|
8c680da98c | ||
|
|
21e682f54d | ||
|
|
70593c2b6a | ||
|
|
6b64667767 | ||
|
|
96d1777132 | ||
|
|
f6bd725633 | ||
|
|
dd7e2d933f | ||
|
|
97177e379e | ||
|
|
8df00a18e8 | ||
|
|
6668fa5505 | ||
|
|
c348ffa34e | ||
|
|
275e1be6be | ||
|
|
0c005bb3ff | ||
|
|
57b9a1b4c7 | ||
|
|
b29b1b3630 | ||
|
|
3a071ea071 | ||
|
|
25efe15329 | ||
|
|
d544d69524 | ||
|
|
bcc0fc9e00 | ||
|
|
7f2d2d6588 | ||
|
|
2e70eb6718 | ||
|
|
b7601fded3 | ||
|
|
13ace49230 | ||
|
|
229ecc9ba7 | ||
|
|
602f761552 | ||
|
|
f271c7a74e | ||
|
|
c47242e40e | ||
|
|
887f2f9a52 | ||
|
|
6b528a6d99 | ||
|
|
7ff8564077 | ||
|
|
77e79653ba | ||
|
|
4665503caa | ||
|
|
1f922362a1 | ||
|
|
f5e095f345 | ||
|
|
a392fc43f9 | ||
|
|
d444e8896c | ||
|
|
935d42af74 | ||
|
|
a9e15c2d33 | ||
|
|
b5ae52a3b6 | ||
|
|
abf405fd20 | ||
|
|
3215eb9a8a | ||
|
|
f2ddd697e9 | ||
|
|
9e6fc59505 | ||
|
|
2dc0e10c34 | ||
|
|
2a086c8b64 | ||
|
|
cf9e3a5f1d | ||
|
|
373e11bc41 | ||
|
|
bb27eab819 | ||
|
|
d99dd5f19f | ||
|
|
d60d350fcb | ||
|
|
b1e7e407a7 | ||
|
|
4e7d338144 | ||
|
|
4528541b3f | ||
|
|
a829dc72bc | ||
|
|
b6395aa463 | ||
|
|
3f1d6238ff | ||
|
|
e990c3648e | ||
|
|
67f9be5d4d | ||
|
|
8650a147aa | ||
|
|
8490f8d90b | ||
|
|
8f1c7bf40a | ||
|
|
ec87b64d0c | ||
|
|
27eee4c8fa | ||
|
|
af32152bcc | ||
|
|
e72edb95b5 | ||
|
|
efed143a11 | ||
|
|
99f64d7565 | ||
|
|
32ef472b0c | ||
|
|
7ade1b9a52 | ||
|
|
cc664d1280 | ||
|
|
84874601a9 | ||
|
|
b47d528bf0 | ||
|
|
59612c45ea | ||
|
|
e8be6f674e | ||
|
|
33554dd9cc | ||
|
|
4dd73930e3 | ||
|
|
af14c33e39 | ||
|
|
7e9312a93c | ||
|
|
4048ba18f3 | ||
|
|
3ed563d3f1 | ||
|
|
11aef9f054 | ||
|
|
556a8e7176 | ||
|
|
465066c04c | ||
|
|
668eaa1a7c | ||
|
|
0bed673340 | ||
|
|
13f606999c | ||
|
|
b0677ca006 | ||
|
|
22846db1a3 | ||
|
|
c9b634a274 | ||
|
|
c37a492cb7 | ||
|
|
92f663c753 | ||
|
|
35a2f9bf2b | ||
|
|
2dcf4b23b7 | ||
|
|
22b2f30ffb | ||
|
|
9a7215a2a2 | ||
|
|
6d4cc6efcb | ||
|
|
45a0c54fb8 | ||
|
|
e8c9c8b90c | ||
|
|
367fcad53c | ||
|
|
3a6c5b0cd5 | ||
|
|
375adab087 | ||
|
|
fbf0f98c3a | ||
|
|
711342829b | ||
|
|
daf3be6346 | ||
|
|
06edf9351a | ||
|
|
ead64864f4 | ||
|
|
8521387312 | ||
|
|
3e5b47185e | ||
|
|
b5dbb7da90 | ||
|
|
03886905bd | ||
|
|
f01282f12f | ||
|
|
5e5fca9722 | ||
|
|
2ff32d3b90 | ||
|
|
3b72bd18f0 | ||
|
|
fcb7f89001 | ||
|
|
38cebf52c3 | ||
|
|
56983b2553 | ||
|
|
04b347b968 | ||
|
|
08a87ea112 | ||
|
|
67e8046af6 | ||
|
|
dc11fad342 | ||
|
|
8f9857e72d | ||
|
|
76bcbc632e | ||
|
|
e04d88977e | ||
|
|
3e613a8b80 | ||
|
|
1f364168e8 | ||
|
|
07439cfeeb | ||
|
|
94eda92d21 | ||
|
|
15715cec10 | ||
|
|
3662bb8e84 | ||
|
|
14fc75be44 | ||
|
|
a489e0845b | ||
|
|
acf7a23965 | ||
|
|
a96a2fd825 | ||
|
|
d455653f11 | ||
|
|
8579ae99a0 | ||
|
|
da5cb3d905 | ||
|
|
81315e3db4 | ||
|
|
8a880a3f89 | ||
|
|
d5da9a80f6 | ||
|
|
670966be57 | ||
|
|
5879690fc0 | ||
|
|
ff054d44b7 | ||
|
|
e909cfc744 | ||
|
|
b51ca84a8f | ||
|
|
3a9af0480d | ||
|
|
ebd0a8096c | ||
|
|
a1f2f8d944 | ||
|
|
0a4dc6961d | ||
|
|
2e196fc41b | ||
|
|
844a7b5d76 | ||
|
|
ce343ffcdb | ||
|
|
25972aaa29 | ||
|
|
0fe8c1f121 | ||
|
|
cb37cf0889 | ||
|
|
b5721a7e36 | ||
|
|
93b40ad867 | ||
|
|
a3e5e40385 | ||
|
|
e13ed6f46e | ||
|
|
65dffbbe3a | ||
|
|
16dafc9c57 | ||
|
|
bcd2f95e4d | ||
|
|
51ae7fee7e | ||
|
|
2eb276f995 | ||
|
|
6a98f9947a | ||
|
|
66cbd1a30e | ||
|
|
c8d1b56d9e | ||
|
|
457ce2958e | ||
|
|
b7b6d5672e | ||
|
|
7dac6197e3 | ||
|
|
ea40b439ee | ||
|
|
7061f7f26a | ||
|
|
de1342a5d7 | ||
|
|
2f976dbeb3 | ||
|
|
e37e242bae | ||
|
|
78c676e858 | ||
|
|
9e74ef9e37 | ||
|
|
1740be22ae | ||
|
|
35c499a074 | ||
|
|
048093522a | ||
|
|
66c696bfec | ||
|
|
5dc5082c4e | ||
|
|
42f79630e6 | ||
|
|
f8ccf0a91e | ||
|
|
b924c1a80c | ||
|
|
42a6340a95 | ||
|
|
8b2b1d453d | ||
|
|
e70207fbac | ||
|
|
846db416ec | ||
|
|
84288aafe9 | ||
|
|
d2d4318659 | ||
|
|
324c3000a0 | ||
|
|
fc6d1d2ab8 | ||
|
|
7fb12fa5b3 | ||
|
|
791e53a935 | ||
|
|
3a705c3e5f | ||
|
|
ca61291995 | ||
|
|
a875557000 | ||
|
|
517590883c | ||
|
|
ffa58f71f0 | ||
|
|
9017f1488f | ||
|
|
c6bac3a247 | ||
|
|
a7e48e8af0 | ||
|
|
41b73da329 | ||
|
|
48d4016f8d | ||
|
|
ed3624ce7b | ||
|
|
58eebd8bfd | ||
|
|
cc95200289 | ||
|
|
7035993a3f | ||
|
|
663b10f767 | ||
|
|
922e45a80d | ||
|
|
3dca3752bc | ||
|
|
336ca104e9 | ||
|
|
7492e7d450 | ||
|
|
7440702fea | ||
|
|
e568f84dcb | ||
|
|
d043161bdd | ||
|
|
9768b80736 | ||
|
|
93185e7e84 | ||
|
|
18a717e90a | ||
|
|
99ce255da8 | ||
|
|
770e328865 | ||
|
|
2776eaf640 | ||
|
|
2d825e4095 | ||
|
|
64cac6cd5a | ||
|
|
8d7eccf92e | ||
|
|
01cd80cc2f | ||
|
|
801e218912 | ||
|
|
c1cb19a139 | ||
|
|
419f4a354f | ||
|
|
569e884a2c | ||
|
|
647e9f6a14 | ||
|
|
a24cffa9df | ||
|
|
58d0921696 | ||
|
|
2b3e39cbb8 | ||
|
|
689e5eecc4 | ||
|
|
36917c0d18 | ||
|
|
4421766850 | ||
|
|
2d2af55849 | ||
|
|
906fef89f9 | ||
|
|
479a468187 | ||
|
|
813e8deb8f | ||
|
|
4c4259ed90 | ||
|
|
b186958422 | ||
|
|
83b41e1479 | ||
|
|
97477644fd | ||
|
|
f3e4f0d1fb | ||
|
|
4175369f96 | ||
|
|
f2fcfe582f | ||
|
|
b2d546be2f | ||
|
|
a7db0dd75e | ||
|
|
94127c1b76 | ||
|
|
220690d4d0 | ||
|
|
fb3ffe679b | ||
|
|
b38f7fe4f5 | ||
|
|
110eb2b072 | ||
|
|
d474e6b8bb | ||
|
|
5b074f4c1c | ||
|
|
8e4ab4d398 | ||
|
|
7eb934d5d3 | ||
|
|
c3b655f675 | ||
|
|
068efaeba7 | ||
|
|
ff7a0f1ae6 | ||
|
|
8b26deb477 | ||
|
|
21fedd0d50 | ||
|
|
ecd4a5dedd | ||
|
|
047ee71259 | ||
|
|
1b97eb777c | ||
|
|
cd31c4e513 | ||
|
|
1582f5a7bd | ||
|
|
32facbbc74 | ||
|
|
7696b9d69c | ||
|
|
d2e1d5aacb | ||
|
|
e70affe6c3 | ||
|
|
d38f9c39e4 | ||
|
|
f7209ef32c | ||
|
|
1f10ba936a | ||
|
|
0332975a9a | ||
|
|
369c4f77a0 | ||
|
|
d8a284ebb9 | ||
|
|
dca62ce725 | ||
|
|
1c64ebd05b | ||
|
|
0727c83a43 | ||
|
|
eb4a8caefc | ||
|
|
253efc6c6d | ||
|
|
d8be06c165 | ||
|
|
e02baac897 | ||
|
|
46f19b6079 | ||
|
|
2f3ba53a89 | ||
|
|
b763e06560 | ||
|
|
2447ea4fb0 | ||
|
|
75f86352be | ||
|
|
19dd58d1d3 | ||
|
|
68843db5b8 | ||
|
|
586c2eb9b0 | ||
|
|
156cdf812d | ||
|
|
5e6d9749fb | ||
|
|
2392aeeb0c | ||
|
|
6d89addb67 | ||
|
|
5a29b05e88 | ||
|
|
90b46f4c0f | ||
|
|
1281af0994 | ||
|
|
b31069866a | ||
|
|
00f896bbb4 | ||
|
|
cd5c03926f | ||
|
|
11218913a4 | ||
|
|
0a039e4f53 | ||
|
|
7e37b3d760 | ||
|
|
d577c229e3 | ||
|
|
6cd74946e4 | ||
|
|
d34b6428ae | ||
|
|
e242e3f371 | ||
|
|
4165004491 | ||
|
|
d290d5fbf4 | ||
|
|
81c99209ce | ||
|
|
1962aac95b | ||
|
|
60a66585f3 | ||
|
|
d7f3842a30 | ||
|
|
81be883bee | ||
|
|
0dfcb225e0 | ||
|
|
06f95efd52 | ||
|
|
0dae411333 | ||
|
|
c1e1b6f29a | ||
|
|
a511bdbcd8 | ||
|
|
b0037faa11 | ||
|
|
6dc6a7a29c | ||
|
|
fd6192a88e | ||
|
|
fcb8e1f8c9 | ||
|
|
2da04fed3d | ||
|
|
00abdccc13 | ||
|
|
9f25644645 | ||
|
|
83b84dfff7 | ||
|
|
c0f5f0567b | ||
|
|
f53a54a126 | ||
|
|
e3ac4433e1 | ||
|
|
1e831513a0 | ||
|
|
a40a6e8023 | ||
|
|
22982178e2 | ||
|
|
63e2854d7a | ||
|
|
7117580446 | ||
|
|
691562324b | ||
|
|
f0e6e20732 | ||
|
|
67ab3e1e01 | ||
|
|
a048ab3327 | ||
|
|
af1be79f78 | ||
|
|
81366a1e14 | ||
|
|
21c4e2eeda | ||
|
|
b866338197 | ||
|
|
98e19db3fc | ||
|
|
5221d871a0 | ||
|
|
d48c93938f | ||
|
|
0aa3583412 | ||
|
|
38adbb7386 | ||
|
|
0ecdb07a0a | ||
|
|
5191777fca | ||
|
|
e1feff66b4 | ||
|
|
e96f0e24b3 | ||
|
|
891b2580c0 | ||
|
|
8fa477a3f2 | ||
|
|
48079df235 | ||
|
|
4bd796c84b | ||
|
|
efa38ea80a | ||
|
|
b0be025f93 | ||
|
|
08bc3170ad | ||
|
|
b126a97e23 | ||
|
|
505940d9b4 | ||
|
|
41fbedbdb9 | ||
|
|
00379f8cd0 | ||
|
|
52e9772ef9 | ||
|
|
634a9e7a6c | ||
|
|
8383aeeddd | ||
|
|
308c07c2b3 | ||
|
|
ad352beb24 | ||
|
|
1e59b75d08 | ||
|
|
bfef8b125c | ||
|
|
b3f1bad6b7 | ||
|
|
8e3e38b72f | ||
|
|
714f21913b | ||
|
|
6e1733821c | ||
|
|
d5c285bb63 | ||
|
|
2be734f137 | ||
|
|
67ab92359f | ||
|
|
89184b97dd | ||
|
|
466eac71a1 | ||
|
|
ce35c6232e | ||
|
|
2e51375c0f | ||
|
|
e38b34e391 | ||
|
|
2e0af3c3fe | ||
|
|
ab1550e24b | ||
|
|
7d57af98fd | ||
|
|
f1cb6b7b8e | ||
|
|
2cb03f1671 | ||
|
|
da68b22601 | ||
|
|
c68f1b8a03 | ||
|
|
02ae5e301d | ||
|
|
0dc7a4af2d | ||
|
|
4d8bb5a844 | ||
|
|
a8ffdecef0 | ||
|
|
adf62cc5f5 | ||
|
|
4aaafdf106 | ||
|
|
95e5f06554 | ||
|
|
361604d7f6 | ||
|
|
97292d5a7f | ||
|
|
3a552823c8 | ||
|
|
a36c06ff2f | ||
|
|
fe7cce1726 | ||
|
|
b4e5313160 | ||
|
|
151fb40e23 | ||
|
|
a111341293 | ||
|
|
4df7554d81 | ||
|
|
e58b0c219f | ||
|
|
e551c98ca4 | ||
|
|
0bd954e0c1 | ||
|
|
23f13ca87e | ||
|
|
2f66df36a7 | ||
|
|
03cc62f4df | ||
|
|
31fb23b83b | ||
|
|
445c87812d | ||
|
|
25b3ffbb6e | ||
|
|
358f5c6046 | ||
|
|
4fdc38b53f | ||
|
|
9fc669d897 | ||
|
|
f40f868429 | ||
|
|
43bdd23b3c | ||
|
|
0bf12f0965 | ||
|
|
5434605984 | ||
|
|
19a1bf0c0e | ||
|
|
16adb81cd2 | ||
|
|
115b77682b | ||
|
|
82839cc500 | ||
|
|
5dca460399 | ||
|
|
0eeb68737c | ||
|
|
389e21aca7 | ||
|
|
1eec2905b1 | ||
|
|
7823368ebc | ||
|
|
8b0f5ff91e | ||
|
|
f45a4d06a3 | ||
|
|
d16a197651 | ||
|
|
95263720f3 | ||
|
|
56f05c54f9 | ||
|
|
b20c5acf75 | ||
|
|
33f296da7c | ||
|
|
429ac3c072 | ||
|
|
4b5a555c1e | ||
|
|
278d64d800 | ||
|
|
6ceb08d41d | ||
|
|
0e513614c3 | ||
|
|
b8ac0819aa | ||
|
|
8380da7a10 | ||
|
|
cb7922fe15 | ||
|
|
3b6cfef67d | ||
|
|
b20bb2d023 | ||
|
|
f8decaac41 | ||
|
|
d7674c15b5 | ||
|
|
9d1c6eaa7f | ||
|
|
1927a94879 | ||
|
|
590b877f76 | ||
|
|
6c93862891 | ||
|
|
cd9289bcdd | ||
|
|
c294e1f6df | ||
|
|
9090cc70bf | ||
|
|
244e2a9e8d | ||
|
|
cc2b2921d9 | ||
|
|
88339864f7 | ||
|
|
f0ad9083a5 | ||
|
|
e66194fb9d | ||
|
|
042693bf53 | ||
|
|
4a8cecd225 | ||
|
|
1a9003c999 | ||
|
|
e35f09ea2f | ||
|
|
056d66b865 | ||
|
|
ceafaf5e2e | ||
|
|
5207e697dd | ||
|
|
39a5da9a1b | ||
|
|
4ed99e026f | ||
|
|
cdff15dade | ||
|
|
16f2b16d22 | ||
|
|
3efde7a2e3 | ||
|
|
032bcec313 | ||
|
|
a5d5ed8524 | ||
|
|
c1e5d346c8 | ||
|
|
0a004b8f7e | ||
|
|
ffcd5b2f2c | ||
|
|
11ed680b5a | ||
|
|
3abc22a855 | ||
|
|
ef9d6203c0 | ||
|
|
016df27726 | ||
|
|
246ce114e2 | ||
|
|
f3767417b0 | ||
|
|
67fb62ec52 | ||
|
|
27f2dcc8d5 | ||
|
|
9f37393a69 | ||
|
|
a7af6ecfc0 | ||
|
|
f772ab5fcc | ||
|
|
6076954881 | ||
|
|
581bb74b9c | ||
|
|
81a8c93db4 | ||
|
|
6d8e162df3 | ||
|
|
9f44b46186 | ||
|
|
e15f826190 | ||
|
|
f5ea2c0345 | ||
|
|
fff0051540 | ||
|
|
0fd3df021a | ||
|
|
44bbcfeebb | ||
|
|
5ae8e4c391 | ||
|
|
1e039cefcb | ||
|
|
eeafd84625 | ||
|
|
6955ad861c | ||
|
|
9fff6d70fe | ||
|
|
742e718c43 | ||
|
|
d3020d591c | ||
|
|
ab44402886 | ||
|
|
13cf87fa7a | ||
|
|
c9a2ccb9f9 | ||
|
|
3f78a5d595 | ||
|
|
cdfdcba477 | ||
|
|
1dbbfb5112 | ||
|
|
d528d3854b | ||
|
|
f6b5467663 | ||
|
|
77e22b355b | ||
|
|
74b2b1d4cf | ||
|
|
9736d6c3e4 | ||
|
|
4c8f1be770 | ||
|
|
3f88293d8e | ||
|
|
40828d8f57 | ||
|
|
c97d67e4fc | ||
|
|
e3265ace4b | ||
|
|
2e3afcbe45 | ||
|
|
236ac800d6 | ||
|
|
d395c47d4d | ||
|
|
178944b3da | ||
|
|
61457cbba8 | ||
|
|
a74d4ab0c6 | ||
|
|
3f561f004a | ||
|
|
26a3d1b3be | ||
|
|
a4e619fba2 | ||
|
|
f6230e76fd | ||
|
|
5a7f35c6ab | ||
|
|
1c5c3eba74 | ||
|
|
59569a9bfc | ||
|
|
9c7e5ae8dd | ||
|
|
bd0049ef57 | ||
|
|
f936f443ea | ||
|
|
4d85237f44 | ||
|
|
1cf8a96c3d | ||
|
|
99242384f5 | ||
|
|
aaed05bef8 | ||
|
|
857e3fb4a1 | ||
|
|
c0ae7f42d9 | ||
|
|
37605ca2b8 | ||
|
|
89ecbda923 | ||
|
|
dc24cabcbf | ||
|
|
ae2d1a438c | ||
|
|
b85917140d | ||
|
|
4373e81508 | ||
|
|
d1293259e7 | ||
|
|
381304867b | ||
|
|
1b4b5752aa | ||
|
|
94b3ae3715 | ||
|
|
b478099a2b | ||
|
|
26d8419678 | ||
|
|
1eb2cbf275 | ||
|
|
e1f3c3ad1c | ||
|
|
98c3787ed4 | ||
|
|
97fcce87c4 | ||
|
|
1536996a02 | ||
|
|
542bc66f86 | ||
|
|
206d1639b5 | ||
|
|
3a9ae627db | ||
|
|
831b5a8ba3 | ||
|
|
9b4f423afa | ||
|
|
e7ce5bcdb6 | ||
|
|
73bbb0d3dd | ||
|
|
1e5d2ef239 | ||
|
|
07b25b78ee | ||
|
|
e67518d0f0 | ||
|
|
c1b7c46eca | ||
|
|
d2d43aea2e | ||
|
|
1fa83a2d84 | ||
|
|
6d1f698882 | ||
|
|
3c4a34daaa | ||
|
|
590b487711 | ||
|
|
7344c934a4 | ||
|
|
9a67bc0f49 | ||
|
|
cbc5dc79a0 | ||
|
|
1fcde1cd4b | ||
|
|
a16e398feb | ||
|
|
25ca71a97a | ||
|
|
23618de189 | ||
|
|
43455723f6 | ||
|
|
d646240c63 | ||
|
|
d60ee9307d | ||
|
|
c2915d909d | ||
|
|
3187969200 | ||
|
|
30f18cd947 | ||
|
|
181a1d2f1a | ||
|
|
3404d267f7 | ||
|
|
0af6b92e9f | ||
|
|
c87a2bff23 | ||
|
|
32a42cf9ec | ||
|
|
e2189ea877 | ||
|
|
fe31d692f2 | ||
|
|
84c2ac918b | ||
|
|
6cf97aa2fc | ||
|
|
b58a561863 | ||
|
|
06c13804a4 | ||
|
|
bf8e88f76b | ||
|
|
84528b5251 | ||
|
|
f76428b668 | ||
|
|
d8abdf1a2e | ||
|
|
f9f83660f5 | ||
|
|
ddfd97c395 | ||
|
|
f658ba29c9 | ||
|
|
57cf22af43 | ||
|
|
eaea379532 | ||
|
|
2ac3829b6f | ||
|
|
29e3cf4a79 | ||
|
|
d84e2e4e23 | ||
|
|
02b8d39791 | ||
|
|
f7a74edc6e | ||
|
|
8feaf0795f | ||
|
|
87a857220f | ||
|
|
403550af5f | ||
|
|
d6ffdddfc6 | ||
|
|
7663df024c | ||
|
|
704c735bc5 | ||
|
|
ec72e9f7d7 | ||
|
|
f95ada602c | ||
|
|
7c10272d03 | ||
|
|
3c3c64a11a | ||
|
|
722c16e925 | ||
|
|
4a799cf45f | ||
|
|
16acf2d46c | ||
|
|
413c14a1dc | ||
|
|
826f37e4b9 | ||
|
|
39654e498c | ||
|
|
e3432ced3c | ||
|
|
917ae1daab | ||
|
|
a09086bfc2 | ||
|
|
2ac38e6639 | ||
|
|
3dc54790cc | ||
|
|
ce780c5175 | ||
|
|
35a76b4a43 | ||
|
|
999f990870 | ||
|
|
e050a62b0c | ||
|
|
1cd278afcf | ||
|
|
208089a5cd | ||
|
|
0c66c256b3 | ||
|
|
66e8154243 | ||
|
|
ff4db64802 | ||
|
|
4c29790b6c | ||
|
|
3efc4807cf | ||
|
|
a3b543cdd2 | ||
|
|
a12eedfa93 | ||
|
|
a97125d733 | ||
|
|
7b5d2fc053 | ||
|
|
0e587c5b3a | ||
|
|
6ea9a07a3d | ||
|
|
090a47b17f | ||
|
|
f79b326010 | ||
|
|
4c6aa708c1 | ||
|
|
7ac66a298b | ||
|
|
9e1baa9521 | ||
|
|
c1870b4ef8 | ||
|
|
a9b068c89e | ||
|
|
dab7d5030e | ||
|
|
120a314a96 | ||
|
|
bd4305dd13 | ||
|
|
d8b7214a2b | ||
|
|
95122ffcf8 | ||
|
|
3b1b0bc92f | ||
|
|
49f789c404 | ||
|
|
9cf447d422 | ||
|
|
1dc10b57c6 | ||
|
|
3d11422191 | ||
|
|
fa0fc14c25 | ||
|
|
22cdb7e721 | ||
|
|
d215657641 | ||
|
|
df33b9388f | ||
|
|
ac53f16098 | ||
|
|
36d6342e77 | ||
|
|
72e7822616 | ||
|
|
ea8f990172 | ||
|
|
8003ade509 | ||
|
|
3795232b2c | ||
|
|
dda26098f4 | ||
|
|
ae042d98f4 | ||
|
|
649faa3371 | ||
|
|
cea0c3f628 | ||
|
|
dce2ebbafb | ||
|
|
c80d703acf | ||
|
|
f0418b8d2a | ||
|
|
f363ae0f1e | ||
|
|
24d3801316 | ||
|
|
2a3f2c6dbd | ||
|
|
318a6e2924 | ||
|
|
800311fed8 | ||
|
|
e49d45bf4f | ||
|
|
356bd9c635 | ||
|
|
b400ea6d33 | ||
|
|
7352f9094a | ||
|
|
c90e8bd356 | ||
|
|
9f79073d14 | ||
|
|
249f8a4673 | ||
|
|
fe8ec70159 | ||
|
|
1f5375fed2 | ||
|
|
804d47df7b | ||
|
|
c6dfc1ceb2 | ||
|
|
b3727f6821 | ||
|
|
bc538da8ea | ||
|
|
465e25adff | ||
|
|
809fc513ea | ||
|
|
ea101baf0d | ||
|
|
19f88d6e6d | ||
|
|
7499d05bbc | ||
|
|
de2e3fc070 | ||
|
|
9b85293349 | ||
|
|
bee34c064b | ||
|
|
7ba64ce136 | ||
|
|
d80d2c5cda | ||
|
|
2419b93682 | ||
|
|
740b3f0c49 | ||
|
|
0925379a6d | ||
|
|
0c89087579 | ||
|
|
0565082309 | ||
|
|
cf7fcb07a1 | ||
|
|
ad99d21cdf | ||
|
|
3ad4da4f49 | ||
|
|
62238e0f81 | ||
|
|
2928f157fa | ||
|
|
cacb64b2ca | ||
|
|
1f65c4723d | ||
|
|
c2d4923706 | ||
|
|
5ea07389b4 | ||
|
|
8c56bb51ed | ||
|
|
7c8057dbc6 | ||
|
|
841b9ae668 | ||
|
|
f847a39eaa | ||
|
|
f2eafb99d4 | ||
|
|
847becccfe | ||
|
|
536c2483b0 | ||
|
|
af80228bcd | ||
|
|
334b086efb | ||
|
|
bddce9872f | ||
|
|
3d4e544573 | ||
|
|
7fe68a1c4a | ||
|
|
8693b6ce81 | ||
|
|
200b3f7092 | ||
|
|
69afbde352 | ||
|
|
052fb65518 | ||
|
|
c2ec6978c4 | ||
|
|
941e25d7a7 | ||
|
|
0bddb07ba0 | ||
|
|
869697352d | ||
|
|
9757fdb16b | ||
|
|
4d718177b6 | ||
|
|
2dff30f2cd | ||
|
|
8b5128510b | ||
|
|
1efa8d030b | ||
|
|
c855a107ab | ||
|
|
a379279956 | ||
|
|
3fb38ae682 | ||
|
|
ee29d4289c | ||
|
|
eeccc8490d | ||
|
|
2ae735db74 | ||
|
|
248d6c57c9 | ||
|
|
5eba9bfc14 | ||
|
|
7bc14a526d | ||
|
|
769521c978 | ||
|
|
bc71ef6a18 | ||
|
|
422ab9b835 | ||
|
|
410f9b1acf | ||
|
|
f6b3cb4b53 | ||
|
|
6a72aab106 | ||
|
|
f3695e18ac | ||
|
|
f64846c690 | ||
|
|
79bb27d2a7 | ||
|
|
f0dba0ca0e | ||
|
|
b70627bd3f | ||
|
|
91b894958c | ||
|
|
ef10e83f1c | ||
|
|
e958cb665e | ||
|
|
8413ec92d5 | ||
|
|
92d77b610d | ||
|
|
42f445b1d2 | ||
|
|
ca886f4c0c | ||
|
|
531ac8fc50 | ||
|
|
628c8f5ff1 | ||
|
|
b63037caf3 | ||
|
|
19127cb09d | ||
|
|
004e0a4265 | ||
|
|
9bbc23d550 | ||
|
|
77cea9f74e | ||
|
|
0bc57c145a | ||
|
|
92545309e1 | ||
|
|
bdc2fe8e69 | ||
|
|
1d3c5c85ec | ||
|
|
cd14da0454 | ||
|
|
73ccaac656 | ||
|
|
b19df1be9f | ||
|
|
f10bfe8e8d | ||
|
|
3418fdf822 | ||
|
|
f31098bf15 | ||
|
|
27ec3db33d | ||
|
|
8b618df712 | ||
|
|
a87d428563 | ||
|
|
d778138f92 | ||
|
|
1197b1c59a | ||
|
|
e5cf4e8cea | ||
|
|
4bbfb1852f | ||
|
|
d88ecc8505 | ||
|
|
6d1ef8257f | ||
|
|
5167eabb5c | ||
|
|
c8f08f61e5 | ||
|
|
9ed4822a93 | ||
|
|
0db2fba9fa | ||
|
|
117ee3d7bc | ||
|
|
c30261f9b8 | ||
|
|
5bd0fcf866 | ||
|
|
c888a09a40 | ||
|
|
c88818aa55 | ||
|
|
bafbdc8a5a | ||
|
|
a04c9f144a | ||
|
|
3301d91ae7 | ||
|
|
6c4ed98ad4 | ||
|
|
273894b734 | ||
|
|
2fd3b13d86 | ||
|
|
e3ec921e3d | ||
|
|
8e4ce46891 | ||
|
|
9c50f45866 | ||
|
|
2b3733a752 | ||
|
|
ae709d210e | ||
|
|
8f94bceaf2 | ||
|
|
36fbb71e1d | ||
|
|
464a1fe51c | ||
|
|
4067caa144 | ||
|
|
c576b0f65c | ||
|
|
6156ad254f | ||
|
|
3b0465aafe | ||
|
|
d7da7a3fd1 | ||
|
|
3fb515c73b | ||
|
|
8ef5fbc84b | ||
|
|
51f1c281e7 | ||
|
|
d93e9170ad | ||
|
|
418d4d5019 | ||
|
|
bf6c569c01 | ||
|
|
6e1a994971 | ||
|
|
0ed44d4afb | ||
|
|
92cc1c7718 | ||
|
|
b4347ff53d | ||
|
|
3e2b4418ef | ||
|
|
691264c2c3 | ||
|
|
a21b2acf6f | ||
|
|
54a3052fdb | ||
|
|
33fdd2969a | ||
|
|
2f146c9648 | ||
|
|
d7ead4cc3d | ||
|
|
f5b275d0d1 | ||
|
|
5b18502378 | ||
|
|
792fa2e9ca | ||
|
|
c1f4400441 | ||
|
|
18e813b3ed | ||
|
|
5bc66f4c76 | ||
|
|
df7d5e9c8e | ||
|
|
a9c68d2942 | ||
|
|
541afd807e | ||
|
|
598a391c12 | ||
|
|
a9c536322e | ||
|
|
2635c1798d | ||
|
|
92f1596336 | ||
|
|
9d41132be6 | ||
|
|
5c367ef940 | ||
|
|
423d38c94e | ||
|
|
4ad20fd7d6 | ||
|
|
7cff83168f | ||
|
|
7ea109aaf8 | ||
|
|
beb29502a7 | ||
|
|
1fed0a1264 | ||
|
|
d2182ad49c | ||
|
|
bb01e2a4d4 | ||
|
|
b46c0bb05d | ||
|
|
192ef621f6 | ||
|
|
4a43abc905 | ||
|
|
07dfb2e9df | ||
|
|
2f8beff993 | ||
|
|
72ec9a2b70 | ||
|
|
1ce6979910 | ||
|
|
c0794aff76 | ||
|
|
371ed047c6 | ||
|
|
b4cc04f32b | ||
|
|
42596623a9 | ||
|
|
345c809a23 | ||
|
|
fbe169f06d | ||
|
|
4f5fc25195 | ||
|
|
e1f3fb6ac0 | ||
|
|
9915d3c9ac | ||
|
|
0bbed86241 | ||
|
|
5076d03143 | ||
|
|
52b5ef7647 | ||
|
|
e9f8877dec | ||
|
|
ed87267633 | ||
|
|
e4d469a408 | ||
|
|
5c13e8d9aa | ||
|
|
7afdd4cbf5 | ||
|
|
c155317a6b | ||
|
|
a70513985a | ||
|
|
ffc2324dd5 | ||
|
|
9ef975d467 | ||
|
|
fa9c88c7be | ||
|
|
2d6b9b775e | ||
|
|
8a48371aec | ||
|
|
c891a0fa26 | ||
|
|
99728fd237 | ||
|
|
b76c65441b | ||
|
|
4ca8ec005d | ||
|
|
db3c1b22ce | ||
|
|
b0525bf06b | ||
|
|
d884731691 | ||
|
|
a4e4d089f7 | ||
|
|
0b31ee8562 | ||
|
|
aaf8969af2 | ||
|
|
f7c34aa572 | ||
|
|
16e4c11961 | ||
|
|
116003f189 | ||
|
|
6c87de30dd | ||
|
|
60b2601d8c | ||
|
|
ca37bda739 | ||
|
|
c4698212ce | ||
|
|
0e0d4031c9 | ||
|
|
be1f885341 | ||
|
|
a6c8eedfd4 | ||
|
|
32f9600d51 | ||
|
|
03f99c88ae | ||
|
|
47882191bc | ||
|
|
d7962ae659 | ||
|
|
cbd17b0290 | ||
|
|
839325ab70 | ||
|
|
13dc4304fc | ||
|
|
c49a937fb8 | ||
|
|
36d3905d0e | ||
|
|
9ff748dfd5 | ||
|
|
40878794cf | ||
|
|
a17a69c93a | ||
|
|
469ca98b8a | ||
|
|
330cc1cd3a | ||
|
|
5c61819b25 | ||
|
|
2c31c26cb9 | ||
|
|
a4f490b607 | ||
|
|
36007330e9 | ||
|
|
fba890af81 | ||
|
|
619fee0821 | ||
|
|
4f7d3f4e4e | ||
|
|
ac1f1d2de4 | ||
|
|
5856edd962 | ||
|
|
ac463d081e | ||
|
|
dbd1a872c4 | ||
|
|
ff600d652d | ||
|
|
8a512a13a4 | ||
|
|
a9f1a25da4 | ||
|
|
0fd05f2c36 | ||
|
|
aa0754a53f | ||
|
|
d31d0aa735 | ||
|
|
c92b26928c | ||
|
|
70f5f1be30 | ||
|
|
4ae769506e | ||
|
|
73c6b67830 | ||
|
|
f05963c6ea | ||
|
|
d99b687989 | ||
|
|
d57e4bd077 | ||
|
|
e5c2a18bd6 | ||
|
|
796a2b44c8 | ||
|
|
5e67cf8292 | ||
|
|
5e4880f1d1 | ||
|
|
ea161d83fa | ||
|
|
0f28ca779d | ||
|
|
7711af1214 | ||
|
|
9817eb303b | ||
|
|
21d782b4be | ||
|
|
f14d816e6b | ||
|
|
025ee4df18 | ||
|
|
1b2870a1b2 | ||
|
|
03760521ac | ||
|
|
e4d8983280 | ||
|
|
64431e3661 | ||
|
|
ace56ece7e | ||
|
|
135724fab5 | ||
|
|
5d72b7ce98 | ||
|
|
6697a44259 | ||
|
|
85165850c1 | ||
|
|
3bd24d80c0 | ||
|
|
aab31719d4 | ||
|
|
1ffc84663a | ||
|
|
9f703cf64e | ||
|
|
44b1e2a4e5 | ||
|
|
3e7fa3e440 | ||
|
|
20154d8411 | ||
|
|
26b2a2278e | ||
|
|
886901b48e | ||
|
|
a817967aa0 | ||
|
|
52bbfdb9f4 | ||
|
|
8c74096a7f | ||
|
|
153362393b | ||
|
|
3185d0b38b | ||
|
|
0be8b711ac | ||
|
|
9e60002210 | ||
|
|
fe169f267d | ||
|
|
26c09c3993 | ||
|
|
49f516685f | ||
|
|
89ca710932 | ||
|
|
f1d01341cf | ||
|
|
c30beda09d | ||
|
|
4b4d9da7b1 | ||
|
|
32a94fbb37 | ||
|
|
a589857582 | ||
|
|
f9c20c4a91 | ||
|
|
886d07a4c3 | ||
|
|
b1ca421e52 | ||
|
|
1cb583a3ba | ||
|
|
4683318351 | ||
|
|
e17a4756e7 | ||
|
|
7622f7c550 | ||
|
|
e25332ed27 | ||
|
|
1f270a5231 | ||
|
|
cc32286389 | ||
|
|
d78b2f2fc0 | ||
|
|
a406bc94a8 | ||
|
|
765a743d5b | ||
|
|
40a0d3a3e4 | ||
|
|
b612791fc8 | ||
|
|
c4cc51cf8e | ||
|
|
3494d388bb | ||
|
|
88b887c579 | ||
|
|
061f3ce78f | ||
|
|
944d2475dd | ||
|
|
1d1e57a265 | ||
|
|
522b874f54 | ||
|
|
dea2cfad1c | ||
|
|
1c8369d6fd | ||
|
|
98d8d65687 | ||
|
|
c5c9f5a447 | ||
|
|
c42a3ac6e0 | ||
|
|
be60c4f865 | ||
|
|
fd9bb6f36a | ||
|
|
27118a0f7a | ||
|
|
665d4af5a2 | ||
|
|
1fd2aa17d5 | ||
|
|
bd8db95098 | ||
|
|
fcb3f64d9b | ||
|
|
ee36ab94b6 | ||
|
|
ac86437b35 | ||
|
|
30d8d9f3fe | ||
|
|
f2ace5cd3d | ||
|
|
cc1a71f246 | ||
|
|
b3d8eaee7d | ||
|
|
c70861d7d3 | ||
|
|
4cfa915aec | ||
|
|
2bf31add95 | ||
|
|
61526dea82 | ||
|
|
5c719c27b0 | ||
|
|
342207375e | ||
|
|
ce5b26b0d0 | ||
|
|
6056bfbc49 | ||
|
|
4e925b0385 | ||
|
|
edff4117f5 | ||
|
|
64f2d62605 | ||
|
|
1efe09eeca | ||
|
|
2375176d72 | ||
|
|
3bfc7da93a | ||
|
|
34b4e90d75 | ||
|
|
48ab353521 | ||
|
|
73f872bf6e | ||
|
|
f04c8be999 | ||
|
|
d6eb49db68 | ||
|
|
c03ada1e1c | ||
|
|
9ce3d6182b | ||
|
|
ff3f60cc68 | ||
|
|
89aa4bfab3 | ||
|
|
746e60b517 | ||
|
|
8634cc561f | ||
|
|
d384f0896a | ||
|
|
9f5cc344ad | ||
|
|
45bebdff0c | ||
|
|
59ef9e132a | ||
|
|
af8e9c2466 | ||
|
|
e11163f483 | ||
|
|
d3ff20484d | ||
|
|
76cbbb2bfe | ||
|
|
c51ad4aaa6 | ||
|
|
6d5e56f6bf | ||
|
|
251569e81c | ||
|
|
be2759c354 | ||
|
|
69ee8f3630 | ||
|
|
610991a222 | ||
|
|
f08a0eaffd | ||
|
|
a0baa659ec | ||
|
|
536a19b373 | ||
|
|
27a2d7f642 | ||
|
|
fc29cec8c8 | ||
|
|
5fe19d75a6 | ||
|
|
3252489971 | ||
|
|
f90af4784e | ||
|
|
a2f11fb829 | ||
|
|
b6e6d308a9 | ||
|
|
dd04888106 | ||
|
|
0c397a414e | ||
|
|
2a62001048 | ||
|
|
3132e5dd5b | ||
|
|
8843a12333 | ||
|
|
b2e97ac73b | ||
|
|
11c440209a | ||
|
|
400adeed1c | ||
|
|
abcdc7253e | ||
|
|
b6b8b9440f | ||
|
|
c9a64b8e96 | ||
|
|
327795855a | ||
|
|
b3c5494e74 | ||
|
|
9fc4a247aa | ||
|
|
013cd0a9a8 | ||
|
|
05a9da4eb8 | ||
|
|
81d853ac04 | ||
|
|
af8dfa685a | ||
|
|
946a360652 | ||
|
|
071fe9a910 | ||
|
|
ccb4e87002 | ||
|
|
cd9adb166a | ||
|
|
4f95f2c842 | ||
|
|
7e6771d550 | ||
|
|
0b025fd0ae | ||
|
|
9445204ffc | ||
|
|
0b69923ad7 | ||
|
|
00eb90bcc6 | ||
|
|
8478967c04 | ||
|
|
08f4b5bbe6 | ||
|
|
828a5e7f46 | ||
|
|
2e6151fe25 | ||
|
|
c5fad910ba | ||
|
|
8d7c672b94 | ||
|
|
67de9ba209 | ||
|
|
d3486341f3 | ||
|
|
ac12412ec3 | ||
|
|
73e0a4d35a | ||
|
|
1e09e9e3f8 | ||
|
|
d97265f2f2 | ||
|
|
cd55c4077e | ||
|
|
0a9d5b73d8 | ||
|
|
e5e1de83a1 | ||
|
|
359ab4233b | ||
|
|
715f991d0d | ||
|
|
e00b18bf40 | ||
|
|
77869fd273 | ||
|
|
1f4de4af80 | ||
|
|
6d34e04138 | ||
|
|
1761ccffd0 | ||
|
|
4de53b29f0 | ||
|
|
08c02b7473 | ||
|
|
a648e888c5 | ||
|
|
af94789b7f | ||
|
|
c4520a0ac2 | ||
|
|
75bec0a132 | ||
|
|
b680878558 | ||
|
|
3f95ca2869 | ||
|
|
003d7ac606 | ||
|
|
b61267d3bd | ||
|
|
a6728853db | ||
|
|
d95c46e1b8 | ||
|
|
d3a18c55d3 | ||
|
|
8bffadeb14 | ||
|
|
4abb037081 | ||
|
|
07d6cebba4 | ||
|
|
abd929070e | ||
|
|
ea937a410b | ||
|
|
64159bab6f | ||
|
|
164e6656e8 | ||
|
|
0bac699cbf | ||
|
|
2030ffcea3 | ||
|
|
6433eff8ec | ||
|
|
6c51f7c6bc | ||
|
|
0c82e0614c | ||
|
|
acd30aaf08 | ||
|
|
ce2f75d43d | ||
|
|
3f3875cc0c | ||
|
|
e939218a65 | ||
|
|
d929a8b140 | ||
|
|
3e1aef4a58 | ||
|
|
93dad0bf44 | ||
|
|
d5259e8bfb | ||
|
|
440d7fe2b8 | ||
|
|
0e666f07e9 | ||
|
|
1b542e936b | ||
|
|
13487bbeed | ||
|
|
7a71716c7f | ||
|
|
e72baa9dd4 | ||
|
|
4c9f188b0c | ||
|
|
e78f90bc62 | ||
|
|
19d5e8457f | ||
|
|
0fb0837561 | ||
|
|
6cff21f47b | ||
|
|
477408676d | ||
|
|
97b675bc9f | ||
|
|
65dab56ad0 | ||
|
|
faba97a6f5 | ||
|
|
c10a2e1133 | ||
|
|
83d13cc153 | ||
|
|
bdbdd990a7 | ||
|
|
70e4d80965 | ||
|
|
6ec8b15a34 | ||
|
|
ee21afee61 | ||
|
|
08a08855aa | ||
|
|
95ee7ba2f8 | ||
|
|
cde55f37f3 | ||
|
|
5b147d4ab2 | ||
|
|
058ef0c4c5 | ||
|
|
bb48bd22a3 | ||
|
|
0aecd7068f | ||
|
|
0d4a837be6 | ||
|
|
ac70e6504a | ||
|
|
a64f19a686 | ||
|
|
9a12f8ecab | ||
|
|
37d6e5aa3d | ||
|
|
7cbf5ff74e | ||
|
|
b6ec9728d8 | ||
|
|
b5a167df05 | ||
|
|
f01409ab2c | ||
|
|
a96778a94d | ||
|
|
e9e898da79 | ||
|
|
a8d73f6172 | ||
|
|
84a1d9997b | ||
|
|
5aa489d769 | ||
|
|
fd520f2ea8 | ||
|
|
a1a7cb747b | ||
|
|
3d79e77f47 | ||
|
|
f6c6ecd494 | ||
|
|
52cca9532b | ||
|
|
d412bcc718 | ||
|
|
6cb93e51fa | ||
|
|
84bc9205a9 | ||
|
|
92aea19abd | ||
|
|
5dc94fdf0e | ||
|
|
2089450a1d | ||
|
|
eb92fe944c | ||
|
|
3f426f63a1 | ||
|
|
8c3a96db7f | ||
|
|
edd9142f64 | ||
|
|
424b83ddeb | ||
|
|
610e844747 | ||
|
|
a6fbe153b5 | ||
|
|
0c1108b92d | ||
|
|
684b84690c | ||
|
|
df808f5690 | ||
|
|
d960623dac | ||
|
|
d30c95c58c | ||
|
|
8c1139c151 | ||
|
|
3a6adc5503 | ||
|
|
dbc1556381 | ||
|
|
bd85ed3577 | ||
|
|
1340e43f02 | ||
|
|
aee891cd7e | ||
|
|
9f39c1d305 | ||
|
|
43c23059ba | ||
|
|
12c1caf54d | ||
|
|
3848ba311e | ||
|
|
dcb6cbf5fa | ||
|
|
f70e33a3ea | ||
|
|
857d6f87a0 | ||
|
|
d3444621b9 | ||
|
|
05bfc51a20 | ||
|
|
851c593695 | ||
|
|
8ed711ad34 | ||
|
|
e8eaf60c2c | ||
|
|
df480b67c0 | ||
|
|
31e7316854 | ||
|
|
b183588046 | ||
|
|
2620769b19 | ||
|
|
9f4184974e | ||
|
|
e586debb58 | ||
|
|
eccaf83e7d | ||
|
|
a9a6227b8f | ||
|
|
f16251d046 | ||
|
|
27c508caa0 | ||
|
|
aa10845ef0 | ||
|
|
fe4c836cd2 | ||
|
|
a34afb1a81 | ||
|
|
9ee58451b5 | ||
|
|
faa8ca0b34 | ||
|
|
e084f66354 | ||
|
|
c68887f5f5 | ||
|
|
43862a4b7b | ||
|
|
036d672e7c | ||
|
|
abbbae011f | ||
|
|
bbab6a0d68 | ||
|
|
2e492dfc39 | ||
|
|
3d5606cc7b | ||
|
|
553d059113 | ||
|
|
990345cda0 | ||
|
|
1cbf075036 | ||
|
|
6e518df398 | ||
|
|
8e1af41938 | ||
|
|
da2faaeff4 | ||
|
|
66af4ef831 | ||
|
|
cc0fdb302b | ||
|
|
fd1f686469 | ||
|
|
a45a8a6011 | ||
|
|
a40c362764 | ||
|
|
b3c372a803 | ||
|
|
0ffe6051b6 | ||
|
|
3613cb704e | ||
|
|
0733f7a69a | ||
|
|
d514944657 | ||
|
|
5172aaafdb | ||
|
|
d8ec34aa6f | ||
|
|
0257acce1d | ||
|
|
f7746a0da6 | ||
|
|
52605f6a4a | ||
|
|
d865a8ba0c | ||
|
|
2869337f52 | ||
|
|
083f28c89d | ||
|
|
bd78dff68f | ||
|
|
db291eb9c4 | ||
|
|
f155244997 | ||
|
|
fa8d2f0a25 | ||
|
|
f3b0259884 | ||
|
|
258a9c046e | ||
|
|
97bda7a949 | ||
|
|
6b306860ec | ||
|
|
697c41b0d0 | ||
|
|
ee8ec490d7 | ||
|
|
c1674b568a | ||
|
|
2c7d75996e | ||
|
|
579c08a9e8 | ||
|
|
2ba6e616af | ||
|
|
5d32321aa2 | ||
|
|
ccd50fa98b | ||
|
|
69c9cb1244 | ||
|
|
35e1fca22d | ||
|
|
039dfb63ea | ||
|
|
cf53d03104 | ||
|
|
d4e72f07f9 | ||
|
|
08a6af547c | ||
|
|
dd8a526a30 | ||
|
|
298a6b48ad | ||
|
|
08d305ebfa | ||
|
|
e4f0463299 | ||
|
|
b586182160 | ||
|
|
4051da7ee1 | ||
|
|
1705cc21b4 | ||
|
|
4a3e0b244c | ||
|
|
3855fa3568 | ||
|
|
70b0e7d6c0 | ||
|
|
e40f3cb34d | ||
|
|
434deb92f8 | ||
|
|
d962905799 | ||
|
|
bb10ce78a0 | ||
|
|
0670847700 | ||
|
|
c539acb823 | ||
|
|
f233c2683e | ||
|
|
e5a7f5844c | ||
|
|
db1839c402 | ||
|
|
24b54a0937 | ||
|
|
c3f7640c4d | ||
|
|
dab74b7bba | ||
|
|
c0e9bb223e | ||
|
|
0630658d4d | ||
|
|
3094850579 | ||
|
|
7b29e2e603 | ||
|
|
53784e154d | ||
|
|
20e3bbf288 | ||
|
|
43d016c288 | ||
|
|
320cc45fc8 | ||
|
|
a1831ab338 | ||
|
|
6f57f56d1d | ||
|
|
41062dfc47 | ||
|
|
e6628d4569 | ||
|
|
7c389dbf8e | ||
|
|
651f7cc7cd | ||
|
|
bd70662314 | ||
|
|
9a45a84663 | ||
|
|
5b12d0e3f6 | ||
|
|
d9e1121796 | ||
|
|
b0f37ca217 | ||
|
|
7459f7a9fa | ||
|
|
3f3f2f2c18 | ||
|
|
42f2b0a8e3 | ||
|
|
8326dc1dc7 | ||
|
|
cc68c5cfcb | ||
|
|
9e46cc2b0b | ||
|
|
2fc827fcd7 | ||
|
|
8108bdc91b | ||
|
|
345f6fc2af | ||
|
|
af06cb5cdd | ||
|
|
ba74b333cd | ||
|
|
895d282cc8 | ||
|
|
33831ee7cc | ||
|
|
8aaea65a72 | ||
|
|
e20ba2c0a0 | ||
|
|
e276950caa | ||
|
|
a17663655b | ||
|
|
9bb5a69b56 | ||
|
|
b9afd98189 | ||
|
|
89da789a99 | ||
|
|
f78f8006d6 | ||
|
|
46c79b4213 | ||
|
|
e4fb8c20ac | ||
|
|
5a8762edb1 | ||
|
|
6285c76935 | ||
|
|
77942b632f | ||
|
|
29261c34d5 | ||
|
|
2152e5f731 | ||
|
|
b659833f71 | ||
|
|
f7d626d8c6 | ||
|
|
f1c8ace081 | ||
|
|
655015d33b | ||
|
|
ec24a92987 | ||
|
|
04237c6118 | ||
|
|
86f13e05d2 | ||
|
|
9a6146e1ba | ||
|
|
0a8ce34252 | ||
|
|
18529f47e7 | ||
|
|
98215be0fc | ||
|
|
f2698ef38d | ||
|
|
215ee66a02 | ||
|
|
e055657d13 | ||
|
|
ec7f47dbe7 | ||
|
|
e3e7e4d26a | ||
|
|
118d7d735a | ||
|
|
d7a8534354 | ||
|
|
9fe86bf72a | ||
|
|
1d76f89428 | ||
|
|
fc26af5a89 | ||
|
|
7f85a8f2e1 | ||
|
|
e3ff8029de | ||
|
|
70f5bdd339 | ||
|
|
2ecc36ab12 | ||
|
|
a4c14c6be1 | ||
|
|
6cbda005e8 | ||
|
|
8ea99648f0 | ||
|
|
58a5d9b0e1 | ||
|
|
b969f2eac6 | ||
|
|
5c295e9080 | ||
|
|
64a6c3c5bb | ||
|
|
22cc4c635a | ||
|
|
b5b96f1155 | ||
|
|
06b887cd93 | ||
|
|
c6ef376d15 | ||
|
|
6ef249195e | ||
|
|
f6e99cf748 | ||
|
|
6d39088777 | ||
|
|
e5cb026608 | ||
|
|
ce9dbc2b5f | ||
|
|
bfef677e93 | ||
|
|
0d18a1d19a | ||
|
|
91b3b7194d | ||
|
|
306d515e7f | ||
|
|
0a21e03eac | ||
|
|
e112704a63 | ||
|
|
c0bbf107c6 | ||
|
|
84a6876265 | ||
|
|
add9ffe5d9 | ||
|
|
c71f0b7e39 | ||
|
|
e934071716 | ||
|
|
5693cddc3a | ||
|
|
561d27be0a | ||
|
|
2e19a2a99c | ||
|
|
4055e421bc | ||
|
|
792255b676 | ||
|
|
0ddb8d9644 | ||
|
|
5c29555ae7 | ||
|
|
cf5e6bde9a | ||
|
|
515ddbb28d | ||
|
|
18720e3693 | ||
|
|
6b21664dff | ||
|
|
ca92f90f6d | ||
|
|
8a1ab40f3c | ||
|
|
f738822cce | ||
|
|
2ef900443b | ||
|
|
917c6b7c54 | ||
|
|
966db8af9f | ||
|
|
07b26f03a7 | ||
|
|
d685997040 | ||
|
|
fce6807a3b | ||
|
|
e91b428bd5 | ||
|
|
6581466239 | ||
|
|
380a5bbadd | ||
|
|
4da83ff5f8 | ||
|
|
d5cf8848fa | ||
|
|
4ea6f9dd6a | ||
|
|
d8027b002d | ||
|
|
623cda83d5 | ||
|
|
99b3e4493b | ||
|
|
025a201a7d | ||
|
|
3243555181 | ||
|
|
867eae442b | ||
|
|
e8e80a7ac8 | ||
|
|
480c88a73e | ||
|
|
6f34a42034 | ||
|
|
312421ed28 | ||
|
|
93e81fdc01 | ||
|
|
2d7f0f907b | ||
|
|
6aca67efa6 | ||
|
|
bab2b9f528 | ||
|
|
07886140fb | ||
|
|
90341ee27a | ||
|
|
f78da4f637 | ||
|
|
d3b8ffe328 | ||
|
|
e0b25527b3 | ||
|
|
56798a76c9 | ||
|
|
2faab75bd8 | ||
|
|
d4d7c5b35e | ||
|
|
bf07df8f7b | ||
|
|
3622103a90 | ||
|
|
31680b3849 | ||
|
|
aa7611fceb | ||
|
|
9f0a34455b | ||
|
|
da51f8af37 | ||
|
|
170853f2cc | ||
|
|
df4b625ac4 | ||
|
|
2d6ff3b74c | ||
|
|
a5b3b61052 | ||
|
|
1b1a56e77c | ||
|
|
e18dd07491 | ||
|
|
f94771730e | ||
|
|
cb24df8890 | ||
|
|
499d72d5d6 | ||
|
|
1bab2617b7 | ||
|
|
b5ed2daa81 | ||
|
|
c32cd456b6 | ||
|
|
0f9a71f07d | ||
|
|
a1711daead | ||
|
|
5b6031f41d | ||
|
|
c3787ab02f | ||
|
|
9d1a216a20 | ||
|
|
2603d08aa4 | ||
|
|
990c0afee2 | ||
|
|
1bfd401ed7 | ||
|
|
22414bab7c | ||
|
|
08222b0d5c | ||
|
|
d599d29514 | ||
|
|
e5120c7074 | ||
|
|
68f36bf172 | ||
|
|
ee025f9a13 | ||
|
|
68891d18f4 | ||
|
|
be4b7e7232 | ||
|
|
c3e03c17e8 | ||
|
|
1dc63294b4 | ||
|
|
63191515d3 | ||
|
|
f8b072f14a | ||
|
|
d09862522b | ||
|
|
ce5240223b | ||
|
|
5b8d2cb36a | ||
|
|
d9b35ca1ee | ||
|
|
e8433a1d80 | ||
|
|
092aac82ea | ||
|
|
9039178927 | ||
|
|
a8b2a627f8 | ||
|
|
da8b85decd | ||
|
|
73786bfd94 | ||
|
|
e06ea82f62 | ||
|
|
085732868c | ||
|
|
3f7c512c63 | ||
|
|
28d8b49de7 | ||
|
|
c79d04f29a | ||
|
|
d1ba022ce9 | ||
|
|
d36fb16b12 | ||
|
|
26e94d3ba6 | ||
|
|
d2cd5e7202 | ||
|
|
0bfd5cc292 | ||
|
|
deb25f8299 | ||
|
|
aab28fd3fd | ||
|
|
d9759305a3 | ||
|
|
e015e57dda | ||
|
|
44a2f1880d | ||
|
|
2e3617ad1c | ||
|
|
ae5e7d1e60 | ||
|
|
a31c3dc611 | ||
|
|
2ee3ec303d | ||
|
|
3d8a85a145 | ||
|
|
fc18153f02 | ||
|
|
77c1875b6b | ||
|
|
4388a84787 | ||
|
|
0d3a9c8b16 | ||
|
|
2ca05b5634 | ||
|
|
4569bca39d | ||
|
|
c401a609a4 | ||
|
|
07a71f1059 | ||
|
|
82f6f7d4d5 | ||
|
|
97f68a8247 | ||
|
|
1d9f867c75 | ||
|
|
ebd3c33051 | ||
|
|
00cec4faa0 | ||
|
|
690fa7bf78 | ||
|
|
ace6474157 | ||
|
|
974a017044 | ||
|
|
89917d4f75 | ||
|
|
f651364e00 | ||
|
|
2a8b43b7a3 | ||
|
|
aff991690d | ||
|
|
7f095947ab | ||
|
|
b18b2cff44 | ||
|
|
befd16238b | ||
|
|
20d3e540b0 | ||
|
|
cede2927c5 | ||
|
|
543aae0893 | ||
|
|
e9c55345b8 | ||
|
|
8c7dc08844 | ||
|
|
3ae20885c4 | ||
|
|
09042f1dbf | ||
|
|
6b5c52d2c8 | ||
|
|
e02e2c462b | ||
|
|
30a1ea7a4f | ||
|
|
51a2383574 | ||
|
|
7fbbb9b030 | ||
|
|
f2d88796ed | ||
|
|
1659ca88ca | ||
|
|
fd7ba65203 | ||
|
|
ae66502fd5 | ||
|
|
cc06fb6b40 | ||
|
|
a60c6af30e | ||
|
|
91e7cd6576 | ||
|
|
d717a68445 | ||
|
|
7f4dcf54a7 | ||
|
|
613238e0f9 | ||
|
|
72f7189aeb | ||
|
|
ea2616434c | ||
|
|
0261f25996 | ||
|
|
f5db79ce69 | ||
|
|
236a7c91d5 | ||
|
|
c6bae2116a | ||
|
|
05d42d9518 | ||
|
|
19a6236fa9 | ||
|
|
115dbc8b12 | ||
|
|
03084c3ce3 | ||
|
|
0260afa068 | ||
|
|
2f9044f53f | ||
|
|
f195e85fb8 | ||
|
|
65065343f9 | ||
|
|
50c8014977 | ||
|
|
911d0a4fae | ||
|
|
d1bb81e404 | ||
|
|
76526a0b2d | ||
|
|
a9020d92ca | ||
|
|
74be6e6c14 | ||
|
|
f203168542 | ||
|
|
3c03f5fd71 | ||
|
|
8cfc9d7793 | ||
|
|
ec5c500b8f | ||
|
|
8f7cbeb4ab | ||
|
|
fb32273477 | ||
|
|
1be49e8d8a | ||
|
|
b7edc01952 | ||
|
|
0470878ddf | ||
|
|
d07a65d553 | ||
|
|
58218f10da | ||
|
|
690eebb6a1 | ||
|
|
9bd9936781 | ||
|
|
bf62325329 | ||
|
|
a3775e0b57 | ||
|
|
03c0794b48 | ||
|
|
b5edcaf5ac | ||
|
|
3723a4e656 | ||
|
|
b332038722 | ||
|
|
33df8dd5eb | ||
|
|
7e982b327b | ||
|
|
91585464bd | ||
|
|
3c3f494034 | ||
|
|
87cbc6229a | ||
|
|
bd10364d27 | ||
|
|
36721dde56 | ||
|
|
6a74bd841a | ||
|
|
093b5451c3 | ||
|
|
896dff3fdc | ||
|
|
e8e23603c3 | ||
|
|
426fe0fe40 | ||
|
|
3e2ef43a0a | ||
|
|
a73b480b1f | ||
|
|
4d374dd587 | ||
|
|
3b97f8c396 | ||
|
|
e7a559327c | ||
|
|
ede35abe1a | ||
|
|
edc36f915d | ||
|
|
05a9d457aa | ||
|
|
cc7ce6bae9 | ||
|
|
57e5135346 | ||
|
|
74980f84d5 | ||
|
|
fa7f1cc21a | ||
|
|
8c90fb3c15 | ||
|
|
021a48c070 | ||
|
|
8be583083e | ||
|
|
8d15a7249a | ||
|
|
6259793f39 | ||
|
|
772faf8cd2 | ||
|
|
dd7141aeaa | ||
|
|
521a3f9341 | ||
|
|
452bdd7b4f | ||
|
|
37ebb5731d | ||
|
|
933ce64cce | ||
|
|
b8d0019ece | ||
|
|
4df3da0856 | ||
|
|
59f482e52e | ||
|
|
538ea82899 | ||
|
|
0b7fc67c3f | ||
|
|
7a92712f0b | ||
|
|
6ed9a8b336 | ||
|
|
24df4e78c7 | ||
|
|
a0abaf62b4 | ||
|
|
85ece1ec63 | ||
|
|
ba284aafe3 | ||
|
|
f25ab2f892 | ||
|
|
b026111ef8 | ||
|
|
9850f2e242 | ||
|
|
9313fa9193 | ||
|
|
f19c9183f0 | ||
|
|
5af3384b02 | ||
|
|
46a5512209 | ||
|
|
63c83e97c4 | ||
|
|
cf311fdeb1 | ||
|
|
04ef82bc70 | ||
|
|
7182ad2e9c | ||
|
|
072508e1c1 | ||
|
|
91101a7c40 | ||
|
|
d34fa71797 | ||
|
|
bee7bccc12 | ||
|
|
74eea096b1 | ||
|
|
02c028584b | ||
|
|
2a87d338b4 | ||
|
|
8759532964 | ||
|
|
f332db93ad | ||
|
|
6ffc687174 | ||
|
|
ba6079164c | ||
|
|
372ee945de | ||
|
|
0f0eed3de6 | ||
|
|
9eddb37c3e | ||
|
|
6a262ea604 | ||
|
|
94693f530e | ||
|
|
ee9eced602 | ||
|
|
58fa9b8182 | ||
|
|
89b377ab8f | ||
|
|
1d93fccbb3 | ||
|
|
f8786bcd47 | ||
|
|
8adf2043f7 | ||
|
|
8af3fe3db8 | ||
|
|
09c1db9afe | ||
|
|
2f738ff2b7 | ||
|
|
4429e36c3f | ||
|
|
0b8a3cfcea | ||
|
|
c3d43f6021 | ||
|
|
2a5badbc7c | ||
|
|
3c5f24f99d | ||
|
|
105212a667 | ||
|
|
f5decd2221 | ||
|
|
48bd65cd3b | ||
|
|
90046b11af | ||
|
|
6eede614f8 | ||
|
|
8cd9c5e8a3 | ||
|
|
0ef1019d1c | ||
|
|
f2a55e525b | ||
|
|
0f9eecd821 | ||
|
|
a45848cbe3 | ||
|
|
07af7d7f97 | ||
|
|
87186ca940 | ||
|
|
990bd7e291 | ||
|
|
c39aa35d36 | ||
|
|
3b187d75bb | ||
|
|
84694773a2 | ||
|
|
e8d9083f05 | ||
|
|
25ccebe617 | ||
|
|
c6d3d07b3c | ||
|
|
c6078f9314 | ||
|
|
db5462b79f | ||
|
|
3b8e6a2c3a | ||
|
|
eae7e79ecf | ||
|
|
5264873645 | ||
|
|
6009f097cd | ||
|
|
96265e5ac1 | ||
|
|
5ee33a35fc | ||
|
|
55f3c484f2 | ||
|
|
0bd678318c | ||
|
|
89ab049391 | ||
|
|
aa6e4c5b53 | ||
|
|
ec8a75be52 | ||
|
|
3ae7fe7f04 | ||
|
|
3e4b5830be | ||
|
|
8ea28dad68 | ||
|
|
17c1fc79e1 | ||
|
|
801eaaaf37 | ||
|
|
bfd72d7540 | ||
|
|
1341ef0c3e | ||
|
|
95bda0d4bb | ||
|
|
adf412ea74 | ||
|
|
a00a6734b3 | ||
|
|
e410eab336 | ||
|
|
1367357e68 | ||
|
|
4cb4b3bb19 | ||
|
|
ce92ab77ec | ||
|
|
e365fc60bc | ||
|
|
edb1343676 | ||
|
|
f1426103ff | ||
|
|
7a767327c0 | ||
|
|
e2f9139aa9 | ||
|
|
a80b504379 | ||
|
|
d73b3c44e3 | ||
|
|
c05f54c105 | ||
|
|
c917bafde5 | ||
|
|
b349106f08 | ||
|
|
1c91f345fc | ||
|
|
2b11afbb2d | ||
|
|
e9479dd24f | ||
|
|
d676412291 | ||
|
|
5fd5d032ef | ||
|
|
66ee09f656 | ||
|
|
8f57a55286 | ||
|
|
6498250114 | ||
|
|
bf7b9a7057 | ||
|
|
8de7099c0f | ||
|
|
c2b3c4919d | ||
|
|
90b5531041 | ||
|
|
e57de3268f | ||
|
|
b9e68834de | ||
|
|
5352514c2b | ||
|
|
aa58b3df35 | ||
|
|
dfe5a90b06 | ||
|
|
893486eb93 | ||
|
|
bdc684d2d1 | ||
|
|
80bc435df6 | ||
|
|
00cbfa45d7 | ||
|
|
5c18270826 | ||
|
|
aa86ef631b | ||
|
|
c67a2bd458 | ||
|
|
f796ec32a7 | ||
|
|
9ce43ea7a6 | ||
|
|
19702794e4 | ||
|
|
9015bfcac3 | ||
|
|
ad13ef9187 | ||
|
|
4a1b147a25 | ||
|
|
5970908a3f | ||
|
|
e3d37ca831 | ||
|
|
4c9afb8c80 | ||
|
|
253049ec5a | ||
|
|
c1d7f86542 | ||
|
|
aa44fba1e5 | ||
|
|
5dbf2b0ff5 | ||
|
|
8fcbaef064 | ||
|
|
08c3509823 | ||
|
|
d9961bc1a0 | ||
|
|
9fd8c1f546 | ||
|
|
d265102f31 | ||
|
|
063a50b14a | ||
|
|
ebb06b34b0 | ||
|
|
bbc3a75e4a | ||
|
|
86c5ef363e | ||
|
|
daefcab8f5 | ||
|
|
3d5f9beac5 | ||
|
|
0b01855bf4 | ||
|
|
cc78904582 | ||
|
|
306b4652eb | ||
|
|
721a629ba8 | ||
|
|
4f00aeeed7 | ||
|
|
88ca671965 | ||
|
|
7c3cfe1326 | ||
|
|
735f1007cf | ||
|
|
66df063e2a | ||
|
|
e7547905a3 | ||
|
|
206b01376d | ||
|
|
e50ef893e8 | ||
|
|
496cc470e2 | ||
|
|
b82516ba4d | ||
|
|
bbe7ff3c9b | ||
|
|
d33770f107 | ||
|
|
6523500d09 | ||
|
|
1a7a5fe6a8 | ||
|
|
9426c531e3 | ||
|
|
35a6977df8 | ||
|
|
c06280a1ea | ||
|
|
8b3ff137d1 | ||
|
|
0fa826f926 | ||
|
|
5c7b40248c | ||
|
|
7a3a8b0490 | ||
|
|
78d13749d5 | ||
|
|
1143a43d30 | ||
|
|
3cdf553142 | ||
|
|
f9569895b4 | ||
|
|
905b469515 | ||
|
|
aac3443b78 | ||
|
|
bd9b05463e | ||
|
|
bb82c6fa02 | ||
|
|
7232f9446a | ||
|
|
639f2b8e02 | ||
|
|
7d860542b1 | ||
|
|
7593dad6c8 | ||
|
|
ecc4834110 | ||
|
|
2e1adc6f9b | ||
|
|
18a0866735 | ||
|
|
4036fc44e3 | ||
|
|
9f7bbd6efc | ||
|
|
1ff11a09ed | ||
|
|
be3d4723ef | ||
|
|
190703d74b | ||
|
|
eac361d892 | ||
|
|
a772cf9910 | ||
|
|
77eab9e6f8 | ||
|
|
c4fc168369 | ||
|
|
41409fb5c1 | ||
|
|
71eb88eb7b | ||
|
|
dcbde94d78 | ||
|
|
11bbcf5b42 | ||
|
|
badf68b80a | ||
|
|
01e1c2ab0a | ||
|
|
bef51732e0 | ||
|
|
77046cf38a | ||
|
|
2a623957c3 | ||
|
|
65322925a4 | ||
|
|
346c4db3d9 | ||
|
|
7212698863 | ||
|
|
1779544d70 | ||
|
|
0270420da8 | ||
|
|
a1461851ee | ||
|
|
eaabad923e | ||
|
|
a9e12f5aca | ||
|
|
0637d5511d | ||
|
|
27682b2d07 | ||
|
|
0982852563 | ||
|
|
8147a315c2 | ||
|
|
fdf787e9b4 | ||
|
|
01d2338388 | ||
|
|
1dfec40f49 | ||
|
|
a82602faae | ||
|
|
c2aac371ae | ||
|
|
9aa6e0d39a | ||
|
|
3c177c4ebf | ||
|
|
d8b090bae2 | ||
|
|
928792bea7 | ||
|
|
99775194fd | ||
|
|
beb73828e0 | ||
|
|
87dc72b874 | ||
|
|
528ae68282 | ||
|
|
a44031cd39 | ||
|
|
af922d5171 | ||
|
|
bd6599a309 | ||
|
|
2e062b1ad6 | ||
|
|
5fc6654045 | ||
|
|
32c3494098 | ||
|
|
379461820b | ||
|
|
424cb7a9f2 | ||
|
|
b41e2b6f51 | ||
|
|
84e73f6f52 | ||
|
|
39fb42db10 | ||
|
|
85c18ce07b | ||
|
|
3fe548d8b7 | ||
|
|
eb32222d39 | ||
|
|
e70d7ab5c1 | ||
|
|
a3da735087 | ||
|
|
8c12b5a47e | ||
|
|
65c27c6c93 | ||
|
|
4c31e1a4b5 | ||
|
|
7be776d417 | ||
|
|
1a93e8f331 | ||
|
|
b6107f79dd | ||
|
|
fb0acb6a8b | ||
|
|
240a898fe5 | ||
|
|
3aad5916f5 | ||
|
|
c2a7f19d38 | ||
|
|
0a5fe2e2af | ||
|
|
f6e5256ae4 | ||
|
|
46aefe6b8a | ||
|
|
af7ef111b4 | ||
|
|
c918743187 | ||
|
|
817a691248 | ||
|
|
04e33448e0 | ||
|
|
aa4624e344 | ||
|
|
1b7a819a2b | ||
|
|
096036f2a4 | ||
|
|
b488ff053a | ||
|
|
04f24faf32 | ||
|
|
c180343853 | ||
|
|
da0739ab21 | ||
|
|
aa1d2a1879 | ||
|
|
9d9ef0069a | ||
|
|
50790b94a3 | ||
|
|
59b7f9c775 | ||
|
|
0f83f23052 | ||
|
|
1da95433ae | ||
|
|
d0579b5f75 | ||
|
|
1d19f56de8 | ||
|
|
3688e13137 | ||
|
|
45d71d0c5f | ||
|
|
d8be84b617 | ||
|
|
f7c4fe8e91 | ||
|
|
9005598f90 | ||
|
|
5e508770ef | ||
|
|
8331f92775 | ||
|
|
4c3c7e3807 | ||
|
|
1e39e3e815 | ||
|
|
5f6519d4c6 | ||
|
|
e93c35bcae | ||
|
|
a9d522d1d9 | ||
|
|
44ac42a4b5 | ||
|
|
78a60c750a | ||
|
|
6012b06a38 | ||
|
|
3ff370a903 | ||
|
|
6147d64a74 | ||
|
|
91b839dd26 | ||
|
|
cdb6d10519 | ||
|
|
586abb5466 | ||
|
|
710ac14202 | ||
|
|
a8735d1b4c | ||
|
|
f9ede752f2 | ||
|
|
5bdb205168 | ||
|
|
b606b939a6 | ||
|
|
0e0b12dcc0 | ||
|
|
b9e7b1f29b | ||
|
|
f930f2aa80 | ||
|
|
8698f7e4ab | ||
|
|
40b62c09f7 | ||
|
|
8ddc3ae172 | ||
|
|
b3e2fb4046 | ||
|
|
42558f2bd4 | ||
|
|
1e23b0a17e | ||
|
|
5b6e0d5c10 | ||
|
|
a53de75873 | ||
|
|
b3dc4671d0 | ||
|
|
ef4dd57032 | ||
|
|
ad1e17f329 | ||
|
|
2ba53e2dab | ||
|
|
909b8e7127 | ||
|
|
8280401e10 | ||
|
|
6096fc1c92 | ||
|
|
0378d54acc | ||
|
|
d33b642b8e | ||
|
|
27d68bcf45 | ||
|
|
b0122a8e38 | ||
|
|
96cbb63c84 | ||
|
|
bd16899c56 | ||
|
|
b874a2b76d | ||
|
|
aaf16273a9 | ||
|
|
f83e03e775 | ||
|
|
5328e443db | ||
|
|
ed49943039 | ||
|
|
220fdcdc75 | ||
|
|
a1b2a5149d | ||
|
|
3090a991a6 | ||
|
|
e0cae27f2f | ||
|
|
21c7c74b31 | ||
|
|
441693ac36 | ||
|
|
77910d82fc | ||
|
|
05258a5b48 | ||
|
|
387abd1606 | ||
|
|
3e4efc22a6 | ||
|
|
2851e51af8 | ||
|
|
ad8ad35c03 | ||
|
|
20d27bb9b3 | ||
|
|
30e878cbf9 | ||
|
|
374f6fcc1b | ||
|
|
4ea7d68bff | ||
|
|
88491f61e2 | ||
|
|
abe710f3ab | ||
|
|
3e4e87db23 | ||
|
|
e7ca1a13a6 | ||
|
|
aca78410f6 | ||
|
|
989331cbb4 | ||
|
|
7a8267a772 | ||
|
|
37ca383360 | ||
|
|
90eb55fbf8 | ||
|
|
e96c503b11 | ||
|
|
99ce6cf29d | ||
|
|
e14952607d | ||
|
|
de3efa0fe7 | ||
|
|
9af123523a | ||
|
|
7bc5adbde2 | ||
|
|
5287978e2f | ||
|
|
d364cc0f51 | ||
|
|
e7c30da23c | ||
|
|
1e6467cf80 | ||
|
|
12452c9b2a | ||
|
|
92b985d24d | ||
|
|
a0f640c739 | ||
|
|
46ecbc9c42 | ||
|
|
96f97e41e1 | ||
|
|
5726f05531 | ||
|
|
b389209c05 | ||
|
|
a82b3fd88a | ||
|
|
04a125d20f | ||
|
|
fc2e93a57f | ||
|
|
cfe9c17b58 | ||
|
|
b24f31f98c | ||
|
|
5263edc3e2 | ||
|
|
99b80cccad | ||
|
|
9794c5a188 | ||
|
|
2bd19736f4 | ||
|
|
13376d0ced | ||
|
|
1aa8475295 | ||
|
|
4bffe22f68 | ||
|
|
094941ab8c | ||
|
|
1a1bcc8d5c | ||
|
|
e77eb8a563 | ||
|
|
c063ccfa02 | ||
|
|
b1ccb5c3de | ||
|
|
29b4704c4b | ||
|
|
931d7d55dd | ||
|
|
2df0403c94 | ||
|
|
9cd878bad1 | ||
|
|
4c816eabc1 | ||
|
|
d28cbf3863 | ||
|
|
6269719b70 | ||
|
|
8a347e71ed | ||
|
|
9c44662f2e | ||
|
|
884291a8a3 | ||
|
|
97428909a4 | ||
|
|
a0bb52ff5f | ||
|
|
1a766c3ccc | ||
|
|
19bea70dc0 | ||
|
|
322a7020e3 | ||
|
|
242444ecc9 | ||
|
|
4820ebba84 | ||
|
|
7658481db2 | ||
|
|
f41644bc97 | ||
|
|
2163eb5910 | ||
|
|
ffbaf1e54f | ||
|
|
76ac47bbc6 | ||
|
|
d499f1af65 | ||
|
|
a249eeab22 | ||
|
|
712e7dbeb8 | ||
|
|
22571acd0c | ||
|
|
eba8d0bd80 | ||
|
|
4b52dea65a | ||
|
|
a5eeaae0d0 | ||
|
|
0d85eb6b90 | ||
|
|
3abb226b5c | ||
|
|
d38fdfb291 | ||
|
|
a63361302d | ||
|
|
069dbce000 | ||
|
|
5970c575c8 | ||
|
|
6ba659ff55 | ||
|
|
2c9f0e3c7d | ||
|
|
06359b9d06 | ||
|
|
7f31fd5092 | ||
|
|
01cdd82cc5 | ||
|
|
c6094cf3ee | ||
|
|
2ba7da0486 | ||
|
|
dcc0295b4b | ||
|
|
8632b0ffec | ||
|
|
bc0b69857a | ||
|
|
198d48fd84 | ||
|
|
e85d6bebd0 | ||
|
|
0679d9a30f | ||
|
|
b0584d4499 | ||
|
|
51bfc37a27 | ||
|
|
c979b27653 | ||
|
|
184f0fad99 | ||
|
|
f9b88c2738 | ||
|
|
f47a679dda | ||
|
|
e5e56b5260 | ||
|
|
c62f6b41d2 | ||
|
|
6b32c4997d | ||
|
|
cb89c96dac | ||
|
|
b31ca263fd | ||
|
|
a0451cfab0 | ||
|
|
4baecd9f79 | ||
|
|
381851aba7 | ||
|
|
23d3d5973b | ||
|
|
fcdf9cf2ec | ||
|
|
9831236d79 | ||
|
|
c644d6bf2f | ||
|
|
d82b008c33 | ||
|
|
d53f9e99af | ||
|
|
870b168cea | ||
|
|
094e2478f9 | ||
|
|
110b9f5058 | ||
|
|
d959753641 | ||
|
|
027ecf29ae | ||
|
|
d4392635bf | ||
|
|
c72370eff0 | ||
|
|
6b799be7dd | ||
|
|
244d5bba47 | ||
|
|
d539ddf018 | ||
|
|
f279792273 | ||
|
|
aa4a793566 | ||
|
|
1aaa2340f8 | ||
|
|
329247391b | ||
|
|
3fc5daef01 | ||
|
|
e087d04c17 | ||
|
|
a3a713b608 | ||
|
|
98251a8631 | ||
|
|
b147a16487 | ||
|
|
411b86197a | ||
|
|
8acab6b662 | ||
|
|
dba550550c | ||
|
|
74cfc733dd | ||
|
|
23981accd1 | ||
|
|
209f34124e | ||
|
|
14cff4facf | ||
|
|
dac089b0fb | ||
|
|
52aeda7faf | ||
|
|
5273f5c9b6 | ||
|
|
d2a30ea049 | ||
|
|
716ba3eea8 | ||
|
|
5cece53e50 | ||
|
|
ba73f8a323 | ||
|
|
81379a49a7 | ||
|
|
77e4862b3e | ||
|
|
1a3bd2aa74 | ||
|
|
9997b4a2de | ||
|
|
74b12606ef | ||
|
|
14d5034b8e | ||
|
|
8b282818e9 | ||
|
|
48a4f8ba67 | ||
|
|
1dd048eed4 | ||
|
|
e398d6af75 | ||
|
|
39b1c1f7e4 | ||
|
|
ff78b384ac | ||
|
|
d45a6f94f5 | ||
|
|
48fe8d5762 | ||
|
|
36b493f03a | ||
|
|
670ccaf891 | ||
|
|
d16a48a1a2 | ||
|
|
3f4eedbeab | ||
|
|
ef3dd4a833 | ||
|
|
fee074db42 | ||
|
|
5463af7f60 | ||
|
|
b776cd4a91 | ||
|
|
45945e839f | ||
|
|
043ad7e3aa | ||
|
|
d04e186dea | ||
|
|
4253365e03 | ||
|
|
07437b7880 | ||
|
|
5ddd007f67 | ||
|
|
814978a178 | ||
|
|
5350e77125 | ||
|
|
22e2e32377 | ||
|
|
58b2c36498 | ||
|
|
0e4af7dbcf | ||
|
|
3aba1c5ccf | ||
|
|
44af80f336 | ||
|
|
4591f5b372 | ||
|
|
2270b2a224 | ||
|
|
4eb68aae77 | ||
|
|
c92e3cca6b | ||
|
|
13ba0121d7 | ||
|
|
be4943a9d6 | ||
|
|
9484e3b52d | ||
|
|
ef731703e8 | ||
|
|
11b86806de | ||
|
|
b872f59a01 | ||
|
|
b2fc1cc1f2 | ||
|
|
6907c9c550 | ||
|
|
93701585f8 | ||
|
|
50e596e63a | ||
|
|
51ece30c34 | ||
|
|
110a078074 | ||
|
|
85b222d9e2 | ||
|
|
c974d4f30a | ||
|
|
75916a21a6 | ||
|
|
8e2fc40105 | ||
|
|
f81bd857ed | ||
|
|
e7ac05db75 | ||
|
|
eb3f526a96 | ||
|
|
371b0bdce1 | ||
|
|
06e70e7476 | ||
|
|
a748fe6610 | ||
|
|
f43dd4d3dc | ||
|
|
9c01b18922 | ||
|
|
932fd032e8 | ||
|
|
c392deaf38 | ||
|
|
c575c1bea3 | ||
|
|
38ec5efa32 | ||
|
|
70764e73c3 | ||
|
|
03fd7fba79 | ||
|
|
33d56a287b | ||
|
|
c3523f93aa | ||
|
|
26f08c7a30 | ||
|
|
a3266cda45 | ||
|
|
80052fabe8 | ||
|
|
3819a845f0 | ||
|
|
d66c0f2d61 | ||
|
|
0bf9da71ab | ||
|
|
a07ff51c68 | ||
|
|
48faabee49 | ||
|
|
d2f9eeab12 | ||
|
|
c8ba4f0379 | ||
|
|
fba0fe1866 | ||
|
|
9685b92bc9 | ||
|
|
3ae9779eec | ||
|
|
ebbdc46305 | ||
|
|
4fb1c866da | ||
|
|
431d5ef8a8 | ||
|
|
174d1f7838 | ||
|
|
b57ecea305 | ||
|
|
64f39dcb79 | ||
|
|
86149145f6 | ||
|
|
f3c41e9a66 | ||
|
|
2a6723f8db | ||
|
|
2bb8d8b52a | ||
|
|
ac86cf19a0 | ||
|
|
ae29fef6d4 | ||
|
|
54486cb5be | ||
|
|
22e41df8ea | ||
|
|
d6dfc2ffb6 | ||
|
|
cb9d0ce3ed | ||
|
|
cef5117e29 | ||
|
|
466af94499 | ||
|
|
ab9dea6930 | ||
|
|
cc2c585a55 | ||
|
|
f9b1a59368 | ||
|
|
23cbe917c2 | ||
|
|
971f9694da | ||
|
|
32f057fa26 | ||
|
|
8c7edaaca4 | ||
|
|
205323200a | ||
|
|
9b880eb669 | ||
|
|
3ce53cedc8 | ||
|
|
37e44968dc | ||
|
|
4fb58bc005 | ||
|
|
b24832dd84 | ||
|
|
57aa32f0c1 | ||
|
|
60c20cc628 | ||
|
|
eaae999878 | ||
|
|
f0af71d8c0 | ||
|
|
b65fed7a0d | ||
|
|
364205cd9b | ||
|
|
081ea769a1 | ||
|
|
7fef69635f | ||
|
|
e3c3315873 | ||
|
|
bb0218d3c6 | ||
|
|
4e3e721c5a | ||
|
|
79321a0ffb | ||
|
|
230644141a | ||
|
|
480fa113b7 | ||
|
|
9016d937a3 | ||
|
|
a361576f8a | ||
|
|
bee695afd4 | ||
|
|
f3fcdf808f | ||
|
|
8e469493ac | ||
|
|
872df9ee70 | ||
|
|
69806f1260 | ||
|
|
0a24feab13 | ||
|
|
e40766583e | ||
|
|
fd3fb5041b | ||
|
|
8ffcaf328e | ||
|
|
5381e54469 | ||
|
|
f571685648 | ||
|
|
701d363e3c | ||
|
|
afa697031c | ||
|
|
7239c98a4c | ||
|
|
48c0297fe7 | ||
|
|
a02be14f1a | ||
|
|
a0b8c758b4 | ||
|
|
97a8029f74 | ||
|
|
4d6781b26b | ||
|
|
b2d24725de | ||
|
|
b04ac67860 | ||
|
|
61784fd48d | ||
|
|
f29738a96b | ||
|
|
cb313ed513 | ||
|
|
e23656a2cd | ||
|
|
8a39ff469f | ||
|
|
89c369189a | ||
|
|
1108c92beb | ||
|
|
e43cfce016 | ||
|
|
5931b53896 | ||
|
|
020b31cb6f | ||
|
|
8fc41c4c45 | ||
|
|
d9b3c2c15c | ||
|
|
10a9825f82 | ||
|
|
2bbe168200 | ||
|
|
d6e8a96b19 | ||
|
|
c34fae302e | ||
|
|
7957135979 | ||
|
|
c9330d0b7f | ||
|
|
9df5fc12cd | ||
|
|
dd64685049 | ||
|
|
eb54b90001 | ||
|
|
b1ba5790cc | ||
|
|
56d79f36dc | ||
|
|
bbcf7b638f | ||
|
|
94cc314738 | ||
|
|
39eb6482e3 | ||
|
|
85e66ebd8a | ||
|
|
322b7084ea | ||
|
|
8ac2c0462a | ||
|
|
24e2e29894 | ||
|
|
e00aeb39e5 | ||
|
|
6ec2849bd5 | ||
|
|
698c9d2923 | ||
|
|
82f379ecbe | ||
|
|
a502ceea53 | ||
|
|
53e4b39066 | ||
|
|
fceea79323 | ||
|
|
6adf49de45 | ||
|
|
0b85346ac4 | ||
|
|
53f0544da8 | ||
|
|
839ced1b32 | ||
|
|
b6fcbba57d | ||
|
|
b7f220d02b | ||
|
|
398ae8946c | ||
|
|
65357e1441 | ||
|
|
0a7f579bd8 | ||
|
|
2e0d2bb5e5 | ||
|
|
0612d447dc | ||
|
|
fd7e19d339 | ||
|
|
7f8dec161d | ||
|
|
26f50d109a | ||
|
|
acfdf23c22 | ||
|
|
a15588120b | ||
|
|
da28816967 | ||
|
|
53bf9922ca | ||
|
|
363ff9610d | ||
|
|
772c9bc77d | ||
|
|
c1f10c32c0 | ||
|
|
0a8c36e086 | ||
|
|
ae35e4b589 | ||
|
|
92a760541b | ||
|
|
3c67546f4a | ||
|
|
695670cc96 | ||
|
|
0f42aa4ec7 | ||
|
|
8aca69f845 | ||
|
|
63be38a3c3 | ||
|
|
e7f6e5c740 | ||
|
|
2ae8d49ec8 | ||
|
|
f9e987e933 | ||
|
|
8e9c76a9e8 | ||
|
|
ad52b60798 | ||
|
|
012cc28f8a | ||
|
|
1668717cf8 |
21682
.gitattributes
vendored
21682
.gitattributes
vendored
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.1</version>
|
||||
<version>1.6.7</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
|
||||
32
forge-ai/pom.xml.releaseBackup
Normal file
32
forge-ai/pom.xml.releaseBackup
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
<name>Forge AI</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>forge</groupId>
|
||||
<artifactId>forge-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>forge</groupId>
|
||||
<artifactId>forge-game</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>3.6.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
package forge;
|
||||
package forge.ai;
|
||||
|
||||
public enum AIOption {
|
||||
USE_SIMULATION;
|
||||
@@ -35,11 +35,13 @@ import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.combat.GlobalAttackRestrictions;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Expressions;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
@@ -243,6 +245,19 @@ public class AiAttackController {
|
||||
return false;
|
||||
}
|
||||
|
||||
public final static Card getCardCanBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) {
|
||||
final List<Card> attackerList = new ArrayList<Card>(attackers);
|
||||
if (!c.isCreature()) {
|
||||
return null;
|
||||
}
|
||||
for (final Card attacker : attackerList) {
|
||||
if (CombatUtil.canBlock(attacker, c, nextTurn)) {
|
||||
return attacker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// this checks to make sure that the computer player doesn't lose when the human player attacks
|
||||
// this method is used by getAttackers()
|
||||
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
|
||||
@@ -362,8 +377,12 @@ public class AiAttackController {
|
||||
blockersLeft--;
|
||||
continue;
|
||||
}
|
||||
totalAttack += ComputerUtilCombat.damageIfUnblocked(attacker, ai, null, false);
|
||||
totalPoison += ComputerUtilCombat.poisonIfUnblocked(attacker, ai);
|
||||
|
||||
// Test for some special triggers that can change the creature in combat
|
||||
Card effectiveAttacker = ComputerUtilCombat.applyPotentialAttackCloneTriggers(attacker);
|
||||
|
||||
totalAttack += ComputerUtilCombat.damageIfUnblocked(effectiveAttacker, ai, null, false);
|
||||
totalPoison += ComputerUtilCombat.poisonIfUnblocked(effectiveAttacker, ai);
|
||||
}
|
||||
|
||||
if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) {
|
||||
@@ -424,16 +443,45 @@ public class AiAttackController {
|
||||
|
||||
final Player opp = this.defendingOpponent;
|
||||
|
||||
for (Card attacker : attackers) {
|
||||
if (!CombatUtil.canBeBlocked(attacker, this.blockers, null)
|
||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||
unblockedAttackers.add(attacker);
|
||||
// if true, the AI will attempt to identify which blockers will already be taken,
|
||||
// thus attempting to predict how many creatures with evasion can actively block
|
||||
boolean predictEvasion = false;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
if (aic.getBooleanProperty(AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION)) {
|
||||
predictEvasion = true;
|
||||
}
|
||||
}
|
||||
|
||||
CardCollection accountedBlockers = new CardCollection(this.blockers);
|
||||
CardCollection categorizedAttackers = new CardCollection();
|
||||
|
||||
if (predictEvasion) {
|
||||
// split categorizedAttackers such that the ones with evasion come first and
|
||||
// can be properly accounted for. Note that at this point the attackers need
|
||||
// to be sorted by power already (see the Collections.sort call above).
|
||||
categorizedAttackers.addAll(ComputerUtilCombat.categorizeAttackersByEvasion(this.attackers));
|
||||
} else {
|
||||
categorizedAttackers.addAll(this.attackers);
|
||||
}
|
||||
|
||||
for (Card attacker : categorizedAttackers) {
|
||||
if (!CombatUtil.canBeBlocked(attacker, accountedBlockers, null)
|
||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||
unblockedAttackers.add(attacker);
|
||||
} else {
|
||||
if (predictEvasion) {
|
||||
List<Card> potentialBestBlockers = CombatUtil.getPotentialBestBlockers(attacker, accountedBlockers, null);
|
||||
accountedBlockers.removeAll(potentialBestBlockers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remainingAttackers.removeAll(unblockedAttackers);
|
||||
|
||||
for (Card blocker : this.blockers) {
|
||||
if (blocker.hasKeyword("CARDNAME can block any number of creatures.")
|
||||
|| blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) {
|
||||
|| blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
|
||||
for (Card attacker : this.attackers) {
|
||||
if (CombatUtil.canBlock(attacker, blocker)) {
|
||||
remainingAttackers.remove(attacker);
|
||||
@@ -449,7 +497,7 @@ public class AiAttackController {
|
||||
if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) {
|
||||
break;
|
||||
}
|
||||
if (blocker.hasKeyword("CARDNAME can block an additional creature.")) {
|
||||
if (blocker.hasKeyword("CARDNAME can block an additional creature each combat.")) {
|
||||
blockedAttackers.add(remainingAttackers.get(0));
|
||||
remainingAttackers.remove(0);
|
||||
maxBlockersAfterCrew--;
|
||||
@@ -498,11 +546,16 @@ public class AiAttackController {
|
||||
}
|
||||
Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0));
|
||||
|
||||
final GameEntity entity = ai.getMustAttackEntity();
|
||||
// Attempt to see if there's a defined entity that must be attacked strictly this turn...
|
||||
GameEntity entity = ai.getMustAttackEntityThisTurn();
|
||||
if (entity == null) {
|
||||
// ...or during the attacking creature controller's turn
|
||||
entity = ai.getMustAttackEntity();
|
||||
}
|
||||
if (null != entity) {
|
||||
int n = defs.indexOf(entity);
|
||||
if (-1 == n) {
|
||||
System.out.println("getMustAttackEntity() returned something not in defenders.");
|
||||
System.out.println("getMustAttackEntity() or getMustAttackEntityThisTurn() returned something not in defenders.");
|
||||
return prefDefender;
|
||||
} else {
|
||||
return entity;
|
||||
@@ -544,10 +597,21 @@ public class AiAttackController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Aggro options
|
||||
boolean playAggro = false;
|
||||
int chanceToAttackToTrade = 0;
|
||||
boolean tradeIfTappedOut = false;
|
||||
int extraChanceIfOppHasMana = 0;
|
||||
boolean tradeIfLowerLifePressure = false;
|
||||
if (ai.getController().isAI()) {
|
||||
playAggro = ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true");
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
playAggro = aic.getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
chanceToAttackToTrade = aic.getIntProperty(AiProps.CHANCE_TO_ATTACK_INTO_TRADE);
|
||||
tradeIfTappedOut = aic.getBooleanProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
|
||||
extraChanceIfOppHasMana = aic.getIntProperty(AiProps.CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA);
|
||||
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
|
||||
}
|
||||
|
||||
final boolean bAssault = this.doAssault(ai);
|
||||
// TODO: detect Lightmine Field by presence of a card with a specific trigger
|
||||
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
|
||||
@@ -558,7 +622,11 @@ public class AiAttackController {
|
||||
|
||||
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
||||
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
|
||||
final int attackMax = restrict.getMax();
|
||||
int attackMax = restrict.getMax();
|
||||
if (attackMax == -1) {
|
||||
// check with the local limitations vs. the chosen defender
|
||||
attackMax = ComputerUtilCombat.getMaxAttackersFor(defender);
|
||||
}
|
||||
|
||||
if (attackMax == 0) {
|
||||
// can't attack anymore
|
||||
@@ -581,7 +649,8 @@ public class AiAttackController {
|
||||
&& isEffectiveAttacker(ai, attacker, combat)) {
|
||||
mustAttack = true;
|
||||
} else {
|
||||
for (String s : attacker.getKeywords()) {
|
||||
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||
String s = inst.getOriginal();
|
||||
if (s.equals("CARDNAME attacks each turn if able.")
|
||||
|| s.startsWith("CARDNAME attacks specific player each combat if able")
|
||||
|| s.equals("CARDNAME attacks each combat if able.")) {
|
||||
@@ -590,7 +659,7 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mustAttack || attacker.getController().getMustAttackEntity() != null) {
|
||||
if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) {
|
||||
combat.addAttacker(attacker, defender);
|
||||
attackersLeft.remove(attacker);
|
||||
numForcedAttackers++;
|
||||
@@ -711,7 +780,20 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
|
||||
for (final Card pCard : this.oppList) {
|
||||
boolean predictEvasion = (ai.getController().isAI()
|
||||
&& ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION));
|
||||
|
||||
CardCollection categorizedOppList = new CardCollection();
|
||||
if (predictEvasion) {
|
||||
// If predicting evasion, make sure that attackers with evasion are considered first
|
||||
// (to avoid situations where the AI would predict his non-flyers to be blocked with
|
||||
// flying creatures and then believe that flyers will necessarily be left unblocked)
|
||||
categorizedOppList.addAll(ComputerUtilCombat.categorizeAttackersByEvasion(this.oppList));
|
||||
} else {
|
||||
categorizedOppList.addAll(this.oppList);
|
||||
}
|
||||
|
||||
for (final Card pCard : categorizedOppList) {
|
||||
// if the creature can attack next turn add it to counter attackers list
|
||||
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
|
||||
nextTurnAttackers.add(pCard);
|
||||
@@ -719,8 +801,13 @@ public class AiAttackController {
|
||||
humanForces += 1; // player forces they might use to attack
|
||||
}
|
||||
// increment player forces that are relevant to an attritional attack - includes walls
|
||||
if (canBlockAnAttacker(pCard, candidateAttackers, true)) {
|
||||
|
||||
Card potentialOppBlocker = getCardCanBlockAnAttacker(pCard, candidateAttackers, true);
|
||||
if (potentialOppBlocker != null) {
|
||||
humanForcesForAttritionalAttack += 1;
|
||||
if (predictEvasion) {
|
||||
candidateAttackers.remove(potentialOppBlocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,8 +934,18 @@ public class AiAttackController {
|
||||
if (ratioDiff > 0 && doAttritionalAttack) {
|
||||
this.aiAggression = 5; // attack at all costs
|
||||
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|
||||
|| (playAggro && humanLifeToDamageRatio > 1)) {
|
||||
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
|
||||
this.aiAggression = 4; // attack expecting to trade or damage player.
|
||||
} else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
|
||||
&& defendingOpponent != null
|
||||
&& ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent)
|
||||
&& ai.getLife() > defendingOpponent.getLife()
|
||||
&& !ComputerUtilCombat.lifeInDanger(ai, combat)
|
||||
&& (ComputerUtilMana.getAvailableManaEstimate(ai) > 0) || tradeIfTappedOut
|
||||
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
|
||||
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
|
||||
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
|
||||
this.aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
|
||||
} else if (ratioDiff >= 0 && this.attackers.size() > 1) {
|
||||
this.aiAggression = 3; // attack expecting to make good trades or damage player.
|
||||
} else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
|
||||
@@ -986,6 +1083,28 @@ public class AiAttackController {
|
||||
boolean canBeBlocked = false;
|
||||
int numberOfPossibleBlockers = 0;
|
||||
|
||||
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
|
||||
if ((attacker.hasSVar("NonCombatPriority"))
|
||||
&& (!attacker.hasKeyword("Vigilance"))) {
|
||||
// For each level of priority, enemy has to have life as much as the creature's power
|
||||
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
|
||||
// the lower the priroity, the less willing the AI is to use the creature for attacking.
|
||||
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
|
||||
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
|
||||
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
|
||||
boolean wantability = false;
|
||||
for (SpellAbility sa : attacker.getSpellAbilities()) {
|
||||
// Do not attack if we can afford using the ability.
|
||||
if (sa.isAbility()) {
|
||||
if (ComputerUtilCost.canPayCost(sa, ai)) {
|
||||
return false;
|
||||
}
|
||||
// TODO Eventually The Ai will need to learn to predict if they have any use for the ability before next untap or not.
|
||||
// TODO abilities that tap enemy creatures should probably only be saved if the enemy has nonzero creatures? Haste can be a threat though...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isEffectiveAttacker(ai, attacker, combat)) {
|
||||
return false;
|
||||
@@ -995,8 +1114,10 @@ public class AiAttackController {
|
||||
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|
||||
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
||||
if (!hasCombatEffect) {
|
||||
for (String keyword : attacker.getKeywords()) {
|
||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
||||
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||
String keyword = inst.getOriginal();
|
||||
if (keyword.equals("Wither") || keyword.equals("Infect")
|
||||
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
|
||||
hasCombatEffect = true;
|
||||
break;
|
||||
}
|
||||
@@ -1029,7 +1150,8 @@ public class AiAttackController {
|
||||
if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) {
|
||||
canKillAllDangerous = false;
|
||||
} else {
|
||||
for (String keyword : defender.getKeywords()) {
|
||||
for (KeywordInterface inst : defender.getKeywords()) {
|
||||
String keyword = inst.getOriginal();
|
||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
||||
canKillAllDangerous = false;
|
||||
break;
|
||||
@@ -1037,12 +1159,28 @@ public class AiAttackController {
|
||||
// and combat will have negative effects
|
||||
}
|
||||
}
|
||||
|
||||
if (canKillAllDangerous
|
||||
&& !hasAttackEffect && !hasCombatEffect
|
||||
&& (this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0)) {
|
||||
if (ai.getController().isAI()) {
|
||||
if (((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK)) {
|
||||
// We can't kill a blocker, there is no reason to attack unless we can cripple a
|
||||
// blocker or gain life from attacking or we have some kind of another attack/combat effect,
|
||||
// or if we can deal damage to the opponent via the sheer number of potential attackers
|
||||
// (note that the AI will sometimes still miscount here, and thus attack into a block,
|
||||
// because there is no way to check which attackers are actually guaranteed to attack at this point)
|
||||
canKillAllDangerous = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!attacker.hasKeyword("vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||
if (!attacker.hasKeyword("Vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||
canKillAllDangerous = false;
|
||||
canBeKilled = true;
|
||||
canBeKilledByOne = true;
|
||||
@@ -1147,18 +1285,51 @@ public class AiAttackController {
|
||||
}
|
||||
if (sa.usesTargeting()) {
|
||||
sa.setActivatingPlayer(c.getController());
|
||||
if (CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).isEmpty()) {
|
||||
List<Card> validTargets = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
|
||||
if (validTargets.isEmpty()) {
|
||||
missTarget = true;
|
||||
break;
|
||||
} else if (sa.isCurse() && CardLists.filter(validTargets,
|
||||
CardPredicates.isControlledByAnyOf(c.getController().getOpponents())).isEmpty()) {
|
||||
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
|
||||
missTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (missTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (random.nextBoolean()) {
|
||||
// A specific AI condition for Exert: if specified on the card, the AI will always
|
||||
// exert creatures that meet this condition
|
||||
if (c.hasSVar("AIExertCondition")) {
|
||||
if (!c.getSVar("AIExertCondition").isEmpty()) {
|
||||
final String needsToExert = c.getSVar("AIExertCondition");
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
String sVar = needsToExert.split(" ")[0];
|
||||
String comparator = needsToExert.split(" ")[1];
|
||||
String compareTo = comparator.substring(2);
|
||||
try {
|
||||
x = Integer.parseInt(sVar);
|
||||
} catch (final NumberFormatException e) {
|
||||
x = CardFactoryUtil.xCount(c, c.getSVar(sVar));
|
||||
}
|
||||
try {
|
||||
y = Integer.parseInt(compareTo);
|
||||
} catch (final NumberFormatException e) {
|
||||
y = CardFactoryUtil.xCount(c, c.getSVar(compareTo));
|
||||
}
|
||||
if (Expressions.compare(x, comparator, y)) {
|
||||
shouldExert = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldExert && random.nextBoolean()) {
|
||||
// TODO Improve when the AI wants to use Exert powers
|
||||
shouldExert = true;
|
||||
}
|
||||
|
||||
@@ -19,26 +19,20 @@ package forge.ai;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.CardTraitBase;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -89,8 +83,10 @@ public class AiBlockController {
|
||||
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
||||
final List<Card> blockers = new ArrayList<>();
|
||||
|
||||
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
|
||||
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
|
||||
for (final Card b : blockersLeft) {
|
||||
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false)) {
|
||||
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, true)) {
|
||||
blockers.add(b);
|
||||
}
|
||||
}
|
||||
@@ -102,8 +98,10 @@ public class AiBlockController {
|
||||
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
||||
final List<Card> blockers = new ArrayList<>();
|
||||
|
||||
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
|
||||
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
|
||||
for (final Card b : blockersLeft) {
|
||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false)) {
|
||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, true)) {
|
||||
blockers.add(b);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +149,7 @@ public class AiBlockController {
|
||||
for (final Card c : attackers) {
|
||||
sortedAttackers.add(c);
|
||||
}
|
||||
} else {
|
||||
} else if (defender instanceof Player && defender.equals(ai)){
|
||||
firstAttacker = combat.getAttackersOf(defender);
|
||||
}
|
||||
}
|
||||
@@ -417,7 +415,8 @@ public class AiBlockController {
|
||||
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
|
||||
return false;
|
||||
}
|
||||
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
|
||||
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
|
||||
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker) || randomTrade;
|
||||
}
|
||||
});
|
||||
if (usableBlockers.size() < 2) {
|
||||
@@ -526,7 +525,56 @@ public class AiBlockController {
|
||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||
}
|
||||
|
||||
private void makeGangNonLethalBlocks(final Combat combat) {
|
||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||
List<Card> blockers;
|
||||
|
||||
// Try to block a Menace attacker with two blockers, neither of which will die
|
||||
for (final Card attacker : attackersLeft) {
|
||||
if (!attacker.hasKeyword("Menace") && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
|
||||
List<Card> usableBlockers;
|
||||
final List<Card> blockGang = new ArrayList<>();
|
||||
int absorbedDamage; // The amount of damage needed to kill the first blocker
|
||||
|
||||
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.getNetToughness() > attacker.getNetCombatDamage();
|
||||
}
|
||||
});
|
||||
if (usableBlockers.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Card leader = ComputerUtilCard.getWorstCreatureAI(usableBlockers);
|
||||
blockGang.add(leader);
|
||||
usableBlockers.remove(leader);
|
||||
absorbedDamage = ComputerUtilCombat.getEnoughDamageToKill(leader, attacker.getNetCombatDamage(), attacker, true);
|
||||
|
||||
// consider a double block
|
||||
for (final Card blocker : usableBlockers) {
|
||||
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(blocker, attacker.getNetCombatDamage(), attacker, true);
|
||||
// only do it if neither blocking creature will die
|
||||
if (absorbedDamage > attacker.getNetCombatDamage() && absorbedDamage2 > attacker.getNetCombatDamage()) {
|
||||
currentAttackers.remove(attacker);
|
||||
combat.addBlocker(attacker, blocker);
|
||||
if (CombatUtil.canBlock(attacker, leader, combat)) {
|
||||
combat.addBlocker(attacker, leader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||
}
|
||||
|
||||
// Bad Trade Blocks (should only be made if life is in danger)
|
||||
// Random Trade Blocks (performed randomly if enabled in profile and only when in favorable conditions)
|
||||
/**
|
||||
* <p>
|
||||
* makeTradeBlocks.
|
||||
@@ -546,18 +594,31 @@ public class AiBlockController {
|
||||
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
|
||||
if (!killingBlockers.isEmpty() && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
|
||||
|
||||
if (!killingBlockers.isEmpty()) {
|
||||
final Card blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers);
|
||||
boolean doTrade = false;
|
||||
|
||||
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
// Always trade when life in danger
|
||||
doTrade = true;
|
||||
} else {
|
||||
// Randomly trade creatures with lower power and [hopefully] worse abilities, if enabled in profile
|
||||
doTrade = wouldLikeToRandomlyTrade(attacker, blocker, combat);
|
||||
}
|
||||
|
||||
if (doTrade) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
currentAttackers.remove(attacker);
|
||||
}
|
||||
}
|
||||
}
|
||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||
}
|
||||
|
||||
@@ -760,6 +821,105 @@ public class AiBlockController {
|
||||
}
|
||||
}
|
||||
|
||||
private void makeChumpBlocksToSavePW(Combat combat) {
|
||||
if (ComputerUtilCombat.lifeInDanger(ai, combat) || ai.getLife() <= ai.getStartingLife() / 5) {
|
||||
// most likely not worth trying to protect planeswalkers when at threateningly low life or in
|
||||
// dangerous combat which threatens lethal or severe damage to face
|
||||
return;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
|
||||
|
||||
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
|
||||
// detect how much damage is threatened to each of the planeswalkers, see which ones would be
|
||||
// worth protecting according to the AI profile properties
|
||||
CardCollection threatenedPWs = new CardCollection();
|
||||
for (final Card attacker : attackers) {
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card) {
|
||||
if (!onlyIfLethal) {
|
||||
threatenedPWs.add((Card) def);
|
||||
} else {
|
||||
int damageToPW = 0;
|
||||
for (final Card pwatkr : combat.getAttackersOf(def)) {
|
||||
if (!combat.isBlocked(pwatkr)) {
|
||||
damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true);
|
||||
}
|
||||
}
|
||||
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= ((Card) def).getCounters(CounterType.LOYALTY)) {
|
||||
threatenedPWs.add((Card) def);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CardCollection pwsWithChumpBlocks = new CardCollection();
|
||||
CardCollection chosenChumpBlockers = new CardCollection();
|
||||
CardCollection chumpPWDefenders = CardLists.filter(new CardCollection(this.blockersLeft), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken
|
||||
: evalThresholdNonToken);
|
||||
}
|
||||
});
|
||||
CardLists.sortByPowerAsc(chumpPWDefenders);
|
||||
if (!chumpPWDefenders.isEmpty()) {
|
||||
for (final Card attacker : attackers) {
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card && threatenedPWs.contains((Card) def)) {
|
||||
if (attacker.hasKeyword("Trample")) {
|
||||
// don't bother trying to chump a trampling creature
|
||||
continue;
|
||||
}
|
||||
if (!combat.getBlockers(attacker).isEmpty()) {
|
||||
// already blocked by something, no need to chump
|
||||
continue;
|
||||
}
|
||||
Card blockerDecided = null;
|
||||
for (final Card blocker : chumpPWDefenders) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
pwsWithChumpBlocks.add((Card) combat.getDefenderByAttacker(attacker));
|
||||
chosenChumpBlockers.add(blocker);
|
||||
blockerDecided = blocker;
|
||||
blockersLeft.remove(blocker);
|
||||
break;
|
||||
}
|
||||
}
|
||||
chumpPWDefenders.remove(blockerDecided);
|
||||
}
|
||||
}
|
||||
// check to see if we managed to cover all the blockers of the planeswalker; if not, bail
|
||||
for (final Card pw : pwsWithChumpBlocks) {
|
||||
CardCollection pwAttackers = combat.getAttackersOf(pw);
|
||||
CardCollection pwDefenders = new CardCollection();
|
||||
boolean isFullyBlocked = true;
|
||||
if (!pwAttackers.isEmpty()) {
|
||||
int damageToPW = 0;
|
||||
for (Card pwAtk : pwAttackers) {
|
||||
if (!combat.getBlockers(pwAtk).isEmpty()) {
|
||||
pwDefenders.addAll(combat.getBlockers(pwAtk));
|
||||
} else {
|
||||
isFullyBlocked = false;
|
||||
damageToPW += ComputerUtilCombat.predictDamageTo((Card) pw, pwAtk.getNetCombatDamage(), pwAtk, true);
|
||||
}
|
||||
}
|
||||
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) {
|
||||
for (Card chump : pwDefenders) {
|
||||
if (chosenChumpBlockers.contains(chump)) {
|
||||
combat.removeFromCombat(chump);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
|
||||
|
||||
final List<Card> oldBlockers = combat.getAllBlockers();
|
||||
@@ -824,7 +984,7 @@ public class AiBlockController {
|
||||
List<Card> chumpBlockers;
|
||||
|
||||
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
|
||||
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true")) {
|
||||
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
|
||||
diff = 0;
|
||||
}
|
||||
|
||||
@@ -856,9 +1016,8 @@ public class AiBlockController {
|
||||
// When the AI holds some Fog effect, don't bother about lifeInDanger
|
||||
if (!ComputerUtil.hasAFogEffect(ai)) {
|
||||
lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
|
||||
if (lifeInDanger) {
|
||||
makeTradeBlocks(combat); // choose necessary trade blocks
|
||||
}
|
||||
|
||||
// if life is still in danger
|
||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
makeChumpBlocks(combat); // choose necessary chump blocks
|
||||
@@ -927,6 +1086,7 @@ public class AiBlockController {
|
||||
|
||||
// assign blockers that have to block
|
||||
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able.");
|
||||
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
|
||||
// if an attacker with lure attacks - all that can block
|
||||
for (final Card blocker : blockersLeft) {
|
||||
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
|
||||
@@ -940,7 +1100,8 @@ public class AiBlockController {
|
||||
for (final Card blocker : blockers) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
|
||||
&& (CombatUtil.mustBlockAnAttacker(blocker, combat)
|
||||
|| blocker.hasKeyword("CARDNAME blocks each turn if able."))) {
|
||||
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|
||||
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
if (blocker.getMustBlockCards() != null) {
|
||||
int mustBlockAmt = blocker.getMustBlockCards().size();
|
||||
@@ -956,6 +1117,18 @@ public class AiBlockController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
|
||||
// unless life is low enough to be more worried about saving preserving the life total
|
||||
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
makeChumpBlocksToSavePW(combat);
|
||||
}
|
||||
|
||||
// if there are still blockers left, see if it's possible to block Menace creatures with
|
||||
// non-lethal blockers that won't kill the attacker but won't die to it as well
|
||||
makeGangNonLethalBlocks(combat);
|
||||
|
||||
//Check for validity of blocks in case something slipped through
|
||||
for (Card attacker : attackers) {
|
||||
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
|
||||
@@ -1056,4 +1229,91 @@ public class AiBlockController {
|
||||
return first;
|
||||
}
|
||||
|
||||
private boolean wouldLikeToRandomlyTrade(Card attacker, Card blocker, Combat combat) {
|
||||
// Determines if the AI would like to randomly trade its blocker for the attacker in given combat
|
||||
boolean enableRandomTrades = false;
|
||||
boolean randomTradeIfBehindOnBoard = false;
|
||||
boolean randomTradeIfCreatInHand = false;
|
||||
int chanceModForEmbalm = 0;
|
||||
int chanceToTradeToSaveWalker = 0;
|
||||
int chanceToTradeDownToSaveWalker = 0;
|
||||
int minRandomTradeChance = 0;
|
||||
int maxRandomTradeChance = 0;
|
||||
int maxCreatDiff = 0;
|
||||
int maxCreatDiffWithRepl = 0;
|
||||
int aiCreatureCount = 0;
|
||||
int oppCreatureCount = 0;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
|
||||
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
|
||||
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
|
||||
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
|
||||
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
|
||||
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
|
||||
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
|
||||
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
|
||||
}
|
||||
|
||||
if (!enableRandomTrades) {
|
||||
return false;
|
||||
}
|
||||
|
||||
aiCreatureCount = ComputerUtil.countUsefulCreatures(ai);
|
||||
|
||||
if (!attackersLeft.isEmpty()) {
|
||||
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
|
||||
}
|
||||
|
||||
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
|
||||
// Temporarily controlled object - don't trade with it
|
||||
// TODO: find a more reliable way to figure out that control will be reestablished next turn
|
||||
return false;
|
||||
}
|
||||
|
||||
int numSteps = ai.getStartingLife() - 5; // e.g. 15 steps between 5 life and 20 life
|
||||
float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps;
|
||||
int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep));
|
||||
if (chance > maxRandomTradeChance) {
|
||||
chance = maxRandomTradeChance;
|
||||
}
|
||||
|
||||
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false);
|
||||
int evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
|
||||
boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
|
||||
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
|
||||
|
||||
if (atkEmbalm && !blkEmbalm) {
|
||||
// The opponent will eventually get his creature back, while the AI won't
|
||||
chance = Math.max(0, chance - chanceModForEmbalm);
|
||||
}
|
||||
|
||||
if (blocker.isFaceDown() && blocker.getState(CardStateName.Original).getType().isCreature()) {
|
||||
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it
|
||||
// in relation to the original state, not to the Morph state
|
||||
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true);
|
||||
}
|
||||
int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
|
||||
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();
|
||||
boolean creatureParityOrAllowedDiff = aiCreatureCount
|
||||
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
|
||||
boolean wantToTradeWithCreatInHand = randomTradeIfCreatInHand
|
||||
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).isEmpty()
|
||||
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
||||
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
||||
&& combat.getDefenderByAttacker(attacker) instanceof Card
|
||||
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
|
||||
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
||||
|
||||
if (((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
||||
&& powerParityOrHigher
|
||||
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
|
||||
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +41,23 @@ import java.util.Set;
|
||||
public class AiCardMemory {
|
||||
|
||||
private final Set<Card> memMandatoryAttackers;
|
||||
private final Set<Card> memTrickAttackers;
|
||||
private final Set<Card> memHeldManaSources;
|
||||
private final Set<Card> memHeldManaSourcesForCombat;
|
||||
private final Set<Card> memAttachedThisTurn;
|
||||
private final Set<Card> memAnimatedThisTurn;
|
||||
private final Set<Card> memBouncedThisTurn;
|
||||
private final Set<Card> memActivatedThisTurn;
|
||||
|
||||
public AiCardMemory() {
|
||||
this.memMandatoryAttackers = new HashSet<>();
|
||||
this.memHeldManaSources = new HashSet<>();
|
||||
this.memHeldManaSourcesForCombat = new HashSet<>();
|
||||
this.memAttachedThisTurn = new HashSet<>();
|
||||
this.memAnimatedThisTurn = new HashSet<>();
|
||||
this.memBouncedThisTurn = new HashSet<>();
|
||||
this.memActivatedThisTurn = new HashSet<>();
|
||||
this.memTrickAttackers = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,10 +67,13 @@ public class AiCardMemory {
|
||||
*/
|
||||
public enum MemorySet {
|
||||
MANDATORY_ATTACKERS,
|
||||
HELD_MANA_SOURCES,
|
||||
TRICK_ATTACKERS,
|
||||
HELD_MANA_SOURCES_FOR_MAIN2,
|
||||
HELD_MANA_SOURCES_FOR_DECLBLK,
|
||||
ATTACHED_THIS_TURN,
|
||||
ANIMATED_THIS_TURN,
|
||||
BOUNCED_THIS_TURN,
|
||||
ACTIVATED_THIS_TURN,
|
||||
//REVEALED_CARDS // stub, not linked to AI code yet
|
||||
}
|
||||
|
||||
@@ -72,14 +81,20 @@ public class AiCardMemory {
|
||||
switch (set) {
|
||||
case MANDATORY_ATTACKERS:
|
||||
return memMandatoryAttackers;
|
||||
case HELD_MANA_SOURCES:
|
||||
case TRICK_ATTACKERS:
|
||||
return memTrickAttackers;
|
||||
case HELD_MANA_SOURCES_FOR_MAIN2:
|
||||
return memHeldManaSources;
|
||||
case HELD_MANA_SOURCES_FOR_DECLBLK:
|
||||
return memHeldManaSourcesForCombat;
|
||||
case ATTACHED_THIS_TURN:
|
||||
return memAttachedThisTurn;
|
||||
case ANIMATED_THIS_TURN:
|
||||
return memAnimatedThisTurn;
|
||||
case BOUNCED_THIS_TURN:
|
||||
return memBouncedThisTurn;
|
||||
case ACTIVATED_THIS_TURN:
|
||||
return memActivatedThisTurn;
|
||||
//case REVEALED_CARDS:
|
||||
// return memRevealedCards;
|
||||
default:
|
||||
@@ -249,10 +264,13 @@ public class AiCardMemory {
|
||||
*/
|
||||
public void clearAllRemembered() {
|
||||
clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
|
||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES);
|
||||
clearMemorySet(MemorySet.TRICK_ATTACKERS);
|
||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
|
||||
clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
|
||||
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
|
||||
clearMemorySet(MemorySet.BOUNCED_THIS_TURN);
|
||||
clearMemorySet(MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
|
||||
// Static functions to simplify access to AI card memory of a given AI player.
|
||||
@@ -265,6 +283,9 @@ public class AiCardMemory {
|
||||
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
|
||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
|
||||
}
|
||||
public static boolean isRememberedCardByName(Player ai, String name, MemorySet set) {
|
||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCardByName(name, set);
|
||||
}
|
||||
public static void clearMemorySet(Player ai, MemorySet set) {
|
||||
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
|
||||
}
|
||||
|
||||
@@ -17,14 +17,6 @@
|
||||
*/
|
||||
package forge.ai;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.esotericsoftware.minlog.Log;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
@@ -32,52 +24,31 @@ import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.ability.ChangeZoneAi;
|
||||
import forge.ai.ability.ExploreAi;
|
||||
import forge.ai.simulation.SpellAbilityPicker;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.deck.CardPool;
|
||||
import forge.deck.Deck;
|
||||
import forge.deck.DeckSection;
|
||||
import forge.game.CardTraitBase;
|
||||
import forge.game.CardTraitPredicates;
|
||||
import forge.game.Direction;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.*;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.SpellApiBased;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.replacement.ReplaceMoved;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.OptionalCost;
|
||||
import forge.game.spellability.Spell;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityCondition;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.spellability.SpellPermanent;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
@@ -88,6 +59,10 @@ import forge.util.Expressions;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* AiController class.
|
||||
@@ -605,11 +580,32 @@ public class AiController {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void reserveManaSourcesForMain2(SpellAbility sa) {
|
||||
public void reserveManaSources(SpellAbility sa) {
|
||||
reserveManaSources(sa, PhaseType.MAIN2);
|
||||
}
|
||||
|
||||
public void reserveManaSources(SpellAbility sa, PhaseType phaseType) {
|
||||
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
|
||||
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
||||
|
||||
AiCardMemory.MemorySet memSet;
|
||||
|
||||
switch (phaseType) {
|
||||
case MAIN2:
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||
break;
|
||||
case COMBAT_DECLARE_BLOCKERS:
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
|
||||
break;
|
||||
default:
|
||||
System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
|
||||
+ phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||
break;
|
||||
}
|
||||
|
||||
for (Card c : manaSources) {
|
||||
AiCardMemory.rememberCard(player, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
|
||||
AiCardMemory.rememberCard(player, c, memSet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,6 +662,11 @@ public class AiController {
|
||||
if (sa instanceof SpellPermanent) {
|
||||
return canPlayFromEffectAI((SpellPermanent)sa, false, true);
|
||||
}
|
||||
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
|
||||
if (!sa.getTargetRestrictions().hasCandidates(sa, true)) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
}
|
||||
}
|
||||
if (sa instanceof Spell) {
|
||||
if (ComputerUtil.getDamageForPlaying(player, sa) >= player.getLife()
|
||||
&& !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) {
|
||||
@@ -676,42 +677,34 @@ public class AiController {
|
||||
return AiPlayDecision.WillPlay;
|
||||
}
|
||||
|
||||
public boolean isNonDisabledCardInPlay(final String cardName) {
|
||||
for (Card card : player.getCardsIn(ZoneType.Battlefield)) {
|
||||
if (card.getName().equals(cardName)) {
|
||||
// TODO - Better logic to detemine if a permanent is disabled by local effects
|
||||
// currently assuming any permanent enchanted by another player
|
||||
// is disabled and a second copy is necessary
|
||||
// will need actual logic that determines if the enchantment is able
|
||||
// to disable the permanent or it's still functional and a duplicate is unneeded.
|
||||
boolean disabledByEnemy = false;
|
||||
for (Card card2 : card.getEnchantedBy(false)) {
|
||||
if (card2.getOwner() != player) {
|
||||
disabledByEnemy = true;
|
||||
}
|
||||
}
|
||||
if (!disabledByEnemy) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AiPlayDecision canPlaySpellBasic(final Card card, final SpellAbility sa) {
|
||||
boolean isRightSplit = sa != null && sa.isRightSplit();
|
||||
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
|
||||
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar": "NeedsToPlayVar";
|
||||
|
||||
if (card.hasSVar(needsToPlayName)) {
|
||||
final String needsToPlay = card.getSVar(needsToPlayName);
|
||||
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
|
||||
if (list.isEmpty()) {
|
||||
return AiPlayDecision.MissingNeededCards;
|
||||
}
|
||||
}
|
||||
if (card.getSVar(needsToPlayVarName).length() > 0) {
|
||||
final String needsToPlay = card.getSVar(needsToPlayVarName);
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
String sVar = needsToPlay.split(" ")[0];
|
||||
String comparator = needsToPlay.split(" ")[1];
|
||||
String compareTo = comparator.substring(2);
|
||||
try {
|
||||
x = Integer.parseInt(sVar);
|
||||
} catch (final NumberFormatException e) {
|
||||
x = CardFactoryUtil.xCount(card, card.getSVar(sVar));
|
||||
}
|
||||
try {
|
||||
y = Integer.parseInt(compareTo);
|
||||
} catch (final NumberFormatException e) {
|
||||
y = CardFactoryUtil.xCount(card, card.getSVar(compareTo));
|
||||
}
|
||||
if (!Expressions.compare(x, comparator, y)) {
|
||||
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
|
||||
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||
}
|
||||
}
|
||||
return AiPlayDecision.WillPlay;
|
||||
// add any other necessary logic to play a basic spell here
|
||||
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
|
||||
}
|
||||
|
||||
// not sure "playing biggest spell" matters?
|
||||
@@ -724,12 +717,31 @@ public class AiController {
|
||||
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
|
||||
|
||||
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return 1;
|
||||
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// deprioritize pump spells with pure energy cost (can be activated last,
|
||||
// since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler)
|
||||
int a2 = 0, b2 = 0;
|
||||
if (a.getApi() == ApiType.Pump && a.getPayCosts() != null && a.getPayCosts().getCostEnergy() != null) {
|
||||
if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
a2 = a.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (b.getApi() == ApiType.Pump && b.getPayCosts() != null && b.getPayCosts().getCostEnergy() != null) {
|
||||
if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
b2 = b.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (a2 == 0 && b2 > 0) {
|
||||
return -1;
|
||||
} else if (b2 == 0 && a2 > 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cast 0 mana cost spells first (might be a Mox)
|
||||
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
|
||||
return -1;
|
||||
@@ -737,9 +749,9 @@ public class AiController {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return -1;
|
||||
} else if (b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
} else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -752,8 +764,14 @@ public class AiController {
|
||||
private int getSpellAbilityPriority(SpellAbility sa) {
|
||||
int p = 0;
|
||||
Card source = sa.getHostCard();
|
||||
final Player ai = source.getController();
|
||||
final Player ai = source == null ? sa.getActivatingPlayer() : source.getController();
|
||||
if (ai == null) {
|
||||
System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa);
|
||||
return 0;
|
||||
}
|
||||
final boolean noCreatures = ai.getCreaturesInPlay().isEmpty();
|
||||
|
||||
if (source != null) {
|
||||
// puts creatures in front of spells
|
||||
if (source.isCreature()) {
|
||||
p += 1;
|
||||
@@ -762,14 +780,9 @@ public class AiController {
|
||||
if (source.isEquipment() && noCreatures) {
|
||||
p -= 9;
|
||||
}
|
||||
// use Surge and Prowl costs when able to
|
||||
if (sa.isSurged() ||
|
||||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
||||
p += 9;
|
||||
}
|
||||
// 1. increase chance of using Surge effects
|
||||
// 2. non-surged versions are usually inefficient
|
||||
if (sa.getHostCard().getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||
p -= 9;
|
||||
}
|
||||
// move snap-casted spells to front
|
||||
@@ -778,9 +791,16 @@ public class AiController {
|
||||
p += 50;
|
||||
}
|
||||
}
|
||||
// artifacts and enchantments with effects that do not stack
|
||||
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
|
||||
p -= 9;
|
||||
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
|
||||
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||
}
|
||||
}
|
||||
|
||||
// use Surge and Prowl costs when able to
|
||||
if (sa.isSurged() ||
|
||||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
||||
p += 9;
|
||||
}
|
||||
// sort planeswalker abilities with most costly first
|
||||
if (sa.getRestrictions().isPwAbility()) {
|
||||
@@ -801,11 +821,6 @@ public class AiController {
|
||||
p -= 9;
|
||||
}
|
||||
|
||||
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
|
||||
p -= (((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||
}
|
||||
|
||||
// try to cast mana ritual spells before casting spells to maximize potential mana
|
||||
if ("ManaRitual".equals(sa.getParam("AILogic"))) {
|
||||
p += 9;
|
||||
@@ -840,7 +855,36 @@ public class AiController {
|
||||
sourceCard = sa.getHostCard();
|
||||
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) {
|
||||
min = 1;
|
||||
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
|
||||
}
|
||||
|
||||
if (sa.hasParam("AnyNumber")) {
|
||||
if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) {
|
||||
CardCollection discards = new CardCollection();
|
||||
final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand);
|
||||
final int numLandsOTB = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||
int numOppInHand = 0;
|
||||
for (Player p : player.getGame().getPlayers()) {
|
||||
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
|
||||
numOppInHand = p.getCardsIn(ZoneType.Hand).size();
|
||||
}
|
||||
}
|
||||
for (Card c : inHand) {
|
||||
if (c.hasSVar("DoNotDiscardIfAble") || c.hasSVar("IsReanimatorCard")) { continue; }
|
||||
if (c.isCreature() && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getSpellPermanent(), player)) {
|
||||
discards.add(c);
|
||||
}
|
||||
if ((c.isLand() && numLandsOTB >= 5) || (c.getFirstSpellAbility() != null && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getFirstSpellAbility(), player))) {
|
||||
if (discards.size() + 1 <= numOppInHand) {
|
||||
discards.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return discards;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// look for good discards
|
||||
@@ -858,6 +902,9 @@ public class AiController {
|
||||
}
|
||||
if (prefCard == null) {
|
||||
prefCard = ComputerUtil.getCardPreference(player, sourceCard, "DiscardCost", validCards);
|
||||
if (prefCard != null && prefCard.hasSVar("DoNotDiscardIfAble")) {
|
||||
prefCard = null;
|
||||
}
|
||||
}
|
||||
if (prefCard != null) {
|
||||
discardList.add(prefCard);
|
||||
@@ -894,13 +941,52 @@ public class AiController {
|
||||
if (numLandsInHand > 0) {
|
||||
numLandsAvailable++;
|
||||
}
|
||||
|
||||
//Discard unplayable card
|
||||
if (validCards.get(0).getCMC() > numLandsAvailable) {
|
||||
discardList.add(validCards.get(0));
|
||||
validCards.remove(validCards.get(0));
|
||||
boolean discardedUnplayable = false;
|
||||
for (int j = 0; j < validCards.size(); j++) {
|
||||
if (validCards.get(j).getCMC() > numLandsAvailable && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
|
||||
discardList.add(validCards.get(j));
|
||||
validCards.remove(validCards.get(j));
|
||||
discardedUnplayable = true;
|
||||
break;
|
||||
} else if (validCards.get(j).getCMC() <= numLandsAvailable) {
|
||||
// cut short to avoid looping over cards which are guaranteed not to fit the criteria
|
||||
break;
|
||||
}
|
||||
else { //Discard worst card
|
||||
}
|
||||
|
||||
if (!discardedUnplayable) {
|
||||
// discard worst card
|
||||
Card worst = ComputerUtilCard.getWorstAI(validCards);
|
||||
if (worst == null) {
|
||||
// there were only instants and sorceries, and maybe cards that are not good to discard, so look
|
||||
// for more discard options
|
||||
worst = ComputerUtilCard.getCheapestSpellAI(validCards);
|
||||
}
|
||||
if (worst == null && !validCards.isEmpty()) {
|
||||
// still nothing chosen, so choose the first thing that works, trying not to make DoNotDiscardIfAble
|
||||
// discards
|
||||
for (Card c : validCards) {
|
||||
if (!c.hasSVar("DoNotDiscardIfAble")) {
|
||||
worst = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Only DoNotDiscardIfAble cards? If we have a duplicate for something, discard it
|
||||
if (worst == null) {
|
||||
for (Card c : validCards) {
|
||||
if (CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c.getName())).size() > 1) {
|
||||
worst = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (worst == null) {
|
||||
// Otherwise just grab a random card and discard it
|
||||
worst = Aggregates.random(validCards);
|
||||
}
|
||||
}
|
||||
}
|
||||
discardList.add(worst);
|
||||
validCards.remove(worst);
|
||||
}
|
||||
@@ -1059,6 +1145,20 @@ public class AiController {
|
||||
}
|
||||
|
||||
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
|
||||
CardCollection playBeforeLand = CardLists.filter(player.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return "true".equalsIgnoreCase(card.getSVar("PlayBeforeLandDrop"));
|
||||
}
|
||||
});
|
||||
|
||||
if (!playBeforeLand.isEmpty()) {
|
||||
SpellAbility wantToPlayBeforeLand = chooseSpellAbilityToPlayFromList(ComputerUtilAbility.getSpellAbilities(playBeforeLand, player), false);
|
||||
if (wantToPlayBeforeLand != null) {
|
||||
return singleSpellAbilityList(wantToPlayBeforeLand);
|
||||
}
|
||||
}
|
||||
|
||||
if (landsWannaPlay != null) {
|
||||
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
|
||||
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
|
||||
@@ -1066,6 +1166,7 @@ public class AiController {
|
||||
Card land = chooseBestLandToPlay(landsWannaPlay);
|
||||
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|
||||
|| player.cantLoseForZeroOrLessLife() ) {
|
||||
if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
|
||||
game.PLAY_LAND_SURROGATE.setHostCard(land);
|
||||
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||
abilities.add(game.PLAY_LAND_SURROGATE);
|
||||
@@ -1073,10 +1174,114 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return singleSpellAbilityList(getSpellAbilityToPlay());
|
||||
}
|
||||
|
||||
private boolean isSafeToHoldLandDropForMain2(Card landToPlay) {
|
||||
if (!MyRandom.percentTrue(getIntProperty(AiProps.HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED))) {
|
||||
// check against the chance specified in the profile
|
||||
return false;
|
||||
}
|
||||
if (game.getPhaseHandler().getTurn() <= 2) {
|
||||
// too obvious when doing it on the very first turn of the game
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand),
|
||||
Predicates.not(CardPredicates.Presets.LANDS));
|
||||
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
|
||||
if (CardLists.filter(otb, Predicates.not(CardPredicates.Presets.LANDS)).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: improve the detection of taplands
|
||||
boolean isTapLand = false;
|
||||
for (ReplacementEffect repl : landToPlay.getReplacementEffects()) {
|
||||
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
|
||||
isTapLand = true;
|
||||
}
|
||||
}
|
||||
|
||||
int totalCMCInHand = Aggregates.sum(inHand, CardPredicates.Accessors.fnGetCmc);
|
||||
int minCMCInHand = Aggregates.min(inHand, CardPredicates.Accessors.fnGetCmc);
|
||||
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
|
||||
|
||||
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && !isTapLand;
|
||||
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
|
||||
|
||||
boolean hasRelevantAbsOTB = !CardLists.filter(otb, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
boolean isTapLand = false;
|
||||
for (ReplacementEffect repl : card.getReplacementEffects()) {
|
||||
// TODO: improve the detection of taplands
|
||||
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
|
||||
isTapLand = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (SpellAbility sa : card.getSpellAbilities()) {
|
||||
if (sa.getPayCosts() != null && sa.isAbility()
|
||||
&& sa.getPayCosts().getCostMana() != null
|
||||
&& sa.getPayCosts().getCostMana().getMana().getCMC() > 0
|
||||
&& (!sa.getPayCosts().hasTapCost() || !isTapLand)
|
||||
&& (!sa.hasParam("ActivationZone") || sa.getParam("ActivationZone").contains("Battlefield"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}).isEmpty();
|
||||
|
||||
boolean hasLandBasedEffect = !CardLists.filter(otb, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
for (Trigger t : card.getTriggers()) {
|
||||
Map<String, String> params = t.getMapParams();
|
||||
if ("ChangesZone".equals(params.get("Mode"))
|
||||
&& params.containsKey("ValidCard")
|
||||
&& !params.get("ValidCard").contains("nonLand")
|
||||
&& ((params.get("ValidCard").contains("Land")) || (params.get("ValidCard").contains("Permanent")))
|
||||
&& "Battlefield".equals(params.get("Destination"))) {
|
||||
// Landfall and other similar triggers
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (String sv : card.getSVars().keySet()) {
|
||||
String varValue = card.getSVar(sv);
|
||||
if (varValue.startsWith("Count$Valid") || sv.equals("BuffedBy")) {
|
||||
if (varValue.contains("Land") || varValue.contains("Plains") || varValue.contains("Forest")
|
||||
|| varValue.contains("Mountain") || varValue.contains("Island") || varValue.contains("Swamp")
|
||||
|| varValue.contains("Wastes")) {
|
||||
// In presence of various cards that get buffs like "equal to the number of lands you control",
|
||||
// safer for our AI model to just play the land earlier rather than make a blunder
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}).isEmpty();
|
||||
|
||||
// TODO: add prediction for effects that will untap a tapland as it enters the battlefield
|
||||
if (!canCastWithLandDrop && cantCastAnythingNow && !hasLandBasedEffect && (!hasRelevantAbsOTB || isTapLand)) {
|
||||
// Hopefully there's not much to do with the extra mana immediately, can wait for Main 2
|
||||
return true;
|
||||
}
|
||||
if ((predictedMana <= totalCMCInHand && canCastWithLandDrop) || (hasRelevantAbsOTB && !isTapLand) || hasLandBasedEffect) {
|
||||
// Might need an extra land to cast something, or for some kind of an ETB ability with a cost or an
|
||||
// alternative cost (if we cast it in Main 1), or to use an activated ability on the battlefield
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private final SpellAbility getSpellAbilityToPlay() {
|
||||
// if top of stack is owned by me
|
||||
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
|
||||
@@ -1306,7 +1511,7 @@ public class AiController {
|
||||
+ MyRandom.getRandom().nextInt(3);
|
||||
return Math.max(remaining, min) / 2;
|
||||
} else if ("LowestLoseLife".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getOpponent().getLife())) + 1;
|
||||
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, ComputerUtil.getOpponentFor(player).getLife())) + 1;
|
||||
} else if ("HighestGetCounter".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(3);
|
||||
} else if (source.hasSVar("EnergyToPay")) {
|
||||
@@ -1428,6 +1633,24 @@ public class AiController {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// Special case for Bow to My Command which simulates a complex tap cost via ChooseCard
|
||||
// TODO: consider enhancing support for tapXType<Any/...> in UnlessCost to get rid of this hack
|
||||
if ("BowToMyCommand".equals(sa.getParam("AILogic"))) {
|
||||
if (!sa.getHostCard().getZone().is(ZoneType.Command)) {
|
||||
// Make sure that other opponents do not tap for an already abandoned scheme
|
||||
result.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
int totPower = 0;
|
||||
for (Card p : result) {
|
||||
totPower += p.getNetPower();
|
||||
}
|
||||
if (totPower >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,8 +1776,12 @@ public class AiController {
|
||||
if (useSimulation) {
|
||||
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
if (sa.getApi() == ApiType.Explore) {
|
||||
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
|
||||
} else {
|
||||
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
}
|
||||
|
||||
public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {
|
||||
// list is only one or empty, no need to filter
|
||||
@@ -1575,6 +1802,10 @@ public class AiController {
|
||||
|
||||
List<SpellAbility> evolve = filterList(putCounter, SpellAbilityPredicates.hasParam("Evolve"));
|
||||
|
||||
List<SpellAbility> token = filterListByApi(activePlayerSAs, ApiType.Token);
|
||||
List<SpellAbility> pump = filterListByApi(activePlayerSAs, ApiType.Pump);
|
||||
List<SpellAbility> pumpAll = filterListByApi(activePlayerSAs, ApiType.PumpAll);
|
||||
|
||||
// do mandatory discard early if hand is empty or has DiscardMe card
|
||||
boolean discardEarly = false;
|
||||
CardCollectionView playerHand = player.getCardsIn(ZoneType.Hand);
|
||||
@@ -1583,6 +1814,11 @@ public class AiController {
|
||||
result.addAll(mandatoryDiscard);
|
||||
}
|
||||
|
||||
// token should be added first so they might get the pump bonus
|
||||
result.addAll(token);
|
||||
result.addAll(pump);
|
||||
result.addAll(pumpAll);
|
||||
|
||||
// do Evolve Trigger before other PutCounter SpellAbilities
|
||||
// do putCounter before Draw/Discard because it can cause a Draw Trigger
|
||||
result.addAll(evolve);
|
||||
@@ -1598,6 +1834,9 @@ public class AiController {
|
||||
}
|
||||
|
||||
result.addAll(activePlayerSAs);
|
||||
|
||||
//need to reverse because of magic stack
|
||||
Collections.reverse(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
@Override
|
||||
public PaymentDecision visit(CostDiscard cost) {
|
||||
final String type = cost.getType();
|
||||
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
||||
|
||||
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
||||
if (type.equals("LastDrawn")) {
|
||||
if (!hand.contains(player.getLastDrawnCard())) {
|
||||
return null;
|
||||
@@ -79,6 +79,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return PaymentDecision.card(source);
|
||||
}
|
||||
else if (type.equals("Hand")) {
|
||||
if (hand.size() > 1 && ability.getActivatingPlayer() != null) {
|
||||
hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard);
|
||||
}
|
||||
return PaymentDecision.card(hand);
|
||||
}
|
||||
|
||||
@@ -95,7 +98,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
}
|
||||
|
||||
if (type.equals("Random")) {
|
||||
return PaymentDecision.card(CardLists.getRandomSubList(new CardCollection(hand), c));
|
||||
CardCollectionView randomSubset = CardLists.getRandomSubList(new CardCollection(hand), c);
|
||||
if (randomSubset.size() > 1 && ability.getActivatingPlayer() != null) {
|
||||
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard);
|
||||
}
|
||||
return PaymentDecision.card(randomSubset);
|
||||
}
|
||||
else {
|
||||
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||
@@ -340,6 +347,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
if (source.getName().equals("Maralen of the Mornsong Avatar")) {
|
||||
return PaymentDecision.number(2);
|
||||
}
|
||||
if (source.getName().equals("Necrologia")) {
|
||||
return PaymentDecision.number(Integer.parseInt(ability.getSVar("ChosenX")));
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
|
||||
@@ -373,6 +383,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostPutCardToLib cost) {
|
||||
if (cost.payCostFromSource()) {
|
||||
return PaymentDecision.card(source);
|
||||
}
|
||||
Integer c = cost.convertAmount();
|
||||
final Game game = player.getGame();
|
||||
CardCollection chosen = new CardCollection();
|
||||
@@ -469,7 +482,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
CardCollectionView totap;
|
||||
if (isVehicle) {
|
||||
totalP = type.split("withTotalPowerGE")[1];
|
||||
type = type.replace("+withTotalPowerGE" + totalP, "");
|
||||
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
|
||||
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped);
|
||||
} else {
|
||||
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
|
||||
|
||||
@@ -21,6 +21,7 @@ import forge.LobbyPlayer;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.FileUtil;
|
||||
|
||||
import forge.util.TextUtil;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import java.io.File;
|
||||
@@ -52,7 +53,7 @@ public class AiProfileUtil {
|
||||
* @return the full relative path and file name for the given profile.
|
||||
*/
|
||||
private static String buildFileName(final String profileName) {
|
||||
return String.format("%s/%s%s", AI_PROFILE_DIR, profileName, AI_PROFILE_EXT);
|
||||
return TextUtil.concatNoSpace(AI_PROFILE_DIR, "/", profileName, AI_PROFILE_EXT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,20 +29,51 @@ public enum AiProps { /** */
|
||||
DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */
|
||||
MULLIGAN_THRESHOLD ("5"), /** */
|
||||
PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"),
|
||||
HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED ("0"), /** */
|
||||
HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS ("true"), /** */
|
||||
CHEAT_WITH_MANA_ON_SHUFFLE ("false"),
|
||||
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
|
||||
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
|
||||
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
|
||||
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
|
||||
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
|
||||
PLAY_AGGRO ("false"), /** */
|
||||
MIN_SPELL_CMC_TO_COUNTER ("0"),
|
||||
PLAY_AGGRO ("false"),
|
||||
CHANCE_TO_ATTACK_INTO_TRADE ("40"), /** */
|
||||
RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE ("true"), /** */
|
||||
ATTACK_INTO_TRADE_WHEN_TAPPED_OUT ("false"), /** */
|
||||
CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA ("0"), /** */
|
||||
TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK ("true"), /** */
|
||||
TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK ("false"), /** */
|
||||
CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK ("30"), /** */
|
||||
ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK ("true"), /** */
|
||||
RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS ("false"), /** */
|
||||
MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE ("1"), /** */
|
||||
ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT ("true"), /** */
|
||||
MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL ("1"), /** */
|
||||
MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK ("30"), /** */
|
||||
MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK ("70"), /** */
|
||||
CHANCE_DECREASE_TO_TRADE_VS_EMBALM ("30"), /** */
|
||||
CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER ("70"), /** */
|
||||
CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER ("0"), /** */
|
||||
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
||||
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
||||
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
||||
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
||||
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
||||
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
||||
CHANCE_TO_COUNTER_CMC_3 ("100"), /** */
|
||||
ALWAYS_COUNTER_OTHER_COUNTERSPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_DAMAGE_SPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS ("true"), /** */
|
||||
ALWAYS_COUNTER_REMOVAL_SPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_AURAS ("true"), /** */
|
||||
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
|
||||
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("false"), /** */
|
||||
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
|
||||
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
|
||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
|
||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
|
||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
|
||||
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
||||
USE_BERSERK_AGGRESSIVELY ("false"), /** */
|
||||
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
|
||||
@@ -50,7 +81,34 @@ public enum AiProps { /** */
|
||||
STRIPMINE_MIN_LANDS_FOR_NO_TIMING_CHECK ("3"), /** */
|
||||
STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK ("6"), /** */
|
||||
STRIPMINE_MAX_LANDS_TO_ATTEMPT_MANALOCKING ("3"), /** */
|
||||
STRIPMINE_HIGH_PRIORITY_ON_SKIPPED_LANDDROP ("false"); /** */
|
||||
STRIPMINE_HIGH_PRIORITY_ON_SKIPPED_LANDDROP ("false"),
|
||||
TOKEN_GENERATION_ABILITY_CHANCE ("80"), /** */
|
||||
TOKEN_GENERATION_ALWAYS_IF_FROM_PLANESWALKER ("true"), /** */
|
||||
TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS ("true"), /** */
|
||||
SCRY_NUM_LANDS_TO_STILL_NEED_MORE ("4"), /** */
|
||||
SCRY_NUM_LANDS_TO_NOT_NEED_MORE ("7"), /** */
|
||||
SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES ("4"), /** */
|
||||
SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC ("3"), /** */
|
||||
SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE ("160"), /** */
|
||||
SCRY_EVALTHR_CMC_THRESHOLD ("3"), /** */
|
||||
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
|
||||
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
|
||||
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
|
||||
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
|
||||
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
|
||||
CONSERVATIVE_ENERGY_PAYMENT_ONLY_DEFENSIVELY ("true"), /** */
|
||||
BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF ("200"), /** */
|
||||
BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF ("200"), /** */
|
||||
BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF ("3"), /** */
|
||||
BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF ("3"), /** */
|
||||
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
|
||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
|
||||
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE("2"), /** */
|
||||
// Experimental features, must be removed after extensive testing and, ideally, defaulting
|
||||
// <-- There are no experimental options here -->
|
||||
AI_IN_DANGER_THRESHOLD("4"),
|
||||
AI_IN_DANGER_MAX_THRESHOLD("4");
|
||||
|
||||
|
||||
private final String strDefaultVal;
|
||||
|
||||
|
||||
@@ -17,23 +17,9 @@
|
||||
*/
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import forge.ai.ability.ProtectAi;
|
||||
import forge.ai.ability.TokenAi;
|
||||
import forge.card.CardType;
|
||||
@@ -46,32 +32,17 @@ import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -79,7 +50,11 @@ import forge.game.zone.Zone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollection;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -99,15 +74,27 @@ public class ComputerUtil {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (sa.isSpell() && !source.isCopiedSpell()) {
|
||||
if (source.getType().hasStringType("Arcane")) {
|
||||
sa = AbilityUtils.addSpliceEffects(sa);
|
||||
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
|
||||
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
|
||||
// otherwise the AI will fail to add the card to stack and that'll knock it out of the game
|
||||
sa.resetTargets();
|
||||
if (((PlayerControllerAi) ai.getController()).getAi().canPlaySa(sa) != AiPlayDecision.WillPlay) {
|
||||
// for whatever reason the AI doesn't want to play the thing with the spliced subs anymore,
|
||||
// proceeding past this point may result in an illegal play
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source.setCastSA(sa);
|
||||
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
||||
sa.setLastStateGraveyard(game.getLastStateGraveyard());
|
||||
sa.setHostCard(game.getAction().moveToStack(source, sa));
|
||||
}
|
||||
|
||||
if (source.getType().hasStringType("Arcane")) {
|
||||
sa = AbilityUtils.addSpliceEffects(sa);
|
||||
}
|
||||
}
|
||||
sa.resetPaidHash();
|
||||
|
||||
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
|
||||
CharmEffect.makeChoices(sa);
|
||||
@@ -192,7 +179,7 @@ public class ComputerUtil {
|
||||
if (unless != null && !unless.endsWith(">")) {
|
||||
final int amount = AbilityUtils.calculateAmount(source, unless, sa);
|
||||
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size();
|
||||
|
||||
// If the Unless isn't enough, this should be less likely to be used
|
||||
if (amount > usableManaSources) {
|
||||
@@ -310,18 +297,20 @@ public class ComputerUtil {
|
||||
|
||||
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
|
||||
final Game game = ai.getGame();
|
||||
String prefDef = "";
|
||||
if (activate != null) {
|
||||
prefDef = activate.getSVar("AIPreference");
|
||||
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
|
||||
for (String prefGroup : prefGroups) {
|
||||
final String[] prefValid = prefGroup.trim().split("\\$");
|
||||
if (prefValid[0].equals(pref)) {
|
||||
final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","), activate.getController(), activate, null);
|
||||
if (prefValid[0].equals(pref) && !prefValid[1].startsWith("Special:")) {
|
||||
CardCollection overrideList = null;
|
||||
|
||||
if (activate.hasSVar("AIPreferenceOverride")) {
|
||||
overrideList = CardLists.getValidCards(typeList, activate.getSVar("AIPreferenceOverride"), activate.getController(), activate, null);
|
||||
}
|
||||
|
||||
for (String validItem : prefValid[1].split(",")) {
|
||||
final CardCollection prefList = CardLists.getValidCards(typeList, validItem, activate.getController(), activate, null);
|
||||
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold");
|
||||
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold");
|
||||
|
||||
@@ -345,12 +334,13 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (!prefList.isEmpty()) {
|
||||
if (!prefList.isEmpty() || (overrideList != null && !overrideList.isEmpty())) {
|
||||
return ComputerUtilCard.getWorstAI(overrideList == null ? prefList : overrideList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pref.contains("SacCost")) {
|
||||
// search for permanents with SacMe. priority 1 is the lowest, priority 5 the highest
|
||||
for (int ip = 0; ip < 6; ip++) {
|
||||
@@ -411,6 +401,11 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// Survival of the Fittest logic
|
||||
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
|
||||
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
|
||||
}
|
||||
|
||||
// Discard lands
|
||||
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
|
||||
if (!landsInHand.isEmpty()) {
|
||||
@@ -707,6 +702,8 @@ public class ComputerUtil {
|
||||
return sacrificed; // sacrifice none
|
||||
}
|
||||
}
|
||||
boolean exceptSelf = "ExceptSelf".equals(source.getParam("AILogic"));
|
||||
boolean removedSelf = false;
|
||||
|
||||
if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) {
|
||||
if (source.hasParam("Exploit")) {
|
||||
@@ -764,11 +761,22 @@ public class ComputerUtil {
|
||||
|
||||
final int max = Math.min(remaining.size(), amount);
|
||||
|
||||
if (exceptSelf) {
|
||||
removedSelf = remaining.remove(source.getHostCard());
|
||||
}
|
||||
|
||||
for (int i = 0; i < max; i++) {
|
||||
Card c = chooseCardToSacrifice(remaining, ai, destroy);
|
||||
remaining.remove(c);
|
||||
if (c != null) {
|
||||
sacrificed.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (sacrificed.isEmpty() && removedSelf) {
|
||||
sacrificed.add(source.getHostCard());
|
||||
}
|
||||
|
||||
return sacrificed;
|
||||
}
|
||||
|
||||
@@ -848,11 +856,11 @@ public class ComputerUtil {
|
||||
continue; // Won't play ability
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c, sa)) {
|
||||
continue; // Won't play ability
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c)) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c, sa)) {
|
||||
continue; // Won't play ability
|
||||
}
|
||||
}
|
||||
@@ -868,7 +876,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
} catch (final Exception ex) {
|
||||
throw new RuntimeException(String.format("There is an error in the card code for %s:%s", c.getName(), ex.getMessage()), ex);
|
||||
throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -907,7 +915,7 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
} catch (final Exception ex) {
|
||||
throw new RuntimeException(String.format("There is an error in the card code for %s:%s", c.getName(), ex.getMessage()), ex);
|
||||
throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -917,16 +925,12 @@ public class ComputerUtil {
|
||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
|
||||
if ("True".equals(card.getSVar("NonStackingEffect")) && card.getController().isCardInPlay(card.getName())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (card.hasSVar("PlayMain1")) {
|
||||
if (card.getSVar("PlayMain1").equals("ALWAYS") || sa.getPayCosts().hasNoManaCost()) {
|
||||
return true;
|
||||
} else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
|
||||
//Only play these main1 when the opponent has creatures (stealing and giving them haste)
|
||||
if (!card.getController().getOpponent().getCreaturesInPlay().isEmpty()) {
|
||||
if (!ai.getOpponents().getCreaturesInPlay().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
|
||||
@@ -934,6 +938,15 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// try not to cast Raid creatures in main 1 if an attack is likely
|
||||
if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword("Haste")) {
|
||||
for (Card potentialAtkr: ai.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (card.getManaCost().isZero()) {
|
||||
return true;
|
||||
}
|
||||
@@ -973,7 +986,7 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ai.getOpponent())) {
|
||||
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) {
|
||||
return true;
|
||||
}
|
||||
if (card.isCreature()) {
|
||||
@@ -993,7 +1006,7 @@ public class ComputerUtil {
|
||||
} // BuffedBy
|
||||
|
||||
// get all cards the human controls with AntiBuffedBy
|
||||
final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
for (Card buffedcard : antibuffed) {
|
||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||
@@ -1033,11 +1046,11 @@ public class ComputerUtil {
|
||||
return ret;
|
||||
} else {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
Combat combat = new Combat(ai.getOpponent());
|
||||
CardCollectionView attackers = ai.getOpponent().getCreaturesInPlay();
|
||||
Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
|
||||
CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||
for (Card att : attackers) {
|
||||
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
||||
combat.addAttacker(att, att.getController().getOpponent());
|
||||
combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
|
||||
}
|
||||
}
|
||||
AiBlockController aiBlock = new AiBlockController(ai);
|
||||
@@ -1101,7 +1114,7 @@ public class ComputerUtil {
|
||||
return (sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
|| !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases"));
|
||||
}
|
||||
@@ -1111,6 +1124,10 @@ public class ComputerUtil {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
|
||||
if (source != null && "ALWAYS".equals(source.getSVar("PlayMain1"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
@@ -1145,7 +1162,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
// get all cards the human controls with AntiBuffedBy
|
||||
final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
for (Card buffedcard : antibuffed) {
|
||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||
@@ -1331,7 +1348,7 @@ public class ComputerUtil {
|
||||
if (tgt == null) {
|
||||
continue;
|
||||
}
|
||||
final Player enemy = ai.getOpponent();
|
||||
final Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
if (!sa.canTarget(enemy)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1436,17 +1453,26 @@ public class ComputerUtil {
|
||||
objects = canBeTargeted;
|
||||
}
|
||||
|
||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
||||
toughness = saviour.hasParam("NumDef") ?
|
||||
AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("NumDef"), saviour) : 0;
|
||||
final List<String> keywords = saviour.hasParam("KW") ?
|
||||
Arrays.asList(saviour.getParam("KW").split(" & ")) : new ArrayList<String>();
|
||||
SpellAbility saviorWithSubs = saviour;
|
||||
ApiType saviorWithSubsApi = saviorWithSubs == null ? null : saviorWithSubs.getApi();
|
||||
while (saviorWithSubs != null) {
|
||||
ApiType curApi = saviorWithSubs.getApi();
|
||||
if (curApi == ApiType.Pump || curApi == ApiType.PumpAll) {
|
||||
toughness = saviorWithSubs.hasParam("NumDef") ?
|
||||
AbilityUtils.calculateAmount(saviorWithSubs.getHostCard(), saviorWithSubs.getParam("NumDef"), saviour) : 0;
|
||||
final List<String> keywords = saviorWithSubs.hasParam("KW") ?
|
||||
Arrays.asList(saviorWithSubs.getParam("KW").split(" & ")) : new ArrayList<String>();
|
||||
if (keywords.contains("Indestructible")) {
|
||||
grantIndestructible = true;
|
||||
}
|
||||
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
|
||||
grantShroud = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Consider pump in subabilities, e.g. Bristling Hydra hexproof subability
|
||||
saviorWithSubs = saviorWithSubs.getSubAbility();
|
||||
|
||||
}
|
||||
|
||||
if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
|
||||
@@ -1590,7 +1616,8 @@ public class ComputerUtil {
|
||||
&& (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
|
||||
&& !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone
|
||||
|| saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null)) {
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null
|
||||
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
@@ -1604,7 +1631,9 @@ public class ComputerUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviorWithSubsApi == ApiType.Pump
|
||||
|| saviorWithSubsApi == ApiType.PumpAll) {
|
||||
if ((tgt == null && !grantIndestructible)
|
||||
|| (!grantShroud && !grantIndestructible)) {
|
||||
continue;
|
||||
@@ -1857,6 +1886,27 @@ public class ComputerUtil {
|
||||
public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) {
|
||||
boolean bottom = false;
|
||||
|
||||
// AI profile-based toggles
|
||||
int maxLandsToScryLandsToTop = 3;
|
||||
int minLandsToScryLandsAway = 8;
|
||||
int minCreatsToScryCreatsAway = 5;
|
||||
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
|
||||
int lowCMCThreshold = 3;
|
||||
int maxCreatsToScryLowCMCAway = 3;
|
||||
boolean uncastablesToBottom = false;
|
||||
int uncastableCMCThreshold = 1;
|
||||
if (player.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||
maxLandsToScryLandsToTop = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||
minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
|
||||
minCreatsToScryCreatsAway = aic.getIntProperty(AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
|
||||
minCreatEvalThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
|
||||
lowCMCThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
|
||||
maxCreatsToScryLowCMCAway = aic.getIntProperty(AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
|
||||
uncastablesToBottom = aic.getBooleanProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
|
||||
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
|
||||
}
|
||||
|
||||
CardCollectionView allCards = player.getAllCards();
|
||||
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
|
||||
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
|
||||
@@ -1871,8 +1921,9 @@ public class ComputerUtil {
|
||||
CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player)));
|
||||
int numCards = allCreatures.size();
|
||||
|
||||
if (landsOTB.size() < 3 && landsInHand.isEmpty()) {
|
||||
if ((!c.isLand() && !manaArts.contains(c.getName())) || c.getManaAbilities().isEmpty()) {
|
||||
if (landsOTB.size() < maxLandsToScryLandsToTop && landsInHand.isEmpty()) {
|
||||
if ((!c.isLand() && !manaArts.contains(c.getName()))
|
||||
|| (c.getManaAbilities().isEmpty() && !c.hasABasicLandType())) {
|
||||
// scry away non-lands and non-manaproducing lands in situations when the land count
|
||||
// on the battlefield is low, to try to improve the mana base early
|
||||
bottom = true;
|
||||
@@ -1880,7 +1931,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
if (c.isLand()) {
|
||||
if (landsOTB.size() >= 8) {
|
||||
if (landsOTB.size() >= minLandsToScryLandsAway) {
|
||||
// probably enough lands not to urgently need another one, so look for more gas instead
|
||||
bottom = true;
|
||||
} else if (landsInHand.size() >= Math.max(cardsInHand.size() / 2, 2)) {
|
||||
@@ -1898,16 +1949,15 @@ public class ComputerUtil {
|
||||
} else if (c.isCreature()) {
|
||||
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
|
||||
int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0;
|
||||
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
|
||||
int maxControlledCMC = Aggregates.max(creaturesOTB, CardPredicates.Accessors.fnGetCmc);
|
||||
|
||||
if (ComputerUtilCard.evaluateCreature(c) < avgCreatureValue) {
|
||||
if (creaturesOTB.size() > 5) {
|
||||
if (creaturesOTB.size() > minCreatsToScryCreatsAway) {
|
||||
// if there are more than five creatures and the creature is question is below average for
|
||||
// the deck, scry it to the bottom
|
||||
bottom = true;
|
||||
} else if (creaturesOTB.size() > 3 && c.getCMC() <= 3
|
||||
&& maxControlledCMC >= 4 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
|
||||
} else if (creaturesOTB.size() > maxCreatsToScryLowCMCAway && c.getCMC() <= lowCMCThreshold
|
||||
&& maxControlledCMC >= lowCMCThreshold + 1 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
|
||||
// if we are already at a stage when we have 4+ CMC creatures on the battlefield,
|
||||
// probably worth it to scry away very low value creatures with low CMC
|
||||
bottom = true;
|
||||
@@ -1915,6 +1965,15 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (uncastablesToBottom && !c.isLand()) {
|
||||
int cmc = c.isSplitCard() ? Math.min(c.getCMC(Card.SplitCMCMode.LeftSplitCMC), c.getCMC(Card.SplitCMCMode.RightSplitCMC))
|
||||
: c.getCMC();
|
||||
int maxCastable = ComputerUtilMana.getAvailableManaEstimate(player, false) + landsInHand.size();
|
||||
if (cmc - maxCastable >= uncastableCMCThreshold) {
|
||||
bottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
return bottom;
|
||||
}
|
||||
|
||||
@@ -2047,7 +2106,7 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
else if (logic.equals("ChosenLandwalk")) {
|
||||
for (Card c : ai.getOpponent().getLandsInPlay()) {
|
||||
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
|
||||
for (String t : c.getType()) {
|
||||
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
|
||||
chosen = t;
|
||||
@@ -2065,7 +2124,7 @@ public class ComputerUtil {
|
||||
else if (kindOfType.equals("Land")) {
|
||||
if (logic != null) {
|
||||
if (logic.equals("ChosenLandwalk")) {
|
||||
for (Card c : ai.getOpponent().getLandsInPlay()) {
|
||||
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
|
||||
for (String t : c.getType().getLandTypes()) {
|
||||
if (!invalidTypes.contains(t)) {
|
||||
chosen = t;
|
||||
@@ -2098,7 +2157,7 @@ public class ComputerUtil {
|
||||
case "Torture":
|
||||
return "Torture";
|
||||
case "GraceOrCondemnation":
|
||||
return ai.getCreaturesInPlay().size() > ai.getOpponent().getCreaturesInPlay().size() ? "Grace"
|
||||
return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace"
|
||||
: "Condemnation";
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists
|
||||
@@ -2683,4 +2742,124 @@ public class ComputerUtil {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public static final Player getOpponentFor(final Player player) {
|
||||
// This method is deprecated and currently functions as a synonym for player.getWeakestOpponent
|
||||
// until it can be replaced everywhere in the code.
|
||||
|
||||
// Consider replacing calls to this method either with a multiplayer-friendly determination of
|
||||
// opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent
|
||||
// where that is applicable and makes sense from the point of view of multiplayer AI logic.
|
||||
Player opponent = player.getWeakestOpponent();
|
||||
if (opponent != null) {
|
||||
return opponent;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No opponents left ingame for " + player);
|
||||
}
|
||||
|
||||
public static int countUsefulCreatures(Player p) {
|
||||
CardCollection creats = p.getCreaturesInPlay();
|
||||
int count = 0;
|
||||
|
||||
for (Card c : creats) {
|
||||
if (!ComputerUtilCard.isUselessCreature(p, c)) {
|
||||
count ++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public static boolean isPlayingReanimator(final Player ai) {
|
||||
// TODO: either add SVars to other reanimator cards, or improve the prediction so that it avoids using a SVar
|
||||
// at all but detects this effect from SA parameters (preferred, but difficult)
|
||||
CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
|
||||
CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library});
|
||||
|
||||
Predicate<Card> markedAsReanimator = new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return "true".equalsIgnoreCase(card.getSVar("IsReanimatorCard"));
|
||||
}
|
||||
};
|
||||
|
||||
int numInHand = CardLists.filter(inHand, markedAsReanimator).size();
|
||||
int numInDeck = CardLists.filter(inDeck, markedAsReanimator).size();
|
||||
|
||||
return numInHand > 0 || numInDeck >= 3;
|
||||
}
|
||||
|
||||
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||
final Card source = sa.getHostCard();
|
||||
if (source == null) { return srcList; }
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection list;
|
||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||
int value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy(false)) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return ComputerUtilCard.evaluateCreature(c) > totalValue + 30;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
} else {
|
||||
return srcList;
|
||||
}
|
||||
}
|
||||
|
||||
return srcList;
|
||||
}
|
||||
|
||||
// Check if AI life is in danger/serious danger based on next expected combat
|
||||
// assuming a loss of "payment" life
|
||||
// call this to determine if it's safe to use a life payment spell
|
||||
// or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell.
|
||||
public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) {
|
||||
Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
// test whether the human can kill the ai next turn
|
||||
Combat combat = new Combat(opponent);
|
||||
boolean containsAttacker = false;
|
||||
for (Card att : opponent.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
||||
combat.addAttacker(att, ai);
|
||||
containsAttacker = true;
|
||||
}
|
||||
}
|
||||
if (!containsAttacker) {
|
||||
return false;
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai);
|
||||
block.assignBlockersForCombat(combat);
|
||||
|
||||
// TODO predict other, noncombat sources of damage and add them to the "payment" variable.
|
||||
// examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc
|
||||
// If added, might need a parameter to define whether we want to check all threats or combat threats.
|
||||
|
||||
if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) {
|
||||
return true;
|
||||
}
|
||||
if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
@@ -17,7 +19,6 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.zone.ZoneType;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class ComputerUtilAbility {
|
||||
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
||||
@@ -78,7 +79,7 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
|
||||
public static List<SpellAbility> getSpellAbilities(final CardCollectionView l, final Player player) {
|
||||
final List<SpellAbility> spellAbilities = new ArrayList<SpellAbility>();
|
||||
final List<SpellAbility> spellAbilities = Lists.newArrayList();
|
||||
for (final Card c : l) {
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
spellAbilities.add(sa);
|
||||
@@ -93,7 +94,7 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
|
||||
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
||||
final List<SpellAbility> newAbilities = new ArrayList<SpellAbility>();
|
||||
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
||||
for (SpellAbility sa : originList) {
|
||||
sa.setActivatingPlayer(player);
|
||||
//add alternative costs as additional spell abilities
|
||||
@@ -101,7 +102,7 @@ public class ComputerUtilAbility {
|
||||
newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
|
||||
}
|
||||
|
||||
final List<SpellAbility> result = new ArrayList<SpellAbility>();
|
||||
final List<SpellAbility> result = Lists.newArrayList();
|
||||
for (SpellAbility sa : newAbilities) {
|
||||
sa.setActivatingPlayer(player);
|
||||
result.addAll(GameActionUtil.getOptionalCosts(sa));
|
||||
@@ -132,6 +133,42 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
|
||||
public static String getAbilitySourceName(SpellAbility sa) {
|
||||
return sa.getOriginalHost() != null ? sa.getOriginalHost().getName() : sa.getHostCard() != null ? sa.getHostCard().getName() : "";
|
||||
final Card c = getAbilitySource(sa);
|
||||
return c != null ? c.getName() : "";
|
||||
}
|
||||
|
||||
public static CardCollection getCardsTargetedWithApi(Player ai, CardCollection cardList, SpellAbility sa, ApiType api) {
|
||||
// Returns a collection of cards which have already been targeted with the given API either in the parent ability,
|
||||
// in the sub ability, or by something on stack. If "sa" is specified, the parent and sub abilities of this SA will
|
||||
// be checked for targets. If "sa" is null, only the stack instances will be checked.
|
||||
CardCollection targeted = new CardCollection();
|
||||
if (sa != null) {
|
||||
SpellAbility saSub = sa.getRootAbility();
|
||||
while (saSub != null) {
|
||||
if (saSub.getApi() == api && saSub.getTargets() != null) {
|
||||
for (Card c : cardList) {
|
||||
if (saSub.getTargets().getTargetCards().contains(c)) {
|
||||
// Was already targeted with this API in a parent or sub SA
|
||||
targeted.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
saSub = saSub.getSubAbility();
|
||||
}
|
||||
}
|
||||
for (SpellAbilityStackInstance si : ai.getGame().getStack()) {
|
||||
SpellAbility ab = si.getSpellAbility(false);
|
||||
if (ab != null && ab.getApi() == api && si.getTargetChoices() != null) {
|
||||
for (Card c : cardList) {
|
||||
// TODO: somehow ensure that the detected SA won't be countered
|
||||
if (si.getTargetChoices().getTargetCards().contains(c)) {
|
||||
// Was already targeted by a spell ability instance on stack
|
||||
targeted.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targeted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.card.CardType;
|
||||
@@ -29,19 +18,13 @@ import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordCollection;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -54,8 +37,14 @@ import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperCard;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.Expressions;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class ComputerUtilCard {
|
||||
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
|
||||
@@ -364,7 +353,12 @@ public class ComputerUtilCard {
|
||||
}
|
||||
|
||||
if (hasEnchantmants || hasArtifacts) {
|
||||
final List<Card> ae = CardLists.filter(list, Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS));
|
||||
final List<Card> ae = CardLists.filter(list, Predicates.and(Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return !card.hasSVar("DoNotDiscardIfAble");
|
||||
}
|
||||
}));
|
||||
return getCheapestPermanentAI(ae, null, false);
|
||||
}
|
||||
|
||||
@@ -377,6 +371,32 @@ public class ComputerUtilCard {
|
||||
return getCheapestPermanentAI(list, null, false);
|
||||
}
|
||||
|
||||
public static final Card getCheapestSpellAI(final Iterable<Card> list) {
|
||||
if (!Iterables.isEmpty(list)) {
|
||||
CardCollection cc = CardLists.filter(new CardCollection(list),
|
||||
Predicates.or(CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||
Collections.sort(cc, CardLists.CmcComparatorInv);
|
||||
|
||||
if (cc.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Card cheapest = cc.getLast();
|
||||
if (cheapest.hasSVar("DoNotDiscardIfAble")) {
|
||||
for (int i = cc.size() - 1; i >= 0; i--) {
|
||||
if (!cc.get(i).hasSVar("DoNotDiscardIfAble")) {
|
||||
cheapest = cc.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cheapest;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
|
||||
@Override
|
||||
public int compare(final Card a, final Card b) {
|
||||
@@ -399,6 +419,10 @@ public class ComputerUtilCard {
|
||||
return creatureEvaluator.evaluateCreature(c);
|
||||
}
|
||||
|
||||
public static int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
|
||||
return creatureEvaluator.evaluateCreature(c, considerPT, considerCMC);
|
||||
}
|
||||
|
||||
public static int evaluatePermanentList(final CardCollectionView list) {
|
||||
int value = 0;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
@@ -482,7 +506,7 @@ public class ComputerUtilCard {
|
||||
*/
|
||||
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
||||
AiBlockController aiBlk = new AiBlockController(ai);
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
Combat combat = new Combat(opp);
|
||||
//Use actual attackers if available, else consider all possible attackers
|
||||
Combat currentCombat = ai.getGame().getCombat();
|
||||
@@ -845,9 +869,10 @@ public class ComputerUtilCard {
|
||||
List<String> chosen = new ArrayList<String>();
|
||||
Player ai = sa.getActivatingPlayer();
|
||||
final Game game = ai.getGame();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.hasParam("AILogic")) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
if (logic.equals("MostProminentInHumanDeck")) {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
|
||||
}
|
||||
@@ -873,7 +898,7 @@ public class ComputerUtilCard {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
}
|
||||
else if (logic.equals("MostProminentHumanControls")) {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getOpponent().getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(opp.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
}
|
||||
else if (logic.equals("MostProminentPermanent")) {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
@@ -897,7 +922,7 @@ public class ComputerUtilCard {
|
||||
String bestColor = Constant.GREEN;
|
||||
for (byte color : MagicColor.WUBRG) {
|
||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = opp.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||
@@ -934,7 +959,7 @@ public class ComputerUtilCard {
|
||||
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final PhaseType phaseType = ph.getPhase();
|
||||
@@ -1174,6 +1199,18 @@ public class ComputerUtilCard {
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
final Combat combat = phase.getCombat();
|
||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||
final boolean loseCardAtEOT = "Sacrifice".equals(sa.getParam("AtEOT")) || "Exile".equals(sa.getParam("AtEOT"))
|
||||
|| "Destroy".equals(sa.getParam("AtEOT")) || "ExileCombat".equals(sa.getParam("AtEOT"));
|
||||
|
||||
boolean combatTrick = false;
|
||||
boolean holdCombatTricks = false;
|
||||
int chanceToHoldCombatTricks = -1;
|
||||
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
}
|
||||
|
||||
if (!c.canBeTargetedBy(sa)) {
|
||||
return false;
|
||||
@@ -1186,11 +1223,11 @@ public class ComputerUtilCard {
|
||||
/* -- currently disabled until better conditions are devised and the spell prediction is made smarter --
|
||||
// Determine if some mana sources need to be held for the future spell to cast in Main 2 before determining whether to pump.
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
|
||||
if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
|
||||
// only hold mana sources once
|
||||
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Pump);
|
||||
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
||||
aic.reserveManaSourcesForMain2(futureSpell);
|
||||
aic.reserveManaSources(futureSpell);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1204,8 +1241,8 @@ public class ComputerUtilCard {
|
||||
return true;
|
||||
}
|
||||
|
||||
// buff attacker/blocker using triggered pump
|
||||
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
// buff attacker/blocker using triggered pump (unless it's lethal and we don't want to be reckless)
|
||||
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !loseCardAtEOT) {
|
||||
if (phase.isPlayerTurn(ai)) {
|
||||
if (CombatUtil.canAttack(c)) {
|
||||
return true;
|
||||
@@ -1217,7 +1254,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
}
|
||||
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
|
||||
List<Card> oppCreatures = opp.getCreaturesInPlay();
|
||||
float chance = 0;
|
||||
@@ -1234,6 +1271,28 @@ public class ComputerUtilCard {
|
||||
threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
|
||||
}
|
||||
chance += threat;
|
||||
|
||||
// -- Hold combat trick (the AI will try to delay the pump until Declare Blockers) --
|
||||
// Enable combat trick mode only in case it's a pure buff spell in hand with no keywords or with Trample,
|
||||
// First Strike, or Double Strike, otherwise the AI is unlikely to cast it or it's too late to
|
||||
// cast it during Declare Blockers, thus ruining its attacker
|
||||
if (holdCombatTricks && sa.getApi() == ApiType.Pump
|
||||
&& sa.hasParam("NumAtt") && sa.getHostCard() != null
|
||||
&& sa.getHostCard().getZone() != null && sa.getHostCard().getZone().is(ZoneType.Hand)
|
||||
&& c.getNetPower() > 0 // too obvious if attacking with a 0-power creature
|
||||
&& sa.getHostCard().isInstant() // only do it for instant speed spells in hand
|
||||
&& ComputerUtilMana.hasEnoughManaSourcesToCast(sa, ai)) {
|
||||
combatTrick = true;
|
||||
|
||||
final List<String> kws = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
|
||||
: Lists.<String>newArrayList();
|
||||
for (String kw : kws) {
|
||||
if (!kw.equals("Trample") && !kw.equals("First Strike") && !kw.equals("Double Strike")) {
|
||||
combatTrick = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//2. grant haste
|
||||
@@ -1261,7 +1320,7 @@ public class ComputerUtilCard {
|
||||
boolean pumpedWillDie = false;
|
||||
final boolean isAttacking = combat.isAttacking(c);
|
||||
|
||||
if (isBerserk && isAttacking) { pumpedWillDie = true; }
|
||||
if ((isBerserk && isAttacking) || loseCardAtEOT) { pumpedWillDie = true; }
|
||||
|
||||
if (isAttacking) {
|
||||
pumpedCombat.addAttacker(pumped, opp);
|
||||
@@ -1319,6 +1378,16 @@ public class ComputerUtilCard {
|
||||
if (combat.isAttacking(c) && opp.getLife() > 0) {
|
||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
|
||||
int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true);
|
||||
int poisonOrig = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0;
|
||||
int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
|
||||
|
||||
// predict Infect
|
||||
if (pumpedDmg == 0 && c.hasKeyword("Infect")) {
|
||||
if (poisonPumped > poisonOrig) {
|
||||
pumpedDmg = poisonPumped;
|
||||
}
|
||||
}
|
||||
|
||||
if (combat.isBlocked(c)) {
|
||||
if (!c.hasKeyword("Trample")) {
|
||||
dmg = 0;
|
||||
@@ -1331,9 +1400,12 @@ public class ComputerUtilCard {
|
||||
pumpedDmg = 0;
|
||||
}
|
||||
}
|
||||
if (pumpedDmg >= opp.getLife()) {
|
||||
if (pumpedDmg > dmg) {
|
||||
if ((!c.hasKeyword("Infect") && pumpedDmg >= opp.getLife())
|
||||
|| (c.hasKeyword("Infect") && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// try to determine if pumping a creature for more power will give lethal on board
|
||||
// considering all unblocked creatures after the blockers are already declared
|
||||
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
|
||||
@@ -1395,14 +1467,47 @@ public class ComputerUtilCard {
|
||||
|
||||
if (isBerserk) {
|
||||
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
||||
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
|
||||
if (ai.getController() instanceof PlayerControllerAi) {
|
||||
boolean aggr = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY);
|
||||
boolean aggr = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
|
||||
|| sa.hasParam("AtEOT");
|
||||
if (!aggr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean wantToHoldTrick = holdCombatTricks;
|
||||
if (chanceToHoldCombatTricks >= 0) {
|
||||
// Obey the chance specified in the AI profile for holding combat tricks
|
||||
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
|
||||
} else {
|
||||
// Use standard considerations dependent solely on the buff chance determined above
|
||||
wantToHoldTrick &= MyRandom.getRandom().nextFloat() < chance;
|
||||
}
|
||||
|
||||
boolean isHeldCombatTrick = combatTrick && wantToHoldTrick;
|
||||
|
||||
if (isHeldCombatTrick) {
|
||||
if (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.TRICK_ATTACKERS)) {
|
||||
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
|
||||
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
|
||||
// an army of opposing blockers with only one combat trick in hand)
|
||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
|
||||
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
|
||||
// the combat trick
|
||||
if (ai.getController().isAI()) {
|
||||
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
|
||||
// the AI overextending the attack
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return MyRandom.getRandom().nextFloat() < chance;
|
||||
}
|
||||
|
||||
@@ -1417,8 +1522,7 @@ public class ComputerUtilCard {
|
||||
* @return
|
||||
*/
|
||||
public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
|
||||
final Card c, final int toughness, final int power,
|
||||
final List<String> keywords) {
|
||||
final Card c, int toughness, int power, final List<String> keywords) {
|
||||
Card pumped = CardFactory.copyCard(c, true);
|
||||
pumped.setSickness(c.hasSickness());
|
||||
final long timestamp = c.getGame().getNextTimestamp();
|
||||
@@ -1431,8 +1535,25 @@ public class ComputerUtilCard {
|
||||
}
|
||||
}
|
||||
|
||||
// Berserk (and other similar cards)
|
||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||
final int berserkPower = isBerserk ? c.getCurrentPower() : 0;
|
||||
int berserkPower = 0;
|
||||
if (isBerserk && sa.hasSVar("X")) {
|
||||
if ("Targeted$CardPower".equals(sa.getSVar("X"))) {
|
||||
berserkPower = c.getCurrentPower();
|
||||
} else {
|
||||
berserkPower = AbilityUtils.calculateAmount(sa.getHostCard(), "X", sa);
|
||||
}
|
||||
}
|
||||
|
||||
// Electrostatic Pummeler
|
||||
for (SpellAbility ab : c.getSpellAbilities()) {
|
||||
if ("Pummeler".equals(ab.getParam("AILogic"))) {
|
||||
Pair<Integer, Integer> newPT = SpecialCardAi.ElectrostaticPummeler.getPumpedPT(ai, power, toughness);
|
||||
power = newPT.getLeft();
|
||||
toughness = newPT.getRight();
|
||||
}
|
||||
}
|
||||
|
||||
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
|
||||
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
|
||||
@@ -1446,19 +1567,21 @@ public class ComputerUtilCard {
|
||||
if (c.isTapped()) {
|
||||
pumped.setTapped(true);
|
||||
}
|
||||
final List<String> copiedKeywords = pumped.getKeywords();
|
||||
List<String> toCopy = new ArrayList<String>();
|
||||
for (String kw : c.getKeywords()) {
|
||||
if (!copiedKeywords.contains(kw)) {
|
||||
if (kw.startsWith("HIDDEN")) {
|
||||
pumped.addHiddenExtrinsicKeyword(kw);
|
||||
|
||||
KeywordCollection copiedKeywords = new KeywordCollection();
|
||||
copiedKeywords.insertAll(pumped.getKeywords());
|
||||
List<KeywordInterface> toCopy = Lists.newArrayList();
|
||||
for (KeywordInterface k : c.getKeywords()) {
|
||||
if (!copiedKeywords.contains(k.getOriginal())) {
|
||||
if (k.getHidden()) {
|
||||
pumped.addHiddenExtrinsicKeyword(k);
|
||||
} else {
|
||||
toCopy.add(kw);
|
||||
toCopy.add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
|
||||
pumped.addChangedCardKeywords(toCopy, new ArrayList<String>(), false, timestamp2);
|
||||
pumped.addChangedCardKeywordsInternal(toCopy, Lists.<KeywordInterface>newArrayList(), false, timestamp2, true);
|
||||
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
|
||||
return pumped;
|
||||
}
|
||||
@@ -1612,4 +1735,95 @@ public class ComputerUtilCard {
|
||||
|
||||
return maxEnergyCost;
|
||||
}
|
||||
|
||||
public static CardCollection prioritizeCreaturesWorthRemovingNow(final Player ai, CardCollection oppCards, final boolean temporary) {
|
||||
if (!CardLists.getNotType(oppCards, "Creature").isEmpty()) {
|
||||
// non-creatures were passed, nothing to do here
|
||||
return oppCards;
|
||||
}
|
||||
|
||||
boolean enablePriorityRemoval = false;
|
||||
boolean priorityRemovalOnlyInDanger = false;
|
||||
int priorityRemovalThreshold = 0;
|
||||
int lifeInDanger = 5;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
enablePriorityRemoval = aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
|
||||
priorityRemovalThreshold = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
|
||||
priorityRemovalOnlyInDanger = aic.getBooleanProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
|
||||
lifeInDanger = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
|
||||
}
|
||||
|
||||
if (!enablePriorityRemoval) {
|
||||
// Nothing to do here, the profile does not allow prioritizing
|
||||
return oppCards;
|
||||
}
|
||||
|
||||
CardCollection aiCreats = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||
if (temporary) {
|
||||
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
|
||||
oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED);
|
||||
}
|
||||
|
||||
CardCollection priorityCards = new CardCollection();
|
||||
for (Card atk : oppCards) {
|
||||
if (isUselessCreature(atk.getController(), atk)) {
|
||||
continue;
|
||||
}
|
||||
for (Card blk : aiCreats) {
|
||||
if (!CombatUtil.canBlock(atk, blk, true)) {
|
||||
boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
|
||||
if (!priorityRemovalOnlyInDanger || threat) {
|
||||
priorityCards.add(atk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!priorityCards.isEmpty() && priorityCards.size() <= priorityRemovalThreshold) {
|
||||
return priorityCards;
|
||||
}
|
||||
|
||||
return oppCards;
|
||||
}
|
||||
|
||||
public static AiPlayDecision checkNeedsToPlayReqs(final Card card, final SpellAbility sa) {
|
||||
Game game = card.getGame();
|
||||
boolean isRightSplit = sa != null && sa.isRightSplit();
|
||||
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
|
||||
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
|
||||
|
||||
if (card.hasSVar(needsToPlayName)) {
|
||||
final String needsToPlay = card.getSVar(needsToPlayName);
|
||||
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
|
||||
if (list.isEmpty()) {
|
||||
return AiPlayDecision.MissingNeededCards;
|
||||
}
|
||||
}
|
||||
if (card.getSVar(needsToPlayVarName).length() > 0) {
|
||||
final String needsToPlay = card.getSVar(needsToPlayVarName);
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
String sVar = needsToPlay.split(" ")[0];
|
||||
String comparator = needsToPlay.split(" ")[1];
|
||||
String compareTo = comparator.substring(2);
|
||||
try {
|
||||
x = Integer.parseInt(sVar);
|
||||
} catch (final NumberFormatException e) {
|
||||
x = CardFactoryUtil.xCount(card, card.getSVar(sVar));
|
||||
}
|
||||
try {
|
||||
y = Integer.parseInt(compareTo);
|
||||
} catch (final NumberFormatException e) {
|
||||
y = CardFactoryUtil.xCount(card, card.getSVar(compareTo));
|
||||
}
|
||||
if (!Expressions.compare(x, comparator, y)) {
|
||||
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||
}
|
||||
}
|
||||
|
||||
return AiPlayDecision.WillPlay;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package forge.ai;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
@@ -32,16 +33,11 @@ import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
@@ -53,6 +49,7 @@ import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
|
||||
@@ -109,7 +106,8 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final String keyword : atacker.getKeywords()) {
|
||||
for (final KeywordInterface inst : atacker.getKeywords()) {
|
||||
final String keyword = inst.getOriginal();
|
||||
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
|
||||
final String defined = keyword.split(":")[1];
|
||||
final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0);
|
||||
@@ -385,11 +383,27 @@ public class ComputerUtilCombat {
|
||||
* @return a boolean.
|
||||
*/
|
||||
public static boolean lifeInDanger(final Player ai, final Combat combat) {
|
||||
return lifeInDanger(ai, combat, 0);
|
||||
}
|
||||
|
||||
public static boolean lifeInDanger(final Player ai, final Combat combat, final int payment) {
|
||||
// life in danger only cares about the player's life. Not Planeswalkers' life
|
||||
if (ai.cantLose() || combat == null || combat.getAttackingPlayer() == ai) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
|
||||
// Special cases:
|
||||
// AI can't lose in combat in presence of Worship (with creatures)
|
||||
if (!CardLists.filter(otb, CardPredicates.nameEquals("Worship")).isEmpty() && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// AI can't lose in combat in presence of Elderscale Wurm (at 7 life or more)
|
||||
if (!CardLists.filter(otb, CardPredicates.nameEquals("Elderscale Wurm")).isEmpty() && ai.getLife() >= 7) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// check for creatures that must be blocked
|
||||
final List<Card> attackers = combat.getAttackersOf(ai);
|
||||
|
||||
@@ -401,15 +415,37 @@ public class ComputerUtilCombat {
|
||||
|
||||
if (blockers.isEmpty()) {
|
||||
if (!attacker.getSVar("MustBeBlocked").equals("")) {
|
||||
boolean cond = false;
|
||||
String condVal = attacker.getSVar("MustBeBlocked");
|
||||
boolean isAttackingPlayer = combat.getDefenderByAttacker(attacker) instanceof Player;
|
||||
|
||||
cond |= "true".equalsIgnoreCase(condVal);
|
||||
cond |= "attackingplayer".equalsIgnoreCase(condVal) && isAttackingPlayer;
|
||||
cond |= "attackingplayerconservative".equalsIgnoreCase(condVal) && isAttackingPlayer
|
||||
&& ai.getCreaturesInPlay().size() >= 3 && ai.getCreaturesInPlay().size() > attacker.getController().getCreaturesInPlay().size();
|
||||
|
||||
if (cond) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (threateningCommanders.contains(attacker)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) < Math.min(4, ai.getLife())
|
||||
int threshold = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
|
||||
int maxTreshold = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD)) - threshold;
|
||||
Random rand = new Random();
|
||||
int chance = rand.nextInt(80) + 5;
|
||||
while (maxTreshold > 0) {
|
||||
if (rand.nextInt(100) < chance) {
|
||||
threshold++;
|
||||
}
|
||||
maxTreshold--;
|
||||
}
|
||||
|
||||
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())
|
||||
&& !ai.cantLoseForZeroOrLessLife()) {
|
||||
return true;
|
||||
}
|
||||
@@ -443,6 +479,10 @@ public class ComputerUtilCombat {
|
||||
* @return a boolean.
|
||||
*/
|
||||
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) {
|
||||
return lifeInSeriousDanger(ai, combat, 0);
|
||||
}
|
||||
|
||||
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) {
|
||||
// life in danger only cares about the player's life. Not about a
|
||||
// Planeswalkers life
|
||||
if (ai.cantLose() || combat == null) {
|
||||
@@ -468,7 +508,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) < 1 && !ai.cantLoseForZeroOrLessLife()) {
|
||||
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) - payment < 1 && !ai.cantLoseForZeroOrLessLife()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -683,12 +723,21 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static boolean attackerWouldBeDestroyed(Player ai, final Card attacker, Combat combat) {
|
||||
final List<Card> blockers = combat.getBlockers(attacker);
|
||||
int firstStrikeBlockerDmg = 0;
|
||||
|
||||
for (final Card defender : blockers) {
|
||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
|
||||
&& !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
|
||||
return true;
|
||||
}
|
||||
if (defender.hasKeyword("First Strike") || defender.hasKeyword("Double Strike")) {
|
||||
firstStrikeBlockerDmg += defender.getNetCombatDamage();
|
||||
}
|
||||
}
|
||||
|
||||
// Consider first strike and double strike
|
||||
if (attacker.hasKeyword("First Strike") || attacker.hasKeyword("Double Strike")) {
|
||||
return firstStrikeBlockerDmg >= ComputerUtilCombat.getDamageToKill(attacker);
|
||||
}
|
||||
|
||||
return ComputerUtilCombat.totalDamageOfBlockers(attacker, blockers) >= ComputerUtilCombat.getDamageToKill(attacker);
|
||||
@@ -781,7 +830,7 @@ public class ComputerUtilCombat {
|
||||
if (validBlocked.contains(".withLesserPower")) {
|
||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||
// ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
|
||||
validBlocked = validBlocked.replace(".withLesserPower", "");
|
||||
validBlocked = TextUtil.fastReplace(validBlocked, ".withLesserPower", "");
|
||||
if (defender.getCurrentPower() <= attacker.getCurrentPower()) {
|
||||
return false;
|
||||
}
|
||||
@@ -795,7 +844,7 @@ public class ComputerUtilCombat {
|
||||
if (validBlocker.contains(".withLesserPower")) {
|
||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
|
||||
validBlocker = validBlocker.replace(".withLesserPower", "");
|
||||
validBlocker = TextUtil.fastReplace(validBlocker, ".withLesserPower", "");
|
||||
if (defender.getCurrentPower() >= attacker.getCurrentPower()) {
|
||||
return false;
|
||||
}
|
||||
@@ -854,9 +903,12 @@ public class ComputerUtilCombat {
|
||||
public static int predictPowerBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
|
||||
int power = 0;
|
||||
|
||||
// Apparently, Flanking is predicted below from a trigger, so using the code below results in double
|
||||
// application of power bonus. A bit more testing may be needed though, so commenting out for now.
|
||||
/*
|
||||
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
||||
power -= attacker.getAmountOfKeyword("Flanking");
|
||||
}
|
||||
}*/
|
||||
|
||||
// Serene Master switches power with attacker
|
||||
if (blocker.getName().equals("Serene Master")) {
|
||||
@@ -874,8 +926,6 @@ public class ComputerUtilCombat {
|
||||
power -= attacker.getNetCombatDamage();
|
||||
}
|
||||
|
||||
power += blocker.getKeywordMagnitude("Bushido");
|
||||
|
||||
final Game game = attacker.getGame();
|
||||
// look out for continuous static abilities that only care for blocking
|
||||
// creatures
|
||||
@@ -889,7 +939,7 @@ public class ComputerUtilCombat {
|
||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = params.get("Affected").replace("blocking", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "blocking", "Creature");
|
||||
if (!blocker.isValid(valid, card.getController(), card, null)) {
|
||||
continue;
|
||||
}
|
||||
@@ -967,6 +1017,9 @@ public class ComputerUtilCombat {
|
||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||
continue;
|
||||
}
|
||||
if (ability.usesTargeting() && !ability.canTarget(blocker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
@@ -1024,7 +1077,6 @@ public class ComputerUtilCombat {
|
||||
toughness += attacker.getNetToughness() - blocker.getNetToughness();
|
||||
}
|
||||
|
||||
toughness += blocker.getKeywordMagnitude("Bushido");
|
||||
final Game game = attacker.getGame();
|
||||
final FCollection<Trigger> theTriggers = new FCollection<Trigger>();
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1129,6 +1181,9 @@ public class ComputerUtilCombat {
|
||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||
continue;
|
||||
}
|
||||
if (ability.usesTargeting() && !ability.canTarget(blocker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
@@ -1177,9 +1232,11 @@ public class ComputerUtilCombat {
|
||||
* @return a int.
|
||||
*/
|
||||
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat, boolean withoutAbilities) {
|
||||
return predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat, boolean withoutAbilities, boolean withoutCombatStaticAbilities) {
|
||||
int power = 0;
|
||||
|
||||
power += attacker.getKeywordMagnitude("Bushido");
|
||||
//check Exalted only for the first attacker
|
||||
if (combat != null && combat.getAttackers().isEmpty()) {
|
||||
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1216,6 +1273,7 @@ public class ComputerUtilCombat {
|
||||
|
||||
// look out for continuous static abilities that only care for attacking
|
||||
// creatures
|
||||
if (!withoutCombatStaticAbilities) {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
@@ -1226,7 +1284,7 @@ public class ComputerUtilCombat {
|
||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1241,6 +1299,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Trigger trigger : theTriggers) {
|
||||
final Map<String, String> trigParams = trigger.getMapParams();
|
||||
@@ -1305,9 +1364,13 @@ public class ComputerUtilCombat {
|
||||
} else {
|
||||
String bonus = new String(source.getSVar(att));
|
||||
if (bonus.contains("TriggerCount$NumBlockers")) {
|
||||
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
|
||||
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
|
||||
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
} else if (bonus.contains("TriggeredAttacker$CardPower")) { // e.g. Arahbo, Roar of the World
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardPower", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetPower())));
|
||||
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
|
||||
}
|
||||
power += CardFactoryUtil.xCount(source, bonus);
|
||||
|
||||
@@ -1323,6 +1386,9 @@ public class ComputerUtilCombat {
|
||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||
continue;
|
||||
}
|
||||
if (ability.usesTargeting() && !ability.canTarget(attacker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
@@ -1372,6 +1438,10 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
|
||||
, boolean withoutAbilities) {
|
||||
return predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
|
||||
, boolean withoutAbilities, boolean withoutCombatStaticAbilities) {
|
||||
int toughness = 0;
|
||||
|
||||
//check Exalted only for the first attacker
|
||||
@@ -1394,12 +1464,12 @@ public class ComputerUtilCombat {
|
||||
theTriggers.addAll(card.getTriggers());
|
||||
}
|
||||
if (blocker != null) {
|
||||
toughness += attacker.getKeywordMagnitude("Bushido");
|
||||
theTriggers.addAll(blocker.getTriggers());
|
||||
}
|
||||
|
||||
// look out for continuous static abilities that only care for attacking
|
||||
// creatures
|
||||
if (!withoutCombatStaticAbilities) {
|
||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
@@ -1408,7 +1478,7 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
|
||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1422,7 +1492,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
|
||||
final String valid = params.get("Affected").replace("untapped", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)
|
||||
|| attacker.hasKeyword("Vigilance")) {
|
||||
continue;
|
||||
@@ -1434,6 +1504,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Trigger trigger : theTriggers) {
|
||||
final Map<String, String> trigParams = trigger.getMapParams();
|
||||
@@ -1517,9 +1588,9 @@ public class ComputerUtilCombat {
|
||||
} else {
|
||||
String bonus = new String(source.getSVar(def));
|
||||
if (bonus.contains("TriggerCount$NumBlockers")) {
|
||||
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
|
||||
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
|
||||
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
}
|
||||
toughness += CardFactoryUtil.xCount(source, bonus);
|
||||
}
|
||||
@@ -1535,6 +1606,9 @@ public class ComputerUtilCombat {
|
||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||
continue;
|
||||
}
|
||||
if (ability.usesTargeting() && !ability.canTarget(attacker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
@@ -1674,6 +1748,10 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
|
||||
final boolean withoutAbilities) {
|
||||
return canDestroyAttacker(ai, attacker, blocker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
|
||||
final boolean withoutAbilities, final boolean withoutAttackerStaticAbilities) {
|
||||
// Can activate transform ability
|
||||
if (!withoutAbilities) {
|
||||
attacker = canTransform(attacker);
|
||||
@@ -1725,10 +1803,10 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
if (attacker.toughnessAssignsDamage()) {
|
||||
attackerDamage = attacker.getNetToughness()
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
} else {
|
||||
attackerDamage = attacker.getNetPower()
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
}
|
||||
|
||||
int possibleDefenderPrevention = 0;
|
||||
@@ -1741,11 +1819,16 @@ public class ComputerUtilCombat {
|
||||
// consider Damage Prevention/Replacement
|
||||
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
|
||||
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
|
||||
if (!attacker.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
|
||||
if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
||||
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
if (blocker.hasKeyword("Double Strike")) {
|
||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||
@@ -1914,6 +1997,10 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
|
||||
final boolean withoutAbilities) {
|
||||
return canDestroyBlocker(ai, blocker, attacker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
|
||||
final boolean withoutAbilities, final boolean withoutAttackerStaticAbilities) {
|
||||
// Can activate transform ability
|
||||
if (!withoutAbilities) {
|
||||
attacker = canTransform(attacker);
|
||||
@@ -1947,10 +2034,10 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
if (attacker.toughnessAssignsDamage()) {
|
||||
attackerDamage = attacker.getNetToughness()
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
} else {
|
||||
attackerDamage = attacker.getNetPower()
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
}
|
||||
|
||||
int possibleDefenderPrevention = 0;
|
||||
@@ -1975,7 +2062,7 @@ public class ComputerUtilCombat {
|
||||
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
||||
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
if (attacker.hasKeyword("Double Strike")) {
|
||||
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
|
||||
@@ -2192,6 +2279,7 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public final static int getDamageToKill(final Card c) {
|
||||
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
|
||||
|
||||
if ((killDamage > c.getPreventNextDamageTotalShields())
|
||||
&& c.hasSVar("DestroyWhenDamaged")) {
|
||||
killDamage = 1 + c.getPreventNextDamageTotalShields();
|
||||
@@ -2398,7 +2486,7 @@ public class ComputerUtilCombat {
|
||||
for (SpellAbility sa : original.getSpellAbilities()) {
|
||||
if (sa.getApi() == ApiType.SetState && ComputerUtilCost.canPayCost(sa, original.getController())) {
|
||||
Card transformed = CardUtil.getLKICopy(original);
|
||||
transformed.getCurrentState().copyFrom(original, original.getAlternateState());
|
||||
transformed.getCurrentState().copyFrom(original.getAlternateState(), true);
|
||||
transformed.updateStateForView();
|
||||
return transformed;
|
||||
}
|
||||
@@ -2431,6 +2519,95 @@ public class ComputerUtilCombat {
|
||||
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
|
||||
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
|
||||
}
|
||||
|
||||
public static int getMaxAttackersFor(final GameEntity defender) {
|
||||
if (defender instanceof Player) {
|
||||
for (final Card card : ((Player) defender).getCardsIn(ZoneType.Battlefield)) {
|
||||
if (card.hasKeyword("No more than one creature can attack you each combat.")) {
|
||||
return 1;
|
||||
} else if (card.hasKeyword("No more than two creatures can attack you each combat.")) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<Card> categorizeAttackersByEvasion(List<Card> attackers) {
|
||||
List<Card> categorizedAttackers = Lists.newArrayList();
|
||||
|
||||
CardCollection withEvasion = new CardCollection();
|
||||
CardCollection withoutEvasion = new CardCollection();
|
||||
|
||||
for (Card atk : attackers) {
|
||||
boolean hasProtection = false;
|
||||
for (KeywordInterface inst : atk.getKeywords()) {
|
||||
String kw = inst.getOriginal();
|
||||
if (kw.startsWith("Protection")) {
|
||||
hasProtection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (atk.hasKeyword("Flying") || atk.hasKeyword("Shadow")
|
||||
|| atk.hasKeyword("Horsemanship") || (atk.hasKeyword("Fear")
|
||||
|| atk.hasKeyword("Intimidate") || atk.hasKeyword("Skulk") || hasProtection)) {
|
||||
withEvasion.add(atk);
|
||||
} else {
|
||||
withoutEvasion.add(atk);
|
||||
}
|
||||
}
|
||||
|
||||
// attackers that can only be blocked by cards with specific keywords or color, etc.
|
||||
// (maybe will need to split into 2 or 3 tiers depending on importance)
|
||||
categorizedAttackers.addAll(withEvasion);
|
||||
// all other attackers that have no evasion
|
||||
// (Menace and other abilities that limit blocking by amount of blockers is likely handled
|
||||
// elsewhere, but that needs testing and possibly fine-tuning).
|
||||
categorizedAttackers.addAll(withoutEvasion);
|
||||
|
||||
return categorizedAttackers;
|
||||
}
|
||||
|
||||
public static Card applyPotentialAttackCloneTriggers(Card attacker) {
|
||||
// This method returns the potentially cloned card if the creature turns into something else during the attack
|
||||
// (currently looks for the creature with maximum raw power since that's what the AI usually judges by when
|
||||
// deciding whether the creature is worth blocking).
|
||||
// If the creature doesn't change into anything, returns the original creature.
|
||||
if (attacker == null) { return null; }
|
||||
Card attackerAfterTrigs = attacker;
|
||||
|
||||
// Test for some special triggers that can change the creature in combat
|
||||
for (Trigger t : attacker.getTriggers()) {
|
||||
if (t.getMode() == TriggerType.Attacks && t.hasParam("Execute")) {
|
||||
if (!attacker.hasSVar(t.getParam("Execute"))) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility exec = AbilityFactory.getAbility(attacker, t.getParam("Execute"));
|
||||
if (exec != null) {
|
||||
if (exec.getApi() == ApiType.Clone && "Self".equals(exec.getParam("CloneTarget"))
|
||||
&& exec.hasParam("ValidTgts") && exec.getParam("ValidTgts").contains("Creature")
|
||||
&& exec.getParam("ValidTgts").contains("attacking")) {
|
||||
// Tilonalli's Skinshifter and potentially other similar cards that can clone other stuff
|
||||
// while attacking
|
||||
if (exec.getParam("ValidTgts").contains("nonLegendary") && attacker.getType().isLegendary()) {
|
||||
continue;
|
||||
}
|
||||
int maxPwr = 0;
|
||||
for (Card c : attacker.getController().getCreaturesInPlay()) {
|
||||
if (c.getNetPower() > maxPwr || (c.getNetPower() == maxPwr && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(attackerAfterTrigs))) {
|
||||
maxPwr = c.getNetPower();
|
||||
attackerAfterTrigs = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attackerAfterTrigs;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.ai.ability.AnimateAi;
|
||||
import forge.card.ColorSet;
|
||||
import forge.game.GameActionUtil;
|
||||
@@ -27,6 +21,10 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public class ComputerUtilCost {
|
||||
@@ -90,6 +88,15 @@ public class ComputerUtilCost {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove X counters - set ChosenX to max possible value here, the SAs should correct that
|
||||
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
|
||||
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
|
||||
final String sVar = sa.getSVar(remCounter.getAmount());
|
||||
if (sVar.equals("XChoice")) {
|
||||
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
|
||||
}
|
||||
}
|
||||
|
||||
// check the sa what the PaymentDecision is.
|
||||
// ignore Loyality abilities with Zero as Cost
|
||||
if (sa != null && !CounterType.LOYALTY.equals(type)) {
|
||||
@@ -231,13 +238,15 @@ public class ComputerUtilCost {
|
||||
* the source
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean checkCreatureSacrificeCost(final Player ai, final Cost cost, final Card source) {
|
||||
public static boolean checkCreatureSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
if (sac.payCostFromSource() && source.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
@@ -247,10 +256,19 @@ public class ComputerUtilCost {
|
||||
continue;
|
||||
}
|
||||
|
||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(","), source.getController(), source, null);
|
||||
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
|
||||
final CardCollection sacList = new CardCollection();
|
||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
||||
|
||||
int count = 0;
|
||||
while (count < amount) {
|
||||
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
|
||||
if (prefCard == null) {
|
||||
return false;
|
||||
}
|
||||
sacList.add(prefCard);
|
||||
typeList.remove(prefCard);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -267,13 +285,14 @@ public class ComputerUtilCost {
|
||||
* is the gain important enough?
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final boolean important) {
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility, final boolean important) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
final String type = sac.getType();
|
||||
|
||||
@@ -286,10 +305,20 @@ public class ComputerUtilCost {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
final CardCollection sacList = new CardCollection();
|
||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
||||
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
|
||||
|
||||
int count = 0;
|
||||
while (count < amount) {
|
||||
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
|
||||
if (prefCard == null) {
|
||||
return false;
|
||||
}
|
||||
sacList.add(prefCard);
|
||||
typeList.remove(prefCard);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -336,7 +365,7 @@ public class ComputerUtilCost {
|
||||
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
|
||||
String type = part.getType();
|
||||
String totalP = type.split("withTotalPowerGE")[1];
|
||||
type = type.replace("+withTotalPowerGE" + totalP, "");
|
||||
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
|
||||
CardCollection exclude = CardLists.getValidCards(
|
||||
new CardCollection(ai.getCardsIn(ZoneType.Battlefield)), type.split(";"),
|
||||
source.getController(), source, sa);
|
||||
@@ -364,8 +393,8 @@ public class ComputerUtilCost {
|
||||
* the source
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source) {
|
||||
return checkSacrificeCost(ai, cost, source, true);
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
|
||||
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,12 +590,12 @@ public class ComputerUtilCost {
|
||||
|
||||
return checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& checkDamageCost(payer, cost, source, 4)
|
||||
&& (isMine || checkSacrificeCost(payer, cost, source))
|
||||
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || checkDiscardCost(payer, cost, source))
|
||||
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
|
||||
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
|
||||
&& (!source.getName().equals("Chain of Vapor") || (payer.getOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||
&& (!source.getName().equals("Chain of Vapor") || (ComputerUtil.getOpponentFor(payer).getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||
}
|
||||
|
||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package forge.ai;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.*;
|
||||
import forge.ai.ability.AnimateAi;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
@@ -18,11 +14,7 @@ import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostAdjustment;
|
||||
@@ -41,7 +33,6 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
@@ -203,7 +194,8 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sa.getHostCard() != null && sa.getApi() == ApiType.Animate) {
|
||||
if (sa.getHostCard() != null) {
|
||||
if (sa.getApi() == ApiType.Animate) {
|
||||
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
|
||||
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
||||
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard()
|
||||
@@ -213,31 +205,61 @@ public class ComputerUtilMana {
|
||||
|
||||
// If a manland was previously animated this turn, do not tap it to animate another manland
|
||||
if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
|
||||
&& ai.getController() instanceof PlayerControllerAi
|
||||
&& ai.getController().isAI()
|
||||
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
|
||||
continue;
|
||||
}
|
||||
} else if (sa.getApi() == ApiType.Pump) {
|
||||
if ((sa.getHostCard().isInstant() || sa.getHostCard().isSorcery())
|
||||
&& ma.getHostCard().isCreature()
|
||||
&& ai.getController().isAI()
|
||||
&& ma.getPayCosts().hasTapCost()
|
||||
&& sa.getTargets().getTargetCards().contains(ma.getHostCard())) {
|
||||
// do not activate pump instants/sorceries targeting creatures by tapping targeted
|
||||
// creatures for mana (for example, Servant of the Conduit)
|
||||
continue;
|
||||
}
|
||||
} else if (sa.getApi() == ApiType.Attach
|
||||
&& "AvoidPayingWithAttachTarget".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
|
||||
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
|
||||
// be tapped to pay its own cost if there's another untapped land like that available
|
||||
if (ma.getHostCard().equals(sa.getTargetCard())) {
|
||||
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)).size() > 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SpellAbility paymentChoice = ma;
|
||||
|
||||
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
|
||||
// to attempt to make the spell uncounterable when possible.
|
||||
if ((toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)
|
||||
&& ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
||||
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
||||
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
|
||||
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
|
||||
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
|
||||
continue;
|
||||
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
|
||||
for (SpellAbility ab : saList) {
|
||||
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
||||
return ab;
|
||||
if (!ab.getHostCard().isTapped()) {
|
||||
paymentChoice = ab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String typeRes = cost.getSourceRestriction();
|
||||
if (StringUtils.isNotBlank(typeRes) && !ma.getHostCard().getType().hasStringType(typeRes)) {
|
||||
if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (canPayShardWithSpellAbility(toPay, ai, ma, sa, checkCosts)) {
|
||||
return ma;
|
||||
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts)) {
|
||||
return paymentChoice;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -491,6 +513,16 @@ public class ComputerUtilMana {
|
||||
// extraMana, sa.getHostCard(), sa.toUnsuppressedString(), StringUtils.join(paymentPlan, "\n\t"));
|
||||
// }
|
||||
|
||||
// See if it's possible to pay with something that was left in the mana pool in corner cases,
|
||||
// e.g. Gemstone Caverns with a Luck counter on it generating colored mana (which fails to be
|
||||
// processed correctly on a per-ability basis, leaving floating mana in the pool)
|
||||
if (!cost.isPaid() && !manapool.isEmpty()) {
|
||||
for (byte color : MagicColor.WUBRGC) {
|
||||
manapool.tryPayCostWithColor(color, sa, cost);
|
||||
}
|
||||
}
|
||||
|
||||
// The cost is still unpaid, so refund the mana and report
|
||||
if (!cost.isPaid()) {
|
||||
refundMana(manaSpentToPay, ai, sa);
|
||||
if (test) {
|
||||
@@ -802,7 +834,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// isManaSourceReserved returns true if sourceCard is reserved as a mana source for payment
|
||||
// for the future spell to be cast in Mana 2. However, if "sa" (the spell ability that is
|
||||
// for the future spell to be cast in another phase. However, if "sa" (the spell ability that is
|
||||
// being considered for casting) is high priority, then mana source reservation will be
|
||||
// ignored.
|
||||
private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) {
|
||||
@@ -816,8 +848,21 @@ public class ComputerUtilMana {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
|
||||
|
||||
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
||||
|
||||
// For combat tricks, always obey mana reservation
|
||||
if (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP) {
|
||||
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
|
||||
}
|
||||
else {
|
||||
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) {
|
||||
// This mana source is held elsewhere for a combat trick.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a low priority spell (it's explicitly marked so elsewhere in the AI with a SVar), always
|
||||
// obey mana reservations; otherwise, obey mana reservations depending on the "chance to reserve"
|
||||
// obey mana reservations for Main 2; otherwise, obey mana reservations depending on the "chance to reserve"
|
||||
// AI profile variable.
|
||||
if (sa.getSVar("LowPriorityAI").equals("")) {
|
||||
if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) {
|
||||
@@ -825,16 +870,16 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
||||
if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) {
|
||||
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
|
||||
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
}
|
||||
else {
|
||||
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
|
||||
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
|
||||
// This mana source is held elsewhere for a Main Phase 2 spell.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1092,8 +1137,65 @@ public class ComputerUtilMana {
|
||||
return cost;
|
||||
}
|
||||
|
||||
// This method can be used to estimate the total amount of mana available to the player,
|
||||
// including the mana available in that player's mana pool
|
||||
public static int getAvailableManaEstimate(final Player p) {
|
||||
return getAvailableManaEstimate(p, true);
|
||||
}
|
||||
|
||||
public static int getAvailableManaEstimate(final Player p, final boolean checkPlayable) {
|
||||
int availableMana = 0;
|
||||
|
||||
final CardCollectionView list = new CardCollection(p.getCardsIn(ZoneType.Battlefield));
|
||||
final List<Card> srcs = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return !c.getManaAbilities().isEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
int maxProduced = 0;
|
||||
int producedWithCost = 0;
|
||||
boolean hasSourcesWithNoManaCost = false;
|
||||
|
||||
for (Card src : srcs) {
|
||||
maxProduced = 0;
|
||||
|
||||
for (SpellAbility ma : src.getManaAbilities()) {
|
||||
ma.setActivatingPlayer(p);
|
||||
if (!checkPlayable || ma.canPlay()) {
|
||||
int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
|
||||
int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length;
|
||||
int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma);
|
||||
|
||||
int producedTotal = producedMana * producedAmount - costsToActivate;
|
||||
|
||||
if (costsToActivate > 0) {
|
||||
producedWithCost += producedTotal;
|
||||
} else if (!hasSourcesWithNoManaCost) {
|
||||
hasSourcesWithNoManaCost = true;
|
||||
}
|
||||
|
||||
if (producedTotal > maxProduced) {
|
||||
maxProduced = producedTotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableMana += maxProduced;
|
||||
}
|
||||
|
||||
availableMana += p.getManaPool().totalMana();
|
||||
|
||||
if (producedWithCost > 0 && !hasSourcesWithNoManaCost) {
|
||||
availableMana -= producedWithCost; // probably can't activate them, no other mana available
|
||||
}
|
||||
|
||||
return availableMana;
|
||||
}
|
||||
|
||||
//This method is currently used by AI to estimate available mana
|
||||
public static CardCollection getAvailableMana(final Player ai, final boolean checkPlayable) {
|
||||
public static CardCollection getAvailableManaSources(final Player ai, final boolean checkPlayable) {
|
||||
final CardCollectionView list = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand));
|
||||
final List<Card> manaSources = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -1200,7 +1302,7 @@ public class ComputerUtilMana {
|
||||
System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources);
|
||||
}
|
||||
return sortedManaSources;
|
||||
} // getAvailableMana()
|
||||
} // getAvailableManaSources()
|
||||
|
||||
//This method is currently used by AI to estimate mana available
|
||||
private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final Player ai, boolean checkPlayable) {
|
||||
@@ -1221,7 +1323,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// Loop over all current available mana sources
|
||||
for (final Card sourceCard : getAvailableMana(ai, checkPlayable)) {
|
||||
for (final Card sourceCard : getAvailableManaSources(ai, checkPlayable)) {
|
||||
if (DEBUG_MANA_PAYMENT) {
|
||||
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor sourceCard = " + sourceCard);
|
||||
}
|
||||
@@ -1264,7 +1366,7 @@ public class ComputerUtilMana {
|
||||
Card crd = replacementEffect.getHostCard();
|
||||
String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement"));
|
||||
if (repType.contains("Chosen")) {
|
||||
repType = repType.replace("Chosen", MagicColor.toShortString(crd.getChosenColor()));
|
||||
repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor()));
|
||||
}
|
||||
mp.setManaReplaceType(repType);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ package forge.ai;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
@@ -19,13 +24,18 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
}
|
||||
|
||||
public int evaluateCreature(final Card c) {
|
||||
return evaluateCreature(c, true, true);
|
||||
}
|
||||
|
||||
public int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
|
||||
int value = 80;
|
||||
if (!c.isToken()) {
|
||||
value += addValue(20, "non-token"); // tokens should be worth less than actual cards
|
||||
}
|
||||
int power = getEffectivePower(c);
|
||||
final int toughness = getEffectiveToughness(c);
|
||||
for (String keyword : c.getKeywords()) {
|
||||
for (KeywordInterface kw : c.getKeywords()) {
|
||||
String keyword = kw.getOriginal();
|
||||
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
@@ -34,9 +44,13 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (considerPT) {
|
||||
value += addValue(power * 15, "power");
|
||||
value += addValue(toughness * 10, "toughness: " + toughness);
|
||||
}
|
||||
if (considerCMC) {
|
||||
value += addValue(c.getCMC() * 5, "cmc");
|
||||
}
|
||||
|
||||
// Evasion keywords
|
||||
if (c.hasKeyword("Flying")) {
|
||||
@@ -91,6 +105,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(power * 15, "infect");
|
||||
}
|
||||
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
|
||||
value += addValue(c.getKeywordMagnitude("Afflict") * 5, "afflict");
|
||||
}
|
||||
|
||||
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
|
||||
@@ -99,6 +114,14 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
|
||||
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
|
||||
|
||||
// Keywords that may produce temporary or permanent buffs over time
|
||||
if (c.hasKeyword("Prowess")) {
|
||||
value += addValue(5, "prowess");
|
||||
}
|
||||
if (c.hasKeyword("Outlast")) {
|
||||
value += addValue(10, "outlast");
|
||||
}
|
||||
|
||||
// Defensive Keywords
|
||||
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
||||
value += addValue(5, "reach");
|
||||
@@ -184,7 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(10, "sa: " + sa);
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
@@ -203,9 +226,44 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
if (!c.getEncodedCards().isEmpty()) {
|
||||
value += addValue(24, "encoded");
|
||||
}
|
||||
|
||||
// card-specific evaluation modifier
|
||||
if (c.hasSVar("AIEvaluationModifier")) {
|
||||
int mod = AbilityUtils.calculateAmount(c, c.getSVar("AIEvaluationModifier"), null);
|
||||
value += mod;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private int evaluateSpellAbility(SpellAbility sa) {
|
||||
// Pump abilities
|
||||
if (sa.getApi() == ApiType.Pump) {
|
||||
// Pump abilities that grant +X/+X to the card
|
||||
if ("+X".equals(sa.getParam("NumAtt"))
|
||||
&& "+X".equals(sa.getParam("NumDef"))
|
||||
&& !sa.usesTargeting()
|
||||
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
|
||||
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
// Electrostatic Pummeler, can be expanded for similar cards
|
||||
int initPower = getEffectivePower(sa.getHostCard());
|
||||
int pumpedPower = initPower;
|
||||
int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY);
|
||||
if (energy > 0) {
|
||||
int numActivations = energy / 3;
|
||||
for (int i = 0; i < numActivations; i++) {
|
||||
pumpedPower *= 2;
|
||||
}
|
||||
return (pumpedPower - initPower) * 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default value
|
||||
return 10;
|
||||
}
|
||||
|
||||
protected int addValue(int value, String text) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import forge.StaticData;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
@@ -21,16 +13,29 @@ import forge.game.ability.effects.DetachedCardEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.event.GameEventAttackersDeclared;
|
||||
import forge.game.event.GameEventCombatChanged;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.PlayerZone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperCard;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public abstract class GameState {
|
||||
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
|
||||
@@ -48,17 +53,42 @@ public abstract class GameState {
|
||||
private String humanCounters = "";
|
||||
private String computerCounters = "";
|
||||
|
||||
private boolean puzzleCreatorState = false;
|
||||
|
||||
private final Map<ZoneType, String> humanCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
|
||||
private final Map<ZoneType, String> aiCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
|
||||
|
||||
private final Map<Integer, Card> idToCard = new HashMap<>();
|
||||
private final Map<Card, Integer> cardToAttachId = new HashMap<>();
|
||||
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
||||
private final Map<Card, String> cardToChosenType = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
|
||||
private final Map<Card, String> cardToNamedCard = new HashMap<>();
|
||||
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
|
||||
private final Map<Card, Card> cardAttackMap = new HashMap<>();
|
||||
|
||||
private final Map<Card, String> cardToScript = new HashMap<>();
|
||||
|
||||
private final Map<String, String> abilityString = new HashMap<>();
|
||||
|
||||
private final Set<Card> cardsReferencedByID = new HashSet<>();
|
||||
private final Set<Card> cardsWithoutETBTrigs = new HashSet<>();
|
||||
|
||||
private String tChangePlayer = "NONE";
|
||||
private String tChangePhase = "NONE";
|
||||
|
||||
private String precastHuman = null;
|
||||
private String precastAI = null;
|
||||
|
||||
private int turn;
|
||||
|
||||
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
|
||||
private final int TARGET_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
|
||||
private final int TARGET_HUMAN = -2;
|
||||
private final int TARGET_AI = -3;
|
||||
|
||||
public GameState() {
|
||||
}
|
||||
|
||||
@@ -67,18 +97,32 @@ public abstract class GameState {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(String.format("humanlife=%d\n", humanLife));
|
||||
sb.append(String.format("ailife=%d\n", computerLife));
|
||||
|
||||
if (puzzleCreatorState) {
|
||||
// append basic puzzle metadata if we're dumping from the puzzle creator screen
|
||||
sb.append("[metadata]\n");
|
||||
sb.append("Name:New Puzzle\n");
|
||||
sb.append("URL:https://www.cardforge.org\n");
|
||||
sb.append("Goal:Win\n");
|
||||
sb.append("Turns:1\n");
|
||||
sb.append("Difficulty:Easy\n");
|
||||
sb.append("Description:Win this turn.\n");
|
||||
sb.append("[state]\n");
|
||||
}
|
||||
|
||||
sb.append(TextUtil.concatNoSpace("humanlife=", String.valueOf(humanLife), "\n"));
|
||||
sb.append(TextUtil.concatNoSpace("ailife=", String.valueOf(computerLife), "\n"));
|
||||
sb.append(TextUtil.concatNoSpace("turn=", String.valueOf(turn), "\n"));
|
||||
|
||||
if (!humanCounters.isEmpty()) {
|
||||
sb.append(String.format("humancounters=%s\n", humanCounters));
|
||||
sb.append(TextUtil.concatNoSpace("humancounters=", humanCounters, "\n"));
|
||||
}
|
||||
if (!computerCounters.isEmpty()) {
|
||||
sb.append(String.format("aicounters=%s\n", computerCounters));
|
||||
sb.append(TextUtil.concatNoSpace("aicounters=", computerCounters, "\n"));
|
||||
}
|
||||
|
||||
sb.append(String.format("activeplayer=%s\n", tChangePlayer));
|
||||
sb.append(String.format("activephase=%s\n", tChangePhase));
|
||||
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
|
||||
sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
|
||||
appendCards(humanCardTexts, "human", sb);
|
||||
appendCards(aiCardTexts, "ai", sb);
|
||||
return sb.toString();
|
||||
@@ -86,13 +130,13 @@ public abstract class GameState {
|
||||
|
||||
private void appendCards(Map<ZoneType, String> cardTexts, String categoryPrefix, StringBuilder sb) {
|
||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
||||
sb.append(String.format("%s%s=%s\n", categoryPrefix, ZONES.get(kv.getKey()), kv.getValue()));
|
||||
sb.append(TextUtil.concatNoSpace(categoryPrefix, ZONES.get(kv.getKey()), "=", kv.getValue(), "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
public void initFromGame(Game game) throws Exception {
|
||||
FCollectionView<Player> players = game.getPlayers();
|
||||
// Can only serialized a two player game with one AI and one human.
|
||||
// Can only serialize a two player game with one AI and one human.
|
||||
if (players.size() != 2) {
|
||||
throw new Exception("Game not supported");
|
||||
}
|
||||
@@ -108,14 +152,55 @@ public abstract class GameState {
|
||||
|
||||
tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human";
|
||||
tChangePhase = game.getPhaseHandler().getPhase().toString();
|
||||
turn = game.getPhaseHandler().getTurn();
|
||||
aiCardTexts.clear();
|
||||
humanCardTexts.clear();
|
||||
|
||||
// Mark the cards that need their ID remembered for various reasons
|
||||
cardsReferencedByID.clear();
|
||||
for (ZoneType zone : ZONES.keySet()) {
|
||||
for (Card card : game.getCardsIn(zone)) {
|
||||
if (card.getExiledWith() != null) {
|
||||
// Remember the ID of the card that exiled this card
|
||||
cardsReferencedByID.add(card.getExiledWith());
|
||||
}
|
||||
if (zone == ZoneType.Battlefield) {
|
||||
if (!card.getEnchantedBy(false).isEmpty()
|
||||
|| !card.getEquippedBy(false).isEmpty()
|
||||
|| !card.getFortifiedBy(false).isEmpty()) {
|
||||
// Remember the ID of cards that have attachments
|
||||
cardsReferencedByID.add(card);
|
||||
}
|
||||
}
|
||||
for (Object o : card.getRemembered()) {
|
||||
// Remember the IDs of remembered cards
|
||||
if (o instanceof Card) {
|
||||
cardsReferencedByID.add((Card)o);
|
||||
}
|
||||
}
|
||||
for (Card i : card.getImprintedCards()) {
|
||||
// Remember the IDs of imprinted cards
|
||||
cardsReferencedByID.add(i);
|
||||
}
|
||||
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
|
||||
// Remember the IDs of attacked planeswalkers
|
||||
GameEntity def = game.getCombat().getDefenderByAttacker(card);
|
||||
if (def instanceof Card) {
|
||||
cardsReferencedByID.add((Card)def);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ZoneType zone : ZONES.keySet()) {
|
||||
// Init texts to empty, so that restoring will clear the state
|
||||
// if the zone had no cards in it (e.g. empty hand).
|
||||
aiCardTexts.put(zone, "");
|
||||
humanCardTexts.put(zone, "");
|
||||
for (Card card : game.getCardsIn(zone)) {
|
||||
if (card.getName().equals("Puzzle Goal") && card.getOracleText().contains("New Puzzle")) {
|
||||
puzzleCreatorState = true;
|
||||
}
|
||||
if (card instanceof DetachedCardEffect) {
|
||||
continue;
|
||||
}
|
||||
@@ -130,7 +215,7 @@ public abstract class GameState {
|
||||
newText.append(";");
|
||||
}
|
||||
if (c.isToken()) {
|
||||
newText.append("t:" + new CardFactory.TokenInfo(c).toString());
|
||||
newText.append("t:" + new TokenInfo(c).toString());
|
||||
} else {
|
||||
if (c.getPaperCard() == null) {
|
||||
return;
|
||||
@@ -140,6 +225,11 @@ public abstract class GameState {
|
||||
if (c.isCommander()) {
|
||||
newText.append("|IsCommander");
|
||||
}
|
||||
|
||||
if (cardsReferencedByID.contains(c)) {
|
||||
newText.append("|Id:").append(c.getId());
|
||||
}
|
||||
|
||||
if (zoneType == ZoneType.Battlefield) {
|
||||
if (c.isTapped()) {
|
||||
newText.append("|Tapped");
|
||||
@@ -147,6 +237,16 @@ public abstract class GameState {
|
||||
if (c.isSick()) {
|
||||
newText.append("|SummonSick");
|
||||
}
|
||||
if (c.isRenowned()) {
|
||||
newText.append("|Renowned");
|
||||
}
|
||||
if (c.isMonstrous()) {
|
||||
newText.append("|Monstrous:");
|
||||
newText.append(c.getMonstrosityNum());
|
||||
}
|
||||
if (c.isPhasedOut()) {
|
||||
newText.append("|PhasedOut");
|
||||
}
|
||||
if (c.isFaceDown()) {
|
||||
newText.append("|FaceDown");
|
||||
if (c.isManifested()) {
|
||||
@@ -155,11 +255,10 @@ public abstract class GameState {
|
||||
}
|
||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||
newText.append("|Transformed");
|
||||
}
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
if (!counters.isEmpty()) {
|
||||
newText.append("|Counters:");
|
||||
newText.append(countersToString(counters));
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
newText.append("|Flipped");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||
newText.append("|Meld");
|
||||
}
|
||||
if (c.getEquipping() != null) {
|
||||
newText.append("|Attaching:").append(c.getEquipping().getId());
|
||||
@@ -169,10 +268,69 @@ public abstract class GameState {
|
||||
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
||||
}
|
||||
|
||||
if (!c.getEnchantedBy(false).isEmpty() || !c.getEquippedBy(false).isEmpty() || !c.getFortifiedBy(false).isEmpty()) {
|
||||
newText.append("|Id:").append(c.getId());
|
||||
if (c.getDamage() > 0) {
|
||||
newText.append("|Damage:").append(c.getDamage());
|
||||
}
|
||||
|
||||
if (!c.getChosenColor().isEmpty()) {
|
||||
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
|
||||
}
|
||||
if (!c.getChosenType().isEmpty()) {
|
||||
newText.append("|ChosenType:").append(c.getChosenType());
|
||||
}
|
||||
if (!c.getNamedCard().isEmpty()) {
|
||||
newText.append("|NamedCard:").append(c.getNamedCard());
|
||||
}
|
||||
|
||||
List<String> rememberedCardIds = Lists.newArrayList();
|
||||
for (Object obj : c.getRemembered()) {
|
||||
if (obj instanceof Card) {
|
||||
int id = ((Card)obj).getId();
|
||||
rememberedCardIds.add(String.valueOf(id));
|
||||
}
|
||||
}
|
||||
if (!rememberedCardIds.isEmpty()) {
|
||||
newText.append("|RememberedCards:").append(TextUtil.join(rememberedCardIds, ","));
|
||||
}
|
||||
|
||||
List<String> imprintedCardIds = Lists.newArrayList();
|
||||
for (Card impr : c.getImprintedCards()) {
|
||||
int id = impr.getId();
|
||||
imprintedCardIds.add(String.valueOf(id));
|
||||
}
|
||||
if (!imprintedCardIds.isEmpty()) {
|
||||
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneType == ZoneType.Exile) {
|
||||
if (c.getExiledWith() != null) {
|
||||
newText.append("|ExiledWith:").append(c.getExiledWith().getId());
|
||||
}
|
||||
if (c.isFaceDown()) {
|
||||
newText.append("|FaceDown"); // Exiled face down
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Exile) {
|
||||
// A card can have counters on the battlefield and in exile (e.g. exiled by Mairsil, the Pretender)
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
if (!counters.isEmpty()) {
|
||||
newText.append("|Counters:");
|
||||
newText.append(countersToString(counters));
|
||||
}
|
||||
}
|
||||
|
||||
if (c.getGame().getCombat() != null) {
|
||||
if (c.getGame().getCombat().isAttacking(c)) {
|
||||
newText.append("|Attacking");
|
||||
GameEntity def = c.getGame().getCombat().getDefenderByAttacker(c);
|
||||
if (def instanceof Card) {
|
||||
newText.append(":" + def.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cardTexts.put(zoneType, newText.toString());
|
||||
}
|
||||
|
||||
@@ -186,7 +344,7 @@ public abstract class GameState {
|
||||
}
|
||||
|
||||
first = false;
|
||||
counterString.append(String.format("%s=%d", kv.getKey().toString(), kv.getValue()));
|
||||
counterString.append(TextUtil.concatNoSpace(kv.getKey().toString(), "=", String.valueOf(kv.getValue())));
|
||||
}
|
||||
return counterString.toString();
|
||||
}
|
||||
@@ -238,6 +396,11 @@ public abstract class GameState {
|
||||
|
||||
boolean isHuman = categoryName.startsWith("human");
|
||||
|
||||
if (categoryName.equals("turn")) {
|
||||
turn = Integer.parseInt(categoryValue);
|
||||
} else {
|
||||
turn = 1;
|
||||
}
|
||||
if (categoryName.endsWith("life")) {
|
||||
if (isHuman)
|
||||
humanLife = Integer.parseInt(categoryValue);
|
||||
@@ -298,6 +461,12 @@ public abstract class GameState {
|
||||
abilityString.put(categoryName.substring("ability".length()), categoryValue);
|
||||
}
|
||||
|
||||
else if (categoryName.endsWith("precast")) {
|
||||
if (isHuman)
|
||||
precastHuman = categoryValue;
|
||||
else
|
||||
precastAI = categoryValue;
|
||||
}
|
||||
else {
|
||||
System.out.println("Unknown key: " + categoryName);
|
||||
}
|
||||
@@ -318,6 +487,13 @@ public abstract class GameState {
|
||||
|
||||
idToCard.clear();
|
||||
cardToAttachId.clear();
|
||||
cardToRememberedId.clear();
|
||||
cardToExiledWithId.clear();
|
||||
markedDamage.clear();
|
||||
cardToChosenClrs.clear();
|
||||
cardToChosenType.clear();
|
||||
cardToScript.clear();
|
||||
cardAttackMap.clear();
|
||||
|
||||
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
|
||||
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
|
||||
@@ -331,22 +507,369 @@ public abstract class GameState {
|
||||
if (!computerCounters.isEmpty()) {
|
||||
applyCountersToGameEntity(ai, computerCounters);
|
||||
}
|
||||
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
|
||||
|
||||
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
||||
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn, turn);
|
||||
|
||||
game.getTriggerHandler().setSuppressAllTriggers(true);
|
||||
|
||||
setupPlayerState(humanLife, humanCardTexts, human);
|
||||
setupPlayerState(computerLife, aiCardTexts, ai);
|
||||
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
|
||||
handleCardAttachments();
|
||||
handleChosenEntities();
|
||||
handleRememberedEntities();
|
||||
handleScriptExecution(game);
|
||||
handlePrecastSpells(game);
|
||||
handleMarkedDamage();
|
||||
|
||||
game.getTriggerHandler().setSuppressAllTriggers(false);
|
||||
|
||||
// Combat only works for 1v1 matches for now (which are the only matches dev mode supports anyway)
|
||||
// Note: triggers may fire during combat declarations ("whenever X attacks, ...", etc.)
|
||||
if (newPhase == PhaseType.COMBAT_DECLARE_ATTACKERS || newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS) {
|
||||
boolean toDeclareBlockers = newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS;
|
||||
handleCombat(game, newPlayerTurn, newPlayerTurn.getSingleOpponent(), toDeclareBlockers);
|
||||
}
|
||||
|
||||
game.getStack().setResolving(false);
|
||||
|
||||
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
|
||||
}
|
||||
|
||||
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
|
||||
// First we need to ensure that all attackers are declared in the Declare Attackers step,
|
||||
// even if proceeding straight to Declare Blockers
|
||||
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, attackingPlayer, turn);
|
||||
|
||||
if (game.getPhaseHandler().getCombat() == null) {
|
||||
game.getPhaseHandler().setCombat(new Combat(attackingPlayer));
|
||||
game.updateCombatForView();
|
||||
}
|
||||
|
||||
Combat combat = game.getPhaseHandler().getCombat();
|
||||
for (Entry<Card, Card> attackMap : cardAttackMap.entrySet()) {
|
||||
Card attacker = attackMap.getKey();
|
||||
Card attacked = attackMap.getValue();
|
||||
|
||||
combat.addAttacker(attacker, attacked == null ? defendingPlayer : attacked);
|
||||
}
|
||||
|
||||
// Run the necessary combat events and triggers to set things up correctly as if the
|
||||
// attack was actually declared by the attacking player
|
||||
Multimap<GameEntity, Card> attackersMap = ArrayListMultimap.create();
|
||||
for (GameEntity ge : combat.getDefenders()) {
|
||||
attackersMap.putAll(ge, combat.getAttackersOf(ge));
|
||||
}
|
||||
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
|
||||
|
||||
if (!combat.getAttackers().isEmpty()) {
|
||||
List<GameEntity> attackedTarget = Lists.newArrayList();
|
||||
for (final Card c : combat.getAttackers()) {
|
||||
attackedTarget.add(combat.getDefenderByAttacker(c));
|
||||
}
|
||||
final Map<String, Object> runParams = Maps.newHashMap();
|
||||
runParams.put("Attackers", combat.getAttackers());
|
||||
runParams.put("AttackingPlayer", combat.getAttackingPlayer());
|
||||
runParams.put("AttackedTarget", attackedTarget);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
|
||||
}
|
||||
|
||||
for (final Card c : combat.getAttackers()) {
|
||||
CombatUtil.checkDeclaredAttacker(game, c, combat);
|
||||
}
|
||||
|
||||
game.getTriggerHandler().resetActiveTriggers();
|
||||
game.updateCombatForView();
|
||||
game.fireEvent(new GameEventCombatChanged());
|
||||
|
||||
// Gracefully proceed to Declare Blockers, giving priority to the defending player,
|
||||
// but only if the stack is empty (otherwise the game will crash).
|
||||
game.getStack().addAllTriggeredAbilitiesToStack();
|
||||
if (toDeclareBlockers && game.getStack().isEmpty()) {
|
||||
game.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRememberedEntities() {
|
||||
// Remembered: X
|
||||
for (Entry<Card, List<String>> rememberedEnts : cardToRememberedId.entrySet()) {
|
||||
Card c = rememberedEnts.getKey();
|
||||
List<String> ids = rememberedEnts.getValue();
|
||||
|
||||
for (String id : ids) {
|
||||
Card tgt = idToCard.get(Integer.parseInt(id));
|
||||
c.addRemembered(tgt);
|
||||
}
|
||||
}
|
||||
|
||||
// Imprinting: X
|
||||
for (Entry<Card, List<String>> imprintedCards : cardToImprintedId.entrySet()) {
|
||||
Card c = imprintedCards.getKey();
|
||||
List<String> ids = imprintedCards.getValue();
|
||||
|
||||
for (String id : ids) {
|
||||
Card tgt = idToCard.get(Integer.parseInt(id));
|
||||
c.addImprintedCard(tgt);
|
||||
}
|
||||
}
|
||||
|
||||
// Exiled with X
|
||||
for (Entry<Card, String> rememberedEnts : cardToExiledWithId.entrySet()) {
|
||||
Card c = rememberedEnts.getKey();
|
||||
String id = rememberedEnts.getValue();
|
||||
|
||||
Card exiledWith = idToCard.get(Integer.parseInt(id));
|
||||
c.setExiledWith(exiledWith);
|
||||
}
|
||||
}
|
||||
|
||||
private int parseTargetInScript(final String tgtDef) {
|
||||
int tgtID = TARGET_NONE;
|
||||
|
||||
if (tgtDef.equalsIgnoreCase("human")) {
|
||||
tgtID = TARGET_HUMAN;
|
||||
} else if (tgtDef.equalsIgnoreCase("ai")) {
|
||||
tgtID = TARGET_AI;
|
||||
} else {
|
||||
tgtID = Integer.parseInt(tgtDef);
|
||||
}
|
||||
|
||||
return tgtID;
|
||||
}
|
||||
|
||||
private void handleScriptedTargetingForSA(final Game game, final SpellAbility sa, int tgtID) {
|
||||
Player human = game.getPlayers().get(0);
|
||||
Player ai = game.getPlayers().get(1);
|
||||
|
||||
if (tgtID != TARGET_NONE) {
|
||||
switch (tgtID) {
|
||||
case TARGET_HUMAN:
|
||||
sa.getTargets().add(human);
|
||||
break;
|
||||
case TARGET_AI:
|
||||
sa.getTargets().add(ai);
|
||||
break;
|
||||
default:
|
||||
sa.getTargets().add(idToCard.get(tgtID));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleScriptExecution(final Game game) {
|
||||
for (Entry<Card, String> scriptPtr : cardToScript.entrySet()) {
|
||||
Card c = scriptPtr.getKey();
|
||||
String sPtr = scriptPtr.getValue();
|
||||
|
||||
executeScript(game, c, sPtr);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeScript(Game game, Card c, String sPtr) {
|
||||
int tgtID = TARGET_NONE;
|
||||
if (sPtr.contains("->")) {
|
||||
String tgtDef = sPtr.substring(sPtr.lastIndexOf("->") + 2);
|
||||
|
||||
tgtID = parseTargetInScript(tgtDef);
|
||||
sPtr = sPtr.substring(0, sPtr.lastIndexOf("->"));
|
||||
}
|
||||
|
||||
SpellAbility sa = null;
|
||||
if (StringUtils.isNumeric(sPtr)) {
|
||||
int numSA = Integer.parseInt(sPtr);
|
||||
if (c.getSpellAbilities().size() >= numSA) {
|
||||
sa = c.getSpellAbilities().get(numSA);
|
||||
} else {
|
||||
System.err.println("ERROR: Unable to find SA with index " + numSA + " on card " + c + " to execute!");
|
||||
}
|
||||
} else {
|
||||
// Special handling for keyworded abilities
|
||||
if (sPtr.startsWith("KW#")) {
|
||||
String kwName = sPtr.substring(3);
|
||||
FCollectionView<SpellAbility> saList = c.getSpellAbilities();
|
||||
|
||||
if (kwName.equals("Awaken") || kwName.equals("AwakenOnly")) {
|
||||
// AwakenOnly only creates the Awaken effect, while Awaken precasts the whole spell with Awaken
|
||||
for (SpellAbility ab : saList) {
|
||||
if (ab.getDescription().startsWith("Awaken")) {
|
||||
ab.setActivatingPlayer(c.getController());
|
||||
ab.getSubAbility().setActivatingPlayer(c.getController());
|
||||
// target for Awaken is set in its first subability
|
||||
handleScriptedTargetingForSA(game, ab.getSubAbility(), tgtID);
|
||||
sa = kwName.equals("AwakenOnly") ? ab.getSubAbility() : ab;
|
||||
}
|
||||
}
|
||||
if (sa == null) {
|
||||
System.err.println("ERROR: Could not locate keyworded ability Awaken in card " + c + " to execute!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SVar-based script execution
|
||||
String svarValue = "";
|
||||
|
||||
if (sPtr.startsWith("CustomScript:")) {
|
||||
// A custom line defined in the game state file
|
||||
svarValue = sPtr.substring(sPtr.indexOf(":") + 1);
|
||||
} else {
|
||||
// A SVar from the card script file
|
||||
if (!c.hasSVar(sPtr)) {
|
||||
System.err.println("ERROR: Unable to find SVar " + sPtr + " on card " + c + " + to execute!");
|
||||
return;
|
||||
}
|
||||
|
||||
svarValue = c.getSVar(sPtr);
|
||||
|
||||
if (tgtID != TARGET_NONE && svarValue.contains("| Defined$")) {
|
||||
// We want a specific target, so try to undefine a predefined target if possible
|
||||
svarValue = TextUtil.fastReplace(svarValue, "| Defined$", "| Undefined$");
|
||||
if (tgtID == TARGET_HUMAN || tgtID == TARGET_AI) {
|
||||
svarValue += " | ValidTgts$ Player";
|
||||
} else {
|
||||
svarValue += " | ValidTgts$ Card";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa = AbilityFactory.getAbility(svarValue, c);
|
||||
if (sa == null) {
|
||||
System.err.println("ERROR: Unable to generate ability for SVar " + svarValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.setActivatingPlayer(c.getController());
|
||||
handleScriptedTargetingForSA(game, sa, tgtID);
|
||||
|
||||
sa.resolve();
|
||||
|
||||
// resolve subabilities
|
||||
SpellAbility subSa = sa.getSubAbility();
|
||||
while (subSa != null) {
|
||||
subSa.resolve();
|
||||
subSa = subSa.getSubAbility();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePrecastSpells(final Game game) {
|
||||
Player human = game.getPlayers().get(0);
|
||||
Player ai = game.getPlayers().get(1);
|
||||
|
||||
if (precastHuman != null) {
|
||||
String[] spellList = TextUtil.split(precastHuman, ';');
|
||||
for (String spell : spellList) {
|
||||
precastSpellFromCard(spell, human, game);
|
||||
}
|
||||
}
|
||||
if (precastAI != null) {
|
||||
String[] spellList = TextUtil.split(precastAI, ';');
|
||||
for (String spell : spellList) {
|
||||
precastSpellFromCard(spell, ai, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void precastSpellFromCard(String spellDef, final Player activator, final Game game) {
|
||||
int tgtID = TARGET_NONE;
|
||||
String scriptID = "";
|
||||
|
||||
if (spellDef.contains(":")) {
|
||||
// targeting via -> will be handled in executeScript
|
||||
scriptID = spellDef.substring(spellDef.indexOf(":") + 1).trim();
|
||||
spellDef = spellDef.substring(0, spellDef.indexOf(":")).trim();
|
||||
} else if (spellDef.contains("->")) {
|
||||
String tgtDef = spellDef.substring(spellDef.indexOf("->") + 2).trim();
|
||||
tgtID = parseTargetInScript(tgtDef);
|
||||
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
|
||||
}
|
||||
|
||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
|
||||
|
||||
if (pc == null) {
|
||||
System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
|
||||
return;
|
||||
}
|
||||
|
||||
Card c = Card.fromPaperCard(pc, activator);
|
||||
SpellAbility sa = null;
|
||||
|
||||
if (!scriptID.isEmpty()) {
|
||||
executeScript(game, c, scriptID);
|
||||
return;
|
||||
}
|
||||
|
||||
sa = c.getFirstSpellAbility();
|
||||
sa.setActivatingPlayer(activator);
|
||||
|
||||
handleScriptedTargetingForSA(game, sa, tgtID);
|
||||
|
||||
sa.resolve();
|
||||
}
|
||||
|
||||
private void handleMarkedDamage() {
|
||||
for (Entry<Card, Integer> entry : markedDamage.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
Integer dmg = entry.getValue();
|
||||
|
||||
c.setDamage(dmg);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChosenEntities() {
|
||||
// TODO: the AI still gets to choose something (and the notification box pops up) before the
|
||||
// choice is overwritten here. Somehow improve this so that there is at least no notification
|
||||
// about the choice that will be force-changed anyway.
|
||||
|
||||
// Chosen colors
|
||||
for (Entry<Card, List<String>> entry : cardToChosenClrs.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
List<String> colors = entry.getValue();
|
||||
|
||||
c.setChosenColors(colors);
|
||||
}
|
||||
|
||||
// Chosen type
|
||||
for (Entry<Card, String> entry : cardToChosenType.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
c.setChosenType(entry.getValue());
|
||||
}
|
||||
|
||||
// Named card
|
||||
for (Entry<Card, String> entry : cardToNamedCard.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
c.setNamedCard(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCardAttachments() {
|
||||
// Unattach all permanents first
|
||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||
Card attachedTo = idToCard.get(entry.getValue());
|
||||
|
||||
attachedTo.unEnchantAllCards();
|
||||
attachedTo.unEquipAllCards();
|
||||
for (Card c : attachedTo.getFortifiedBy(true)) {
|
||||
attachedTo.unFortifyCard(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach permanents by ID
|
||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||
Card attachedTo = idToCard.get(entry.getValue());
|
||||
Card attacher = entry.getKey();
|
||||
|
||||
if (attacher.isEquipment()) {
|
||||
attacher.equipCard(attachedTo);
|
||||
} else if (attacher.isAura()) {
|
||||
attacher.enchantEntity(attachedTo);
|
||||
} else if (attacher.isFortified()) {
|
||||
attacher.fortifyCard(attachedTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
|
||||
//entity.setCounters(new HashMap<CounterType, Integer>());
|
||||
entity.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
||||
String[] allCounterStrings = counterString.split(",");
|
||||
for (final String counterPair : allCounterStrings) {
|
||||
String[] pair = counterPair.split("=", 2);
|
||||
@@ -356,7 +879,6 @@ public abstract class GameState {
|
||||
|
||||
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
|
||||
// Lock check static as we setup player state
|
||||
final Game game = p.getGame();
|
||||
|
||||
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
||||
@@ -384,11 +906,17 @@ public abstract class GameState {
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
// Note: Not clearCounters() since we want to keep the counters
|
||||
// var as-is.
|
||||
c.setCounters(new HashMap<CounterType, Integer>());
|
||||
p.getZone(ZoneType.Hand).add(c);
|
||||
c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
||||
if (c.isAura()) {
|
||||
p.getGame().getAction().moveToPlay(c, null);
|
||||
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
||||
// (will be overridden later, so the actual value shouldn't matter)
|
||||
c.setEnchanting(c);
|
||||
}
|
||||
|
||||
if (cardsWithoutETBTrigs.contains(c)) {
|
||||
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null);
|
||||
} else {
|
||||
p.getZone(ZoneType.Hand).add(c);
|
||||
p.getGame().getAction().moveToPlay(c, null);
|
||||
}
|
||||
|
||||
@@ -401,25 +929,6 @@ public abstract class GameState {
|
||||
}
|
||||
}
|
||||
|
||||
game.getTriggerHandler().suppressMode(TriggerType.Unequip);
|
||||
|
||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||
Card attachedTo = idToCard.get(entry.getValue());
|
||||
Card attacher = entry.getKey();
|
||||
|
||||
attachedTo.unEnchantAllCards();
|
||||
attachedTo.unEquipAllCards();
|
||||
|
||||
if (attacher.isEquipment()) {
|
||||
attacher.equipCard(attachedTo);
|
||||
} else if (attacher.isAura()) {
|
||||
attacher.enchantEntity(attachedTo);
|
||||
} else if (attacher.isFortified()) {
|
||||
attacher.fortifyCard(attachedTo);
|
||||
}
|
||||
}
|
||||
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.Unequip);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,10 +958,16 @@ public abstract class GameState {
|
||||
Card c;
|
||||
boolean hasSetCurSet = false;
|
||||
if (cardinfo[0].startsWith("t:")) {
|
||||
// TODO Make sure Game State conversion works with new tokens
|
||||
String tokenStr = cardinfo[0].substring(2);
|
||||
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
|
||||
c = new TokenInfo(tokenStr).makeOneToken(player);
|
||||
} else {
|
||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode);
|
||||
if (pc == null) {
|
||||
System.err.println("ERROR: Tried to create a non-existent card named " + cardinfo[0] + " (set: " + (setCode == null ? "any" : setCode) + ") when loading game state!");
|
||||
continue;
|
||||
}
|
||||
|
||||
c = Card.fromPaperCard(pc, player);
|
||||
if (setCode != null) {
|
||||
hasSetCurSet = true;
|
||||
@@ -463,6 +978,13 @@ public abstract class GameState {
|
||||
for (final String info : cardinfo) {
|
||||
if (info.startsWith("Tapped")) {
|
||||
c.tap();
|
||||
} else if (info.startsWith("Renowned")) {
|
||||
c.setRenowned(true);
|
||||
} else if (info.startsWith("Monstrous:")) {
|
||||
c.setMonstrous(true);
|
||||
c.setMonstrosityNum(Integer.parseInt(info.substring((info.indexOf(':') + 1))));
|
||||
} else if (info.startsWith("PhasedOut")) {
|
||||
c.setPhasedOut(true);
|
||||
} else if (info.startsWith("Counters:")) {
|
||||
applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("SummonSick")) {
|
||||
@@ -474,6 +996,10 @@ public abstract class GameState {
|
||||
}
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
} else if (info.startsWith("Flipped")) {
|
||||
c.setState(CardStateName.Flipped, true);
|
||||
} else if (info.startsWith("Meld")) {
|
||||
c.setState(CardStateName.Meld, true);
|
||||
} else if (info.startsWith("IsCommander")) {
|
||||
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
|
||||
c.setCommander(true);
|
||||
@@ -488,6 +1014,32 @@ public abstract class GameState {
|
||||
} else if (info.startsWith("Ability:")) {
|
||||
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
|
||||
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
|
||||
} else if (info.startsWith("Damage:")) {
|
||||
int dmg = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
||||
markedDamage.put(c, dmg);
|
||||
} else if (info.startsWith("ChosenColor:")) {
|
||||
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("ChosenType:")) {
|
||||
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("NamedCard:")) {
|
||||
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("ExecuteScript:")) {
|
||||
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("RememberedCards:")) {
|
||||
cardToRememberedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("Imprinting:")) {
|
||||
cardToImprintedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("ExiledWith:")) {
|
||||
cardToExiledWithId.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("Attacking")) {
|
||||
if (info.contains(":")) {
|
||||
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
||||
cardAttackMap.put(c, idToCard.get(id));
|
||||
} else {
|
||||
cardAttackMap.put(c, null);
|
||||
}
|
||||
} else if (info.equals("NoETBTrigs")) {
|
||||
cardsWithoutETBTrigs.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package forge.ai;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import forge.AIOption;
|
||||
import forge.LobbyPlayer;
|
||||
import forge.game.Game;
|
||||
import forge.game.player.IGameEntitiesFactory;
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import com.esotericsoftware.minlog.Log;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import forge.LobbyPlayer;
|
||||
import forge.ai.ability.ProtectAi;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.ICardFace;
|
||||
import forge.card.MagicColor;
|
||||
@@ -32,21 +24,14 @@ import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPartMana;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.mana.Mana;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.DelayedReveal;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerController;
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.player.*;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperCard;
|
||||
@@ -55,6 +40,12 @@ import forge.util.ITriggerEvent;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -115,7 +106,23 @@ public class PlayerControllerAi extends PlayerController {
|
||||
if (ability.getApi() != null) {
|
||||
switch (ability.getApi()) {
|
||||
case ChooseNumber:
|
||||
return ability.getActivatingPlayer().isOpponentOf(player) ? 0 : ComputerUtilMana.determineLeftoverMana(ability, player);
|
||||
Player payingPlayer = ability.getActivatingPlayer();
|
||||
String logic = ability.getParamOrDefault("AILogic", "");
|
||||
boolean anyController = logic.equals("MaxForAnyController");
|
||||
|
||||
if (logic.startsWith("PowerLeakMaxMana.") && ability.getHostCard().isEnchantingCard()) {
|
||||
// For cards like Power Leak, the payer will be the owner of the enchanted card
|
||||
// TODO: is there any way to generalize this and avoid a special exclusion?
|
||||
payingPlayer = ability.getHostCard().getEnchantingCard().getController();
|
||||
}
|
||||
|
||||
int number = ComputerUtilMana.determineLeftoverMana(ability, player);
|
||||
|
||||
if (logic.startsWith("MaxMana.") || logic.startsWith("PowerLeakMaxMana.")) {
|
||||
number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1)));
|
||||
}
|
||||
|
||||
return payingPlayer.isOpponentOf(player) && !anyController ? 0 : number;
|
||||
case BidLife:
|
||||
return 0;
|
||||
default:
|
||||
@@ -180,7 +187,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
@Override
|
||||
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
|
||||
final SpellAbility sa = wrapper.getWrappedAbility();
|
||||
final Trigger regtrig = wrapper.getTrigger();
|
||||
//final Trigger regtrig = wrapper.getTrigger();
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
|
||||
return true;
|
||||
}
|
||||
@@ -282,8 +289,43 @@ public class PlayerControllerAi extends PlayerController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone) {
|
||||
//TODO Add logic for AI ordering here
|
||||
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
|
||||
//TODO Add more logic for AI ordering here
|
||||
|
||||
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
|
||||
if (destinationZone == ZoneType.Graveyard) {
|
||||
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
|
||||
return card.getName().equals("Volrath's Shapeshifter")
|
||||
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
|
||||
}
|
||||
}).isEmpty()) {
|
||||
int bestValue = 0;
|
||||
Card bestCreature = null;
|
||||
for (Card c : cards) {
|
||||
int curValue = ComputerUtilCard.evaluateCreature(c);
|
||||
if (c.isCreature() && curValue > bestValue) {
|
||||
bestValue = curValue;
|
||||
bestCreature = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCreature != null) {
|
||||
CardCollection reordered = new CardCollection();
|
||||
for (Card c : cards) {
|
||||
if (!c.equals(bestCreature)) {
|
||||
reordered.add(c);
|
||||
}
|
||||
}
|
||||
reordered.add(bestCreature);
|
||||
return reordered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: return with the same order as was passed into this method
|
||||
return cards;
|
||||
}
|
||||
|
||||
@@ -355,7 +397,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty())
|
||||
{
|
||||
chosen = validTypes.get(0);
|
||||
Log.warn("AI has no idea how to choose " + kindOfType +", defaulting to 1st element: chosen");
|
||||
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to 1st element: chosen");
|
||||
}
|
||||
game.getAction().nofityOfValue(sa, player, chosen, player);
|
||||
return chosen;
|
||||
@@ -421,6 +463,24 @@ public class PlayerControllerAi extends PlayerController {
|
||||
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
|
||||
ability.setActivatingPlayer(c.getController());
|
||||
|
||||
// FIXME: This is a hack to check if the AI can play the "exile from library" pay costs (Cumulative Upkeep,
|
||||
// e.g. Thought Lash). We have to do it and bail early if the AI can't pay, because otherwise the AI will
|
||||
// pay the cost partially, which should not be possible
|
||||
int nExileLib = 0;
|
||||
List<CostPart> parts = CostAdjustment.adjust(cost, sa).getCostParts();
|
||||
for (final CostPart part : parts) {
|
||||
if (part instanceof CostExile) {
|
||||
CostExile exile = (CostExile) part;
|
||||
if (exile.from == ZoneType.Library) {
|
||||
nExileLib += exile.convertAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nExileLib > c.getController().getCardsIn(ZoneType.Library).size()) {
|
||||
return false;
|
||||
}
|
||||
// - End of hack for Exile a card from library Cumulative Upkeep -
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
|
||||
ComputerUtil.playNoStack(c.getController(), ability, game);
|
||||
return true;
|
||||
@@ -493,7 +553,41 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Boolean defaultVal) {
|
||||
switch(kindOfChoice) {
|
||||
case TapOrUntap: return true;
|
||||
case UntapOrLeaveTapped: return defaultVal != null && defaultVal.booleanValue();
|
||||
case UntapOrLeaveTapped:
|
||||
Card source = sa.getHostCard();
|
||||
if (source != null && source.hasSVar("AIUntapPreference")) {
|
||||
switch (source.getSVar("AIUntapPreference")) {
|
||||
case "Always":
|
||||
return true;
|
||||
case "Never":
|
||||
return false;
|
||||
case "NothingRemembered":
|
||||
if (source.getRememberedCount() == 0) {
|
||||
return true;
|
||||
} else {
|
||||
Card rem = (Card) source.getFirstRemembered();
|
||||
if (!rem.getZone().is(ZoneType.Battlefield)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case "BetterTgtThanRemembered":
|
||||
if (source.getRememberedCount() > 0) {
|
||||
Card rem = (Card) source.getFirstRemembered();
|
||||
if (!rem.getZone().is(ZoneType.Battlefield)) {
|
||||
return true;
|
||||
}
|
||||
for (Card c : source.getController().getCreaturesInPlay()) {
|
||||
if (c != rem && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(rem) + 30) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return defaultVal != null && defaultVal.booleanValue();
|
||||
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
|
||||
case LeftOrRight: return brains.chooseDirection(sa);
|
||||
default:
|
||||
@@ -780,11 +874,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return brains.getBooleanProperty(AiProps.CHEAT_WITH_MANA_ON_SHUFFLE) ? brains.cheatShuffle(list) : list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardShields chooseRegenerationShield(Card c) {
|
||||
return Iterables.getFirst(c.getShields(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PaperCard> chooseCardsYouWonToAddToDeck(List<PaperCard> losses) {
|
||||
// TODO AI takes all by default
|
||||
@@ -844,21 +933,40 @@ public class PlayerControllerAi extends PlayerController {
|
||||
@Override
|
||||
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
|
||||
CardCollectionView oppLibrary = ComputerUtil.getOpponentFor(player).getCardsIn(ZoneType.Library);
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
// If any Conspiracies are present, try not to choose the same name twice
|
||||
// (otherwise the AI will spam the same name)
|
||||
for (Card consp : player.getCardsIn(ZoneType.Command)) {
|
||||
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
String chosenName = consp.getNamedCard();
|
||||
if (!chosenName.isEmpty()) {
|
||||
aiLibrary = CardLists.filter(aiLibrary, Predicates.not(CardPredicates.nameEquals(chosenName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("MostProminentInComputerDeck")) {
|
||||
return ComputerUtilCard.getMostProminentCardName(player.getCardsIn(ZoneType.Library));
|
||||
return ComputerUtilCard.getMostProminentCardName(aiLibrary);
|
||||
} else if (logic.equals("MostProminentInHumanDeck")) {
|
||||
return ComputerUtilCard.getMostProminentCardName(player.getOpponent().getCardsIn(ZoneType.Library));
|
||||
return ComputerUtilCard.getMostProminentCardName(oppLibrary);
|
||||
} else if (logic.equals("MostProminentCreatureInComputerDeck")) {
|
||||
CardCollectionView cards = CardLists.getValidCards(player.getCardsIn(ZoneType.Library), "Creature", player, sa.getHostCard());
|
||||
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard());
|
||||
return ComputerUtilCard.getMostProminentCardName(cards);
|
||||
} else if (logic.equals("BestCreatureInComputerDeck")) {
|
||||
return ComputerUtilCard.getBestCreatureAI(player.getCardsIn(ZoneType.Library)).getName();
|
||||
return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
|
||||
} else if (logic.equals("RandomInComputerDeck")) {
|
||||
return Aggregates.random(player.getCardsIn(ZoneType.Library)).getName();
|
||||
return Aggregates.random(aiLibrary).getName();
|
||||
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
|
||||
CardCollectionView cards = CardLists.getValidCards(player.getCardsIn(ZoneType.Library), "Card.Instant,Card.Sorcery", player, sa.getHostCard());
|
||||
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard());
|
||||
return ComputerUtilCard.getMostProminentCardName(cards);
|
||||
} else if (logic.equals("CursedScroll")) {
|
||||
return SpecialCardAi.CursedScroll.chooseCard(player, sa);
|
||||
}
|
||||
} else {
|
||||
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,7 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.ICardFace;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostParser;
|
||||
@@ -25,6 +20,10 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityCondition;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base class for API-specific AI logic
|
||||
* <p>
|
||||
@@ -103,6 +102,12 @@ public abstract class SpellAbilityAi {
|
||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||
*/
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (aiLogic.equals("CheckCondition")) {
|
||||
SpellAbility saCopy = sa.copy();
|
||||
saCopy.setActivatingPlayer(ai);
|
||||
return saCopy.getConditions().areMet(saCopy);
|
||||
}
|
||||
|
||||
return !("Never".equals(aiLogic));
|
||||
}
|
||||
|
||||
@@ -118,7 +123,7 @@ public abstract class SpellAbilityAi {
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.ability.*;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.util.ReflectionUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public enum SpellApiToAi {
|
||||
Converter;
|
||||
|
||||
@@ -25,12 +24,14 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.Animate, AnimateAi.class)
|
||||
.put(ApiType.AnimateAll, AnimateAllAi.class)
|
||||
.put(ApiType.Attach, AttachAi.class)
|
||||
.put(ApiType.Ascend, AlwaysPlayAi.class)
|
||||
.put(ApiType.Balance, BalanceAi.class)
|
||||
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
|
||||
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
|
||||
.put(ApiType.BidLife, BidLifeAi.class)
|
||||
.put(ApiType.Bond, BondAi.class)
|
||||
.put(ApiType.Branch, AlwaysPlayAi.class)
|
||||
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
|
||||
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
||||
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
||||
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
|
||||
@@ -71,12 +72,14 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.ExchangeControlVariant, CannotPlayAi.class)
|
||||
.put(ApiType.ExchangePower, PowerExchangeAi.class)
|
||||
.put(ApiType.ExchangeZone, ZoneExchangeAi.class)
|
||||
.put(ApiType.Explore, ExploreAi.class)
|
||||
.put(ApiType.Fight, FightAi.class)
|
||||
.put(ApiType.FlipACoin, FlipACoinAi.class)
|
||||
.put(ApiType.Fog, FogAi.class)
|
||||
.put(ApiType.GainControl, ControlGainAi.class)
|
||||
.put(ApiType.GainLife, LifeGainAi.class)
|
||||
.put(ApiType.GainOwnership, CannotPlayAi.class)
|
||||
.put(ApiType.GameDrawn, CannotPlayAi.class)
|
||||
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
|
||||
.put(ApiType.Goad, GoadAi.class)
|
||||
.put(ApiType.Haunt, HauntAi.class)
|
||||
@@ -114,6 +117,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.RearrangeTopOfLibrary, RearrangeTopOfLibraryAi.class)
|
||||
.put(ApiType.Regenerate, RegenerateAi.class)
|
||||
.put(ApiType.RegenerateAll, RegenerateAllAi.class)
|
||||
.put(ApiType.Regeneration, AlwaysPlayAi.class)
|
||||
.put(ApiType.RemoveCounter, CountersRemoveAi.class)
|
||||
.put(ApiType.RemoveCounterAll, CannotPlayAi.class)
|
||||
.put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -21,7 +22,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Random r = MyRandom.getRandom();
|
||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
@@ -46,7 +47,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -87,7 +88,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class AlwaysPlayAi extends SpellAbilityAi {
|
||||
@@ -13,4 +14,9 @@ public class AlwaysPlayAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import forge.ai.AiCardMemory;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardType;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -38,6 +25,10 @@ import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -86,13 +77,13 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
num = (num == null) ? "1" : num;
|
||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
||||
ai.getOpponent(), topStack.getHostCard(), topStack);
|
||||
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), topStack);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
|
||||
Card animatedCopy = becomeAnimated(source, sa);
|
||||
list.add(animatedCopy);
|
||||
list = CardLists.getValidCards(list, valid.split(","), ai.getOpponent(), topStack.getHostCard(),
|
||||
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(),
|
||||
topStack);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||
@@ -172,11 +163,17 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
|
||||
if (sa.hasParam("Crew") && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return false;
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
@@ -186,9 +183,21 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return false;
|
||||
}
|
||||
this.rememberAnimatedThisTurn(aiPlayer, c);
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
||||
if (!sa.getHostCard().isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(sa.getHostCard()))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bFlag) {
|
||||
this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||
}
|
||||
return bFlag; // All of the defined stuff is animated, not very useful
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
@@ -251,13 +260,8 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// need to targetable
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
// try to look for AI targets if able
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection prefList = CardLists.getValidCards(list, sa.getParam("AITgts").split(","), ai, source, sa);
|
||||
if(!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
||||
list = prefList;
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||
|
||||
// list is empty, no possible targets
|
||||
if (list.isEmpty()) {
|
||||
@@ -301,8 +305,9 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// if its player turn,
|
||||
// check if its Permanent or that creature would attack
|
||||
if (ph.isPlayerTurn(ai)) {
|
||||
if (!sa.hasParam("Permanent") &&
|
||||
!ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)) {
|
||||
if (!sa.hasParam("Permanent")
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
|
||||
&& !sa.hasParam("UntilHostLeavesPlay")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
@@ -24,7 +24,10 @@ import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
@@ -38,7 +41,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
@@ -250,6 +253,18 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mandatory) {
|
||||
if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) {
|
||||
// try to identify if this thing can actually tap
|
||||
for (SpellAbility ab : c.getAllSpellAbilities()) {
|
||||
if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!c.isEnchanted()) {
|
||||
return true;
|
||||
}
|
||||
@@ -455,6 +470,65 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return c;
|
||||
}
|
||||
|
||||
// Cards that trigger on dealing damage
|
||||
private static Card attachAICuriosityPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||
final Card attachSource) {
|
||||
Card chosen = null;
|
||||
int priority = 0;
|
||||
for (Card card : list) {
|
||||
int cardPriority = 0;
|
||||
// Prefer Evasion
|
||||
if (card.hasKeyword("Trample")) {
|
||||
cardPriority += 10;
|
||||
}
|
||||
if (card.hasKeyword("Menace")) {
|
||||
cardPriority += 10;
|
||||
}
|
||||
// Avoid this for Sleepers Robe?
|
||||
if (card.hasKeyword("Fear")) {
|
||||
cardPriority += 15;
|
||||
}
|
||||
if (card.hasKeyword("Flying")) {
|
||||
cardPriority += 20;
|
||||
}
|
||||
if (card.hasKeyword("Shadow")) {
|
||||
cardPriority += 30;
|
||||
}
|
||||
if (card.hasKeyword("Horsemanship")) {
|
||||
cardPriority += 40;
|
||||
}
|
||||
if (card.hasKeyword("Unblockable")) {
|
||||
cardPriority += 50;
|
||||
}
|
||||
// Prefer "tap to deal damage"
|
||||
// TODO : Skip this one if triggers on combat damage only?
|
||||
for (SpellAbility sa2 : card.getSpellAbilities()) {
|
||||
if ((sa2.getApi().equals(ApiType.DealDamage))
|
||||
&& (sa2.getTargetRestrictions().canTgtPlayer())) {
|
||||
cardPriority += 300;
|
||||
}
|
||||
}
|
||||
// Prefer stronger creatures, avoid if can't attack
|
||||
cardPriority += card.getCurrentToughness() * 2;
|
||||
cardPriority += card.getCurrentPower();
|
||||
if (card.getCurrentPower() <= 0) {
|
||||
cardPriority = -100;
|
||||
}
|
||||
if (card.hasKeyword("Defender")) {
|
||||
cardPriority = -100;
|
||||
}
|
||||
if (card.hasKeyword("Indestructible")) {
|
||||
cardPriority += 15;
|
||||
}
|
||||
if (cardPriority > priority) {
|
||||
priority = cardPriority;
|
||||
chosen = card;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return chosen;
|
||||
}
|
||||
/**
|
||||
* Attach ai specific card preference.
|
||||
*
|
||||
@@ -478,7 +552,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if ("Guilty Conscience".equals(sourceName)) {
|
||||
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
|
||||
} else if ("Bonds of Faith".equals(sourceName)) {
|
||||
chosen = SpecialCardAi.BondsOfFaith.getBestAttachTarget(ai, sa, list);
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
|
||||
} else if ("Clutch of Undeath".equals(sourceName)) {
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
|
||||
}
|
||||
|
||||
// If Mandatory (brought directly into play without casting) gotta
|
||||
@@ -490,6 +566,44 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return chosen;
|
||||
}
|
||||
|
||||
private static Card attachAIInstantReequipPreference(final SpellAbility sa, final Card attachSource) {
|
||||
// e.g. Cranial Plating
|
||||
PhaseHandler ph = attachSource.getGame().getPhaseHandler();
|
||||
Combat combat = attachSource.getGame().getCombat();
|
||||
Card equipped = sa.getHostCard().getEquipping();
|
||||
if (equipped == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int powerBuff = 0;
|
||||
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
|
||||
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
|
||||
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null);
|
||||
}
|
||||
}
|
||||
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
|
||||
int damage = 0;
|
||||
for (Card c : combat.getUnblockedAttackers()) {
|
||||
damage += ComputerUtilCombat.predictDamageTo(combat.getDefenderPlayerByAttacker(equipped), c.getNetCombatDamage(), c, true);
|
||||
}
|
||||
if (combat.isBlocked(equipped)) {
|
||||
for (Card atk : combat.getAttackers()) {
|
||||
if (!combat.isBlocked(atk) && !ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null).contains(atk)) {
|
||||
if (ComputerUtilCombat.predictDamageTo(combat.getDefenderPlayerByAttacker(atk),
|
||||
atk.getNetCombatDamage(), atk, true) > 0) {
|
||||
if (damage + powerBuff >= combat.getDefenderPlayerByAttacker(atk).getLife()) {
|
||||
sa.resetTargets(); // this is needed to avoid bugs with adding two targets to a single SA
|
||||
return atk; // lethal damage, we can win right now, so why not?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Should generalize this code a bit since they all have similar structures
|
||||
/**
|
||||
* Attach ai control preference.
|
||||
@@ -580,8 +694,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
|
||||
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||
|
||||
String kws = stabMap.get("AddKeyword");
|
||||
if (kws != null) {
|
||||
@@ -702,30 +816,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* parseSVar TODO - flesh out javadoc for this method.
|
||||
*
|
||||
* @param hostCard
|
||||
* the Card with the SVar on it
|
||||
* @param amount
|
||||
* a String
|
||||
* @return the calculated number
|
||||
*/
|
||||
public static int parseSVar(final Card hostCard, final String amount) {
|
||||
int num = 0;
|
||||
if (amount == null) {
|
||||
return num;
|
||||
}
|
||||
|
||||
try {
|
||||
num = Integer.valueOf(amount);
|
||||
} catch (final NumberFormatException e) {
|
||||
num = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(amount).split("\\$")[1]);
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach preference.
|
||||
*
|
||||
@@ -874,8 +964,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
|
||||
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||
|
||||
grantingAbilities |= stabMap.containsKey("AddAbility");
|
||||
|
||||
@@ -1046,9 +1136,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
CardCollection prefList = list;
|
||||
if (sa.hasParam("AITgts")) {
|
||||
prefList = CardLists.getValidCards(list, sa.getParam("AITgts"), sa.getActivatingPlayer(), attachSource);
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
prefList = ComputerUtil.filterAITgts(sa, aiPlayer, (CardCollection)list, true);
|
||||
|
||||
Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic"));
|
||||
|
||||
@@ -1061,6 +1151,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ("InstantReequipPowerBuff".equals(sa.getParam("AILogic"))) {
|
||||
return c;
|
||||
}
|
||||
|
||||
boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping());
|
||||
|
||||
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) {
|
||||
@@ -1077,10 +1171,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
// make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around
|
||||
boolean decideMoveFromUseless = uselessCreature && aic.getBooleanProperty(AiProps.PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS);
|
||||
|
||||
if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
|
||||
if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
|
||||
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach);
|
||||
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
||||
aic.reserveManaSourcesForMain2(futureSpell);
|
||||
aic.reserveManaSources(futureSpell);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,7 +1217,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
private static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||
final Card attachSource, final String logic) {
|
||||
Player prefPlayer = ai.getWeakestOpponent();
|
||||
if ("Pump".equals(logic) || "Animate".equals(logic)) {
|
||||
if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic)) {
|
||||
prefPlayer = ai;
|
||||
}
|
||||
// Some ChangeType cards are beneficial, and PrefPlayer should be
|
||||
@@ -1137,6 +1231,12 @@ public class AttachAi extends SpellAbilityAi {
|
||||
prefList = CardLists.filterControlledBy(list, prefPlayer);
|
||||
}
|
||||
|
||||
// AI logic types that do not require a prefList and that evaluate the
|
||||
// usefulness of attach action autonomously
|
||||
if ("InstantReequipPowerBuff".equals(logic)) {
|
||||
return attachAIInstantReequipPreference(sa, attachSource);
|
||||
}
|
||||
|
||||
// If there are no preferred cards, and not mandatory bail out
|
||||
if (prefList.isEmpty()) {
|
||||
return chooseUnpreferred(mandatory, list);
|
||||
@@ -1151,6 +1251,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
c = attachAICursePreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("Pump".equals(logic)) {
|
||||
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
|
||||
} else if ("Curiosity".equals(logic)) {
|
||||
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("ChangeType".equals(logic)) {
|
||||
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("KeepTapped".equals(logic)) {
|
||||
@@ -1287,9 +1389,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) {
|
||||
return false;
|
||||
}
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) {
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
|
||||
if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.")
|
||||
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) {
|
||||
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
|
||||
return false;
|
||||
}
|
||||
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
|
||||
@@ -1386,6 +1488,43 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Card doPumpOrCurseAILogic(final Player ai, final SpellAbility sa, final List<Card> list, final String type) {
|
||||
Card chosen = null;
|
||||
|
||||
List<Card> aiType = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
// Don't buff opponent's creatures of given type
|
||||
if (!c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return c.getType().hasCreatureType(type);
|
||||
}
|
||||
});
|
||||
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
// Don't debuff AI's own creatures not of given type
|
||||
if (c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
|
||||
}
|
||||
});
|
||||
|
||||
if (!aiType.isEmpty() && !oppNonType.isEmpty()) {
|
||||
Card bestAi = ComputerUtilCard.getBestCreatureAI(aiType);
|
||||
Card bestOpp = ComputerUtilCard.getBestCreatureAI(oppNonType);
|
||||
chosen = ComputerUtilCard.evaluateCreature(bestAi) > ComputerUtilCard.evaluateCreature(bestOpp) ? bestAi : bestOpp;
|
||||
} else if (!aiType.isEmpty()) {
|
||||
chosen = ComputerUtilCard.getBestCreatureAI(aiType);
|
||||
} else if (!oppNonType.isEmpty()) {
|
||||
chosen = ComputerUtilCard.getBestCreatureAI(oppNonType);
|
||||
}
|
||||
|
||||
return chosen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -16,7 +17,7 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
|
||||
int diff = 0;
|
||||
// TODO Add support for multiplayer logic
|
||||
final Player opp = aiPlayer.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
||||
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canTgtCreature()) {
|
||||
List<Card> list = CardLists.getTargetableCards(aiPlayer.getOpponent().getCardsIn(ZoneType.Battlefield), sa);
|
||||
List<Card> list = CardLists.getTargetableCards(ComputerUtil.getOpponentFor(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
|
||||
@@ -36,10 +36,10 @@ public final class BondAi extends SpellAbilityAi {
|
||||
* <p>
|
||||
* bondCanPlayAI.
|
||||
* </p>
|
||||
* @param aiPlayer
|
||||
* a {@link forge.game.player.Player} object.
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
*
|
||||
* @return a boolean.
|
||||
*/
|
||||
@@ -53,4 +53,9 @@ public final class BondAi extends SpellAbilityAi {
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||
return ComputerUtilCard.getBestCreatureAI(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
@@ -22,11 +12,7 @@ import forge.game.GameObject;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
@@ -40,6 +26,9 @@ import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ChangeZoneAi extends SpellAbilityAi {
|
||||
/*
|
||||
@@ -49,6 +38,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* too much: blink/bounce/exile/tutor/Raise Dead/Surgical Extraction/......
|
||||
*/
|
||||
|
||||
// multipleCardsToChoose is used by Intuition and can be adapted to be used by other
|
||||
// cards where multiple cards are fetched at once and they need to be coordinated
|
||||
private static CardCollection multipleCardsToChoose = new CardCollection();
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
|
||||
@@ -64,23 +57,49 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
} else if (aiLogic.equals("PriorityOptionalCost")) {
|
||||
boolean highPriority = false;
|
||||
// if we have more than one of these in hand, might not be worth waiting for optional cost payment on the additional copy
|
||||
highPriority |= CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(sa.getHostCard().getName())).size() > 1;
|
||||
// if we are in danger in combat, no need to wait to pay the optional cost
|
||||
highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat());
|
||||
|
||||
if (!highPriority) {
|
||||
if (Iterables.isEmpty(sa.getOptionalCosts())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.checkAiLogic(ai, sa, aiLogic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
// Checks for "return true" unlike checkAiLogic()
|
||||
|
||||
multipleCardsToChoose.clear();
|
||||
String aiLogic = sa.getParam("AILogic");
|
||||
if (aiLogic != null) {
|
||||
if (aiLogic.equals("Always")) {
|
||||
return true;
|
||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||
return this.doSacAndUpgradeLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||
return this.doSacAndReturnFromGraveLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Necropotence")) {
|
||||
return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("SameName")) { // Declaration in Stone
|
||||
return this.doSameNameLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("ReanimateAll")) {
|
||||
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("TheScarabGod")) {
|
||||
return SpecialCardAi.TheScarabGod.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Intuition")) {
|
||||
// This logic only fills the multiple cards array, the decision to play is made
|
||||
// separately in hiddenOriginCanPlayAI later.
|
||||
multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
if (isHidden(sa)) {
|
||||
@@ -133,6 +152,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return doReturnCommanderLogic(sa, aiPlayer);
|
||||
}
|
||||
|
||||
if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
|
||||
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
|
||||
return true; // debuffed by opponent's auras to the level that it becomes useless
|
||||
}
|
||||
int delta = 0;
|
||||
for (Card enc : sa.getHostCard().getEnchantedBy(false)) {
|
||||
if (enc.getController().isOpponentOf(aiPlayer)) {
|
||||
delta--;
|
||||
} else {
|
||||
delta++;
|
||||
}
|
||||
}
|
||||
return delta <= 0;
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
return hiddenTriggerAI(aiPlayer, sa, mandatory);
|
||||
}
|
||||
@@ -166,7 +200,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
ZoneType origin = null;
|
||||
final Player opponent = ai.getOpponent();
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
@@ -182,7 +216,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
|
||||
&& !(destination.equals("Battlefield") && !source.isLand())) {
|
||||
return false;
|
||||
}
|
||||
@@ -367,7 +401,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Player opp = aiPlayer.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
if (tgt != null && tgt.canTgtPlayer()) {
|
||||
boolean isCurse = sa.isCurse();
|
||||
if (isCurse && sa.canTarget(opp)) {
|
||||
@@ -428,7 +462,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
Iterable<Player> pDefined;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if ((tgt != null) && tgt.canTgtPlayer()) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.isCurse()) {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
@@ -547,8 +581,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static Card chooseCreature(final Player ai, CardCollection list) {
|
||||
// Creating a new combat for testing purposes.
|
||||
Combat combat = new Combat(ai.getOpponent());
|
||||
for (Card att : ai.getOpponent().getCreaturesInPlay()) {
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
Combat combat = new Combat(opponent);
|
||||
for (Card att : opponent.getCreaturesInPlay()) {
|
||||
combat.addAttacker(att, ai);
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai);
|
||||
@@ -647,6 +682,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (destination == ZoneType.Battlefield) {
|
||||
// predict whether something may put a ETBing creature below zero toughness
|
||||
// (e.g. Reassembing Skeleton + Elesh Norn, Grand Cenobite)
|
||||
for (final Card c : retrieval) {
|
||||
if (c.isCreature()) {
|
||||
final Card copy = CardUtil.getLKICopy(c);
|
||||
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
|
||||
if (copy.getNetToughness() <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
@@ -666,6 +715,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
return true;
|
||||
}
|
||||
@@ -720,6 +775,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
if ("MimicVat".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
return true;
|
||||
}
|
||||
@@ -764,9 +823,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, source, sa);
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
if (sa.hasParam("AITgts")) {
|
||||
list = CardLists.getValidCards(list, sa.getParam("AITgts"), ai, source);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (source.isInZone(ZoneType.Hand)) {
|
||||
list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(source.getName()))); // Don't get the same card back.
|
||||
}
|
||||
@@ -828,7 +896,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
&& !currCombat.getBlockers(attacker).isEmpty()) {
|
||||
ComputerUtilCard.sortByEvaluateCreature(blockers);
|
||||
Combat combat = new Combat(ai);
|
||||
combat.addAttacker(attacker, ai.getOpponent());
|
||||
combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
|
||||
for (Card blocker : blockers) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
}
|
||||
@@ -974,6 +1042,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!sa.hasParam("AITgtOwnCards")) {
|
||||
list = CardLists.filterControlledBy(list, ai.getOpponents());
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -988,6 +1058,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
});
|
||||
}
|
||||
|
||||
// See if maybe there's a special priority applicable for this, in case the opponent
|
||||
// has dangerous unblockables in play
|
||||
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Only care about combatants during combat
|
||||
if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) {
|
||||
CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null);
|
||||
@@ -1000,6 +1077,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the opponent can save a creature from bounce/blink/whatever by paying
|
||||
// the Unless cost (for example, Erratic Portal)
|
||||
list.removeAll(getSafeTargetsIfUnlessCostPaid(ai, sa, list));
|
||||
|
||||
if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1077,8 +1158,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
} else {
|
||||
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
||||
boolean aiTgtsOK = false;
|
||||
if (sa.hasParam("AIMinTgts")) {
|
||||
int minTgts = Integer.parseInt(sa.getParam("AIMinTgts"));
|
||||
if (sa.getTargets().getNumTargeted() >= minTgts) {
|
||||
aiTgtsOK = true;
|
||||
}
|
||||
}
|
||||
if (!aiTgtsOK) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1284,6 +1374,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
if ("DeathgorgeScavenger".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
|
||||
} else if ("ExtraplanarLens".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
|
||||
}
|
||||
|
||||
if (sa.getTargetRestrictions() == null) {
|
||||
// Just in case of Defined cases
|
||||
if (!mandatory && sa.hasParam("AttachedTo")) {
|
||||
@@ -1313,12 +1409,22 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("NeverBounceItself".equals(logic)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (fetchList.contains(source)) {
|
||||
if (fetchList.contains(source) && (fetchList.size() > 1 && !sa.getTriggeringAbility().isMandatory())) {
|
||||
// For cards that should never be bounced back to hand with their own [e.g. triggered] abilities, such as guild lands.
|
||||
fetchList.remove(source);
|
||||
}
|
||||
} else if ("WorstCard".equals(logic)) {
|
||||
return ComputerUtilCard.getWorstAI(fetchList);
|
||||
} else if ("Mairsil".equals(logic)) {
|
||||
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
|
||||
} else if ("SurvivalOfTheFittest".equals(logic)) {
|
||||
return SpecialCardAi.SurvivalOfTheFittest.considerCardToGet(decider, sa);
|
||||
} else if ("Intuition".equals(logic)) {
|
||||
if (!multipleCardsToChoose.isEmpty()) {
|
||||
Card choice = multipleCardsToChoose.get(0);
|
||||
multipleCardsToChoose.remove(0);
|
||||
return choice;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -1459,7 +1565,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return Iterables.getFirst(options, null);
|
||||
}
|
||||
|
||||
private boolean doSacAndUpgradeLogic(final Player ai, SpellAbility sa) {
|
||||
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
|
||||
|
||||
CardCollection listToSac = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.restriction(definedSac.split(","), ai, source, sa));
|
||||
listToSac.sort(Collections.reverseOrder(CardLists.CmcComparatorInv));
|
||||
|
||||
CardCollection listToRet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Presets.CREATURES);
|
||||
listToRet.sort(CardLists.CmcComparatorInv);
|
||||
|
||||
if (!listToSac.isEmpty() && !listToRet.isEmpty()) {
|
||||
Card worstSac = listToSac.getFirst();
|
||||
Card bestRet = listToRet.getFirst();
|
||||
|
||||
if (bestRet.getCMC() > worstSac.getCMC()
|
||||
&& ComputerUtilCard.evaluateCreature(bestRet) > ComputerUtilCard.evaluateCreature(worstSac)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestRet);
|
||||
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
String logic = sa.getParam("AILogic");
|
||||
@@ -1604,7 +1736,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// A blink effect implemented using ChangeZone API
|
||||
return false;
|
||||
} else if (subApi == ApiType.DelayedTrigger) {
|
||||
SpellAbility exec = causeSub.getAdditonalAbility("Execute");
|
||||
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
|
||||
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
|
||||
if ("Exile".equals(exec.getParam("Origin")) && "Battlefield".equals(exec.getParam("Destination"))) {
|
||||
// A blink effect implemented using a delayed trigger
|
||||
@@ -1623,6 +1755,45 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
|
||||
// Determines if the controller of each potential target can negate the ChangeZone effect
|
||||
// by paying the Unless cost. Returns the list of targets that can be saved that way.
|
||||
final Card source = sa.getHostCard();
|
||||
final CardCollection canBeSaved = new CardCollection();
|
||||
|
||||
for (Card potentialTgt : potentialTgts) {
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
|
||||
if (unlessCost != null && !unlessCost.endsWith(">")) {
|
||||
Player opp = potentialTgt.getController();
|
||||
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||
|
||||
int toPay = 0;
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
setPayX = true;
|
||||
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
} else {
|
||||
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
|
||||
}
|
||||
|
||||
if (toPay == 0) {
|
||||
canBeSaved.add(potentialTgt);
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
canBeSaved.add(potentialTgt);
|
||||
}
|
||||
|
||||
if (setPayX) {
|
||||
source.setSVar("PayX", Integer.toString(toPay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return canBeSaved;
|
||||
}
|
||||
|
||||
private static void rememberBouncedThisTurn(Player ai, Card c) {
|
||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiPlayerPredicates;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Random;
|
||||
|
||||
public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
@@ -46,9 +37,13 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
|
||||
boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
|
||||
|
||||
if (!aiLogicAllowsDiscard) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Random r = MyRandom.getRandom();
|
||||
// prevent run-away activations - first time will always return true
|
||||
@@ -73,21 +68,55 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
|
||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
// Living Death AI
|
||||
if ("LivingDeath".equals(sa.getParam("AILogic"))) {
|
||||
// Living Death AI
|
||||
return SpecialCardAi.LivingDeath.consider(ai, sa);
|
||||
} else if ("Timetwister".equals(sa.getParam("AILogic"))) {
|
||||
// Timetwister AI
|
||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
||||
} else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
|
||||
// e.g. Shadow of the Grave
|
||||
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
} else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
|
||||
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
|
||||
|
||||
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
|
||||
PlayerCollection players = new PlayerCollection();
|
||||
players.addAll(ai.getOpponents());
|
||||
players.add(ai);
|
||||
int maxSize = 1;
|
||||
for (Player player : players) {
|
||||
Player bestTgt = null;
|
||||
if (player.canBeTargetedBy(sa)) {
|
||||
CardCollectionView cardsGY = CardLists.filter(player.getCardsIn(ZoneType.Graveyard),
|
||||
CardPredicates.Presets.CREATURES);
|
||||
if (cardsGY.size() > maxSize) {
|
||||
maxSize = cardsGY.size();
|
||||
bestTgt = player;
|
||||
}
|
||||
}
|
||||
|
||||
// Timetwister AI
|
||||
if ("Timetwister".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
||||
if (bestTgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestTgt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO improve restrictions on when the AI would want to use this
|
||||
// spBounceAll has some AI we can compare to.
|
||||
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
||||
if (!sa.usesTargeting()) {
|
||||
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar, Timetwister)
|
||||
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar)
|
||||
return true;
|
||||
} else {
|
||||
// search targetable Opponents
|
||||
@@ -130,15 +159,39 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
}
|
||||
computerType = new CardCollection();
|
||||
}
|
||||
|
||||
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
|
||||
int nonCreatureEvalThreshold = 3; // CMC difference
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
if (destination == ZoneType.Hand) {
|
||||
creatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF);
|
||||
nonCreatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF);
|
||||
} else {
|
||||
creatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF);
|
||||
nonCreatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF);
|
||||
}
|
||||
}
|
||||
|
||||
// mass zone change for creatures: if in dire danger, do it; otherwise, only do it if the opponent's
|
||||
// creatures are better in value
|
||||
if ((CardLists.getNotType(oppType, "Creature").size() == 0)
|
||||
&& (CardLists.getNotType(computerType, "Creature").size() == 0)) {
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + 200) >= ComputerUtilCard
|
||||
if (game.getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
|
||||
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
// Life is in serious danger, return all creatures from the battlefield to wherever
|
||||
// so they don't deal lethal damage
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluateCreatureList(oppType)) {
|
||||
return false;
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
} // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human
|
||||
// permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + 3) >= ComputerUtilCard
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluatePermanentList(oppType)) {
|
||||
return false;
|
||||
}
|
||||
@@ -180,6 +233,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
// minimum card advantage unless the hand will be fully reloaded
|
||||
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
|
||||
|
||||
if (numExiledWithSrc > curHandSize) {
|
||||
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
|
||||
// Try to gain some card advantage if the card will die anyway
|
||||
// TODO: ideally, should evaluate the hand value and not discard good hands to it
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
@@ -231,8 +292,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
* @param aiPlayer
|
||||
* a {@link forge.game.player.Player} object.
|
||||
*
|
||||
* @return a boolean.
|
||||
*/
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
@@ -18,6 +10,9 @@ import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class CharmAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
@@ -124,6 +119,8 @@ public class CharmAi extends SpellAbilityAi {
|
||||
|
||||
private List<AbilitySub> chooseTriskaidekaphobia(List<AbilitySub> choices, final Player ai) {
|
||||
List<AbilitySub> chosenList = Lists.newArrayList();
|
||||
if (choices == null || choices.isEmpty()) { return chosenList; }
|
||||
|
||||
AbilitySub gain = choices.get(0);
|
||||
AbilitySub lose = choices.get(1);
|
||||
FCollection<Player> opponents = ai.getOpponents();
|
||||
|
||||
@@ -6,6 +6,8 @@ import java.util.List;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
@@ -16,6 +18,7 @@ import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
@@ -122,7 +125,7 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
}
|
||||
} else if (aiLogic.equals("Duneblast")) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = ai.getOpponent().getCreaturesInPlay();
|
||||
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
|
||||
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
|
||||
|
||||
@@ -228,6 +231,13 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
Collections.reverse(creats);
|
||||
choice = creats.get(0);
|
||||
}
|
||||
} else if ("NegativePowerFirst".equals(logic)) {
|
||||
Card lowest = Aggregates.itemWithMin(options, CardPredicates.Accessors.fnGetNetPower);
|
||||
if (lowest.getNetPower() <= 0) {
|
||||
choice = lowest;
|
||||
} else {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(options);
|
||||
}
|
||||
} else if ("TangleWire".equals(logic)) {
|
||||
CardCollectionView betterList = CardLists.filter(options, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -259,6 +269,10 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
|
||||
Card chosen = ComputerUtilCard.getBestCreatureAI(aiCreatures);
|
||||
return chosen;
|
||||
} else if (logic.equals("OrzhovAdvokist")) {
|
||||
if (ai.equals(sa.getActivatingPlayer())) {
|
||||
choice = ComputerUtilCard.getBestAI(options);
|
||||
} // TODO: improve ai
|
||||
} else {
|
||||
choice = ComputerUtilCard.getBestAI(options);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardRules;
|
||||
import forge.card.CardSplitType;
|
||||
@@ -17,7 +14,6 @@ import forge.card.CardStateName;
|
||||
import forge.card.ICardFace;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
@@ -36,29 +32,16 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
if (logic.equals("MomirAvatar")) {
|
||||
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
|
||||
// Some basic strategy for Momir
|
||||
if (tokenSize < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokenSize > 11) {
|
||||
tokenSize = 11;
|
||||
}
|
||||
|
||||
source.setSVar("PayX", Integer.toString(tokenSize));
|
||||
return SpecialCardAi.MomirVigAvatar.consider(ai, sa);
|
||||
} else if (logic.equals("CursedScroll")) {
|
||||
return SpecialCardAi.CursedScroll.consider(ai, sa);
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
@@ -78,6 +61,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||
|
||||
return ComputerUtilCard.getBestAI(options);
|
||||
}
|
||||
|
||||
@@ -104,9 +88,9 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
Card copy = CardUtil.getLKICopy(card);
|
||||
// for calcing i need only one split side
|
||||
if (isOther) {
|
||||
copy.getCurrentState().copyFrom(card, card.getState(CardStateName.RightSplit));
|
||||
copy.getCurrentState().copyFrom(card.getState(CardStateName.RightSplit), true);
|
||||
} else {
|
||||
copy.getCurrentState().copyFrom(card, card.getState(CardStateName.LeftSplit));
|
||||
copy.getCurrentState().copyFrom(card.getState(CardStateName.LeftSplit), true);
|
||||
}
|
||||
copy.updateStateForView();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
@@ -53,7 +52,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("Addle".equals(sourceName)) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ai.getOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -62,7 +61,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
if (logic.equals("MostExcessOpponentControls")) {
|
||||
for (byte color : MagicColor.WUBRG) {
|
||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||
|
||||
@@ -196,7 +196,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// milling against Tamiyo is pointless
|
||||
if (owner.isCardInCommand("Tamiyo, the Moon Sage emblem")) {
|
||||
if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) {
|
||||
return allow;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -16,8 +17,9 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(aiPlayer.getOpponent())) {
|
||||
sa.getTargets().add(aiPlayer.getOpponent());
|
||||
Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
@@ -51,7 +52,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,8 +64,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(ai.getOpponent())) {
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -15,6 +13,9 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@@ -131,13 +132,19 @@ public class CloneAi extends SpellAbilityAi {
|
||||
* cloneTgtAI.
|
||||
* </p>
|
||||
*
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private boolean cloneTgtAI(final SpellAbility sa) {
|
||||
// Specific logic for cards
|
||||
if ("CloneAttacker".equals(sa.getParam("AILogic"))) {
|
||||
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default:
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
// two are the only things
|
||||
// that clone a target. Those can just use SVar:RemAIDeck:True until
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
|
||||
CardCollection list =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on
|
||||
// purpose
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@@ -227,6 +227,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
||||
}
|
||||
|
||||
if (t != null) {
|
||||
if (t.isCreature())
|
||||
creatures--;
|
||||
if (t.isPlaneswalker())
|
||||
@@ -237,6 +238,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
artifacts--;
|
||||
if (t.isEnchantment())
|
||||
enchantments--;
|
||||
}
|
||||
|
||||
if (!sa.canTarget(t)) {
|
||||
list.remove(t);
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CopyPermanentAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Card source = sa.getHostCard();
|
||||
// TODO - I'm sure someone can do this AI better
|
||||
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("MimicVat".equals(aiLogic)) {
|
||||
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
|
||||
} else if ("AtEOT".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN);
|
||||
} else if ("AtOppEOT".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
}
|
||||
@@ -43,6 +48,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("Embalm") || sa.hasParam("Eternalize")) {
|
||||
// E.g. Vizier of Many Faces: check to make sure it makes sense to make the token now
|
||||
if (ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa) != AiPlayDecision.WillPlay) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) {
|
||||
sa.resetTargets();
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.List;
|
||||
@@ -25,9 +26,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
|
||||
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
|
||||
|
||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return super.chkAIDrawback(sa, aiPlayer);
|
||||
@@ -37,5 +39,17 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
||||
return spells.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
|
||||
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(player, sa);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiProps;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import java.util.Iterator;
|
||||
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostExile;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
@@ -43,7 +40,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
@@ -66,6 +63,8 @@ public class CounterAi extends SpellAbilityAi {
|
||||
// might as well check for player's friendliness
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
|
||||
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|
||||
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
|
||||
return false;
|
||||
@@ -75,6 +74,12 @@ public class CounterAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("OppDiscardsHand".equals(sa.getParam("AILogic"))) {
|
||||
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (sa.canTargetSpellAbility(topSA)) {
|
||||
sa.getTargets().add(topSA);
|
||||
@@ -93,8 +98,9 @@ public class CounterAi extends SpellAbilityAi {
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
|
||||
if (unlessCost != null && !unlessCost.endsWith(">")) {
|
||||
// Is this Usable Mana Sources? Or Total Available Mana?
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
||||
Player opp = tgtSA.getActivatingPlayer();
|
||||
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||
|
||||
int toPay = 0;
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
@@ -137,6 +143,10 @@ public class CounterAi extends SpellAbilityAi {
|
||||
if (tgtCMC < minCMC) {
|
||||
return false;
|
||||
}
|
||||
} else if ("NullBrooch".equals(logic)) {
|
||||
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,14 +156,31 @@ public class CounterAi extends SpellAbilityAi {
|
||||
boolean ctrCmc0ManaPerms = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS);
|
||||
boolean ctrDamageSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_DAMAGE_SPELLS);
|
||||
boolean ctrRemovalSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_REMOVAL_SPELLS);
|
||||
boolean ctrPumpSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_PUMP_SPELLS);
|
||||
boolean ctrAuraSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_AURAS);
|
||||
boolean ctrOtherCounters = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_OTHER_COUNTERSPELLS);
|
||||
int ctrChanceCMC1 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_1);
|
||||
int ctrChanceCMC2 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_2);
|
||||
int ctrChanceCMC3 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_3);
|
||||
String ctrNamed = aic.getProperty(AiProps.ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS);
|
||||
boolean dontCounter = false;
|
||||
|
||||
if (tgtCMC == 1 && !MyRandom.percentTrue(ctrChanceCMC1)) {
|
||||
dontCounter = true;
|
||||
} else if (tgtCMC == 2 && !MyRandom.percentTrue(ctrChanceCMC2)) {
|
||||
dontCounter = true;
|
||||
} else if (tgtCMC == 3 && !MyRandom.percentTrue(ctrChanceCMC3)) {
|
||||
dontCounter = true;
|
||||
}
|
||||
|
||||
if (tgtSA != null && tgtCMC < aic.getIntProperty(AiProps.MIN_SPELL_CMC_TO_COUNTER)) {
|
||||
boolean dontCounter = true;
|
||||
dontCounter = true;
|
||||
Card tgtSource = tgtSA.getHostCard();
|
||||
if ((tgtSource != null && tgtCMC == 0 && tgtSource.isPermanent() && !tgtSource.getManaAbilities().isEmpty() && ctrCmc0ManaPerms)
|
||||
|| (tgtSA.getApi() == ApiType.DealDamage || tgtSA.getApi() == ApiType.LoseLife || tgtSA.getApi() == ApiType.DamageAll && ctrDamageSpells)
|
||||
|| (tgtSA.getApi() == ApiType.Counter && ctrOtherCounters)
|
||||
|| ((tgtSA.getApi() == ApiType.Pump || tgtSA.getApi() == ApiType.PumpAll) && ctrPumpSpells)
|
||||
|| (tgtSA.getApi() == ApiType.Attach && ctrAuraSpells)
|
||||
|| (tgtSA.getApi() == ApiType.Destroy || tgtSA.getApi() == ApiType.DestroyAll || tgtSA.getApi() == ApiType.Sacrifice
|
||||
|| tgtSA.getApi() == ApiType.SacrificeAll && ctrRemovalSpells)) {
|
||||
dontCounter = false;
|
||||
@@ -167,15 +194,38 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// should always counter CMC 1 with Mental Misstep despite a possible limitation by minimum CMC
|
||||
if (tgtCMC == 1 && "Mental Misstep".equals(source.getName())) {
|
||||
// should not refrain from countering a CMC X spell if that's the only CMC
|
||||
// counterable with that particular counterspell type (e.g. Mental Misstep vs. CMC 1 spells)
|
||||
if (sa.getParamOrDefault("ValidTgts", "").startsWith("Card.cmcEQ")) {
|
||||
int validTgtCMC = AbilityUtils.calculateAmount(source, sa.getParam("ValidTgts").substring(10), sa);
|
||||
if (tgtCMC == validTgtCMC) {
|
||||
dontCounter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should ALWAYS counter if it doesn't spend a card, otherwise it wastes an opportunity
|
||||
// to gain card advantage
|
||||
if (sa.isAbility()
|
||||
&& (!sa.getPayCosts().hasSpecificCostType(CostDiscard.class))
|
||||
&& (!sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))
|
||||
&& (!sa.getPayCosts().hasSpecificCostType(CostExile.class))) {
|
||||
// TODO: maybe also disallow CostPayLife?
|
||||
dontCounter = false;
|
||||
}
|
||||
|
||||
// Null Brooch is special - it has a discard cost, but the AI will be
|
||||
// discarding no cards, or is playing a deck where discarding is a benefit
|
||||
// as defined in SpecialCardAi.NullBrooch
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if ("NullBrooch".equals(sa.getParam("AILogic"))) {
|
||||
dontCounter = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (dontCounter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -212,8 +262,9 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
if (unlessCost != null) {
|
||||
// Is this Usable Mana Sources? Or Total Available Mana?
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
||||
Player opp = tgtSA.getActivatingPlayer();
|
||||
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||
|
||||
int toPay = 0;
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -24,6 +16,9 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountersMoveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
@@ -109,6 +104,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
// Make sure that removing the last counter doesn't kill the creature
|
||||
if ("Self".equals(sa.getParam("Source"))) {
|
||||
if (host != null && host.getNetToughness() - 1 <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -188,6 +189,13 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
if (newEval < oldEval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for some specific AI preferences
|
||||
if (src.hasStartOfKeyword("Graft") && "DontMoveCounterIfLethal".equals(src.getSVar("AIGraftPreference"))) {
|
||||
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// no target
|
||||
return true;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
@@ -20,6 +14,7 @@ import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -32,6 +27,11 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class CountersPutAi extends SpellAbilityAi {
|
||||
|
||||
/*
|
||||
@@ -225,13 +225,42 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
|
||||
boolean onlyInCombat = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
|
||||
boolean onlyDefensive = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_DEFENSIVELY);
|
||||
|
||||
if (playAggro) {
|
||||
// aggro profiles ignore conservative play for this AI logic
|
||||
return true;
|
||||
} else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
return true;
|
||||
} else if (ai.getGame().getCombat() != null && sa.getHostCard() != null) {
|
||||
if (ai.getGame().getCombat().isAttacking(sa.getHostCard())) {
|
||||
if (ai.getGame().getCombat().isAttacking(sa.getHostCard()) && !onlyDefensive) {
|
||||
return true;
|
||||
} else if (ai.getGame().getCombat().isBlocking(sa.getHostCard())) {
|
||||
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
|
||||
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(sa.getHostCard());
|
||||
int totBlkPower = Aggregates.sum(blocked, CardPredicates.Accessors.fnGetNetPower);
|
||||
int totBlkToughness = Aggregates.min(blocked, CardPredicates.Accessors.fnGetNetToughness);
|
||||
|
||||
int numActivations = ai.getCounters(CounterType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
|
||||
if (sa.getHostCard().getNetToughness() + numActivations > totBlkPower
|
||||
|| sa.getHostCard().getNetPower() + numActivations >= totBlkToughness) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (sa.getSubAbility() != null
|
||||
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
|
||||
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
|
||||
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ANIMATED_THIS_TURN)) {
|
||||
// Bristling Hydra: save from death using a ping activation
|
||||
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return true;
|
||||
}
|
||||
} else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
// outside of combat, this logic only works if the relevant AI profile option is enabled
|
||||
// and if there is enough energy saved
|
||||
if (!onlyInCombat) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -273,7 +302,8 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
}
|
||||
|
||||
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
|
||||
if (amountStr.equals("X")) {
|
||||
if (source.getSVar(amountStr).equals("Count$xPaid")) {
|
||||
// By default, set PayX here to maximum value (used for most SAs of this type).
|
||||
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
|
||||
@@ -289,10 +319,19 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
amount = Math.min(amount, maxCtrs - curCtrs);
|
||||
if (amount <= 0) { return false; }
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
source.setSVar("PayX", Integer.toString(amount));
|
||||
} else if ("ExiledCreatureFromGraveCMC".equals(sa.getParam("AILogic"))) {
|
||||
// e.g. Necropolis
|
||||
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
|
||||
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't use it if no counters to add
|
||||
@@ -326,7 +365,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
|
||||
if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && !ph.isPlayerTurn(ai)) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -373,6 +412,13 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
});
|
||||
|
||||
if (abCost.hasSpecificCostType(CostSacrifice.class)) {
|
||||
Card sacTarget = ComputerUtil.getCardPreference(ai, source, "SacCost", list);
|
||||
// this card is planned to be sacrificed during cost payment, so don't target it
|
||||
// (otherwise the AI can cheat by activating this SA and not paying the sac cost, e.g. Extruder)
|
||||
list.remove(sacTarget);
|
||||
}
|
||||
|
||||
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -491,7 +537,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
|
||||
boolean immediately = ComputerUtil.playImmediately(ai, sa);
|
||||
|
||||
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, immediately)) {
|
||||
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -528,7 +574,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||
|
||||
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|
||||
|| (sa.getRootAbility().isTrigger() || !sa.getRootAbility().isOptionalTrigger());
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = null;
|
||||
@@ -670,6 +716,13 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
preferred = false;
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
// Still an empty list, but we have to choose something (mandatory); expand targeting to
|
||||
// include AI's own cards to see if there's anything targetable (e.g. Plague Belcher).
|
||||
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
preferred = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -35,10 +37,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
final String type = sa.getParam("CounterType");
|
||||
final String amountStr = sa.getParam("CounterNum");
|
||||
final String valid = sa.getParam("ValidCards");
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final boolean curse = sa.isCurse();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
hList = CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
hList = CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
|
||||
if (abCost != null) {
|
||||
@@ -51,13 +54,23 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("AtEOTOrBlock")) {
|
||||
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return false;
|
||||
}
|
||||
} else if (logic.equals("AtOppEOT")) {
|
||||
if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (tgt != null) {
|
||||
Player pl = curse ? ai.getOpponent() : ai;
|
||||
Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
|
||||
sa.getTargets().add(pl);
|
||||
|
||||
hList = CardLists.filterControlledBy(hList, pl);
|
||||
@@ -138,6 +151,33 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return player.getCreaturesInPlay().size() >= player.getOpponent().getCreaturesInPlay().size();
|
||||
return player.getCreaturesInPlay().size() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
List<Player> players = Lists.newArrayList();
|
||||
if (!sa.isCurse()) {
|
||||
players.add(aiPlayer);
|
||||
}
|
||||
players.addAll(aiPlayer.getOpponents());
|
||||
players.addAll(aiPlayer.getAllies());
|
||||
if (sa.isCurse()) {
|
||||
players.add(aiPlayer);
|
||||
}
|
||||
|
||||
for (final Player p : players) {
|
||||
if (p.canBeTargetedBy(sa) && sa.canTarget(p)) {
|
||||
boolean preferred = false;
|
||||
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return preferred || mandatory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mandatory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,20 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerController.BinaryChoiceType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* AbilityFactory_PutOrRemoveCountersAi class.
|
||||
@@ -75,13 +70,8 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa);
|
||||
if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
||||
list = prefList;
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||
|
||||
if (sa.hasParam("CounterType")) {
|
||||
// currently only Jhoira's Timebug
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
@@ -10,12 +8,7 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -23,6 +16,9 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
/*
|
||||
@@ -102,13 +98,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa);
|
||||
if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
||||
list = prefList;
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||
|
||||
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
|
||||
|
||||
@@ -174,10 +165,11 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Get rid of Planeswalkers:
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
list = game.getPlayers().getCardsIn(ZoneType.Battlefield);
|
||||
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
|
||||
|
||||
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS,
|
||||
CardCollection planeswalkerList = CardLists.filter(list,
|
||||
Predicates.and(CardPredicates.Presets.PLANEWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
|
||||
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
|
||||
|
||||
if (!planeswalkerList.isEmpty()) {
|
||||
|
||||
@@ -1,24 +1,63 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerDamageDone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
protected boolean avoidTargetP(final Player comp, final SpellAbility sa) {
|
||||
Player enemy = ComputerUtil.getOpponentFor(comp);
|
||||
// Logic for cards that damage owner, like Fireslinger
|
||||
// Do not target a player if they aren't below 75% of our health.
|
||||
// Unless Lifelink will cancel the damage to us
|
||||
Card hostcard = sa.getHostCard();
|
||||
boolean lifelink = hostcard.hasKeyword("Lifelink");
|
||||
for (Card ench : hostcard.getEnchantedBy(false)) {
|
||||
// Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
|
||||
if (ench.hasSVar("LikeLifeLink")) {
|
||||
if ("True".equals(ench.getSVar("LikeLifeLink"))) {
|
||||
lifelink = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("SelfDamage".equals(sa.getParam("AILogic"))) {
|
||||
if (comp.getLife() * 0.75 < enemy.getLife()) {
|
||||
if (!lifelink) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
|
||||
int restDamage = d;
|
||||
final Game game = comp.getGame();
|
||||
final Player enemy = comp.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(comp);
|
||||
boolean dmgByCardsInHand = false;
|
||||
|
||||
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
|
||||
sa.getHostCard().getSVar(sa.getParam("NumDmg")).equals("TargetedPlayer$CardsInHand")) {
|
||||
dmgByCardsInHand = true;
|
||||
}
|
||||
// Not sure if type choice implemented for the AI yet but it should at least recognize this spell hits harder on larger enemy hand size
|
||||
if ("Blood Oath".equals(sa.getHostCard().getName())) {
|
||||
dmgByCardsInHand = true;
|
||||
}
|
||||
|
||||
if (!sa.canTarget(enemy)) {
|
||||
return false;
|
||||
}
|
||||
@@ -26,11 +65,27 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Benefits hitting players?
|
||||
// If has triggered ability on dealing damage to an opponent, go for it!
|
||||
Card hostcard = sa.getHostCard();
|
||||
for (Trigger trig : hostcard.getTriggers()) {
|
||||
if (trig instanceof TriggerDamageDone) {
|
||||
if (("Opponent".equals(trig.getParam("ValidTarget")))
|
||||
&& (!"True".equals(trig.getParam("CombatDamage")))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// burn Planeswalkers
|
||||
if (Iterables.any(enemy.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANEWALKERS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (avoidTargetP(comp, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!noPrevention) {
|
||||
restDamage = ComputerUtilCombat.predictDamageTo(enemy, restDamage, sa.getHostCard(), false);
|
||||
} else {
|
||||
@@ -70,10 +125,20 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
value = 1.0f * restDamage / enemy.getLife();
|
||||
}
|
||||
} else {
|
||||
if (phase.isPlayerTurn(enemy) && phase.is(PhaseType.END_OF_TURN)) {
|
||||
// If Sudden Impact type spell, and can hit at least 3 cards during draw phase
|
||||
// have a 100% chance to go for it, enemy hand will only lose cards over time!
|
||||
// But if 3 or less cards, use normal rules, just in case enemy starts holding card or plays a draw spell or we need mana for other instants.
|
||||
if (phase.isPlayerTurn(enemy)) {
|
||||
if (dmgByCardsInHand
|
||||
&& (phase.is(PhaseType.DRAW))
|
||||
&& (enemy.getCardsIn(ZoneType.Hand).size() > 3)) {
|
||||
value = 1;
|
||||
} else if (phase.is(PhaseType.END_OF_TURN)
|
||||
|| ((dmgByCardsInHand && phase.getPhase().isAfter(PhaseType.UPKEEP)))) {
|
||||
value = 1.5f * restDamage / enemy.getLife();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value > 0) { //more likely to burn with larger hand
|
||||
for (int i = 3; i < hand.size(); i++) {
|
||||
value *= 1.1f;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -56,16 +55,35 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
x = source.getCounters(CounterType.LOYALTY);
|
||||
}
|
||||
if (x == -1) {
|
||||
Player bestOpp = determineOppToKill(ai, sa, source, dmg);
|
||||
if (determineOppToKill(ai, sa, source, dmg) != null) {
|
||||
// we already know we can kill a player, so go for it
|
||||
return true;
|
||||
}
|
||||
// look for other value in this (damaging creatures or
|
||||
// creatures + player, e.g. Pestilence, etc.)
|
||||
return evaluateDamageAll(ai, sa, source, dmg) > 0;
|
||||
} else {
|
||||
int best = -1, best_x = -1;
|
||||
for (int i = 0; i < x; i++) {
|
||||
Player bestOpp = determineOppToKill(ai, sa, source, x);
|
||||
if (bestOpp != null) {
|
||||
// we can finish off a player, so go for it
|
||||
|
||||
// TODO: improve this by possibly damaging more creatures
|
||||
// on the battlefield belonging to other opponents at the same
|
||||
// time, if viable
|
||||
best_x = bestOpp.getLife();
|
||||
} else {
|
||||
// see if it's possible to get value from killing off creatures
|
||||
for (int i = 0; i <= x; i++) {
|
||||
final int value = evaluateDamageAll(ai, sa, source, i);
|
||||
if (value > best) {
|
||||
best = value;
|
||||
best_x = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best_x > 0) {
|
||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
source.setSVar("PayX", Integer.toString(best_x));
|
||||
@@ -79,8 +97,31 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
private Player determineOppToKill(Player ai, SpellAbility sa, Card source, int x) {
|
||||
// Attempt to determine which opponent can be finished off such that the most players
|
||||
// are killed at the same time, given X damage tops
|
||||
final String validP = sa.hasParam("ValidPlayers") ? sa.getParam("ValidPlayers") : "";
|
||||
int aiLife = ai.getLife();
|
||||
Player bestOpp = null; // default opponent, if all else fails
|
||||
|
||||
for (int dmg = 1; dmg <= x; dmg++) {
|
||||
// Don't kill yourself in the process
|
||||
if (validP.equals("Player") && aiLife <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)) {
|
||||
break;
|
||||
}
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
if ((validP.equals("Player") || validP.contains("Opponent"))
|
||||
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
||||
bestOpp = opp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestOpp;
|
||||
}
|
||||
|
||||
private int evaluateDamageAll(Player ai, SpellAbility sa, final Card source, int dmg) {
|
||||
Player opp = ai.getOpponent();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final CardCollection humanList = getKillableCreatures(sa, opp, dmg);
|
||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||
|
||||
@@ -98,12 +139,6 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if we can kill human, do it
|
||||
if ((validP.equals("Player") || validP.contains("Opponent"))
|
||||
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int minGain = 200; // The minimum gain in destroyed creatures
|
||||
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
|
||||
if (computerList.isEmpty()) {
|
||||
@@ -179,7 +214,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Evaluate creatures getting killed
|
||||
Player enemy = ai.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -261,7 +296,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Evaluate creatures getting killed
|
||||
Player enemy = ai.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -28,6 +20,9 @@ import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DamageDealAi extends DamageAiBase {
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
@@ -98,6 +93,29 @@ public class DamageDealAi extends DamageAiBase {
|
||||
source.setSVar("PayX", Integer.toString(dmg));
|
||||
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
|
||||
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
|
||||
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
|
||||
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
|
||||
if (sa.getTargetRestrictions().canTgtPlayer()) {
|
||||
int maxDmg = 0;
|
||||
Player maxDamaged = null;
|
||||
for (Player p : ai.getOpponents()) {
|
||||
if (p.canBeTargetedBy(sa)) {
|
||||
if (p.getCardsIn(ZoneType.Hand).size() > maxDmg) {
|
||||
maxDmg = p.getCardsIn(ZoneType.Hand).size();
|
||||
maxDamaged = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxDmg > 0 && maxDamaged != null) {
|
||||
if (shouldTgtP(ai, sa, maxDmg, false)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(maxDamaged);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +123,20 @@ public class DamageDealAi extends DamageAiBase {
|
||||
dmg += 2;
|
||||
}
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("DiscardLands".equals(logic)) {
|
||||
dmg = 2;
|
||||
} else if (logic.startsWith("ProcRaid.")) {
|
||||
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ai.getAttackedWithCreatureThisTurn()) {
|
||||
dmg = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
|
||||
}
|
||||
} else if ("WildHunt".equals(logic)) {
|
||||
// This dummy ability will just deal 0 damage, but holds the logic for the AI for Master of Wild Hunt
|
||||
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source);
|
||||
@@ -131,6 +160,18 @@ public class DamageDealAi extends DamageAiBase {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if ("NinThePainArtist".equals(logic)) {
|
||||
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
|
||||
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||
boolean doTarget = damageTargetAI(ai, sa, dmg, true);
|
||||
if (doTarget) {
|
||||
Card tgt = sa.getTargets().getFirstTargetedCard();
|
||||
if (tgt != null) {
|
||||
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceName.equals("Sorin, Grim Nemesis")) {
|
||||
@@ -157,7 +198,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -250,15 +291,19 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
hPlay = CardLists.getTargetableCards(hPlay, sa);
|
||||
|
||||
final List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||
List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.getSVar("Targeting").equals("Dies")
|
||||
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d) && !ComputerUtil.canRegenerate(ai, c)
|
||||
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d)
|
||||
&& !ComputerUtil.canRegenerate(ai, c)
|
||||
&& !(c.getSVar("SacMe").length() > 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
|
||||
|
||||
Card targetCard = null;
|
||||
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
||||
if (sa.getTargetRestrictions().canTgtPlaneswalker()) {
|
||||
@@ -345,7 +390,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
||||
|
||||
Player enemy = ai.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
|
||||
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
|
||||
@@ -366,6 +411,11 @@ public class DamageDealAi extends DamageAiBase {
|
||||
// target loop
|
||||
TargetChoices tcs = sa.getTargets();
|
||||
|
||||
// Do not use if would kill self
|
||||
if (("SelfDamage".equals(sa.getParam("AILogic"))) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("ChoiceBurn".equals(sa.getParam("AILogic"))) {
|
||||
// do not waste burns on player if other choices are present
|
||||
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
||||
@@ -377,7 +427,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
if ("Polukranos".equals(sa.getParam("AILogic"))) {
|
||||
int dmgTaken = 0;
|
||||
CardCollection humCreatures = ai.getOpponent().getCreaturesInPlay();
|
||||
CardCollection humCreatures = enemy.getCreaturesInPlay();
|
||||
Card lastTgt = null;
|
||||
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
||||
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
|
||||
@@ -486,7 +536,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
if (freePing && sa.canTarget(enemy)) {
|
||||
if (freePing && sa.canTarget(enemy) && (!avoidTargetP(ai, sa))) {
|
||||
tcs.add(enemy);
|
||||
if (divided) {
|
||||
tgt.addDividedAllocation(enemy, dmg);
|
||||
@@ -528,10 +578,11 @@ public class DamageDealAi extends DamageAiBase {
|
||||
// TODO: Improve Damage, we shouldn't just target the player just
|
||||
// because we can
|
||||
else if (sa.canTarget(enemy)) {
|
||||
if ((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|
||||
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|
||||
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|
||||
|| sa.getPayCosts() == null || immediately
|
||||
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
||||
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) &&
|
||||
(!avoidTargetP(ai, sa))) {
|
||||
tcs.add(enemy);
|
||||
if (divided) {
|
||||
tgt.addDividedAllocation(enemy, dmg);
|
||||
@@ -633,7 +684,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
// this is for Triggered targets that are mandatory
|
||||
final boolean noPrevention = sa.hasParam("NoPrevention");
|
||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
System.out.println("damageChooseRequiredTargets " + ai + " " + sa);
|
||||
|
||||
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
|
||||
@@ -3,10 +3,6 @@ package forge.ai.ability;
|
||||
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DamagePreventAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -42,7 +43,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
// temporarily disabled until AI is improved
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
* @return a CardCollection.
|
||||
*/
|
||||
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
|
||||
if (!list.isEmpty()) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@@ -216,7 +217,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
list.remove(c);
|
||||
}
|
||||
|
||||
final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponent());
|
||||
final CardCollection pref = CardLists.filterControlledBy(list, ComputerUtil.getOpponentFor(ai));
|
||||
final CardCollection forced = CardLists.filterControlledBy(list, ai);
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
|
||||
@@ -18,8 +18,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
// TODO: improve ai
|
||||
return true;
|
||||
}
|
||||
final String svarName = sa.getParam("Execute");
|
||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getHostCard().getSVar(svarName), sa.getHostCard());
|
||||
SpellAbility trigsa = null;
|
||||
if (sa.hasAdditionalAbility("Execute")) {
|
||||
trigsa = sa.getAdditionalAbility("Execute");
|
||||
} else {
|
||||
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (trigsa instanceof AbilitySub) {
|
||||
@@ -31,8 +35,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final String svarName = sa.getParam("Execute");
|
||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getHostCard().getSVar(svarName), sa.getHostCard());
|
||||
SpellAbility trigsa = null;
|
||||
if (sa.hasAdditionalAbility("Execute")) {
|
||||
trigsa = sa.getAdditionalAbility("Execute");
|
||||
} else {
|
||||
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||
}
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
@@ -45,8 +53,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final String svarName = sa.getParam("Execute");
|
||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getSVar(svarName), sa.getHostCard());
|
||||
SpellAbility trigsa = null;
|
||||
if (sa.hasAdditionalAbility("Execute")) {
|
||||
trigsa = sa.getAdditionalAbility("Execute");
|
||||
} else {
|
||||
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiProps;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
@@ -49,7 +35,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection list;
|
||||
|
||||
if (abCost != null) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,10 +50,44 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
hasXCost = abCost.getCostMana() != null ? abCost.getCostMana().getAmountOfX() > 0 : false;
|
||||
}
|
||||
|
||||
if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (ph.getPlayerTurn() == ai || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
} else if ("AtEOT".equals(sa.getParam("AILogic"))) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (!ph.is(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
} else if ("AtEOTIfNotAttacking".equals(sa.getParam("AILogic"))) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (!ph.is(PhaseType.END_OF_TURN) || ai.getAttackedWithCreatureThisTurn()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ability that's intended to destroy own useless token to trigger Grave Pacts
|
||||
// should be fired at end of turn or when under attack after blocking to make opponent sac something
|
||||
boolean havepact = false;
|
||||
|
||||
// TODO replace it with look for a dies -> sacrifice trigger check
|
||||
havepact |= ai.isCardInPlay("Grave Pact");
|
||||
havepact |= ai.isCardInPlay("Butcher of Malakir");
|
||||
havepact |= ai.isCardInPlay("Dictate of Erebos");
|
||||
if ("Pactivator".equals(logic) && havepact) {
|
||||
if ((!ai.getGame().getPhaseHandler().isPlayerTurn(ai))
|
||||
&& ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) || (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)))
|
||||
&& (ai.getOpponents().getCreaturesInPlay().size() > 0)) {
|
||||
ai.getController().chooseTargetsFor(sa);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Targeting
|
||||
if (abTgt != null) {
|
||||
sa.resetTargets();
|
||||
@@ -78,6 +98,11 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
if ("MadSarkhanDragon".equals(logic)) {
|
||||
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
|
||||
} else if (logic != null && logic.startsWith("MinLoyalty.")) {
|
||||
int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
|
||||
if (source.getCounters(CounterType.LOYALTY) < minLoyalty) {
|
||||
return false;
|
||||
}
|
||||
} else if ("Polymorph".equals(logic)) {
|
||||
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
@@ -104,26 +129,14 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
final int cmcMax = ai.hasRevolt() ? 4 : 2;
|
||||
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
|
||||
}
|
||||
if (sa.hasParam("AITgts")) {
|
||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||
if (source.isEnchanted()) {
|
||||
if (source.getEnchantedBy(false).get(0).getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
final int value = ComputerUtilCard.evaluateCreature(source);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return ComputerUtilCard.evaluateCreature(c) > value + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
list = CardLists.getValidCards(list, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||
|
||||
list = CardLists.getNotKeyword(list, "Indestructible");
|
||||
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
|
||||
}
|
||||
if (!SpellAbilityAi.playReusable(ai, sa)) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -173,6 +186,11 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if (hasXCost) {
|
||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||
maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa));
|
||||
// X can't be more than the lands we have in our hand for "discard X lands"!
|
||||
if ("ScorchedEarth".equals(logic)) {
|
||||
int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||
maxTargets = Math.min(maxTargets, lands);
|
||||
}
|
||||
}
|
||||
if (sa.hasParam("AIMaxTgtsCount")) {
|
||||
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
|
||||
@@ -267,7 +285,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
} else if (sa.hasParam("Defined")) {
|
||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|
||||
|| ai.getCreaturesInPlay().size() < ai.getOpponent().getCreaturesInPlay().size()
|
||||
|| ai.getCreaturesInPlay().size() < ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size()
|
||||
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
||||
|| ai.getLife() <= 5)) {
|
||||
// Basic ai logic for Lethal Vapors
|
||||
@@ -300,6 +318,9 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection preferred = CardLists.getNotKeyword(list, "Indestructible");
|
||||
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
|
||||
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
|
||||
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
|
||||
}
|
||||
|
||||
// If NoRegen is not set, filter out creatures that have a
|
||||
// regeneration shield
|
||||
@@ -314,25 +335,8 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
});
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||
if (source.isEnchanted()) {
|
||||
if (source.getEnchantedBy(false).get(0).getController().equals(ai)) {
|
||||
preferred.clear();
|
||||
}
|
||||
} else {
|
||||
final int value = ComputerUtilCard.evaluateCreature(source);
|
||||
preferred = CardLists.filter(preferred, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return ComputerUtilCard.evaluateCreature(c) > value + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
preferred = CardLists.getValidCards(preferred, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
preferred = ComputerUtil.filterAITgts(sa, ai, (CardCollection)preferred, true);
|
||||
|
||||
for (final Card c : preferred) {
|
||||
list.remove(c);
|
||||
@@ -453,4 +457,5 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
return tempoCheck;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
|
||||
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
Player opponent = ai.getOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
||||
Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
||||
|
||||
final int CREATURE_EVAL_THRESHOLD = 200;
|
||||
|
||||
@@ -101,6 +101,19 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
|
||||
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if only creatures are affected evaluate both lists and pass only if
|
||||
// human creatures are more valuable
|
||||
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
@@ -26,7 +20,7 @@ public class DigAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
Player libraryOwner = ai;
|
||||
|
||||
@@ -47,6 +41,8 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
|
||||
return game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
// don't deck yourself
|
||||
@@ -103,7 +99,7 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -20,6 +21,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
double chance = .4; // 40 percent chance with instant speed stuff
|
||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
chance = .667; // 66.7% chance for sorcery speed (since it will
|
||||
@@ -29,7 +31,22 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
|
||||
Player libraryOwner = ai;
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if ("DontMillSelf".equals(logic)) {
|
||||
// A card that digs for specific things and puts everything revealed before it into graveyard
|
||||
// (e.g. Hermit Druid) - don't use it to mill itself and also make sure there's enough playable
|
||||
// material in the library after using it several times.
|
||||
// TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard?
|
||||
if (ai.getCardsIn(ZoneType.Library).size() < 20) {
|
||||
return false;
|
||||
}
|
||||
if ("Land.Basic".equals(sa.getParam("Valid"))
|
||||
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA).isEmpty()) {
|
||||
// We already have a mana-producing land in hand, so bail
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -27,10 +26,11 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
||||
}
|
||||
|
||||
final boolean humanHasHand = ai.getOpponent().getCardsIn(ZoneType.Hand).size() > 0;
|
||||
if (aiLogic.equals("VolrathsShapeshifter")) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
||||
}
|
||||
|
||||
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
|
||||
|
||||
if (tgt != null) {
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
@@ -84,7 +88,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("NumCards")) {
|
||||
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getOpponent()
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
|
||||
.getCardsIn(ZoneType.Hand).size());
|
||||
if (cardsToDiscard < 1) {
|
||||
return false;
|
||||
@@ -97,7 +101,35 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement support for Discard AI for cards with AnyNumber set to true.
|
||||
// TODO: Improve support for Discard AI for cards with AnyNumber set to true.
|
||||
if (sa.hasParam("AnyNumber")) {
|
||||
if ("DiscardUncastableAndExcess".equals(aiLogic)) {
|
||||
final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
|
||||
final int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||
int numDiscard = 0;
|
||||
int numOppInHand = 0;
|
||||
for (Player p : ai.getGame().getPlayers()) {
|
||||
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
|
||||
numOppInHand = p.getCardsIn(ZoneType.Hand).size();
|
||||
}
|
||||
}
|
||||
for (Card c : inHand) {
|
||||
if (c.equals(sa.getHostCard())) { continue; }
|
||||
if (c.hasSVar("DoNotDiscardIfAble") || c.hasSVar("IsReanimatorCard")) { continue; }
|
||||
if (c.isCreature() && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getSpellPermanent(), ai)) {
|
||||
numDiscard++;
|
||||
}
|
||||
if ((c.isLand() && numLandsOTB >= 5) || (c.getFirstSpellAbility() != null && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getFirstSpellAbility(), ai))) {
|
||||
if (numDiscard + 1 <= numOppInHand) {
|
||||
numDiscard++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numDiscard == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||
@@ -120,7 +152,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
|
||||
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
|
||||
return false;
|
||||
}
|
||||
@@ -139,7 +171,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
@@ -160,7 +192,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getOpponent()
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
|
||||
.getCardsIn(ZoneType.Hand).size());
|
||||
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -19,7 +20,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Random r = MyRandom.getRandom();
|
||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
@@ -42,7 +43,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -83,7 +84,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
|
||||
@@ -86,7 +86,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,16 @@ public class DrawAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (logic.startsWith("LifeLessThan.")) {
|
||||
// LifeLessThan logic presupposes activation as soon as possible in an
|
||||
// attempt to save the AI from dying
|
||||
return true;
|
||||
} else if (logic.equals("AlwaysAtOppEOT")) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
@@ -204,6 +214,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final boolean drawback = sa.getParent() != null;
|
||||
final Game game = ai.getGame();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
|
||||
@@ -240,11 +251,23 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Logic for cards that require special handling
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
||||
if (num != null && num.equals("ChosenX")) {
|
||||
// Necrologia, Pay X Life : Draw X Cards
|
||||
if (sa.getSVar("X").equals("XChoice")) {
|
||||
// Draw up to max hand size but leave at least 3 in library
|
||||
numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
|
||||
// But no more than what's "safe" and doesn't risk a near death experience
|
||||
// Maybe would be better to check for "serious danger" and take more risk?
|
||||
while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
|
||||
numCards--;
|
||||
}
|
||||
sa.setSVar("ChosenX", Integer.toString(numCards));
|
||||
source.setSVar("ChosenX", Integer.toString(numCards));
|
||||
}
|
||||
}
|
||||
// Logic for cards that require special handling
|
||||
if ("YawgmothsBargain".equals(logic)) {
|
||||
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
||||
}
|
||||
|
||||
// Generic logic for all cards that do not need any special handling
|
||||
@@ -312,6 +335,13 @@ public class DrawAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// we're trying to save ourselves from death
|
||||
// (e.g. Bargain), so target the opp anyway
|
||||
if (logic.startsWith("LifeLessThan.")) {
|
||||
int threshold = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
|
||||
sa.getTargets().add(oppA);
|
||||
return ai.getLife() < threshold;
|
||||
}
|
||||
}
|
||||
|
||||
boolean aiTarget = sa.canTarget(ai);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
@@ -15,8 +16,7 @@ import forge.ai.SpellApiToAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -48,6 +48,21 @@ public class EffectAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
boolean worthHolding = false;
|
||||
CardCollectionView oppCreatsLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield),
|
||||
Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.Presets.CREATURES));
|
||||
CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.Presets.TAPPED);
|
||||
|
||||
if (oppCreatsLandsTapped.size() >= 3 || oppCreatsLands.size() == oppCreatsLandsTapped.size()) {
|
||||
worthHolding = true;
|
||||
}
|
||||
if (!worthHolding) {
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
}
|
||||
} else if (logic.equals("Fog")) {
|
||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||
return false;
|
||||
@@ -179,9 +194,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldPlay) {
|
||||
return true;
|
||||
}
|
||||
return shouldPlay;
|
||||
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
@@ -246,6 +259,12 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (logic.equals("CastFromGraveThisTurn")) {
|
||||
CardCollection list = new CardCollection(game.getCardsIn(ZoneType.Graveyard));
|
||||
list = CardLists.getValidCards(list, sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else { //no AILogic
|
||||
return false;
|
||||
@@ -281,4 +300,35 @@ public class EffectAi extends SpellAbilityAi {
|
||||
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
// E.g. Nova Pentacle
|
||||
if (aiLogic.equals("RedirectFromOppToCreature")) {
|
||||
// try to target the opponent's best targetable permanent, if able
|
||||
CardCollection oppPerms = CardLists.getValidCards(aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
if (!oppPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
// try to target the AI's worst targetable permanent, if able
|
||||
CardCollection aiPerms = CardLists.getValidCards(aiPlayer.getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
if (!aiPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
63
forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
Normal file
63
forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.card.*;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class ExploreAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Explore with a target (e.g. Enter the Unknown)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Card shouldPutInGraveyard(CardCollection top, Player ai) {
|
||||
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
|
||||
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
|
||||
int maxCMCDiff = 1;
|
||||
int numLandsToStillNeedMore = 2;
|
||||
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
maxCMCDiff = aic.getIntProperty(AiProps.EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD);
|
||||
numLandsToStillNeedMore = aic.getIntProperty(AiProps.EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||
}
|
||||
|
||||
if (!top.isEmpty()) {
|
||||
Card topCard = top.getFirst();
|
||||
if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) {
|
||||
// We need more lands to improve our mana base, explore away the non-lands
|
||||
return topCard;
|
||||
}
|
||||
if (topCard.getCMC() - maxCMCDiff >= predictedMana && !topCard.hasSVar("DoNotDiscardIfAble")) {
|
||||
// We're not casting this in foreseeable future, put it in the graveyard
|
||||
return topCard;
|
||||
}
|
||||
}
|
||||
|
||||
// Put on top of the library (do not mark the card for placement in the graveyard)
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -22,6 +14,10 @@ import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class FightAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
@@ -59,6 +55,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false; // bail at this point, otherwise the AI will overtarget and waste the activation
|
||||
}
|
||||
|
||||
if (sa.hasParam("TargetsFromDifferentZone")) {
|
||||
@@ -103,6 +100,11 @@ public class FightAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
return checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (canPlayAI(ai, sa)) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
public class FogAi extends SpellAbilityAi {
|
||||
|
||||
@@ -34,6 +38,25 @@ public class FogAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
|
||||
int dmg = 0;
|
||||
for (Card atk : game.getCombat().getAttackersOf(ai)) {
|
||||
if (game.getCombat().isUnblocked(atk)) {
|
||||
dmg += atk.getNetCombatDamage();
|
||||
} else if (atk.hasKeyword("Trample")) {
|
||||
dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness);
|
||||
}
|
||||
}
|
||||
|
||||
if (dmg > ai.getLife() / 4) {
|
||||
return true;
|
||||
} else if (dmg >= 5) {
|
||||
return true;
|
||||
} else if (ai.getLife() < ai.getStartingLife() / 3) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Cast it if life is in danger
|
||||
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
||||
}
|
||||
@@ -58,7 +81,7 @@ public class FogAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
final Game game = aiPlayer.getGame();
|
||||
boolean chance;
|
||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getOpponent())) {
|
||||
if (game.getPhaseHandler().isPlayerTurn(ComputerUtil.getOpponentFor(sa.getActivatingPlayer()))) {
|
||||
chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE);
|
||||
} else {
|
||||
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -8,7 +9,7 @@ import forge.game.spellability.TargetRestrictions;
|
||||
public class GameLossAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (opp.cantLose()) {
|
||||
return false;
|
||||
}
|
||||
@@ -34,14 +35,14 @@ public class GameLossAi extends SpellAbilityAi {
|
||||
// (Final Fortune would need to attach it's delayed trigger to a
|
||||
// specific turn, which can't be done yet)
|
||||
|
||||
if (!mandatory && ai.getOpponent().cantLose()) {
|
||||
if (!mandatory && ComputerUtil.getOpponentFor(ai).cantLose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -22,7 +23,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
final Random r = MyRandom.getRandom();
|
||||
final int myLife = aiPlayer.getLife();
|
||||
Player opponent = aiPlayer.getOpponent();
|
||||
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
final int hLife = opponent.getLife();
|
||||
|
||||
if (!aiPlayer.canGainLife()) {
|
||||
@@ -78,7 +79,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
||||
final boolean mandatory) {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -35,7 +38,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
|
||||
if (!lifeCritical) {
|
||||
// return super.willPayCosts(ai, sa, cost, source);
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
@@ -60,7 +63,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
skipCheck |= ComputerUtilCost.isSacrificeSelfCost(cost) && !source.isCreature();
|
||||
|
||||
if (!skipCheck) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -80,6 +83,23 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE)
|
||||
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
||||
|
||||
// When life is critical but there is no immediate danger, try to wait until declare blockers
|
||||
// before using the lifegain ability if it's an ability on a creature with a detrimental activation cost
|
||||
if (lifeCritical
|
||||
&& sa.isAbility()
|
||||
&& sa.getHostCard() != null && sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts() != null
|
||||
&& (sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class) || sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))) {
|
||||
if (!game.getStack().isEmpty()) {
|
||||
SpellAbility saTop = game.getStack().peekAbility();
|
||||
if (saTop.getTargets() != null && Iterables.contains(saTop.getTargets().getTargetPlayers(), ai)) {
|
||||
return ComputerUtil.predictDamageFromSpell(saTop, ai) > 0;
|
||||
}
|
||||
}
|
||||
if (game.getCombat() == null) { return false; }
|
||||
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; }
|
||||
}
|
||||
|
||||
// Don't use lifegain before main 2 if possible
|
||||
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -22,7 +23,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
// Ability_Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final int myLife = ai.getLife();
|
||||
final Player opponent = ai.getOpponent();
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
final int hlife = opponent.getLife();
|
||||
final String amountStr = sa.getParam("LifeAmount");
|
||||
|
||||
@@ -93,7 +94,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (amount < myLife) {
|
||||
if (amount <= myLife) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +110,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final int myLife = ai.getLife();
|
||||
final Player opponent = ai.getOpponent();
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
final int hlife = opponent.getLife();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -110,13 +104,28 @@ public class ManaEffectAi extends SpellAbilityAi {
|
||||
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
int numManaSrcs = manaSources.size();
|
||||
int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
|
||||
manaReceived *= sa.getParam("Produced").split(" ").length;
|
||||
|
||||
int selfCost = sa.getPayCosts().getCostMana() != null ? sa.getPayCosts().getCostMana().getMana().getCMC() : 0;
|
||||
byte producedColor = MagicColor.fromName(sa.getParam("Produced"));
|
||||
|
||||
String produced = sa.getParam("Produced");
|
||||
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
|
||||
|
||||
if ("ChosenX".equals(sa.getParam("Amount"))
|
||||
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
|
||||
CounterType ctrType = CounterType.KI; // Petalmane Baku
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
manaReceived = host.getCounters(ctrType);
|
||||
}
|
||||
|
||||
int searchCMC = numManaSrcs - selfCost + manaReceived;
|
||||
|
||||
if ("X".equals(sa.getParam("Produced"))) {
|
||||
@@ -156,6 +165,9 @@ public class ManaEffectAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
SpellAbility testSaNoCost = testSa.copyWithNoManaCost();
|
||||
if (testSaNoCost == null) {
|
||||
continue;
|
||||
}
|
||||
testSaNoCost.setActivatingPlayer(ai);
|
||||
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) {
|
||||
if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword("Haste")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -60,7 +62,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
boolean chance = false;
|
||||
|
||||
if (abTgt != null) {
|
||||
List<Card> list = CardLists.filter(ai.getOpponent().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||
List<Card> list = CardLists.filter(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
@@ -14,13 +17,13 @@ import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class PermanentAi extends SpellAbilityAi {
|
||||
|
||||
@@ -62,6 +65,8 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* -- not used anymore after Ixalan (Planeswalkers are now legendary, not unique by subtype) --
|
||||
if (card.isPlaneswalker()) {
|
||||
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.Presets.PLANEWALKERS);
|
||||
@@ -75,7 +80,7 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if (card.getType().hasSupertype(Supertype.World)) {
|
||||
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
|
||||
@@ -142,7 +147,8 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// don't play cards without being able to pay the upkeep for
|
||||
for (String ability : card.getKeywords()) {
|
||||
for (KeywordInterface inst : card.getKeywords()) {
|
||||
String ability = inst.getOriginal();
|
||||
if (ability.startsWith("UpkeepCost")) {
|
||||
final String[] k = ability.split(":");
|
||||
final String costs = k[1];
|
||||
@@ -175,22 +181,44 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
if (!hasCard) {
|
||||
dontCast = true;
|
||||
}
|
||||
} else if (param.equals("MaxControlled")) {
|
||||
// Only cast unless there are X or more cards like this on the battlefield under AI control already
|
||||
int numControlled = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())).size();
|
||||
} else if (param.startsWith("MaxControlled")) {
|
||||
// Only cast unless there are X or more cards like this on the battlefield under AI control already,
|
||||
CardCollection ctrld = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName()));
|
||||
|
||||
int numControlled = 0;
|
||||
if (param.endsWith("WithoutOppAuras")) {
|
||||
// Check that the permanet does not have any auras attached to it by the opponent (this assumes that if
|
||||
// the opponent cast an aura on the opposing permanent, it's not with good intentions, and thus it might
|
||||
// be better to have a pristine copy of the card - might not always be a correct assumption, but sounds
|
||||
// like a reasonable default for some cards).
|
||||
for (Card c : ctrld) {
|
||||
if (c.getEnchantedBy(false).isEmpty()) {
|
||||
numControlled++;
|
||||
} else {
|
||||
for (Card att : c.getEnchantedBy(false)) {
|
||||
if (!att.getController().isOpponentOf(ai)) {
|
||||
numControlled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
numControlled = ctrld.size();
|
||||
}
|
||||
|
||||
if (numControlled >= Integer.parseInt(value)) {
|
||||
dontCast = true;
|
||||
}
|
||||
} else if (param.equals("NumManaSources")) {
|
||||
// Only cast if there are X or more mana sources controlled by the AI
|
||||
CardCollection m = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
if (m.size() < Integer.parseInt(value)) {
|
||||
dontCast = true;
|
||||
}
|
||||
} else if (param.equals("NumManaSourcesNextTurn")) {
|
||||
// Only cast if there are X or more mana sources controlled by the AI *or*
|
||||
// if there are X-1 mana sources in play but the AI has an extra land in hand
|
||||
CardCollection m = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0;
|
||||
if (card.getName().equals("Illusions of Grandeur")) {
|
||||
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -33,6 +34,10 @@ public class PhasesAi extends SpellAbilityAi {
|
||||
tgtCards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if (tgtCards.contains(source)) {
|
||||
// Protect it from something
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source);
|
||||
if (isThreatened) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Card def = tgtCards.get(0);
|
||||
// Phase this out if it might attack me, or before it can be
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.CardTypeView;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -22,6 +17,8 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PlayAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
@@ -59,6 +56,16 @@ public class PlayAi extends SpellAbilityAi {
|
||||
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
|
||||
}
|
||||
|
||||
if (source != null && source.hasKeyword("Hideaway") && source.hasRemembered()) {
|
||||
// AI is not very good at playing non-permanent spells this way, at least yet
|
||||
// (might be possible to enable it for Sorceries in Main1/Main2 if target is available,
|
||||
// but definitely not for most Instants)
|
||||
Card rem = (Card) source.getFirstRemembered();
|
||||
CardTypeView t = rem.getState(CardStateName.Original).getType();
|
||||
|
||||
return t.isPermanent() && !t.isLand();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -99,12 +106,13 @@ public class PlayAi extends SpellAbilityAi {
|
||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||
*/
|
||||
@Override
|
||||
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
||||
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
|
||||
final boolean isOptional,
|
||||
Player targetedPlayer) {
|
||||
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
for (SpellAbility s : c.getBasicSpells()) {
|
||||
for (SpellAbility s : c.getBasicSpells(c.getState(CardStateName.Original))) {
|
||||
Spell spell = (Spell) s;
|
||||
s.setActivatingPlayer(ai);
|
||||
// timing restrictions still apply
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -31,7 +32,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
|
||||
List<Card> list =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on
|
||||
// purpose
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@@ -147,9 +147,10 @@ public class ProtectAi extends SpellAbilityAi {
|
||||
if (s==null) {
|
||||
return false;
|
||||
} else {
|
||||
Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
Combat combat = ai.getGame().getCombat();
|
||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, ai.getOpponent(), combat, true);
|
||||
float ratio = 1.0f * dmg / ai.getOpponent().getLife();
|
||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
||||
float ratio = 1.0f * dmg / opponent.getLife();
|
||||
Random r = MyRandom.getRandom();
|
||||
return r.nextFloat() < ratio;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ProtectAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostTapType;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -34,7 +21,13 @@ import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityRestriction;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PumpAi extends PumpAiBase {
|
||||
|
||||
@@ -82,16 +75,19 @@ public class PumpAi extends PumpAiBase {
|
||||
System.err.println("MoveCounter AiLogic without MoveCounter SubAbility!");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
} else if ("Aristocrat".equals(aiLogic)) {
|
||||
return doAristocratLogic(sa, ai);
|
||||
} else if (aiLogic.startsWith("AristocratCounters")) {
|
||||
return doAristocratWithCountersLogic(sa, ai);
|
||||
}
|
||||
|
||||
return super.checkAiLogic(ai, sa, aiLogic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
|
||||
final String logic) {
|
||||
// special Phase check for MoveCounter
|
||||
// special Phase check for various AI logics
|
||||
if (logic.equals("MoveCounter")) {
|
||||
if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
return true;
|
||||
@@ -101,6 +97,11 @@ public class PumpAi extends PumpAiBase {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (logic.equals("Aristocrat")) {
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(sa.getHostCard());
|
||||
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
@@ -112,7 +113,7 @@ public class PumpAi extends PumpAiBase {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
|
||||
return false;
|
||||
}
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN) && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -136,12 +137,16 @@ public class PumpAi extends PumpAiBase {
|
||||
final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : "";
|
||||
final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : "";
|
||||
|
||||
final String aiLogic = sa.getParam("AILogic");
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic);
|
||||
final boolean isBerserk = "Berserk".equals(aiLogic);
|
||||
|
||||
if ("MoveCounter".equals(aiLogic)) {
|
||||
if ("Pummeler".equals(aiLogic)) {
|
||||
return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa);
|
||||
} else if (aiLogic.startsWith("AristocratCounters")) {
|
||||
return true; // the preconditions to this are already tested in checkAiLogic
|
||||
} else if ("MoveCounter".equals(aiLogic)) {
|
||||
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
|
||||
|
||||
if (moveSA == null) {
|
||||
@@ -333,6 +338,23 @@ public class PumpAi extends PumpAiBase {
|
||||
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
|
||||
}
|
||||
|
||||
if ("ContinuousBonus".equals(aiLogic)) {
|
||||
// P/T bonus in a continuous static ability
|
||||
for (StaticAbility stAb : source.getStaticAbilities()) {
|
||||
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
attack += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddToughness")) {
|
||||
defense += AbilityUtils.calculateAmount(source, stAb.getParam("AddToughness"), stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddKeyword")) {
|
||||
keywords.addAll(Lists.newArrayList(stAb.getParam("AddKeyword").split(" & ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) {
|
||||
return false;
|
||||
}
|
||||
@@ -359,9 +381,21 @@ public class PumpAi extends PumpAiBase {
|
||||
|
||||
return true;
|
||||
}
|
||||
if (!card.getController().isOpponentOf(ai)
|
||||
&& ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
||||
if (!card.getController().isOpponentOf(ai)) {
|
||||
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
||||
return true;
|
||||
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
|
||||
|
||||
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
|
||||
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)
|
||||
|| game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
|
||||
if (!ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -371,6 +405,21 @@ public class PumpAi extends PumpAiBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("DebuffForXCounters".equals(sa.getParam("AILogic")) && sa.getTargetCard() != null) {
|
||||
// e.g. Skullmane Baku
|
||||
CounterType ctrType = CounterType.KI;
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not pay more counters than necessary to kill the targeted creature
|
||||
int chosenX = Math.min(source.getCounters(ctrType), sa.getTargetCard().getNetToughness());
|
||||
sa.setSVar("ChosenX", String.valueOf(chosenX));
|
||||
}
|
||||
|
||||
return true;
|
||||
} // pumpPlayAI()
|
||||
|
||||
@@ -405,10 +454,28 @@ public class PumpAi extends PumpAiBase {
|
||||
|
||||
CardCollection list;
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (sa.getParam("AILogic").equals("HighestPower")) {
|
||||
if (sa.getParam("AILogic").equals("HighestPower") || sa.getParam("AILogic").equals("ContinuousBonus")) {
|
||||
list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
CardLists.sortByPowerDesc(list);
|
||||
|
||||
// Try not to kill own creatures with this pump
|
||||
CardCollection canDieToPump = new CardCollection();
|
||||
for (Card c : list) {
|
||||
if (c.isCreature() && c.getController() == ai
|
||||
&& c.getNetToughness() - c.getTempToughnessBoost() + defense <= 0) {
|
||||
canDieToPump.add(c);
|
||||
}
|
||||
}
|
||||
list.removeAll(canDieToPump);
|
||||
|
||||
// Generally, don't pump anything that your opponents control
|
||||
if ("ContinuousBonus".equals(sa.getParam("AILogic"))) {
|
||||
// TODO: make it possible for the AI to use this logic to kill opposing creatures
|
||||
// when a toughness debuff is applied
|
||||
list = CardLists.filter(list, CardPredicates.isController(ai));
|
||||
}
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(list.get(0));
|
||||
return true;
|
||||
@@ -461,6 +528,25 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
// Detain target nonland permanent: don't target noncreature permanents that don't have
|
||||
// any activated abilities.
|
||||
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
|
||||
list = CardLists.filter(list, Predicates.or(CardPredicates.Presets.CREATURES, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
for (SpellAbility sa: card.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (ComputerUtil.activateForCost(sa, ai)) {
|
||||
return pumpMandatoryTarget(ai, sa);
|
||||
@@ -473,6 +559,17 @@ public class PumpAi extends PumpAiBase {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
}
|
||||
|
||||
if ("BetterCreatureThanSource".equals(sa.getParam("AILogic"))) {
|
||||
// Don't target cards that are not better in value than the targeting card
|
||||
final int sourceValue = ComputerUtilCard.evaluateCreature(source);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return card.isCreature() && ComputerUtilCard.evaluateCreature(card) > sourceValue + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ("Snapcaster".equals(sa.getParam("AILogic"))) {
|
||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
|
||||
return false;
|
||||
@@ -695,8 +792,6 @@ public class PumpAi extends PumpAiBase {
|
||||
return true;
|
||||
} // pumpDrawbackAI()
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
//TODO Add logic here if necessary but I think the AI won't cast
|
||||
@@ -704,4 +799,248 @@ public class PumpAi extends PumpAiBase {
|
||||
//and the pump isn't mandatory
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean doAristocratLogic(final SpellAbility sa, final Player ai) {
|
||||
// A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final Card source = sa.getHostCard();
|
||||
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
|
||||
final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0;
|
||||
final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0;
|
||||
final boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible");
|
||||
final int selfEval = ComputerUtilCard.evaluateCreature(source);
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||
|
||||
if (numOtherCreats == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to save the card from death by pumping it if it's threatened with a damage spell
|
||||
if (isThreatened && (toughnessBonus > 0 || indestructible)) {
|
||||
SpellAbility saTop = game.getStack().peekAbility();
|
||||
|
||||
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
|
||||
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage();
|
||||
final int numCreatsToSac = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus));
|
||||
|
||||
if (numCreatsToSac > 1) { // probably not worth sacrificing too much
|
||||
return false;
|
||||
}
|
||||
|
||||
if (indestructible || (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg)) {
|
||||
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| card.hasSVar("SacMe")
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
|
||||
}
|
||||
}
|
||||
);
|
||||
if (sacFodder.size() >= numCreatsToSac) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (combat == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (combat.isAttacking(source)) {
|
||||
if (combat.getBlockers(source).isEmpty()) {
|
||||
// Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if
|
||||
// the opponent is tapped out or if we're willing to risk it (will currently risk it
|
||||
// in case it sacs less than half its creatures to deal lethal damage)
|
||||
|
||||
// TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully
|
||||
// for potential damage prevention, various effects like reducing damage to 0, etc.)
|
||||
|
||||
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
|
||||
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
|
||||
|
||||
final boolean isInfect = source.hasKeyword("Infect"); // Flesh-Eater Imp
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / powerBonus;
|
||||
|
||||
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
|
||||
return source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||
// than the card we attacked with.
|
||||
final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (sacTgts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
|
||||
final int DefP = indestructible ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
|
||||
|
||||
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||
}
|
||||
} else {
|
||||
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| card.hasSVar("SacMe")
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return !sacFodder.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) {
|
||||
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
|
||||
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
|
||||
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
|
||||
if (numOtherCreats == 0) {
|
||||
// Cut short if there's nothing to sac at all
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||
if (isDeclareBlockers || isThreatened) {
|
||||
if (doAristocratLogic(sa, ai)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if anything is to be gained from the PutCounter subability
|
||||
if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) {
|
||||
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
|
||||
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter subability!");
|
||||
return false;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final int selfEval = ComputerUtilCard.evaluateCreature(source);
|
||||
|
||||
String typeToGainCtr = "";
|
||||
if (logic.contains(".")) {
|
||||
typeToGainCtr = logic.substring(logic.indexOf(".") + 1);
|
||||
}
|
||||
CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay()
|
||||
: CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr));
|
||||
relevantCreats.remove(source);
|
||||
if (relevantCreats.isEmpty()) {
|
||||
// No relevant creatures to sac
|
||||
return false;
|
||||
}
|
||||
|
||||
int numCtrs = AbilityUtils.calculateAmount(source, sa.getSubAbility().getParam("CounterNum"), sa.getSubAbility());
|
||||
|
||||
if (combat != null && combat.isAttacking(source) && isDeclareBlockers) {
|
||||
if (combat.getBlockers(source).isEmpty()) {
|
||||
// Unblocked. Check if we can deal lethal after receiving counters.
|
||||
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
|
||||
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
|
||||
|
||||
final boolean isInfect = source.hasKeyword("Infect");
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
// Check if there's anything that will die anyway that can be eaten to gain a perma-bonus
|
||||
final CardCollection forcedSacTgts = CardLists.filter(relevantCreats,
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|
||||
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat));
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!forcedSacTgts.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
|
||||
|
||||
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
|
||||
return source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||
// than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing
|
||||
// things that are also threatened to be destroyed anyway.
|
||||
final CardCollection sacTgts = CardLists.filter(relevantCreats,
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval
|
||||
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (sacTgts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
|
||||
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
|
||||
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
|
||||
|
||||
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||
}
|
||||
} else {
|
||||
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||
final boolean isBlocking = combat != null && combat.isBlocking(source);
|
||||
final CardCollection sacFodder = CardLists.filter(relevantCreats,
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| card.hasSVar("SacMe")
|
||||
|| (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval)
|
||||
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return !sacFodder.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,13 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
@@ -181,7 +176,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final int newPower = card.getNetCombatDamage() + attack;
|
||||
//int defense = getNumDefense(sa);
|
||||
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
||||
@@ -207,6 +202,19 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
|
||||
if (ph.isPlayerTurn(opp) && combat != null
|
||||
&& Iterables.any(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))
|
||||
&& CombatUtil.canBlock(card)) {
|
||||
// Use defensively to destroy the opposing Flying creature when possible, or to block with an indestructible
|
||||
// creature buffed with Flying
|
||||
for (Card c : CardLists.filter(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))) {
|
||||
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)
|
||||
&& (card.getNetPower() >= c.getNetToughness() && card.getNetToughness() > c.getNetPower()
|
||||
|| ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, card))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ph.isPlayerTurn(opp) || !(CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
|
||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| newPower <= 0
|
||||
@@ -371,7 +379,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
|| !CombatUtil.canBlock(card)) {
|
||||
return false;
|
||||
}
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) {
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
|
||||
if (ph.isPlayerTurn(ai)
|
||||
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
@@ -532,6 +540,15 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
else {
|
||||
final boolean addsKeywords = !keywords.isEmpty();
|
||||
if (addsKeywords) {
|
||||
|
||||
// If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization
|
||||
if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.")
|
||||
|| keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) {
|
||||
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, true);
|
||||
}
|
||||
}
|
||||
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
|
||||
@@ -48,7 +48,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
}
|
||||
|
||||
if (abCost != null && source.hasSVar("AIPreference")) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, true)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -27,7 +28,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
// ability is targeted
|
||||
sa.resetTargets();
|
||||
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
|
||||
|
||||
if (!canTgtHuman) {
|
||||
|
||||
@@ -17,6 +17,12 @@ public class RemoveFromCombatAi extends SpellAbilityAi {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// AI should only activate this during Human's turn
|
||||
|
||||
if ("RemoveBestAttacker".equals(sa.getParam("AILogic"))) {
|
||||
if (aiPlayer.getGame().getCombat() != null && aiPlayer.getGame().getCombat().getDefenders().contains(aiPlayer)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - implement AI
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -17,7 +15,7 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if (tgt != null) {
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
@@ -27,7 +25,10 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("MaxX".equals(logic)) {
|
||||
if ("MaxX".equals(logic) || "MaxXAtOppEOT".equals(logic)) {
|
||||
if ("MaxXAtOppEOT".equals(logic) && !(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||
return false;
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
final int max = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
source.setSVar("PayX", Integer.toString(max));
|
||||
@@ -48,7 +49,7 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
@@ -59,10 +60,10 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// setup subability to repeat
|
||||
final SpellAbility repeat = sa.getAdditonalAbility("RepeatSubAbility");
|
||||
final SpellAbility repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||
|
||||
if (repeat == null) {
|
||||
return false;
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -15,6 +13,9 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class RepeatEachAi extends SpellAbilityAi {
|
||||
@@ -26,7 +27,9 @@ public class RepeatEachAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
|
||||
if ("Never".equals(logic)) {
|
||||
if ("PriceOfProgress".equals(logic)) {
|
||||
return SpecialCardAi.PriceOfProgress.consider(aiPlayer, sa);
|
||||
} else if ("Never".equals(logic)) {
|
||||
return false;
|
||||
} else if ("CloneMyTokens".equals(logic)) {
|
||||
if (CardLists.filter(aiPlayer.getCreaturesInPlay(), Presets.TOKEN).size() < 2) {
|
||||
@@ -81,11 +84,11 @@ public class RepeatEachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
} else if ("AllPlayerLoseLife".equals(logic)) {
|
||||
final Card source = sa.getHostCard();
|
||||
AbilitySub repeat = sa.getAdditonalAbility("RepeatSubAbility");
|
||||
AbilitySub repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||
|
||||
String svar = repeat.getSVar(repeat.getParam("LifeAmount"));
|
||||
// replace RememberedPlayerCtrl with YouCtrl
|
||||
String svarYou = svar.replace("RememberedPlayer", "You");
|
||||
String svarYou = TextUtil.fastReplace(svar, "RememberedPlayer", "You");
|
||||
|
||||
// Currently all Cards with that are affect all player, including AI
|
||||
if (aiPlayer.canLoseLife()) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
public class RollPlanarDiceAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
@@ -107,7 +108,7 @@ public class RollPlanarDiceAi extends SpellAbilityAi {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
System.out.println(String.format("Unexpected AI hint parameter in card %s in RollPlanarDiceAi: %s.", plane.getName(), paramName));
|
||||
System.out.println(TextUtil.concatNoSpace("Unexpected AI hint parameter in card ", plane.getName(), " in RollPlanarDiceAi: ", paramName, "."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -59,7 +60,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final boolean destroy = sa.hasParam("Destroy");
|
||||
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
@@ -72,7 +73,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
|
||||
|
||||
List<Card> list =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||
CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||
for (Card c : list) {
|
||||
if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) {
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
@@ -12,6 +13,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@@ -34,11 +36,11 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
||||
// Set PayX here to maximum value.
|
||||
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
source.setSVar("PayX", Integer.toString(xPay));
|
||||
valid = valid.replace("X", Integer.toString(xPay));
|
||||
valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
|
||||
}
|
||||
|
||||
CardCollection humanlist =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
CardCollection computerlist =
|
||||
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.Card.SplitCMCMode;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -18,8 +17,6 @@ import forge.util.MyRandom;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
public class ScryAi extends SpellAbilityAi {
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -49,6 +46,24 @@ public class ScryAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
||||
// if the Scry ability requires tapping and has a mana cost, it's best done at the end of opponent's turn
|
||||
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
|
||||
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
|
||||
// even if there's no mana cost.
|
||||
if (sa.getPayCosts() != null) {
|
||||
if (sa.getPayCosts().hasTapCost()
|
||||
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
|
||||
&& !SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
}
|
||||
|
||||
// AI logic to scry in Main 1 if there is no better option, otherwise scry at opponent's EOT
|
||||
// (e.g. Glimmer of Genius)
|
||||
if ("BestOpportunity".equals(sa.getParam("AILogic"))) {
|
||||
return doBestOpportunityLogic(ai, sa, ph);
|
||||
}
|
||||
|
||||
// in the playerturn Scry should only be done in Main1 or in upkeep if able
|
||||
if (ph.isPlayerTurn(ai)) {
|
||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
@@ -60,6 +75,27 @@ public class ScryAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean doBestOpportunityLogic(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
// Check to see if there are any cards in hand that may be worth casting
|
||||
boolean hasSomethingElse = false;
|
||||
for (Card c : CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS))) {
|
||||
for (SpellAbility ab : c.getAllSpellAbilities()) {
|
||||
if (ab.getPayCosts() != null
|
||||
&& ab.getPayCosts().hasManaCost()
|
||||
&& ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) {
|
||||
// TODO: currently looks for non-Scry cards, can most certainly be made smarter.
|
||||
if (ab.getApi() != ApiType.Scry) {
|
||||
hasSomethingElse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (!hasSomethingElse && ph.getPlayerTurn() == ai && ph.getPhase().isAfter(PhaseType.DRAW))
|
||||
|| (ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||
*/
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.card.CardSplitType;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardState;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -67,6 +63,13 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
return super.checkAiLogic(aiPlayer, sa, aiLogic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// Gross generalization, but this always considers alternate
|
||||
@@ -78,6 +81,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
final String mode = sa.getParam("Mode");
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final Game game = source.getGame();
|
||||
|
||||
if("Transform".equals(mode)) {
|
||||
@@ -86,7 +90,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
if (source.hasKeyword("CARDNAME can't transform")) {
|
||||
return false;
|
||||
}
|
||||
return shouldTransformCard(source, ai, ph);
|
||||
return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
|
||||
} else {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
sa.resetTargets();
|
||||
@@ -108,7 +112,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
for (final Card c : list) {
|
||||
if (shouldTransformCard(c, ai, ph)) {
|
||||
if (shouldTransformCard(c, ai, ph) || "Always".equals(logic)) {
|
||||
sa.getTargets().add(c);
|
||||
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
||||
break;
|
||||
@@ -126,7 +130,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return shouldTurnFace(list.get(0), ai, ph);
|
||||
return shouldTurnFace(list.get(0), ai, ph) || "Always".equals(logic);
|
||||
} else {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
sa.resetTargets();
|
||||
@@ -139,7 +143,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
for (final Card c : list) {
|
||||
if (shouldTurnFace(c, ai, ph)) {
|
||||
if (shouldTurnFace(c, ai, ph) || "Always".equals(logic)) {
|
||||
sa.getTargets().add(c);
|
||||
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
||||
break;
|
||||
@@ -163,15 +167,29 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
|
||||
// need a copy for evaluation
|
||||
Card transformed = CardUtil.getLKICopy(card);
|
||||
transformed.getCurrentState().copyFrom(card, card.getAlternateState());
|
||||
transformed.getCurrentState().copyFrom(card.getAlternateState(), true);
|
||||
transformed.updateStateForView();
|
||||
|
||||
// TODO: compareCards assumes that a creature will transform into a creature. Need to improve this
|
||||
// for other things potentially transforming.
|
||||
return compareCards(card, transformed, ai, ph);
|
||||
|
||||
}
|
||||
|
||||
private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
|
||||
if (card.isFaceDown()) {
|
||||
// hidden agenda
|
||||
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
|
||||
&& card.getZone().is(ZoneType.Command)) {
|
||||
String chosenName = card.getNamedCard();
|
||||
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {
|
||||
if (cast.getController() == ai && cast.getName().equals(chosenName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// non-permanent facedown can't be turned face up
|
||||
if (!card.getRules().getType().isPermanent()) {
|
||||
return false;
|
||||
@@ -241,4 +259,9 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
// but for more cleaner way use Evaluate for check
|
||||
return valueCard <= valueTransformed;
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
// TODO: improve the AI for when it may want to transform something that's optional to transform
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -58,6 +61,20 @@ public class TapAi extends TapAiBase {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if ("TapForXCounters".equals(sa.getParam("AILogic"))) {
|
||||
// e.g. Waxmane Baku
|
||||
CounterType ctrType = CounterType.KI;
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int numTargetable = Math.min(sa.getHostCard().getCounters(ctrType), ai.getOpponents().getCreaturesInPlay().size());
|
||||
sa.setSVar("ChosenX", String.valueOf(numTargetable));
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
|
||||
return false;
|
||||
|
||||
@@ -2,11 +2,12 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -111,7 +112,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Game game = ai.getGame();
|
||||
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
||||
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);
|
||||
@@ -155,6 +156,11 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
});
|
||||
}
|
||||
|
||||
//try to exclude things that will already be tapped due to something on stack or because something is
|
||||
//already targeted in a parent or sub SA
|
||||
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap);
|
||||
tapList.removeAll(toExclude);
|
||||
|
||||
if (tapList.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -30,7 +31,7 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
// or during upkeep/begin combat?
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
||||
@@ -126,8 +127,8 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
validTappables = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
validTappables = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.SpellApiToAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
@@ -38,6 +26,11 @@ import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperToken;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -179,7 +172,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
*/
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite tokens?
|
||||
@@ -233,7 +226,29 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
return MyRandom.getRandom().nextFloat() < .8;
|
||||
|
||||
double chance = 1.0F; // 100%
|
||||
boolean alwaysFromPW = true;
|
||||
boolean alwaysOnOppAttack = true;
|
||||
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
chance = (double)aic.getIntProperty(AiProps.TOKEN_GENERATION_ABILITY_CHANCE) / 100;
|
||||
alwaysFromPW = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_FROM_PLANESWALKER);
|
||||
alwaysOnOppAttack = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS);
|
||||
}
|
||||
|
||||
if (sa.getRestrictions() != null && sa.getRestrictions().isPwAbility() && alwaysFromPW) {
|
||||
return true;
|
||||
} else if (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
|
||||
&& ai.getGame().getCombat() != null
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty()
|
||||
&& alwaysOnOppAttack) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return MyRandom.getRandom().nextFloat() <= chance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,13 +269,13 @@ public class TokenAi extends SpellAbilityAi {
|
||||
num = (num == null) ? "1" : num;
|
||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
||||
ai.getOpponent(), topStack.getHostCard(), sa);
|
||||
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), sa);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
// only care about saving single creature for now
|
||||
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
|
||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||
list.add(token);
|
||||
list = CardLists.getValidCards(list, valid.split(","), ai.getOpponent(), topStack.getHostCard(), sa);
|
||||
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), sa);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||
&& list.contains(token)) {
|
||||
@@ -278,7 +293,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
@@ -414,7 +429,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
|
||||
final List<String> imageNames = new ArrayList<String>(1);
|
||||
if (tokenImage.equals("")) {
|
||||
imageNames.add(PaperToken.makeTokenFileName(colorDesc.replace(" ", ""), tokenPower, tokenToughness, tokenName));
|
||||
imageNames.add(PaperToken.makeTokenFileName(TextUtil.fastReplace(colorDesc, " ", ""), tokenPower, tokenToughness, tokenName));
|
||||
} else {
|
||||
imageNames.add(0, tokenImage);
|
||||
}
|
||||
@@ -436,9 +451,9 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
final String substitutedName = tokenName.equals("ChosenType") ? host.getChosenType() : tokenName;
|
||||
final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size()));
|
||||
final CardFactory.TokenInfo tokenInfo = new CardFactory.TokenInfo(substitutedName, imageName,
|
||||
final TokenInfo tokenInfo = new TokenInfo(substitutedName, imageName,
|
||||
cost, substitutedTypes, tokenKeywords, finalPower, finalToughness);
|
||||
Card token = CardFactory.makeOneToken(tokenInfo, ai);
|
||||
Card token = tokenInfo.makeOneToken(ai);
|
||||
|
||||
if (token == null) {
|
||||
return null;
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -66,7 +67,7 @@ public class UnattachAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card card = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
// Check if there are any valid targets
|
||||
List<GameObject> targets = new ArrayList<GameObject>();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user