mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-12 16:58:57 +00:00
Compare commits
1116 Commits
betterImag
...
adv-draft-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35edd16a7e | ||
|
|
31c2686307 | ||
|
|
04f454ae83 | ||
|
|
5b2a56bd4d | ||
|
|
5bbc86c1a1 | ||
|
|
a5765905d8 | ||
|
|
c4a3b5c545 | ||
|
|
81e1306cd7 | ||
|
|
39f6377f33 | ||
|
|
0af4be42c4 | ||
|
|
da8b77acf7 | ||
|
|
4cef049a1d | ||
|
|
8cd386b654 | ||
|
|
c1e87334cd | ||
|
|
3ae3e1b218 | ||
|
|
7acfe6c240 | ||
|
|
66a8e1f04f | ||
|
|
091c61829b | ||
|
|
a2b324c9ce | ||
|
|
4ab8c1414c | ||
|
|
46df4cdb0d | ||
|
|
183100fbdc | ||
|
|
50c7188e04 | ||
|
|
d46d7300b4 | ||
|
|
27a0a8f791 | ||
|
|
5a63250735 | ||
|
|
04fbe9fac3 | ||
|
|
31f8da5687 | ||
|
|
c0fe93ee30 | ||
|
|
a1042e0a1d | ||
|
|
cdbf10f1f7 | ||
|
|
bc6eed5c7d | ||
|
|
b90c14660c | ||
|
|
eeac0272ab | ||
|
|
dcdf2b6e7a | ||
|
|
dab01c9b86 | ||
|
|
74dc09b2e5 | ||
|
|
8dc9a18c4f | ||
|
|
a390284b8e | ||
|
|
1d97a4ec7e | ||
|
|
a21a61cb05 | ||
|
|
cd84663bed | ||
|
|
4d2b634e4f | ||
|
|
4eccfa8177 | ||
|
|
90ea109a17 | ||
|
|
a9c42fb340 | ||
|
|
f1a095fb56 | ||
|
|
4ed06fae87 | ||
|
|
0d020070a2 | ||
|
|
5d39efd9fc | ||
|
|
fd5555fdd6 | ||
|
|
6dd10be044 | ||
|
|
1b419a13e8 | ||
|
|
c0f5615885 | ||
|
|
c10b5706f1 | ||
|
|
9599d01d2c | ||
|
|
f11c2ad8a4 | ||
|
|
20479518da | ||
|
|
1ded8fe5da | ||
|
|
2d1fe88d32 | ||
|
|
7ca0c2b87b | ||
|
|
e4a83de6cf | ||
|
|
7321aac496 | ||
|
|
28dcdf6ddf | ||
|
|
5999909d32 | ||
|
|
0036c27470 | ||
|
|
7325443da5 | ||
|
|
c4c1388f8b | ||
|
|
f4e90e1d38 | ||
|
|
ab3b00cb65 | ||
|
|
a6cbbeef84 | ||
|
|
40629a72a9 | ||
|
|
f6bc6d6742 | ||
|
|
da52bc17e8 | ||
|
|
50b7543933 | ||
|
|
a4378a20e5 | ||
|
|
a6395373f7 | ||
|
|
54f41a28cd | ||
|
|
a2c798ba86 | ||
|
|
8f7a77354a | ||
|
|
b50cdea5df | ||
|
|
a7568b0845 | ||
|
|
e158b5966e | ||
|
|
bea5ffb0a8 | ||
|
|
ac492fac35 | ||
|
|
35e6268a95 | ||
|
|
90bd0c73d0 | ||
|
|
ea293a46b1 | ||
|
|
6ec6d64cb2 | ||
|
|
9bc6ab747d | ||
|
|
a72df6ad16 | ||
|
|
1105b3fcbd | ||
|
|
08239def86 | ||
|
|
cb7fc3df4e | ||
|
|
9b8020bb30 | ||
|
|
398917d010 | ||
|
|
10a68c27d5 | ||
|
|
ba4ee98c3b | ||
|
|
c2c3add52b | ||
|
|
1c7bc1c0d3 | ||
|
|
14f146f8d0 | ||
|
|
0cab03b96c | ||
|
|
8f5f2059a2 | ||
|
|
34323107c9 | ||
|
|
dbf9568866 | ||
|
|
7933893dcb | ||
|
|
415e2c12e1 | ||
|
|
930019db40 | ||
|
|
72139b523c | ||
|
|
6cf2f20cdc | ||
|
|
cebddb7f4b | ||
|
|
4a6275d282 | ||
|
|
bdeaf8cef1 | ||
|
|
40f6a5b472 | ||
|
|
f4bff30680 | ||
|
|
5f32c23dc5 | ||
|
|
ae7159297b | ||
|
|
cedfa68c16 | ||
|
|
1f235c5e71 | ||
|
|
9f8bb8ae4d | ||
|
|
cd3a26e434 | ||
|
|
d2d6b7bc53 | ||
|
|
052e72d8ea | ||
|
|
b902d516a7 | ||
|
|
79fd4a3f8d | ||
|
|
10d359e7d7 | ||
|
|
1cb9e78b21 | ||
|
|
d999c5e213 | ||
|
|
1f7862c970 | ||
|
|
cdc696c506 | ||
|
|
64897f1ab4 | ||
|
|
905cf52c5e | ||
|
|
3139d593a3 | ||
|
|
8f71a5b06e | ||
|
|
e9742a2292 | ||
|
|
a6c893693d | ||
|
|
327ee6853f | ||
|
|
3e7f98db6a | ||
|
|
e49021ffdd | ||
|
|
0c5ff17e94 | ||
|
|
2925e94cec | ||
|
|
02b7e408dc | ||
|
|
b61044abb5 | ||
|
|
f1114eeada | ||
|
|
b131e4eb04 | ||
|
|
8ba2ae4a24 | ||
|
|
ac93233e19 | ||
|
|
6b29e18a58 | ||
|
|
fd2d3c7bb9 | ||
|
|
a1d67cf844 | ||
|
|
2b64f82f98 | ||
|
|
43a1570601 | ||
|
|
db8e084332 | ||
|
|
c2fc877201 | ||
|
|
50f34442f6 | ||
|
|
1dbd358c19 | ||
|
|
bc3faa88c4 | ||
|
|
10618d4f31 | ||
|
|
47e873b4e9 | ||
|
|
acc9ead5b2 | ||
|
|
6125022c9e | ||
|
|
a0be5e25cb | ||
|
|
f2e3cdc111 | ||
|
|
7384aada40 | ||
|
|
1a8a4f63ae | ||
|
|
530d1efcd8 | ||
|
|
d8b1d76f42 | ||
|
|
830b145e88 | ||
|
|
51ed06d60a | ||
|
|
ac9b73935f | ||
|
|
fe2f49fbcc | ||
|
|
3d31494a1b | ||
|
|
ff42e730a2 | ||
|
|
a66fb8791c | ||
|
|
2ad5d20e83 | ||
|
|
4790394698 | ||
|
|
8b1c427809 | ||
|
|
c0b667c373 | ||
|
|
ca61627f5b | ||
|
|
c3e2a5b5ea | ||
|
|
0d26b499d3 | ||
|
|
bbbc5e0ee6 | ||
|
|
714d9442f1 | ||
|
|
d199765e6d | ||
|
|
e5156d8999 | ||
|
|
c0d5397541 | ||
|
|
444897d0f9 | ||
|
|
f2cb7956d3 | ||
|
|
e0c2a49c6b | ||
|
|
d64e5ebc13 | ||
|
|
9bea1bc717 | ||
|
|
bdf9573467 | ||
|
|
042eb4bf79 | ||
|
|
4b0337a836 | ||
|
|
9284775921 | ||
|
|
1671a3298e | ||
|
|
64e3193a80 | ||
|
|
3e6cd92f14 | ||
|
|
b7d4e0129e | ||
|
|
f76127dcea | ||
|
|
c7c0938936 | ||
|
|
9788a815a7 | ||
|
|
cce25a37c4 | ||
|
|
85812d819f | ||
|
|
a974352908 | ||
|
|
45e04cfef7 | ||
|
|
6a83cb89f7 | ||
|
|
494bd75572 | ||
|
|
b065e468b0 | ||
|
|
86aff59e40 | ||
|
|
748c4e1dd7 | ||
|
|
dfb776f526 | ||
|
|
ca5d2bb109 | ||
|
|
b42050a40f | ||
|
|
14220ae37f | ||
|
|
46900c3f49 | ||
|
|
cb2bf996d0 | ||
|
|
edbf6692f2 | ||
|
|
b5f16b83a0 | ||
|
|
ee4902a5ae | ||
|
|
0835817d8a | ||
|
|
15d4e1ac20 | ||
|
|
ecdce50f12 | ||
|
|
247fd7810e | ||
|
|
055cd40fb1 | ||
|
|
f1d448509e | ||
|
|
f2581855a4 | ||
|
|
1e4c7d4b00 | ||
|
|
2d80e44f70 | ||
|
|
a207bdbb51 | ||
|
|
c09ec2f410 | ||
|
|
c579df4c82 | ||
|
|
d39ab505df | ||
|
|
7756dfa5be | ||
|
|
c1b6f3126e | ||
|
|
11d45e0fa3 | ||
|
|
0504b41dbd | ||
|
|
b670cc3736 | ||
|
|
885300ba49 | ||
|
|
ff8fd51bf1 | ||
|
|
80475c6b62 | ||
|
|
88ea36523b | ||
|
|
3386d79cb1 | ||
|
|
f97429d1d4 | ||
|
|
3628d4e22a | ||
|
|
fffc9a4282 | ||
|
|
7cb32b0b25 | ||
|
|
cb13682737 | ||
|
|
9656a9fb4c | ||
|
|
2fd9d3ae14 | ||
|
|
1ea90f6655 | ||
|
|
37342910fa | ||
|
|
3a00727602 | ||
|
|
589d592997 | ||
|
|
83101f7f41 | ||
|
|
c7d24f1c01 | ||
|
|
2a678aa7e9 | ||
|
|
6a758ffc0a | ||
|
|
83421bac56 | ||
|
|
5234365cc1 | ||
|
|
76558a841c | ||
|
|
1338735000 | ||
|
|
44c33e2955 | ||
|
|
b8a5668db6 | ||
|
|
6a84a7a6f0 | ||
|
|
4811c9d726 | ||
|
|
7cea6fce1e | ||
|
|
46ba289329 | ||
|
|
2c9e5ee814 | ||
|
|
230eba687d | ||
|
|
5faf317aaf | ||
|
|
388448c94f | ||
|
|
230282b525 | ||
|
|
0f92bc18d4 | ||
|
|
dc4cee62dc | ||
|
|
ea4c8fbc72 | ||
|
|
068ea955d9 | ||
|
|
f898263bac | ||
|
|
a3147f5e26 | ||
|
|
a309eaee02 | ||
|
|
94b058ec07 | ||
|
|
46bf3348be | ||
|
|
7c4c820e92 | ||
|
|
ed3ea99e1a | ||
|
|
ec21a0c789 | ||
|
|
bfe0a78765 | ||
|
|
0abc224e1b | ||
|
|
fa3ce1c7f7 | ||
|
|
48a5046d93 | ||
|
|
9f9e68f34d | ||
|
|
d7c7aaa51e | ||
|
|
ffe883e7d9 | ||
|
|
b07a9944aa | ||
|
|
997d489a7e | ||
|
|
d181746679 | ||
|
|
693d6cd9e3 | ||
|
|
01d5bf3c21 | ||
|
|
29a630386f | ||
|
|
75c7938f1e | ||
|
|
110e885c67 | ||
|
|
548b448f0d | ||
|
|
a2e80ac0f3 | ||
|
|
238d426202 | ||
|
|
d6f585a80c | ||
|
|
223aeb1bff | ||
|
|
4bf02ea61f | ||
|
|
ea186ae4af | ||
|
|
3e4cd9e189 | ||
|
|
e35c193f92 | ||
|
|
e5443fc394 | ||
|
|
9998092c70 | ||
|
|
9f81e0cd34 | ||
|
|
5569f18053 | ||
|
|
40190b5442 | ||
|
|
a33905241d | ||
|
|
3a40b560f6 | ||
|
|
d79fb96770 | ||
|
|
f50fe2f89c | ||
|
|
a36deab334 | ||
|
|
28feff99d7 | ||
|
|
bab8791012 | ||
|
|
db20b27c9d | ||
|
|
087db3f757 | ||
|
|
db194fab97 | ||
|
|
0585ece2c1 | ||
|
|
1611559909 | ||
|
|
d6dee9575b | ||
|
|
6dd9a731fa | ||
|
|
cf5a8508d6 | ||
|
|
0fa7df090e | ||
|
|
86838d94f7 | ||
|
|
83c4db07ac | ||
|
|
b544f2dd00 | ||
|
|
079e6f04ac | ||
|
|
d04c541578 | ||
|
|
7cc2cf530d | ||
|
|
61a2c7cadb | ||
|
|
efbf2e1a9c | ||
|
|
9b8441d45b | ||
|
|
f8b7a0fb9a | ||
|
|
ab49e97797 | ||
|
|
c27a9d136f | ||
|
|
91241ee53c | ||
|
|
31e537f42b | ||
|
|
e550c307c2 | ||
|
|
b4f01b7ebb | ||
|
|
7744474f39 | ||
|
|
1365b82968 | ||
|
|
5bb532bf9d | ||
|
|
258b8c18a9 | ||
|
|
a66349d8a1 | ||
|
|
1933cf1863 | ||
|
|
0d342c778f | ||
|
|
f452b94cb8 | ||
|
|
9325794e2f | ||
|
|
192a64bbc3 | ||
|
|
deb8369f11 | ||
|
|
b24f536190 | ||
|
|
28ec24069c | ||
|
|
f27472d9bd | ||
|
|
780cc8ddbf | ||
|
|
23555b9564 | ||
|
|
431827be35 | ||
|
|
b0dba74c6c | ||
|
|
ad0a690764 | ||
|
|
977f2c75b9 | ||
|
|
8313414f78 | ||
|
|
89955bf201 | ||
|
|
99d191901a | ||
|
|
1a306b3da3 | ||
|
|
9ff03cbd41 | ||
|
|
07a1dbc099 | ||
|
|
f24b3ea3b3 | ||
|
|
770cc72fcd | ||
|
|
8426a74900 | ||
|
|
8d98eda18d | ||
|
|
6e6509eaff | ||
|
|
c56ddee47d | ||
|
|
07985ac487 | ||
|
|
6c93491d7f | ||
|
|
c9b012c88a | ||
|
|
9b53144976 | ||
|
|
31020296d8 | ||
|
|
65fb3414d8 | ||
|
|
98fee0d86b | ||
|
|
becdadb279 | ||
|
|
26da0ab0d4 | ||
|
|
e1f4d755e0 | ||
|
|
efe7d67d9f | ||
|
|
3a5e11504a | ||
|
|
6657602b80 | ||
|
|
eeca33855d | ||
|
|
cdc63f35bb | ||
|
|
5a9ea8d260 | ||
|
|
5a363922bd | ||
|
|
1ff16ca509 | ||
|
|
c878401197 | ||
|
|
8f518b7b1f | ||
|
|
fb624458f0 | ||
|
|
ee3220f33b | ||
|
|
7bc591fa78 | ||
|
|
c02d942fc3 | ||
|
|
3b9ded8270 | ||
|
|
fdf0c13a7f | ||
|
|
80b1cac394 | ||
|
|
8c80c32113 | ||
|
|
a74b033c26 | ||
|
|
16fcbc0ebf | ||
|
|
62ffada6fe | ||
|
|
e5a0d335af | ||
|
|
ba8a30ebdd | ||
|
|
e25dbe5196 | ||
|
|
3533ad8b74 | ||
|
|
2d20ce07e3 | ||
|
|
e5e0a9240d | ||
|
|
5e83fff859 | ||
|
|
2d0acc734e | ||
|
|
b6bc3a6e96 | ||
|
|
7ec7a65f33 | ||
|
|
2a97b67f72 | ||
|
|
06be01d81c | ||
|
|
386f6aaac6 | ||
|
|
82d11bfb45 | ||
|
|
f39d900fbf | ||
|
|
335edec357 | ||
|
|
e58f035b19 | ||
|
|
f85d610393 | ||
|
|
fce0095af1 | ||
|
|
82bea111df | ||
|
|
0aded8f1f4 | ||
|
|
84130f0586 | ||
|
|
ed4d1059dd | ||
|
|
c73be86211 | ||
|
|
6cb2e7a91a | ||
|
|
58713e65ed | ||
|
|
676c26fae7 | ||
|
|
f2e4e67021 | ||
|
|
18ba476fc9 | ||
|
|
d9ed82972f | ||
|
|
c9affaa1a1 | ||
|
|
8ef89adaef | ||
|
|
f25db898ae | ||
|
|
cddc41b353 | ||
|
|
d4e918660b | ||
|
|
3f16a3e27f | ||
|
|
53d27f1437 | ||
|
|
408dd310d3 | ||
|
|
0050446ff0 | ||
|
|
efb47d949f | ||
|
|
a67303866e | ||
|
|
756e80595c | ||
|
|
089a021d02 | ||
|
|
d89a94dcd2 | ||
|
|
b298f66348 | ||
|
|
fa277c7b7c | ||
|
|
76ec449d33 | ||
|
|
d481ade524 | ||
|
|
1b468efff1 | ||
|
|
9be9a4795b | ||
|
|
49bcf7ded4 | ||
|
|
28989b9c49 | ||
|
|
9e40b1f6cf | ||
|
|
a9e95caa42 | ||
|
|
eb39c97661 | ||
|
|
52f19272d0 | ||
|
|
4cf37379c7 | ||
|
|
746d0476c9 | ||
|
|
cf93d61d0c | ||
|
|
5d9197446b | ||
|
|
753d5560d6 | ||
|
|
1b68d30ff1 | ||
|
|
278eed7af9 | ||
|
|
6eb4e32225 | ||
|
|
e160a7d517 | ||
|
|
476cdda0c2 | ||
|
|
efdabb8bf6 | ||
|
|
ca648df852 | ||
|
|
04afb61351 | ||
|
|
869632361c | ||
|
|
d994abb559 | ||
|
|
d082c2c250 | ||
|
|
586a474168 | ||
|
|
b7268dd6fd | ||
|
|
c3e4ea228a | ||
|
|
237946f569 | ||
|
|
cae32e5b2a | ||
|
|
715ba9803f | ||
|
|
283198f0a4 | ||
|
|
bab889c406 | ||
|
|
92c6f5369d | ||
|
|
6042948aed | ||
|
|
136577fec0 | ||
|
|
180fda53df | ||
|
|
ca6b175fb8 | ||
|
|
dd25dc4b06 | ||
|
|
9893fc3cf9 | ||
|
|
67b6c2c03f | ||
|
|
bb073b8682 | ||
|
|
9bbfad5be3 | ||
|
|
2c7e7d5b63 | ||
|
|
94e7258a03 | ||
|
|
95eb90eac2 | ||
|
|
0f0a4c54e7 | ||
|
|
399923bfa4 | ||
|
|
39f0ab9eae | ||
|
|
8b1cd54417 | ||
|
|
ff2192fa7a | ||
|
|
08443a307c | ||
|
|
bd90e1bccc | ||
|
|
9cb0bd301e | ||
|
|
0923960215 | ||
|
|
0f68dc1ab6 | ||
|
|
09b88b8575 | ||
|
|
fae2f25b69 | ||
|
|
d3e0696ecc | ||
|
|
6a7723eba9 | ||
|
|
10d6fd157e | ||
|
|
048133df30 | ||
|
|
2fe7fec14f | ||
|
|
73e7b27c09 | ||
|
|
2fdbd5a85c | ||
|
|
78e27e0073 | ||
|
|
bad585c8c9 | ||
|
|
11d10a8129 | ||
|
|
0b89e9d137 | ||
|
|
70df1ff0aa | ||
|
|
24d3169592 | ||
|
|
b8db0fea5e | ||
|
|
514519b45b | ||
|
|
70f6bcb63c | ||
|
|
5a3e55f704 | ||
|
|
4452e07443 | ||
|
|
a125e6c0ea | ||
|
|
fa4688e113 | ||
|
|
0236609558 | ||
|
|
3f15fb1b98 | ||
|
|
e22718c72a | ||
|
|
e94985576c | ||
|
|
71e132a9e0 | ||
|
|
8d5d56bed8 | ||
|
|
386c9799c2 | ||
|
|
a1be657460 | ||
|
|
d4994e8fa0 | ||
|
|
fb054d9f64 | ||
|
|
8790843b3b | ||
|
|
38383dff02 | ||
|
|
a0f9923c21 | ||
|
|
812c0ac5b1 | ||
|
|
2e09146f44 | ||
|
|
25080411d5 | ||
|
|
55e20e96f9 | ||
|
|
981ab1c6ed | ||
|
|
a1089f8073 | ||
|
|
fe747a3908 | ||
|
|
a16c9480e7 | ||
|
|
b40e9fc817 | ||
|
|
6eec0e8988 | ||
|
|
d6320caadf | ||
|
|
c4f125525a | ||
|
|
56209261d0 | ||
|
|
5df4d64345 | ||
|
|
d6d6065104 | ||
|
|
333aea0641 | ||
|
|
c7bb3d49a6 | ||
|
|
7c982498dd | ||
|
|
0e2c95afed | ||
|
|
df6eb23341 | ||
|
|
1204de13c3 | ||
|
|
9cb21de11d | ||
|
|
2f16b9e110 | ||
|
|
cf28139b02 | ||
|
|
87e8e7d5e5 | ||
|
|
dcc3a681d9 | ||
|
|
cc7f30da88 | ||
|
|
ed9eabed38 | ||
|
|
99ac95bfca | ||
|
|
eb178fc9d1 | ||
|
|
7567e29cf1 | ||
|
|
b2a456140c | ||
|
|
9c3ff6b570 | ||
|
|
e4d238ba6c | ||
|
|
42363204dd | ||
|
|
de89d557dd | ||
|
|
c5a53b21e7 | ||
|
|
02f43b1ef0 | ||
|
|
d7cb0b7ac1 | ||
|
|
2efeb573e6 | ||
|
|
370ff638ab | ||
|
|
108564efe3 | ||
|
|
43f96657e0 | ||
|
|
cb004bfba2 | ||
|
|
f276597a82 | ||
|
|
4fd9855daa | ||
|
|
ebdce74f92 | ||
|
|
98e9b8f28b | ||
|
|
a31c0358a4 | ||
|
|
8640522fff | ||
|
|
a5fa350e7e | ||
|
|
c4a0cff1ca | ||
|
|
30d02ebbb6 | ||
|
|
9a90359283 | ||
|
|
edbcab544e | ||
|
|
f55bf4691d | ||
|
|
16e44309a6 | ||
|
|
c4d58e3dba | ||
|
|
16155d3670 | ||
|
|
bd929bcc72 | ||
|
|
e2cc52fd02 | ||
|
|
be5c7cfd04 | ||
|
|
9d14949138 | ||
|
|
6fe5dbad3c | ||
|
|
f4f8ee9cb6 | ||
|
|
0767eb03e0 | ||
|
|
feb6062f2f | ||
|
|
113dc4d5e8 | ||
|
|
ca20d98a2c | ||
|
|
599a629068 | ||
|
|
4c05ad655d | ||
|
|
203233f0d2 | ||
|
|
ac8f7357db | ||
|
|
4c396065f5 | ||
|
|
cc4dac38fe | ||
|
|
394818f533 | ||
|
|
bfd4a68e23 | ||
|
|
9d54e82214 | ||
|
|
18c2278066 | ||
|
|
b3d549a712 | ||
|
|
0887e78e90 | ||
|
|
df3f86dd73 | ||
|
|
89af1d0cf1 | ||
|
|
10a68d63a1 | ||
|
|
8ccebbc13c | ||
|
|
4c663cd943 | ||
|
|
ca16bf74f5 | ||
|
|
a1968ef9fd | ||
|
|
de1a999611 | ||
|
|
d0e8bc5de0 | ||
|
|
38fb647cd7 | ||
|
|
90bb83c5d4 | ||
|
|
467fff3651 | ||
|
|
3ab50a5fb5 | ||
|
|
e4d58a0a88 | ||
|
|
10e592c741 | ||
|
|
b3241c85eb | ||
|
|
abd1702c96 | ||
|
|
d91e86c608 | ||
|
|
f0ebac019d | ||
|
|
c18995915a | ||
|
|
55d2c23aa0 | ||
|
|
28c9f719c5 | ||
|
|
bb5a276324 | ||
|
|
e99df5b267 | ||
|
|
54e67b3d57 | ||
|
|
b1d30ab9dd | ||
|
|
f2d65b2515 | ||
|
|
286fcd3fbe | ||
|
|
00332ff3fe | ||
|
|
000fa14c17 | ||
|
|
c5b4be6c97 | ||
|
|
d9b2a8f4e0 | ||
|
|
2898b00300 | ||
|
|
a4671b62d4 | ||
|
|
0c87ed7381 | ||
|
|
4104a0b2a8 | ||
|
|
4d62f436d5 | ||
|
|
506f88bd6f | ||
|
|
a1ad355e25 | ||
|
|
91d6571cff | ||
|
|
e43c5534b3 | ||
|
|
77e3453d35 | ||
|
|
2568df1b91 | ||
|
|
dc830a8432 | ||
|
|
a9cffbd313 | ||
|
|
65775f5c20 | ||
|
|
c6f9056e2c | ||
|
|
967807d441 | ||
|
|
cc7b3505d7 | ||
|
|
3cd965a24f | ||
|
|
3502bf6de2 | ||
|
|
458e05635c | ||
|
|
f4cd9e0f4f | ||
|
|
1f3871d5dc | ||
|
|
f4d304b2c1 | ||
|
|
24937fb32d | ||
|
|
5d222577be | ||
|
|
0e010cece2 | ||
|
|
21080d65f5 | ||
|
|
83475b9063 | ||
|
|
15a6494d9c | ||
|
|
58ad1f3103 | ||
|
|
b2bff7762f | ||
|
|
cd4a673862 | ||
|
|
1f61c6bd00 | ||
|
|
cc2f46795f | ||
|
|
db5ea3ec9c | ||
|
|
21f2f6e2c3 | ||
|
|
65b5f4fac3 | ||
|
|
2291cf9d0c | ||
|
|
3f8b3fa2a7 | ||
|
|
e6fa1268d9 | ||
|
|
eda8205acd | ||
|
|
019131447a | ||
|
|
41cd90bc2a | ||
|
|
0fe1677ced | ||
|
|
b1a2a93f75 | ||
|
|
50a2c283f1 | ||
|
|
83188daba4 | ||
|
|
3312e409fd | ||
|
|
5420a90f05 | ||
|
|
4d340d7bf3 | ||
|
|
5342175a97 | ||
|
|
e5a1c86b1f | ||
|
|
1aca32005b | ||
|
|
6d7853428f | ||
|
|
683d92399d | ||
|
|
9b9bf6cf53 | ||
|
|
c9b826d9b4 | ||
|
|
81ff72f1f2 | ||
|
|
dfda04f7ba | ||
|
|
3bc0e0c386 | ||
|
|
5829192446 | ||
|
|
cbae45be87 | ||
|
|
2b4ec73318 | ||
|
|
7ff426e413 | ||
|
|
25e3ad8432 | ||
|
|
051edff18e | ||
|
|
dde58be5a6 | ||
|
|
1ad4fda3e6 | ||
|
|
b78ef7b010 | ||
|
|
d1776ce37a | ||
|
|
100ae7b164 | ||
|
|
415e247f6c | ||
|
|
ca4a934e0f | ||
|
|
3b4c417549 | ||
|
|
f5e96bc756 | ||
|
|
7428b7420a | ||
|
|
761d7848ef | ||
|
|
e9fc7899cd | ||
|
|
df40fd5cb3 | ||
|
|
dce3b4b142 | ||
|
|
b20110edb4 | ||
|
|
4a05a66bc0 | ||
|
|
39acc78fad | ||
|
|
a0c58e2a1d | ||
|
|
c46ceca140 | ||
|
|
4db0b6a67f | ||
|
|
38f6b699c6 | ||
|
|
9d80f4e78f | ||
|
|
7dc95f8540 | ||
|
|
723ad5fe7c | ||
|
|
94aac494ea | ||
|
|
3e6b7af61f | ||
|
|
1574a88255 | ||
|
|
2e2fea9182 | ||
|
|
55549f1e70 | ||
|
|
71e97146ed | ||
|
|
35e4e580fa | ||
|
|
baab31d2e2 | ||
|
|
3f1295d9e4 | ||
|
|
d759cc4e94 | ||
|
|
4c941df647 | ||
|
|
1f39ad4e91 | ||
|
|
194e1d3356 | ||
|
|
0aa8c933d8 | ||
|
|
cf11802fbe | ||
|
|
2703cbc4c9 | ||
|
|
b87f3bd00b | ||
|
|
a1227167ea | ||
|
|
7ccb925d76 | ||
|
|
beedd17cfe | ||
|
|
332fa94dab | ||
|
|
ae4b5be29f | ||
|
|
cc4a417c0a | ||
|
|
8a6e03e172 | ||
|
|
3ff0c740c4 | ||
|
|
82f8a8f30b | ||
|
|
2f7c315822 | ||
|
|
89b9c050a8 | ||
|
|
eb70e21f3d | ||
|
|
ab2b06500b | ||
|
|
79845eff1d | ||
|
|
9d1b935643 | ||
|
|
ad0c81e984 | ||
|
|
e86f4e80cb | ||
|
|
af4a6de809 | ||
|
|
0d0fdd3c0b | ||
|
|
de4dcb19cf | ||
|
|
cfcbbb5c21 | ||
|
|
6d11361725 | ||
|
|
924d9f57a6 | ||
|
|
8c5dfb61e7 | ||
|
|
895c5c1e65 | ||
|
|
e64faa3cf2 | ||
|
|
11815a4cf2 | ||
|
|
d903e4d80d | ||
|
|
6530515e98 | ||
|
|
28711aa669 | ||
|
|
f3b2192dd2 | ||
|
|
d6451ae486 | ||
|
|
a78c648e77 | ||
|
|
b1afd28556 | ||
|
|
82f5e17705 | ||
|
|
15e955576f | ||
|
|
23b0fa09d9 | ||
|
|
34e31e7e29 | ||
|
|
3fcb9e7a3c | ||
|
|
f69ff10b7c | ||
|
|
bd6ea12cc6 | ||
|
|
10fa3f7f22 | ||
|
|
462b183548 | ||
|
|
4b5bf6fef4 | ||
|
|
1f28e46e06 | ||
|
|
990e515cb9 | ||
|
|
6ee86f9762 | ||
|
|
41dfb3488c | ||
|
|
f61e1cd435 | ||
|
|
1627503248 | ||
|
|
4c87c8a1ff | ||
|
|
8036d4a553 | ||
|
|
a9f18bbf48 | ||
|
|
64738f58d6 | ||
|
|
f426e9f236 | ||
|
|
234237b43a | ||
|
|
854d6640d1 | ||
|
|
c6514e0183 | ||
|
|
9164e76f44 | ||
|
|
5cf155fb94 | ||
|
|
11be569f35 | ||
|
|
00412f6418 | ||
|
|
c66e4a08e6 | ||
|
|
c22934f4af | ||
|
|
2792c57405 | ||
|
|
e3637586d6 | ||
|
|
5787dfbca5 | ||
|
|
9b80408bc6 | ||
|
|
073f32c1c9 | ||
|
|
6564e76aad | ||
|
|
35563fe834 | ||
|
|
9b46581289 | ||
|
|
dbde27bc88 | ||
|
|
b37c280bf8 | ||
|
|
643fc2106b | ||
|
|
48496cfbbc | ||
|
|
3022129fb5 | ||
|
|
5d0a7b5ef9 | ||
|
|
fa1557c775 | ||
|
|
cb47a9c4be | ||
|
|
9e2ab76be3 | ||
|
|
90c8f8e332 | ||
|
|
63397dee07 | ||
|
|
a602b1bdf2 | ||
|
|
8653a26d38 | ||
|
|
8ea5ac97a2 | ||
|
|
d72d36a8b9 | ||
|
|
664ce69993 | ||
|
|
93bca202bf | ||
|
|
72641e000b | ||
|
|
36696f9b1b | ||
|
|
83bfe2a524 | ||
|
|
827a02563b | ||
|
|
f06acf88f9 | ||
|
|
0f78751ce3 | ||
|
|
c9fbd579b6 | ||
|
|
4e12f73444 | ||
|
|
22f4a250e4 | ||
|
|
ed6966b570 | ||
|
|
14c6794ee3 | ||
|
|
9494321965 | ||
|
|
34d009a458 | ||
|
|
ac5128d133 | ||
|
|
12c487f7b8 | ||
|
|
285417275d | ||
|
|
f2db114cb6 | ||
|
|
87226d9bd9 | ||
|
|
a644a4c7bc | ||
|
|
462bdf19b9 | ||
|
|
3d9bb1f437 | ||
|
|
485d2dbaf8 | ||
|
|
8f8abdbab9 | ||
|
|
7ebe74e57c | ||
|
|
b45153bee8 | ||
|
|
55c4d4240f | ||
|
|
0d2d01060d | ||
|
|
f84237f25b | ||
|
|
07cff6f9a9 | ||
|
|
9f7852e76f | ||
|
|
943d6edabd | ||
|
|
800b88911a | ||
|
|
dccbd9912d | ||
|
|
b5f696756e | ||
|
|
9ff528029a | ||
|
|
44046b475b | ||
|
|
65959f9089 | ||
|
|
94d87039e3 | ||
|
|
d3a13d4515 | ||
|
|
b63bee045f | ||
|
|
977d46bca5 | ||
|
|
8293d4de63 | ||
|
|
94feb1b63b | ||
|
|
e37dac6cbe | ||
|
|
c7b98bc578 | ||
|
|
7d4369fdca | ||
|
|
28cb9f9d7d | ||
|
|
94e3c71809 | ||
|
|
5dd6a5b45a | ||
|
|
e486f846d8 | ||
|
|
e7793764a6 | ||
|
|
5aea2e5bbe | ||
|
|
f52410f2f8 | ||
|
|
53e2f199cc | ||
|
|
f86e9a56d1 | ||
|
|
31ebe48bf3 | ||
|
|
ff963b2cb9 | ||
|
|
9a6ebce85a | ||
|
|
eb5e37e3de | ||
|
|
92b96aea75 | ||
|
|
0f4c0488d5 | ||
|
|
6879089149 | ||
|
|
3e2eb86bd4 | ||
|
|
84ff75c9cf | ||
|
|
9d3605ee96 | ||
|
|
83cd6b7291 | ||
|
|
514d40aefd | ||
|
|
31cca02d8d | ||
|
|
5fbd2f9552 | ||
|
|
7ee0431a2f | ||
|
|
0786403fff | ||
|
|
4f7dbfbc80 | ||
|
|
6caf02fc77 | ||
|
|
9dedc9ecfa | ||
|
|
b62434616a | ||
|
|
ad8f3ea06f | ||
|
|
dbed505282 | ||
|
|
1468746045 | ||
|
|
6841b31849 | ||
|
|
a151dff3c5 | ||
|
|
90f6e2b17f | ||
|
|
7b1e96410f | ||
|
|
7ecf619b94 | ||
|
|
b1d8b6fd5c | ||
|
|
a15a990730 | ||
|
|
7402aab2a8 | ||
|
|
2a68427c82 | ||
|
|
14dd9721e3 | ||
|
|
6115eaf6da | ||
|
|
b4922ce353 | ||
|
|
0fb37bf2d4 | ||
|
|
8ce5272e95 | ||
|
|
08135545da | ||
|
|
3cdbb8f1dd | ||
|
|
4f662a5651 | ||
|
|
0e0811a1ac | ||
|
|
71882fe83c | ||
|
|
fce67b847c | ||
|
|
2ff0c6b92d | ||
|
|
b27b8b7ba5 | ||
|
|
f889abd57a | ||
|
|
1d2852fff6 | ||
|
|
48247f7d3b | ||
|
|
d3e6932a23 | ||
|
|
e4d3e061d1 | ||
|
|
e57708543a | ||
|
|
69e2f90c8d | ||
|
|
655d01114c | ||
|
|
0d40508e69 | ||
|
|
b785a84dca | ||
|
|
14af90cd27 | ||
|
|
0278918886 | ||
|
|
b44738cda3 | ||
|
|
ddbe2d6051 | ||
|
|
a15ebe86db | ||
|
|
16225d2894 | ||
|
|
b4c87a198a | ||
|
|
38da204607 | ||
|
|
dbe410f8af | ||
|
|
b05eb4eed3 | ||
|
|
02201c4527 | ||
|
|
6536d39969 | ||
|
|
c9b41c2b81 | ||
|
|
74e587a8a0 | ||
|
|
a751ad186e | ||
|
|
bae41478d9 | ||
|
|
10dbafbe8c | ||
|
|
504946f72a | ||
|
|
e45ae205ac | ||
|
|
4a5969157d | ||
|
|
bdda4d50f7 | ||
|
|
f565943d8d | ||
|
|
3158c0b1d9 | ||
|
|
4346d83582 | ||
|
|
7e016f4ca5 | ||
|
|
38242d0226 | ||
|
|
b565095136 | ||
|
|
93b5fd5f3e | ||
|
|
8ca03c1f50 | ||
|
|
f5204b89fb | ||
|
|
b9dc63b3df | ||
|
|
86910302dc | ||
|
|
a0cca65075 | ||
|
|
0cc2c3b275 | ||
|
|
2cb4856dc7 | ||
|
|
88a53226a2 | ||
|
|
1d42ab50c7 | ||
|
|
cf10851c78 | ||
|
|
c1727ffb3f | ||
|
|
43d79c024e | ||
|
|
3fcf200a86 | ||
|
|
8bc5588342 | ||
|
|
ab27b613e3 | ||
|
|
c6816dbe74 | ||
|
|
41e43a8441 | ||
|
|
ff91114f39 | ||
|
|
b3951555a1 | ||
|
|
f3a68e940e | ||
|
|
1d646753f2 | ||
|
|
b293700b13 | ||
|
|
ea2e0fa3e6 | ||
|
|
bad55a173d | ||
|
|
69538f248d | ||
|
|
c2f4736f08 | ||
|
|
9aa142ba5a | ||
|
|
f4aba94a18 | ||
|
|
910c06b4d2 | ||
|
|
490a9b1693 | ||
|
|
f6a8705849 | ||
|
|
df2f1105c1 | ||
|
|
acaf683166 | ||
|
|
1c6b256268 | ||
|
|
d59ec1527d | ||
|
|
4b6e46ab7d | ||
|
|
68b949bdbf | ||
|
|
a49cc1db97 | ||
|
|
701cc31e1c | ||
|
|
8832920a4d | ||
|
|
d044d0dc65 | ||
|
|
e7427bb180 | ||
|
|
989acaa7d4 | ||
|
|
6351ea2593 | ||
|
|
1a586f07d1 | ||
|
|
69ec802299 | ||
|
|
817b72f67f | ||
|
|
cf1398606b | ||
|
|
31d733aacb | ||
|
|
77544f855b | ||
|
|
1be4880776 | ||
|
|
ff39f82099 | ||
|
|
200e509cda | ||
|
|
6a70c6705d | ||
|
|
6749e5de7e | ||
|
|
9b83c7eeec | ||
|
|
1c8f7eccbe | ||
|
|
13c98c68e9 | ||
|
|
d492cff553 | ||
|
|
875bd54c21 | ||
|
|
9825bebb75 | ||
|
|
f2ab574a3b | ||
|
|
94babc2c53 | ||
|
|
4e42963033 | ||
|
|
6fec40522e | ||
|
|
eea9890f9d | ||
|
|
89d1cffc12 | ||
|
|
40f168bf1e | ||
|
|
891a098d2a | ||
|
|
b6c0d86945 | ||
|
|
2cb69c2d6e | ||
|
|
9a96760061 | ||
|
|
d4a484522e | ||
|
|
7a24b5cd0c | ||
|
|
c0e34799c8 | ||
|
|
964d500414 | ||
|
|
e2949b66fc | ||
|
|
06fcb531cd | ||
|
|
923b12caa0 | ||
|
|
69d75489e7 | ||
|
|
9e985ff471 | ||
|
|
47390abf3b | ||
|
|
59484f30b5 | ||
|
|
e36701a9f3 | ||
|
|
03b6567993 | ||
|
|
56ef1a1df5 | ||
|
|
6d4af69c19 | ||
|
|
b8bd9d92a7 | ||
|
|
6a4018f206 | ||
|
|
986b44af43 | ||
|
|
64e6dbad13 | ||
|
|
2e559effcb | ||
|
|
d02f755fa7 | ||
|
|
17ffda684f | ||
|
|
d6cd2f35ff | ||
|
|
81bba70548 | ||
|
|
831a2d23e4 | ||
|
|
a0f15cdcb1 | ||
|
|
e882bd8264 | ||
|
|
7101d638c6 | ||
|
|
865e3dd783 | ||
|
|
b1c992db26 | ||
|
|
58f79ae6c0 | ||
|
|
fd5af3c93e | ||
|
|
127d0fced6 | ||
|
|
6526d736bf | ||
|
|
cc4a43f0a2 | ||
|
|
286ade792f | ||
|
|
e681775f47 | ||
|
|
67a9a2c34a | ||
|
|
88c0cc00bc | ||
|
|
f16f153448 | ||
|
|
1f93dcf5db | ||
|
|
783fff300d | ||
|
|
935e4a00d5 | ||
|
|
6986b5a53d | ||
|
|
e6ed31e9d9 | ||
|
|
288c845116 | ||
|
|
597222dbf5 | ||
|
|
cea6eeabba | ||
|
|
8475456c0b | ||
|
|
651caccd71 |
4
.github/workflows/maven-publish.yml
vendored
4
.github/workflows/maven-publish.yml
vendored
@@ -129,7 +129,9 @@ jobs:
|
||||
makeLatest: true
|
||||
|
||||
- name: 🔧 Install XML tools
|
||||
run: sudo apt-get install -y libxml2-utils
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: 🔼 Bump versionCode in root POM
|
||||
id: bump_version
|
||||
|
||||
2
.github/workflows/test-build.yaml
vendored
2
.github/workflows/test-build.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '17' ]
|
||||
java: ['17', '21']
|
||||
name: Test with Java ${{ matrix.Java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -66,6 +66,9 @@ forge-gui-mobile-dev/testAssets
|
||||
|
||||
forge-gui/res/cardsfolder/*.bat
|
||||
|
||||
# Generated changelog file
|
||||
forge-gui/release-files/CHANGES.txt
|
||||
|
||||
forge-gui/res/PerSetTrackingResults
|
||||
forge-gui/res/decks
|
||||
forge-gui/res/layouts
|
||||
@@ -87,3 +90,7 @@ forge-gui/tools/PerSetTrackingResults
|
||||
*.tiled-session
|
||||
/forge-gui/res/adventure/*.tiled-project
|
||||
/forge-gui/res/adventure/*.tiled-session
|
||||
|
||||
# Ignore python temporaries
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
|
||||
Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
|
||||
|
||||
|
||||
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
|
||||
|
||||
|
||||
What is the current bug behavior?
|
||||
|
||||
(What actually happens)
|
||||
|
||||
|
||||
What is the expected correct behavior?
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
|
||||
Relevant logs and/or screenshots
|
||||
|
||||
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
|
||||
|
||||
|
||||
Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
/label ~needs-investigation
|
||||
@@ -1,15 +0,0 @@
|
||||
Summary
|
||||
|
||||
(Summarize the feature you wish concisely)
|
||||
|
||||
|
||||
Example screenshots
|
||||
|
||||
(If this is a UI change, please provide an example screenshot of how this feature might work)
|
||||
|
||||
|
||||
Feature type
|
||||
|
||||
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
|
||||
|
||||
/label ~feature request
|
||||
@@ -1,4 +1,4 @@
|
||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
||||
# ⚔️ Forge: The Magic: The Gathering Rules Engine
|
||||
|
||||
Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
|
||||
@@ -47,11 +47,13 @@ Embark on a thrilling single-player journey where you can:
|
||||
- Challenge diverse AI opponents.
|
||||
- Collect cards and items to boost your abilities.
|
||||
|
||||

|
||||
<img width="1282" height="752" alt="Shandalar World" src="https://github.com/user-attachments/assets/9af31471-d688-442f-9418-9807d8635b72" />
|
||||
|
||||
### 🔍 Quest Modes
|
||||
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
|
||||
|
||||
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
|
||||
|
||||
### 🤖 AI Formats
|
||||
Test your skills against AI in multiple formats:
|
||||
- **Sealed**
|
||||
@@ -61,6 +63,8 @@ Test your skills against AI in multiple formats:
|
||||
|
||||
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
|
||||
|
||||
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
|
||||
|
||||
---
|
||||
|
||||
## 💬 Support & Community
|
||||
|
||||
@@ -15,7 +15,7 @@ public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
|
||||
GuiBase.setDeviceInfo("", "", 0, 0);
|
||||
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
|
||||
new EditorMainWindow(Config.instance());
|
||||
}
|
||||
}
|
||||
|
||||
9
forge-ai/src/main/java/forge/ai/AiAbilityDecision.java
Normal file
9
forge-ai/src/main/java/forge/ai/AiAbilityDecision.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package forge.ai;
|
||||
|
||||
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
|
||||
private static int MIN_RATING = 30;
|
||||
|
||||
public boolean willingToPlay() {
|
||||
return rating > MIN_RATING && decision.willingToPlay();
|
||||
}
|
||||
}
|
||||
@@ -145,13 +145,15 @@ public class AiAttackController {
|
||||
sa.setActivatingPlayer(defender);
|
||||
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
|
||||
continue;
|
||||
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
|
||||
continue;
|
||||
}
|
||||
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
|
||||
if (animatedCopy.isCreature()) {
|
||||
// TODO imprecise, only works 100% for colorless mana
|
||||
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
|
||||
sa.getPayCosts().getTotalMana().getCMC() : 0; // FIXME: imprecise, only works 100% for colorless mana
|
||||
sa.getPayCosts().getTotalMana().getCMC() : 0;
|
||||
if (totalMana - manaReserved >= saCMC) {
|
||||
manaReserved += saCMC;
|
||||
defenders.add(animatedCopy);
|
||||
|
||||
@@ -59,7 +59,6 @@ public class AiCardMemory {
|
||||
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
||||
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
|
||||
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
||||
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
||||
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
|
||||
|
||||
@@ -66,13 +66,11 @@ import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -99,6 +97,7 @@ public class AiController {
|
||||
private int lastAttackAggression;
|
||||
private boolean useLivingEnd;
|
||||
private List<SpellAbility> skipped;
|
||||
private boolean timeoutReached;
|
||||
|
||||
public AiController(final Player computerPlayer, final Game game0) {
|
||||
player = computerPlayer;
|
||||
@@ -483,7 +482,7 @@ public class AiController {
|
||||
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
// don't play MDFC land if other side is spell and enough lands are available
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -888,79 +887,18 @@ public class AiController {
|
||||
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
// Check a predefined condition
|
||||
if (sa.hasParam("AICheckSVar")) {
|
||||
final String svarToCheck = sa.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
|
||||
if (sa.hasParam("AISVarCompare")) {
|
||||
final String fullCmp = sa.getParam("AISVarCompare");
|
||||
comparator = fullCmp.substring(0, 2);
|
||||
final String strCmpTo = fullCmp.substring(2);
|
||||
try {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
|
||||
int left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
if (!Expressions.compare(left, comparator, compareTo)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
}
|
||||
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
}
|
||||
|
||||
int oldCMC = -1;
|
||||
boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE) || sa.getApi() == ApiType.Charm;
|
||||
if (!xCost) {
|
||||
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
|
||||
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
|
||||
if (!sa.getAllTargetChoices().isEmpty()) {
|
||||
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
|
||||
}
|
||||
}
|
||||
|
||||
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
// this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
AiPlayDecision canPlay = canPlaySa(sa);
|
||||
|
||||
if (canPlay != AiPlayDecision.WillPlay) {
|
||||
return canPlay;
|
||||
}
|
||||
|
||||
// Account for possible Ward after the spell is fully targeted
|
||||
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
|
||||
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
|
||||
if (!sa.isSpell() || sa.isCounterableBy(null)) {
|
||||
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
||||
for (Card tgt : tc.getTargetCards()) {
|
||||
// TODO some older cards don't use the keyword, so check for trigger instead
|
||||
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
|
||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||
if (wardCost.hasManaCost()) {
|
||||
xCost |= wardCost.getTotalMana().getCMC() > 0;
|
||||
}
|
||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
||||
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
|
||||
return AiPlayDecision.CostNotAcceptable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if some target raised cost
|
||||
if (!xCost && oldCMC > -1) {
|
||||
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
|
||||
if (finalCMC > oldCMC) {
|
||||
xCost = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xCost && !ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
|
||||
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
|
||||
return AiPlayDecision.CantAfford;
|
||||
@@ -968,13 +906,11 @@ public class AiController {
|
||||
|
||||
// check if enough left (pass memory indirectly because we don't want to include those)
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
|
||||
if (tappedForMana != null && tappedForMana.isEmpty() &&
|
||||
if (tappedForMana != null && !tappedForMana.isEmpty() &&
|
||||
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
|
||||
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
|
||||
// are willing to play the SA
|
||||
return AiPlayDecision.WillPlay;
|
||||
}
|
||||
|
||||
@@ -1017,7 +953,7 @@ public class AiController {
|
||||
Sentry.setExtra("Card", card.getName());
|
||||
Sentry.setExtra("SA", sa.toString());
|
||||
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayWithSubs(player, sa).willingToPlay();
|
||||
|
||||
// remove added extra
|
||||
Sentry.removeExtra("Card");
|
||||
@@ -1395,9 +1331,9 @@ public class AiController {
|
||||
if (spell instanceof SpellApiBased) {
|
||||
boolean chance = false;
|
||||
if (withoutPayingManaCost) {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay();
|
||||
} else {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell).doTrigger(player, spell, mandatory);
|
||||
}
|
||||
if (!chance) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
@@ -1710,8 +1646,10 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
// in case of infinite loop reset below would not be reached
|
||||
timeoutReached = false;
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1720,6 +1658,11 @@ public class AiController {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timeoutReached) {
|
||||
timeoutReached = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.getHostCard().hasKeyword(Keyword.STORM)
|
||||
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
||||
&& player.getZone(ZoneType.Hand).contains(
|
||||
@@ -1789,11 +1732,21 @@ public class AiController {
|
||||
return null;
|
||||
});
|
||||
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
Thread t = new Thread(future);
|
||||
t.start();
|
||||
try {
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
future.cancel(true);
|
||||
try {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
timeoutReached = true;
|
||||
future.cancel(true);
|
||||
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
|
||||
// TODO mark some as skipped to increase chance to find something playable next priority
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1804,9 +1757,9 @@ public class AiController {
|
||||
|
||||
for (int i = 0; i < numToExile; i++) {
|
||||
Card chosen = null;
|
||||
for (final Card c : grave) { // Exile noncreatures first in
|
||||
// case we can revive. Might wanna do some additional
|
||||
// checking here for Flashback and the like.
|
||||
for (final Card c : grave) {
|
||||
// Exile noncreatures first in case we can revive
|
||||
// Might wanna do some additional checking here for Flashback and the like
|
||||
if (!c.isCreature()) {
|
||||
chosen = c;
|
||||
break;
|
||||
@@ -1827,12 +1780,12 @@ public class AiController {
|
||||
return toExile;
|
||||
}
|
||||
|
||||
public boolean doTrigger(SpellAbility spell, boolean mandatory) {
|
||||
if (spell instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
|
||||
if (spell.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
|
||||
public boolean doTrigger(SpellAbility sa, boolean mandatory) {
|
||||
if (sa instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) sa).getWrappedAbility(), mandatory);
|
||||
if (sa.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(sa).doTrigger(player, sa, mandatory);
|
||||
if (sa.getPayCosts() == Cost.Zero && !sa.usesTargeting()) {
|
||||
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
|
||||
return true;
|
||||
}
|
||||
@@ -1845,14 +1798,9 @@ public class AiController {
|
||||
* @param sa the sa
|
||||
* @return true, if successful
|
||||
*/
|
||||
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
|
||||
Card hostCard = effect.getHostCard();
|
||||
if (hostCard.hasAlternateState()) {
|
||||
hostCard = game.getCardState(hostCard);
|
||||
}
|
||||
|
||||
public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) {
|
||||
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
|
||||
final Player controller = hostCard.getController();
|
||||
final Player controller = host.getController();
|
||||
if (affected instanceof Player) {
|
||||
return !((Player) affected).isOpponentOf(controller);
|
||||
}
|
||||
@@ -1861,7 +1809,6 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
if (effect.hasParam("AICheckSVar")) {
|
||||
System.out.println("aiShouldRun?" + sa);
|
||||
final String svarToCheck = effect.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
@@ -1874,9 +1821,9 @@ public class AiController {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
if (sa == null) {
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect);
|
||||
} else {
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1884,13 +1831,12 @@ public class AiController {
|
||||
int left = 0;
|
||||
|
||||
if (sa == null) {
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, effect);
|
||||
} else {
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
}
|
||||
System.out.println("aiShouldRun?" + left + comparator + compareTo);
|
||||
return Expressions.compare(left, comparator, compareTo);
|
||||
} else if (effect.hasParam("AICheckDredge")) {
|
||||
} else if (effect.isKeyword(Keyword.DREDGE)) {
|
||||
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
|
||||
} else return sa != null && doTrigger(sa, false);
|
||||
}
|
||||
|
||||
@@ -29,12 +29,15 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
private final CardCollection tapped;
|
||||
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
|
||||
this(ai0, sa, effect, false);
|
||||
}
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
|
||||
super(ai0, effect, sa, sa.getHostCard());
|
||||
|
||||
discarded = new CardCollection();
|
||||
tapped = new CardCollection();
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
|
||||
if (tappedForMana != null) {
|
||||
if (!payMana && tappedForMana != null) {
|
||||
tapped.addAll(tappedForMana);
|
||||
}
|
||||
}
|
||||
@@ -110,7 +113,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
|
||||
}
|
||||
return PaymentDecision.card(randomSubset);
|
||||
} else if (type.equals("DifferentNames")) {
|
||||
} else if (type.contains("+WithDifferentNames")) {
|
||||
CardCollection differentNames = new CardCollection();
|
||||
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
|
||||
while (c > 0) {
|
||||
@@ -563,7 +566,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
|
||||
if (thisRemove > 0) {
|
||||
removed += thisRemove;
|
||||
table.put(null, prefCard, CounterType.get(cType), thisRemove);
|
||||
table.put(null, prefCard, cType, thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,7 +576,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
@Override
|
||||
public PaymentDecision visit(CostRemoveAnyCounter cost) {
|
||||
final int c = cost.getAbilityAmount(ability);
|
||||
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
|
||||
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
|
||||
|
||||
if (c <= 0) {
|
||||
return null;
|
||||
@@ -716,7 +719,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
table.put(null, crd, CounterEnumType.QUEST, over);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,6 +770,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
public PaymentDecision visit(CostRemoveCounter cost) {
|
||||
final String amount = cost.getAmount();
|
||||
final String type = cost.getType();
|
||||
final GameEntityCounterTable counterTable = new GameEntityCounterTable();
|
||||
|
||||
// TODO Help AI filter card with most useless counters and put those counters in countertable for things like
|
||||
// Moxite Refinery, similar to CostRemoveAnyCounter
|
||||
// Probably a lot of that decision making can be re-used or pulled out for both PaymentDecisions to use
|
||||
if (cost.counter == null) return null;
|
||||
|
||||
int c;
|
||||
|
||||
@@ -795,7 +804,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
}
|
||||
for (Card card : typeList) {
|
||||
if (card.getCounters(cost.counter) >= c) {
|
||||
return PaymentDecision.card(card, c);
|
||||
counterTable.put(null, card, cost.counter, c);
|
||||
return PaymentDecision.counters(counterTable);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -806,7 +816,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PaymentDecision.card(source, c);
|
||||
counterTable.put(null, source, cost.counter, c);
|
||||
return PaymentDecision.counters(counterTable);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
package forge.ai;
|
||||
|
||||
public enum AiPlayDecision {
|
||||
// Play decision reasons
|
||||
WillPlay,
|
||||
MandatoryPlay,
|
||||
PlayToEmptyHand,
|
||||
ImpactCombat,
|
||||
ResponseToStackResolve,
|
||||
AddBoardPresence,
|
||||
Removal,
|
||||
Tempo,
|
||||
CardAdvantage,
|
||||
|
||||
// Play later decisions
|
||||
WaitForCombat,
|
||||
WaitForMain2,
|
||||
WaitForEndOfTurn,
|
||||
StackNotEmpty,
|
||||
AnotherTime,
|
||||
|
||||
// Don't play decision reasons
|
||||
CantPlaySa,
|
||||
CantPlayAi,
|
||||
CantAfford,
|
||||
CantAffordX,
|
||||
WaitForMain2,
|
||||
AnotherTime,
|
||||
DoesntImpactCombat,
|
||||
DoesntImpactGame,
|
||||
MissingLogic,
|
||||
MissingNeededCards,
|
||||
TimingRestrictions,
|
||||
MissingPhaseRestrictions,
|
||||
ConditionsNotMet,
|
||||
NeedsToPlayCriteriaNotMet,
|
||||
StopRunawayActivations,
|
||||
TargetingFailed,
|
||||
CostNotAcceptable,
|
||||
LifeInDanger,
|
||||
WouldDestroyLegend,
|
||||
WouldDestroyOtherPlaneswalker,
|
||||
WouldBecomeZeroToughnessCreature,
|
||||
WouldDestroyWorldEnchantment,
|
||||
BadEtbEffects,
|
||||
CurseEffects
|
||||
CurseEffects;
|
||||
|
||||
public boolean willingToPlay() {
|
||||
return switch (this) {
|
||||
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -767,7 +767,7 @@ public class ComputerUtil {
|
||||
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
|
||||
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
|
||||
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
|
||||
|
||||
if (untap) {
|
||||
typeList.remove(activate);
|
||||
@@ -864,7 +864,7 @@ public class ComputerUtil {
|
||||
|
||||
// Run non-mandatory trigger.
|
||||
// These checks only work if the Executing SpellAbility is an Ability_Sub.
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTrigger(ai, exSA, false)) {
|
||||
// AI would not run this trigger if given the chance
|
||||
return sacrificed;
|
||||
}
|
||||
@@ -1074,6 +1074,80 @@ public class ComputerUtil {
|
||||
return prevented;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
|
||||
@@ -1245,80 +1319,6 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
@@ -1327,7 +1327,6 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
@@ -1385,13 +1384,14 @@ public class ComputerUtil {
|
||||
|
||||
// returns true if the AI should stop using the ability
|
||||
public static boolean preventRunAwayActivations(final SpellAbility sa) {
|
||||
int activations = sa.getActivationsThisTurn();
|
||||
|
||||
if (!sa.isIntrinsic()) {
|
||||
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
|
||||
if (!sa.isActivatedAbility()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activations < 10) { //10 activations per turn should still be acceptable
|
||||
int activations = sa.getActivationsThisTurn();
|
||||
|
||||
//10 activations should still be acceptable
|
||||
if (activations < 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1621,7 +1621,6 @@ public class ComputerUtil {
|
||||
damage = dmg;
|
||||
}
|
||||
|
||||
// Triggered abilities
|
||||
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
|
||||
for (final Trigger t : c.getTriggers()) {
|
||||
if (TriggerType.Attacks.equals(t.getMode())) {
|
||||
@@ -2543,7 +2542,7 @@ public class ComputerUtil {
|
||||
|
||||
boolean opponent = controller.isOpponentOf(ai);
|
||||
|
||||
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType p1p1Type = CounterEnumType.P1P1;
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return Aggregates.random(options);
|
||||
@@ -2552,7 +2551,7 @@ public class ComputerUtil {
|
||||
String logic = sa.getParam("AILogic");
|
||||
switch (logic) {
|
||||
case "Torture":
|
||||
return "Torture";
|
||||
return options.get(1);
|
||||
case "GraceOrCondemnation":
|
||||
List<ZoneType> graceZones = new ArrayList<ZoneType>();
|
||||
graceZones.add(ZoneType.Battlefield);
|
||||
@@ -2560,12 +2559,12 @@ public class ComputerUtil {
|
||||
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
|
||||
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
|
||||
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
|
||||
return aiGrace > humanGrace ? "Grace" : "Condemnation";
|
||||
return options.get(aiGrace > humanGrace ? 0 : 1);
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
|
||||
CardCollection computerlist = ai.getCreaturesInPlay();
|
||||
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
|
||||
case "Judgment":
|
||||
if (votes.isEmpty()) {
|
||||
CardCollection list = new CardCollection();
|
||||
@@ -2579,68 +2578,71 @@ public class ComputerUtil {
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "Protection":
|
||||
if (votes.isEmpty()) {
|
||||
List<String> restrictedToColors = Lists.newArrayList();
|
||||
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
|
||||
for (Object o : options) {
|
||||
if (o instanceof String) {
|
||||
restrictedToColors.add((String) o);
|
||||
}
|
||||
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
|
||||
restrictedToColors.put(sp.getOriginalDescription(), sp);
|
||||
}
|
||||
}
|
||||
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
|
||||
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
|
||||
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
|
||||
}
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "FeatherOrQuill":
|
||||
SpellAbility feather = (SpellAbility)options.get(0);
|
||||
SpellAbility quill = (SpellAbility)options.get(1);
|
||||
// try to mill opponent with Quill vote
|
||||
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
|
||||
int numQuill = votes.get("Quill").size();
|
||||
int numQuill = votes.get(quill).size();
|
||||
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
|
||||
}
|
||||
}
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Feather" : "Quill";
|
||||
return opponent ? feather : quill;
|
||||
}
|
||||
// if source is not on the battlefield anymore, choose +1/+1 ones
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? "Feather" : "Quill";
|
||||
return opponent ? feather : quill;
|
||||
}
|
||||
// if no hand cards, try to mill opponent
|
||||
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return opponent ? "Quill" : "Feather";
|
||||
return opponent ? quill : feather;
|
||||
}
|
||||
|
||||
// AI has something to discard
|
||||
if (ai.equals(controller)) {
|
||||
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
|
||||
return "Quill";
|
||||
return quill;
|
||||
}
|
||||
}
|
||||
|
||||
// default card draw and discard are better than +1/+1 counter
|
||||
return opponent ? "Feather" : "Quill";
|
||||
return opponent ? feather : quill;
|
||||
case "StrengthOrNumbers":
|
||||
SpellAbility strength = (SpellAbility)options.get(0);
|
||||
SpellAbility numbers = (SpellAbility)options.get(1);
|
||||
// similar to fabricate choose +1/+1 or Token
|
||||
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
|
||||
int numStrength = votes.get("Strength").size();
|
||||
int numNumbers = votes.get("Numbers").size();
|
||||
int numStrength = votes.get(strength).size();
|
||||
int numNumbers = votes.get(numbers).size();
|
||||
|
||||
Card token = TokenAi.spawnToken(controller, saToken);
|
||||
Card token = TokenAi.spawnToken(controller, numbers);
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
return opponent ? strength : numbers;
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
return opponent ? strength : numbers;
|
||||
}
|
||||
|
||||
// token would not survive
|
||||
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
|
||||
return opponent ? "Numbers" : "Strength";
|
||||
return opponent ? numbers : strength;
|
||||
}
|
||||
|
||||
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
|
||||
@@ -2661,35 +2663,40 @@ public class ComputerUtil {
|
||||
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
|
||||
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
|
||||
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
|
||||
case "SproutOrHarvest":
|
||||
SpellAbility sprout = (SpellAbility)options.get(0);
|
||||
SpellAbility harvest = (SpellAbility)options.get(1);
|
||||
// lifegain would hurt or has no effect
|
||||
if (opponent) {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return "Harvest";
|
||||
return harvest;
|
||||
}
|
||||
} else {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return "Sprout";
|
||||
return sprout;
|
||||
}
|
||||
}
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
return opponent ? sprout : harvest;
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
return opponent ? sprout : harvest;
|
||||
}
|
||||
// TODO add Lifegain to +1/+1 counters trigger
|
||||
|
||||
// for now +1/+1 counters are better
|
||||
return opponent ? "Harvest" : "Sprout";
|
||||
return opponent ? harvest : sprout;
|
||||
case "DeathOrTaxes":
|
||||
int numDeath = votes.get("Death").size();
|
||||
int numTaxes = votes.get("Taxes").size();
|
||||
SpellAbility death = (SpellAbility)options.get(0);
|
||||
SpellAbility taxes = (SpellAbility)options.get(1);
|
||||
|
||||
int numDeath = votes.get(death).size();
|
||||
int numTaxes = votes.get(taxes).size();
|
||||
|
||||
if (opponent) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
@@ -2697,29 +2704,29 @@ public class ComputerUtil {
|
||||
// would need to sacrifice more creatures than AI has
|
||||
// sacrifice even more
|
||||
if (aiCreatures.size() <= numDeath) {
|
||||
return "Death";
|
||||
return death;
|
||||
}
|
||||
// would need to discard more cards than it has
|
||||
if (aiCardsInHand.size() <= numTaxes) {
|
||||
return "Taxes";
|
||||
return taxes;
|
||||
}
|
||||
|
||||
// has cards with SacMe or Token
|
||||
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
|
||||
return "Death";
|
||||
return death;
|
||||
}
|
||||
|
||||
// has cards with DiscardMe
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
|
||||
return "Taxes";
|
||||
return taxes;
|
||||
}
|
||||
|
||||
// discard is probably less worse than sacrifice
|
||||
return "Taxes";
|
||||
return taxes;
|
||||
} else {
|
||||
// ai is first voter or ally of controller
|
||||
// both are not affected, but if opponents control creatures, sacrifice is worse
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
|
||||
}
|
||||
default:
|
||||
return Iterables.getFirst(options, null);
|
||||
@@ -3097,41 +3104,38 @@ public class ComputerUtil {
|
||||
|
||||
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;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
} else {
|
||||
return srcList;
|
||||
}
|
||||
if (source == null || !sa.hasParam("AITgts")) {
|
||||
return srcList;
|
||||
}
|
||||
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
}
|
||||
return srcList;
|
||||
}
|
||||
|
||||
|
||||
@@ -345,6 +345,10 @@ public class ComputerUtilAbility {
|
||||
if (source.hasSVar("AIPriorityModifier")) {
|
||||
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||
}
|
||||
// try to use it before it's gone
|
||||
if (source.isInPlay() && source.hasSVar("EndOfTurnLeavePlay")) {
|
||||
p += 1;
|
||||
}
|
||||
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
||||
p -= 10;
|
||||
}
|
||||
|
||||
@@ -919,14 +919,14 @@ public class ComputerUtilCard {
|
||||
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
|
||||
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
|
||||
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
||||
for (byte c : MagicColor.WUBRG) {
|
||||
if ((colors & c) != 0) {
|
||||
return MagicColor.toLongString(c);
|
||||
}
|
||||
}
|
||||
return restrictedToColors.get(0); // no difference, there was no prominent color
|
||||
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static List<String> getColorByProminence(final List<Card> list) {
|
||||
@@ -1819,18 +1819,18 @@ public class ComputerUtilCard {
|
||||
* @param sa Pump* or CounterPut*
|
||||
* @return
|
||||
*/
|
||||
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||
public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
for (final Card card : cards) {
|
||||
if (objects.contains(card)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
}
|
||||
}
|
||||
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -1849,11 +1849,11 @@ public class ComputerUtilCard {
|
||||
}
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
public static boolean isUselessCreature(Player ai, Card c) {
|
||||
|
||||
@@ -974,17 +974,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -998,12 +994,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1107,17 +1102,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1131,12 +1122,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
@@ -1305,6 +1295,7 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
@@ -1314,11 +1305,8 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
@@ -1333,13 +1321,14 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
return power;
|
||||
}
|
||||
@@ -1530,16 +1519,14 @@ public class ComputerUtilCombat {
|
||||
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1553,10 +1540,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import forge.game.GameObject;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -139,11 +140,13 @@ public class ComputerUtilCost {
|
||||
if (type.equals("CARDNAME")) {
|
||||
if (source.getAbilityText().contains("Bloodrush")) {
|
||||
continue;
|
||||
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
||||
}
|
||||
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
||||
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
|
||||
// Better do something than just discard stuff
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
|
||||
if (typeList.size() > ai.getMaxHandSize()) {
|
||||
@@ -248,11 +251,7 @@ public class ComputerUtilCost {
|
||||
// Does the AI want to use Sacrifice All?
|
||||
return false;
|
||||
} else {
|
||||
Integer c = part.convertAmount();
|
||||
|
||||
if (c == null) {
|
||||
c = part.getAbilityAmount(sourceAbility);
|
||||
}
|
||||
int c = part.getAbilityAmount(sourceAbility);
|
||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
|
||||
if (choices != null) {
|
||||
@@ -522,13 +521,12 @@ public class ComputerUtilCost {
|
||||
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
|
||||
}
|
||||
|
||||
boolean cannotBeCountered = false;
|
||||
|
||||
// Check for stuff like Nether Void
|
||||
int extraManaNeeded = 0;
|
||||
if (!effect) {
|
||||
boolean cannotBeCountered = !sa.isCounterableBy(null);
|
||||
|
||||
if (sa instanceof Spell) {
|
||||
cannotBeCountered = !sa.isCounterableBy(null);
|
||||
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
||||
if (!StringUtils.isBlank(snem)) {
|
||||
@@ -578,12 +576,24 @@ public class ComputerUtilCost {
|
||||
}
|
||||
}
|
||||
|
||||
// Ward - will be accounted for when rechecking a targeted ability
|
||||
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
|
||||
// Account for possible Ward after the spell is fully targeted
|
||||
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
|
||||
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
|
||||
if (!sa.isTrigger() && !cannotBeCountered) {
|
||||
Set<GameObject> distinctObjects = Sets.newHashSet();
|
||||
for (TargetChoices tc : sa.getAllTargetChoices()) {
|
||||
for (Card tgt : tc.getTargetCards()) {
|
||||
if (!distinctObjects.add(tgt)) {
|
||||
continue;
|
||||
}
|
||||
// TODO some older cards don't use the keyword, so check for trigger instead
|
||||
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
|
||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||
// don't use API converter since it might have special part logic not meant for Ward cost
|
||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
||||
if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
|
||||
return false;
|
||||
}
|
||||
if (wardCost.hasManaCost()) {
|
||||
extraManaNeeded += wardCost.getTotalMana().getCMC();
|
||||
}
|
||||
@@ -607,6 +617,7 @@ public class ComputerUtilCost {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO both of these call CostAdjustment.adjust, try to reuse instead
|
||||
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
|
||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay,
|
||||
Collection<SpellAbility> saList, boolean checkCosts) {
|
||||
Collection<SpellAbility> maList, boolean checkCosts) {
|
||||
Card saHost = sa.getHostCard();
|
||||
|
||||
// CastTotalManaSpent (AIPreference:ManaFrom$Type or AIManaPref$ Type)
|
||||
@@ -240,12 +240,12 @@ public class ComputerUtilMana {
|
||||
manaSourceType = sa.getParam("AIManaPref");
|
||||
}
|
||||
if (manaSourceType != "") {
|
||||
List<SpellAbility> filteredList = Lists.newArrayList(saList);
|
||||
List<SpellAbility> filteredList = Lists.newArrayList(maList);
|
||||
switch (manaSourceType) {
|
||||
case "Snow":
|
||||
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().isSnow()
|
||||
&& ab2.getHostCard() != null && !ab2.getHostCard().isSnow() ? -1 : 1);
|
||||
saList = filteredList;
|
||||
maList = filteredList;
|
||||
break;
|
||||
case "Treasure":
|
||||
// Try to spend only one Treasure if possible
|
||||
@@ -253,22 +253,22 @@ public class ComputerUtilMana {
|
||||
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
|
||||
SpellAbility first = filteredList.get(0);
|
||||
if (first.getHostCard() != null && first.getHostCard().getType().hasSubtype("Treasure")) {
|
||||
saList.remove(first);
|
||||
maList.remove(first);
|
||||
List<SpellAbility> updatedList = Lists.newArrayList();
|
||||
updatedList.add(first);
|
||||
updatedList.addAll(saList);
|
||||
saList = updatedList;
|
||||
updatedList.addAll(maList);
|
||||
maList = updatedList;
|
||||
}
|
||||
break;
|
||||
case "TreasureMax":
|
||||
// Ok to spend as many Treasures as possible
|
||||
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure")
|
||||
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
|
||||
saList = filteredList;
|
||||
maList = filteredList;
|
||||
break;
|
||||
case "NotSameCard":
|
||||
String hostName = sa.getHostCard().getName();
|
||||
saList = filteredList.stream()
|
||||
maList = filteredList.stream()
|
||||
.filter(saPay -> !saPay.getHostCard().getName().equals(hostName))
|
||||
.collect(Collectors.toList());
|
||||
break;
|
||||
@@ -277,7 +277,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
for (final SpellAbility ma : saList) {
|
||||
for (final SpellAbility ma : maList) {
|
||||
// this rarely seems like a good idea
|
||||
if (ma.getHostCard() == saHost) {
|
||||
continue;
|
||||
@@ -287,7 +287,9 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
|
||||
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
|
||||
if (amount <= 0) {
|
||||
// wrong gamestate for variable amount
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -336,7 +338,7 @@ public class ComputerUtilMana {
|
||||
// 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) {
|
||||
for (SpellAbility ab : maList) {
|
||||
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
||||
if (!ab.getHostCard().isTapped()) {
|
||||
paymentChoice = ab;
|
||||
@@ -351,9 +353,14 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
// these should come last since they reserve the paying cards
|
||||
// (this means if a mana ability has both parts it doesn't currently undo reservations if the second part fails)
|
||||
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return paymentChoice;
|
||||
}
|
||||
@@ -443,7 +450,6 @@ public class ComputerUtilMana {
|
||||
manaProduced = manaProduced.replace(s, color);
|
||||
}
|
||||
} else if (saMana.hasParam("ReplaceColor")) {
|
||||
// replace color
|
||||
String color = saMana.getParam("ReplaceColor");
|
||||
if ("Chosen".equals(color)) {
|
||||
if (card.hasChosenColor()) {
|
||||
@@ -590,12 +596,12 @@ public class ComputerUtilMana {
|
||||
while (!cost.isPaid()) {
|
||||
toPay = getNextShardToPay(cost, sourcesForShards);
|
||||
|
||||
Collection<SpellAbility> saList = sourcesForShards.get(toPay);
|
||||
if (saList == null) {
|
||||
Collection<SpellAbility> maList = sourcesForShards.get(toPay);
|
||||
if (maList == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, saList, true);
|
||||
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, maList, true);
|
||||
if (saPayment == null) {
|
||||
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
|
||||
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)) {
|
||||
@@ -666,6 +672,7 @@ public class ComputerUtilMana {
|
||||
return true;
|
||||
}
|
||||
|
||||
int phyLifeToPay = 2;
|
||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||
boolean hasConverge = sa.getHostCard().hasConverge();
|
||||
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test, checkPlayable, hasConverge);
|
||||
@@ -693,13 +700,12 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (sourcesForShards == null && !purePhyrexian) {
|
||||
break; // no mana abilities to use for paying
|
||||
// no mana abilities to use for paying
|
||||
break;
|
||||
}
|
||||
|
||||
toPay = getNextShardToPay(cost, sourcesForShards);
|
||||
|
||||
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
|
||||
|
||||
Collection<SpellAbility> saList = null;
|
||||
if (hasConverge &&
|
||||
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
|
||||
@@ -735,7 +741,8 @@ public class ComputerUtilMana {
|
||||
|
||||
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
|
||||
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
|
||||
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
// not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
saExcludeList.add(saPayment);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -752,9 +759,14 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (saPayment == null) {
|
||||
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)
|
||||
|| (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
|
||||
break; // cannot pay
|
||||
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
|
||||
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(phyLifeToPay, false, sa)
|
||||
|| (ai.getLife() <= phyLifeToPay && !ai.cantLoseForZeroOrLessLife())) {
|
||||
// cannot pay
|
||||
break;
|
||||
}
|
||||
if (test) {
|
||||
phyLifeToPay += 2;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AIPhyrexianPayment")) {
|
||||
@@ -804,11 +816,11 @@ public class ComputerUtilMana {
|
||||
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
|
||||
payMultipleMana(cost, manaProduced, ai);
|
||||
|
||||
// remove from available lists
|
||||
// remove to prevent re-usage since resources don't get consumed
|
||||
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
|
||||
} else {
|
||||
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
|
||||
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) {
|
||||
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect, true))) {
|
||||
saList.remove(saPayment);
|
||||
continue;
|
||||
}
|
||||
@@ -817,8 +829,10 @@ public class ComputerUtilMana {
|
||||
// subtract mana from mana pool
|
||||
manapool.payManaFromAbility(sa, cost, saPayment);
|
||||
|
||||
// no need to remove abilities from resource map,
|
||||
// once their costs are paid and consume resources, they can not be used again
|
||||
// need to consider if another use is now prevented
|
||||
if (!cost.isPaid() && saPayment.isActivatedAbility() && !saPayment.getRestrictions().canPlay(saPayment.getHostCard(), saPayment)) {
|
||||
sourcesForShards.values().removeIf(s -> s == saPayment);
|
||||
}
|
||||
|
||||
if (hasConverge) {
|
||||
// hack to prevent converge re-using sources
|
||||
@@ -958,7 +972,6 @@ public class ComputerUtilMana {
|
||||
if (checkCosts) {
|
||||
// Check if AI can still play this mana ability
|
||||
ma.setActivatingPlayer(ai);
|
||||
// if the AI can't pay the additional costs skip the mana ability
|
||||
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
|
||||
return false;
|
||||
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
|
||||
@@ -976,8 +989,9 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s)))
|
||||
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s))){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1491,7 +1505,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (!cost.isReusuableResource()) {
|
||||
for(CostPart part : cost.getCostParts()) {
|
||||
for (CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
|
||||
unpreferredCost = true;
|
||||
}
|
||||
@@ -1503,7 +1517,7 @@ public class ComputerUtilMana {
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
|
||||
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
}
|
||||
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
|
||||
@@ -1582,10 +1596,8 @@ public class ComputerUtilMana {
|
||||
|
||||
// don't use abilities with dangerous drawbacks
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
if (sub != null) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
|
||||
continue;
|
||||
}
|
||||
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
|
||||
@@ -1653,7 +1665,6 @@ public class ComputerUtilMana {
|
||||
if (replaced.contains("C")) {
|
||||
manaMap.put(ManaAtom.COLORLESS, m);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +264,14 @@ public abstract class GameState {
|
||||
}
|
||||
|
||||
if (c.hasMergedCard()) {
|
||||
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
|
||||
// we have to go by the current top card name here
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
|
||||
} else {
|
||||
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
String suffix = c.hasPaperFoil() ? "+" : "";
|
||||
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
.append("|Art:").append(c.getPaperCard().getArtIndex());
|
||||
}
|
||||
}
|
||||
@@ -319,18 +321,21 @@ public abstract class GameState {
|
||||
newText.append(":Cloaked");
|
||||
}
|
||||
}
|
||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||
newText.append("|Transformed");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
newText.append("|Flipped");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||
newText.append("|Meld");
|
||||
if (c.getMeldedWith() != null) {
|
||||
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
|
||||
newText.append(":");
|
||||
newText.append(c.getMeldedWith().getName());
|
||||
newText.append(c.getMeldedWith().getName()).append(suffix);
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
|
||||
if (c.isModal()) {
|
||||
newText.append("|Modal");
|
||||
} else {
|
||||
newText.append("|Transformed");
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
|
||||
newText.append("|Modal");
|
||||
}
|
||||
|
||||
if (c.getPlayerAttachedTo() != null) {
|
||||
@@ -1263,7 +1268,7 @@ public abstract class GameState {
|
||||
} else if (cardinfo[0].startsWith("T:")) {
|
||||
String tokenStr = cardinfo[0].substring(2);
|
||||
PaperToken token = StaticData.instance().getAllTokens().getToken(tokenStr,
|
||||
setCode != null ? setCode : CardEdition.UNKNOWN.getName());
|
||||
setCode != null ? setCode : CardEdition.UNKNOWN_CODE);
|
||||
if (token == null) {
|
||||
System.err.println("ERROR: Tried to create a non-existent token named " + cardinfo[0] + " when loading game state!");
|
||||
continue;
|
||||
@@ -1311,8 +1316,8 @@ public abstract class GameState {
|
||||
if (info.endsWith("Cloaked")) {
|
||||
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
|
||||
}
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Backside, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Flipped")) {
|
||||
c.setState(CardStateName.Flipped, true);
|
||||
@@ -1330,9 +1335,6 @@ public abstract class GameState {
|
||||
}
|
||||
c.setState(CardStateName.Meld, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Modal, true);
|
||||
c.setBackSide(true);
|
||||
}
|
||||
else if (info.startsWith("OnAdventure")) {
|
||||
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";
|
||||
|
||||
@@ -460,7 +460,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, affected);
|
||||
Card host = replacementEffect.getHostCard();
|
||||
if (host.hasAlternateState()) {
|
||||
host = host.getGame().getCardState(host);
|
||||
}
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, host, affected);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1347,6 +1351,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
|
||||
// TODO check if profile detection set to Auto
|
||||
|
||||
@@ -171,7 +171,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ public class SpecialAiLogic {
|
||||
}
|
||||
|
||||
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
||||
public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
|
||||
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);
|
||||
@@ -222,14 +222,14 @@ public class SpecialAiLogic {
|
||||
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;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
// 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(ai, sa)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ public class SpecialAiLogic {
|
||||
if (countersSa == null) {
|
||||
// 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 SA in chain!");
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
@@ -263,7 +263,7 @@ public class SpecialAiLogic {
|
||||
relevantCreats.remove(source);
|
||||
if (relevantCreats.isEmpty()) {
|
||||
// No relevant creatures to sac
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
|
||||
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
@@ -287,16 +287,20 @@ public class SpecialAiLogic {
|
||||
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat))
|
||||
);
|
||||
if (!forcedSacTgts.isEmpty()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
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;
|
||||
if (source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||
@@ -309,7 +313,7 @@ public class SpecialAiLogic {
|
||||
);
|
||||
|
||||
if (sacTgts.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
|
||||
@@ -317,7 +321,10 @@ public class SpecialAiLogic {
|
||||
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
|
||||
|
||||
// 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;
|
||||
if (source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||
@@ -329,7 +336,11 @@ public class SpecialAiLogic {
|
||||
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|
||||
);
|
||||
|
||||
return !sacFodder.isEmpty();
|
||||
if (sacFodder.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,10 +371,10 @@ public class SpecialAiLogic {
|
||||
// FIXME: We're emulating the UnlessCost on the SA to run the proper checks.
|
||||
// This is hacky, but it works. Perhaps a cleaner way exists?
|
||||
sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost"));
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
|
||||
sa.getMapParams().remove("UnlessCost");
|
||||
} else {
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
|
||||
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
|
||||
}
|
||||
return willPlay;
|
||||
}
|
||||
|
||||
@@ -78,16 +78,17 @@ public class SpecialCardAi {
|
||||
|
||||
// Arena and Magus of the Arena
|
||||
public static class Arena {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
|
||||
// TODO This is basically removal, so we may want to play this at other times
|
||||
if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) {
|
||||
return false; // at opponent's EOT only, to conserve mana
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
if (aiCreatures.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
@@ -111,11 +112,11 @@ public class SpecialCardAi {
|
||||
if (canKillAll) {
|
||||
sa.getTargets().clear();
|
||||
sa.getTargets().add(aiCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Removal);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sa.isTargetNumberValid();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +204,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Chain of Acid
|
||||
public static class ChainOfAcid {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.LANDS);
|
||||
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
|
||||
@@ -213,13 +214,22 @@ public class SpecialCardAi {
|
||||
// which it can only distinguish by their CMC, considering >CMC higher value).
|
||||
// Currently ensures that the AI will still have lands provided that the human player goes to
|
||||
// destroy all the AI's lands in order (to avoid manalock).
|
||||
return !OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2;
|
||||
if (!OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2) {
|
||||
// If there are enough lands, target the worst non-creature permanent of the opponent
|
||||
Card worstOppPerm = ComputerUtilCard.getWorstAI(OppPerms);
|
||||
if (worstOppPerm != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(worstOppPerm);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
// Chain of Smog
|
||||
public static class ChainOfSmog {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
if (ai.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
// to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point)
|
||||
// TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a
|
||||
@@ -235,10 +245,10 @@ public class SpecialCardAi {
|
||||
|
||||
sa.getParent().resetTargets();
|
||||
sa.getParent().getTargets().add(targOpp);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +400,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Donate
|
||||
public static class Donate {
|
||||
public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision considerTargetingOpponent(final Player ai, final SpellAbility sa) {
|
||||
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
|
||||
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
||||
if (donateTarget != null) {
|
||||
@@ -400,7 +410,7 @@ public class SpecialCardAi {
|
||||
|
||||
// All opponents have hexproof or something like that
|
||||
if (Iterables.isEmpty(oppList)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// filter for player who does not have donate target already
|
||||
@@ -418,31 +428,30 @@ public class SpecialCardAi {
|
||||
if (opp != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// No targets found to donate, so do nothing.
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
public static boolean considerDonatingPermanent(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) {
|
||||
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
||||
if (donateTarget != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(donateTarget);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail
|
||||
System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first.");
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
// Electrostatic Pummeler
|
||||
public static class ElectrostaticPummeler {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
Game game = ai.getGame();
|
||||
Combat combat = game.getCombat();
|
||||
@@ -455,13 +464,13 @@ public class SpecialCardAi {
|
||||
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
|
||||
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop);
|
||||
if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg)
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not activate if damage will be prevented
|
||||
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
|
||||
}
|
||||
|
||||
// Activate Electrostatic Pummeler's pump only as a combat trick
|
||||
@@ -470,14 +479,14 @@ public class SpecialCardAi {
|
||||
// We'll try to deal lethal trample/unblocked damage, so remember the card for attack
|
||||
// and wait until declare blockers step.
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
boolean isBlocking = combat.isBlocking(source);
|
||||
@@ -502,11 +511,11 @@ public class SpecialCardAi {
|
||||
}
|
||||
if (totalDamageToPW >= oppT + loyalty) {
|
||||
// Already enough damage to take care of the planeswalker
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
|
||||
// Can pump to kill the planeswalker, go for it
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -527,31 +536,31 @@ public class SpecialCardAi {
|
||||
// We can deal a lot of damage (either a lot of damage directly to the opponent,
|
||||
// or kill the blocker(s) and damage the opponent at the same time, so go for it
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
}
|
||||
|
||||
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
|
||||
// Can't survive first strike or double strike, don't pump
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) {
|
||||
// Can't pump enough to kill the blockers and survive, don't pump
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
|
||||
// Already enough to kill the blockers and survive, don't overpump
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage()
|
||||
&& predictedPT.getLeft() <= oppT) {
|
||||
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
|
||||
// If we got here, it should be a favorable combat pump, resulting in at least one
|
||||
// opposing creature dying, and hopefully with the Pummeler surviving combat.
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
|
||||
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
|
||||
@@ -628,15 +637,15 @@ public class SpecialCardAi {
|
||||
|
||||
// Fell the Mighty
|
||||
public static class FellTheMighty {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
CardCollection aiList = ai.getCreaturesInPlay();
|
||||
if (aiList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
CardLists.sortByPowerAsc(aiList);
|
||||
Card lowest = aiList.get(0);
|
||||
if (!sa.canTarget(lowest)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
@@ -646,9 +655,9 @@ public class SpecialCardAi {
|
||||
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(lowest);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,25 +692,25 @@ public class SpecialCardAi {
|
||||
|
||||
// Gideon Blackblade
|
||||
public static class GideonBlackblade {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
sa.resetTargets();
|
||||
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
|
||||
if (!otb.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
// Goblin Polka Band
|
||||
public static class GoblinPolkaBand {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
int maxPotentialTgts = ai.getOpponents().getCreaturesInPlay().filter(CardPredicates.UNTAPPED).size();
|
||||
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
|
||||
|
||||
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
|
||||
if (numTgts == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// Set Announce
|
||||
@@ -711,7 +720,7 @@ public class SpecialCardAi {
|
||||
List<GameEntity> validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().addAll(Aggregates.random(validTgts, numTgts));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,12 +929,12 @@ public class SpecialCardAi {
|
||||
|
||||
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
|
||||
public static class LivingDeath {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
// if there's another reanimator card currently suspended, don't cast a new one until the previous
|
||||
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
|
||||
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
|
||||
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -936,7 +945,7 @@ public class SpecialCardAi {
|
||||
|
||||
if (aiCreaturesInGY.isEmpty()) {
|
||||
// nothing in graveyard, so cut short
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
for (Card c : ai.getCreaturesInPlay()) {
|
||||
@@ -968,17 +977,30 @@ public class SpecialCardAi {
|
||||
}
|
||||
|
||||
// if we get more value out of this than our opponent does (hopefully), go for it
|
||||
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold);
|
||||
if ((aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maze's End
|
||||
public static class MazesEnd {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
|
||||
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty();
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (availableGates.isEmpty()) {
|
||||
// No gates available, so don't activate Maze's End
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
public static Card considerCardToGet(final Player ai, final SpellAbility sa)
|
||||
@@ -1042,29 +1064,33 @@ public class SpecialCardAi {
|
||||
return exiledWith == null || (tgt != null && ComputerUtilCard.evaluateCreature(tgt) > ComputerUtilCard.evaluateCreature(exiledWith));
|
||||
}
|
||||
|
||||
public static boolean considerCopy(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision considerCopy(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Card exiledWith = source.getImprintedCards().isEmpty() ? null : source.getImprintedCards().getFirst();
|
||||
|
||||
if (exiledWith == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
// We want to either be able to attack with the creature, or keep it until our opponent's end of turn as a
|
||||
// potential blocker
|
||||
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
|
||||
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
|
||||
|| (ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && ai.getGame().getCombat() != null
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty());
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Momir Vig, Simic Visionary Avatar
|
||||
public static class MomirVigAvatar {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
|
||||
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
// In MoJhoSto, prefer Jhoira sorcery ability from time to time
|
||||
@@ -1075,7 +1101,7 @@ public class SpecialCardAi {
|
||||
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
|
||||
|
||||
if (ai.getLandsInPlay().size() >= numLandsForJhoira && MyRandom.percentTrue(chanceToPrefJhoira)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1084,7 +1110,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Some basic strategy for Momir
|
||||
if (tokenSize < 2) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
if (tokenSize > 11) {
|
||||
@@ -1093,7 +1119,7 @@ public class SpecialCardAi {
|
||||
|
||||
sa.setXManaCostPaid(tokenSize);
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1132,13 +1158,13 @@ public class SpecialCardAi {
|
||||
|
||||
// Necropotence
|
||||
public static class Necropotence {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
Game game = ai.getGame();
|
||||
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
||||
int maxHandSize = ai.getMaxHandSize();
|
||||
|
||||
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return false; // nothing to draw from the library
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
|
||||
@@ -1146,7 +1172,7 @@ public class SpecialCardAi {
|
||||
|
||||
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
|
||||
// (not sure how to detect the presence of such effects yet)
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
PhaseHandler ph = game.getPhaseHandler();
|
||||
@@ -1168,23 +1194,33 @@ public class SpecialCardAi {
|
||||
// We're in a situation when we have nothing castable in hand, something needs to be done
|
||||
if (!blackViseOTB) {
|
||||
// exile-loot +1 card when at max hand size, hoping to get a workable spell or land
|
||||
return computerHandSize + exiledWithNecro - 1 == maxHandSize;
|
||||
if (computerHandSize + exiledWithNecro - 1 == maxHandSize) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// Loot to 7 in presence of Black Vise, hoping to find what to do
|
||||
// NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead?
|
||||
return computerHandSize + exiledWithNecro <= maxHandSize;
|
||||
if (computerHandSize + exiledWithNecro <= maxHandSize) {
|
||||
// Loot to 7, hoping to find something playable
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// Loot to 8, hoping to find something playable
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) {
|
||||
// try not to overdraw in presence of Black Vise
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) {
|
||||
// Only draw until we reach max hand size
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) {
|
||||
// Only activate in AI's own turn (sans the exception above)
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,7 +1340,7 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision considerSecondTarget(final Player ai, final SpellAbility sa) {
|
||||
Card firstTgt = sa.getParent().getTargetCard();
|
||||
CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter(
|
||||
CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa)));
|
||||
@@ -1312,89 +1348,105 @@ public class SpecialCardAi {
|
||||
if (secondTgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(secondTgt);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Price of Progress
|
||||
public static class PriceOfProgress {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
// Don't play in early game - opponent likely still has lands to play
|
||||
if (ai.getGame().getPhaseHandler().getTurn() < 10) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
|
||||
// TODO Better if we actually calculate the true damage
|
||||
boolean willDieToPCasting = (ai.getLife() <= aiLands * 2);
|
||||
if (!willDieToPCasting) {
|
||||
boolean hasBridge = false;
|
||||
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
|
||||
// Do we have a card in play that makes us want to empty out hand?
|
||||
if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) {
|
||||
hasBridge = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasBridge = false;
|
||||
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
|
||||
// Do we have a card in play that makes us want to empty out hand?
|
||||
if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) {
|
||||
hasBridge = true;
|
||||
break;
|
||||
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
|
||||
// even if suboptimal play, but don't waste the card too early even then!
|
||||
if (hasBridge) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.PlayToEmptyHand);
|
||||
}
|
||||
}
|
||||
|
||||
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
|
||||
// even if suboptimal play, but don't waste the card too early even then!
|
||||
if ((hasBridge) && (ai.getGame().getPhaseHandler().getTurn() >= 10)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean willPlay = true;
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
|
||||
// Don't if no enemy nonbasic lands
|
||||
if (oppLands == 0) {
|
||||
willPlay = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always if enemy would die and we don't!
|
||||
// TODO : predict actual damage instead of assuming it'll be 2*lands
|
||||
// Don't if we lose, unless we lose anyway to unblocked creatures next turn
|
||||
if ((ai.getLife() <= aiLands * 2) &&
|
||||
if (willDieToPCasting &&
|
||||
(!(ComputerUtil.aiLifeInDanger(ai, true, 0)) && ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2))) {
|
||||
return false;
|
||||
willPlay = false;
|
||||
}
|
||||
// Do if we can win
|
||||
if ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2) {
|
||||
return true;
|
||||
if (opp.getLife() <= oppLands * 2) {
|
||||
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// Don't if we'd lose a larger percentage of our remaining life than enemy
|
||||
if ((aiLands / ((double) ai.getLife())) >
|
||||
(oppLands / ((double) ai.getOpponentsSmallestLifeTotal()))) {
|
||||
return false;
|
||||
}
|
||||
// Don't if no enemy nonbasic lands
|
||||
if (oppLands == 0) {
|
||||
return false;
|
||||
willPlay = false;
|
||||
}
|
||||
|
||||
// Don't if loss is equal in percentage but we lose more points
|
||||
if (((aiLands / ((double) ai.getLife())) == (oppLands / ((double) ai.getOpponentsSmallestLifeTotal())))
|
||||
&& (aiLands > oppLands)) {
|
||||
return false;
|
||||
willPlay = false;
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
if (willPlay) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sarkhan the Mad
|
||||
public static class SarkhanTheMad {
|
||||
public static boolean considerDig(final Player ai, final SpellAbility sa) {
|
||||
return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1;
|
||||
public static AiAbilityDecision considerDig(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision considerMakeDragon(final Player ai, final SpellAbility sa) {
|
||||
// TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier?
|
||||
CardCollection creatures = ai.getCreaturesInPlay();
|
||||
boolean hasValidTgt = !CardLists.filter(creatures, t -> t.getNetPower() < 5 && t.getNetToughness() < 5).isEmpty();
|
||||
if (hasValidTgt) {
|
||||
Card worstCreature = ComputerUtilCard.getWorstCreatureAI(creatures);
|
||||
sa.getTargets().add(worstCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
|
||||
public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) {
|
||||
int minLife = weakestOpp.getLife();
|
||||
|
||||
@@ -1450,7 +1502,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Sorin, Vengeful Bloodlord
|
||||
public static class SorinVengefulBloodlord {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
|
||||
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
|
||||
CardPredicates.CREATURES
|
||||
@@ -1464,7 +1516,7 @@ public class SpecialCardAi {
|
||||
CardLists.sortByCmcDesc(creaturesToGet);
|
||||
|
||||
if (creaturesToGet.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// pick the best creature that will stay on the battlefield
|
||||
@@ -1480,10 +1532,10 @@ public class SpecialCardAi {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(best);
|
||||
sa.setXManaCostPaid(best.getCMC());
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1597,23 +1649,27 @@ public class SpecialCardAi {
|
||||
|
||||
// The One Ring
|
||||
public static class TheOneRing {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
|
||||
int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
|
||||
|
||||
return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
|
||||
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1;
|
||||
if (ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
|
||||
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.LifeInDanger);
|
||||
}
|
||||
}
|
||||
|
||||
// The Scarab God
|
||||
public static class TheScarabGod {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
|
||||
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
|
||||
|
||||
@@ -1624,13 +1680,19 @@ public class SpecialCardAi {
|
||||
sa.getTargets().add(worstOwnCreat);
|
||||
}
|
||||
|
||||
return sa.getTargets().size() > 0;
|
||||
if (!sa.getTargets().isEmpty()) {
|
||||
// If we have a target, we can play this ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// No valid targets, can't play this ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timetwister
|
||||
public static class Timetwister {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
int maxOppHandSize = 0;
|
||||
|
||||
@@ -1644,7 +1706,14 @@ public class SpecialCardAi {
|
||||
}
|
||||
|
||||
// use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand
|
||||
return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD;
|
||||
if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) {
|
||||
// if the AI has less than 3 cards in hand or the opponent has more than 3 cards in hand than the AI
|
||||
// then the AI is willing to play this ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// otherwise, don't play this ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1705,12 +1774,12 @@ public class SpecialCardAi {
|
||||
|
||||
// Volrath's Shapeshifter
|
||||
public static class VolrathsShapeshifter {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
|
||||
// try not to do this too early to at least attempt to avoid situations where the AI
|
||||
// would cast a spell which would ruin the shapeshifting
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
|
||||
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
|
||||
@@ -1726,11 +1795,15 @@ public class SpecialCardAi {
|
||||
if (topGY == null
|
||||
|| !topGY.isCreature()
|
||||
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
|
||||
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false);
|
||||
if ( numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) {
|
||||
|
||||
@@ -39,70 +39,75 @@ import forge.util.collect.FCollectionView;
|
||||
*/
|
||||
public abstract class SpellAbilityAi {
|
||||
|
||||
public final boolean canPlayAIWithSubs(final Player aiPlayer, final SpellAbility sa) {
|
||||
if (!canPlayAI(aiPlayer, sa)) {
|
||||
return false;
|
||||
public final AiAbilityDecision canPlayWithSubs(final Player aiPlayer, final SpellAbility sa) {
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (!decision.willingToPlay() && !"PlayForSub".equals(sa.getParam("AILogic"))) {
|
||||
return decision;
|
||||
}
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
if (subAb == null) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a "main" SpellAbility
|
||||
*/
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(source, sa)) {
|
||||
return false;
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(sa.getHostCard(), sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
return canPlayWithoutRestrict(ai, sa);
|
||||
}
|
||||
|
||||
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
if (sa.hasParam("AICheckCanPlayWithDefinedX")) {
|
||||
// FIXME: can this somehow be simplified without the need for an extra AI hint?
|
||||
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
|
||||
}
|
||||
|
||||
if (!checkConditions(ai, sa, sa.getConditions())) {
|
||||
SpellAbility sub = sa.getSubAbility();
|
||||
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
|
||||
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
|
||||
if (!checkAiLogic(ai, sa, logic)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
|
||||
}
|
||||
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
|
||||
} else if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
|
||||
if (!checkApiLogic(ai, sa)) {
|
||||
return false;
|
||||
AiAbilityDecision decision = checkApiLogic(ai, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
// needs to be after API logic because needs to check possible X Cost?
|
||||
|
||||
// needs to be after API logic because needs to check possible X Cost
|
||||
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
}
|
||||
return true;
|
||||
|
||||
// for cards like Figure of Destiny
|
||||
// (it's unlikely many valid effect would work like this -
|
||||
// and while in theory AI could turn some conditions true in response that's far too advanced as default)
|
||||
if (!checkConditions(ai, sa)) {
|
||||
SpellAbility sub = sa.getSubAbility();
|
||||
if (sub == null || !checkConditions(ai, sub)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.NeedsToPlayCriteriaNotMet);
|
||||
}
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
|
||||
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
|
||||
protected boolean checkConditions(final Player ai, final SpellAbility sa) {
|
||||
// copy it to disable some checks that the AI need to check extra
|
||||
con = (SpellAbilityCondition) con.copy();
|
||||
SpellAbilityCondition con = (SpellAbilityCondition) sa.getConditions().copy();
|
||||
|
||||
// if manaspent, check if AI can pay the colored mana as cost
|
||||
if (!con.getManaSpent().isEmpty()) {
|
||||
@@ -116,40 +121,6 @@ public abstract class SpellAbilityAi {
|
||||
return con.areMet(sa);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.metConditions();
|
||||
}
|
||||
|
||||
return !("Never".equals(aiLogic));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI is willing to pay for additional costs
|
||||
* <p>
|
||||
* Evaluated costs are: life, discard, sacrifice and counter-removal
|
||||
*/
|
||||
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI will play a SpellAbility based on its phase restrictions
|
||||
*/
|
||||
@@ -159,19 +130,38 @@ public abstract class SpellAbilityAi {
|
||||
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
|
||||
final String logic) {
|
||||
if (logic.equals("AtOppEOT")) {
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
return checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ("Never".equals(aiLogic)) {
|
||||
return false;
|
||||
}
|
||||
if (!"Once".equals(aiLogic)) {
|
||||
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rest of the logic not covered by the canPlayAI template is defined here
|
||||
*/
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite loop
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
|
||||
// 80% chance to play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
public final boolean doTrigger(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
// this evaluation order is currently intentional as it does more stuff that helps avoiding some crashes
|
||||
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
|
||||
return false;
|
||||
@@ -183,28 +173,48 @@ public abstract class SpellAbilityAi {
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
|
||||
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory);
|
||||
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory).willingToPlay();
|
||||
}
|
||||
|
||||
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
public final AiAbilityDecision doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
AiAbilityDecision decision = doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
if (!decision.willingToPlay() && !"Always".equals(sa.getParam("AILogic"))) {
|
||||
return decision;
|
||||
}
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
|
||||
}
|
||||
if (subAb == null) {
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
decision = chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a triggered SpellAbility
|
||||
*/
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
|
||||
return true;
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
AiAbilityDecision decision = canPlayWithoutRestrict(aiPlayer, sa);
|
||||
if (decision.willingToPlay() && (!mandatory || sa.isTargetNumberValid())) {
|
||||
// This is a weird check. Why do we care if its not mandatory if we WANT to do it?
|
||||
return decision;
|
||||
}
|
||||
|
||||
// not mandatory, short way out
|
||||
if (!mandatory) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// invalid target might prevent it
|
||||
@@ -220,82 +230,13 @@ public abstract class SpellAbilityAi {
|
||||
if (sa.canTarget(p)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a sub-SpellAbility
|
||||
*/
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
// sub-SpellAbility might use targets too
|
||||
if (sa.usesTargeting()) {
|
||||
// no Candidates, no adding to Stack
|
||||
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
|
||||
return false;
|
||||
}
|
||||
// but if it does, it should override this function
|
||||
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* isSorcerySpeed.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* playReusable.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler phase = ai.getGame().getPhaseHandler();
|
||||
|
||||
// TODO probably also consider if winter orb or similar are out
|
||||
|
||||
if (sa instanceof AbilitySub) {
|
||||
return true; // This is only true for Drawbacks and triggers
|
||||
}
|
||||
|
||||
if (!sa.getPayCosts().isReusuableResource()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
|
||||
return true;
|
||||
}
|
||||
if (sa.isSpell() && !sa.isBuyback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,9 +245,35 @@ public abstract class SpellAbilityAi {
|
||||
* @param ab
|
||||
* @return
|
||||
*/
|
||||
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
final AbilitySub subAb = ab.getSubAbility();
|
||||
return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
|
||||
AiAbilityDecision decision = SpellApiToAi.Converter.get(ab).chkDrawback(ab, aiPlayer);
|
||||
if (!decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (subAb == null) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return chkDrawbackWithSubs(aiPlayer, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the AI decision to play a sub-SpellAbility
|
||||
*/
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
// sub-SpellAbility might use targets too
|
||||
if (sa.usesTargeting()) {
|
||||
// no Candidates, no adding to Stack
|
||||
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
// but if it does, it should override this function
|
||||
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
@@ -314,25 +281,6 @@ public abstract class SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParam("UnlessAI");
|
||||
boolean payNever = "Never".equals(aiLogic);
|
||||
boolean isMine = sa.getActivatingPlayer().equals(payer);
|
||||
|
||||
if (payNever) { return false; }
|
||||
|
||||
// AI will only pay when it's not already payed and only opponents abilities
|
||||
if (alreadyPaid || (payers.size() > 1 && isMine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
|
||||
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
boolean hasPlayer = false;
|
||||
@@ -412,6 +360,46 @@ public abstract class SpellAbilityAi {
|
||||
return MyRandom.getRandom().nextBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the AI is willing to pay for additional costs
|
||||
* <p>
|
||||
* Evaluated costs are: life, discard, sacrifice and counter-removal
|
||||
*/
|
||||
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParam("UnlessAI");
|
||||
boolean payNever = "Never".equals(aiLogic);
|
||||
boolean isMine = sa.getActivatingPlayer().equals(payer);
|
||||
|
||||
if (payNever) { return false; }
|
||||
|
||||
// AI will only pay when it's not already payed and only opponents abilities
|
||||
if (alreadyPaid || (payers.size() > 1 && isMine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
|
||||
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
|
||||
}
|
||||
|
||||
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
|
||||
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
|
||||
Cost costSoFar = chosen.getPayCosts().copy();
|
||||
@@ -421,14 +409,14 @@ public abstract class SpellAbilityAi {
|
||||
Cost fullCost = opt.getCost().copy().add(costSoFar);
|
||||
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
|
||||
|
||||
// Playability check for Kicker
|
||||
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
|
||||
SpellAbility kickedSaCopy = fullCostSa.copy();
|
||||
kickedSaCopy.addOptionalCost(opt.getType());
|
||||
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
|
||||
copy.setCastSA(kickedSaCopy);
|
||||
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
|
||||
continue; // don't choose kickers we don't want to play
|
||||
// don't choose kickers we don't want to play
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,4 +428,56 @@ public abstract class SpellAbilityAi {
|
||||
|
||||
return chosenOptCosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* isSorcerySpeed.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* playReusable.
|
||||
* </p>
|
||||
*
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
|
||||
PhaseHandler phase = ai.getGame().getPhaseHandler();
|
||||
|
||||
// TODO probably also consider if winter orb or similar are out
|
||||
|
||||
if (sa instanceof AbilitySub) {
|
||||
return true; // This is only true for Drawbacks and triggers
|
||||
}
|
||||
|
||||
if (!sa.getPayCosts().isReusuableResource()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
|
||||
return true;
|
||||
}
|
||||
if (sa.isSpell() && !sa.isBuyback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -8,7 +10,6 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -16,78 +17,69 @@ import java.util.Map;
|
||||
public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (!defined.contains(opp)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return randomReturn;
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (null == tgt) {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
return defined.contains(opp);
|
||||
if (defined.contains(opp)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
boolean randomReturn = true;
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (defined.contains(ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getWeakestOpponent());
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -11,8 +13,8 @@ import forge.game.spellability.SpellAbility;
|
||||
public class AddPhaseAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return false;
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.player.Player;
|
||||
@@ -38,7 +40,7 @@ import java.util.List;
|
||||
public class AddTurnAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
|
||||
|
||||
@@ -47,41 +49,41 @@ public class AddTurnAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(ai) && (mandatory || !ai.getGame().getReplacementHandler().wouldExtraTurnBeSkipped(ai))) {
|
||||
sa.getTargets().add(ai);
|
||||
} else if (mandatory) {
|
||||
for (final Player ally : ai.getAllies()) {
|
||||
for (final Player ally : ai.getAllies()) {
|
||||
if (sa.canTarget(ally)) {
|
||||
sa.getTargets().add(ally);
|
||||
break;
|
||||
sa.getTargets().add(ally);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
final List<Player> tgtPlayers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
for (final Player p : tgtPlayers) {
|
||||
if (p.isOpponentOf(ai) && !mandatory) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
// TODO: improve ai for Sage of Hours
|
||||
return StringUtils.isNumeric(sa.getParam("NumTurns"));
|
||||
// not sure if the AI should be playing with cards that give the
|
||||
// Human more turns.
|
||||
if (!StringUtils.isNumeric(sa.getParam("NumTurns"))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/* (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) {
|
||||
return doTriggerAINoCost(aiPlayer, sa, false);
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardLists;
|
||||
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.zone.ZoneType;
|
||||
|
||||
public class AdvanceCrankAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
int nextSprocket = (ai.getCrankCounter() % 3) + 1;
|
||||
int crankCount = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isContraptionOnSprocket(nextSprocket));
|
||||
//Could evaluate whether we actually want to crank those, but this is probably fine for now.
|
||||
if(crankCount < 2)
|
||||
return false;
|
||||
return super.canPlayAI(ai, sa);
|
||||
if (crankCount < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return super.canPlay(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
|
||||
if(logic.equals("AtOppEOT"))
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
|
||||
return super.checkPhaseRestrictions(ai, sa, ph, logic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -16,7 +18,7 @@ import java.util.Map;
|
||||
public class AlterAttributeAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true"));
|
||||
String[] attributes = sa.getParam("Attributes").split(",");
|
||||
@@ -24,7 +26,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
// TODO add targeting logic
|
||||
// needed for Suspected
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
@@ -36,7 +38,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
|
||||
case "Solved":
|
||||
// there is currently no effect that would un-solve something
|
||||
if (!c.isSolved() && activate) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
break;
|
||||
case "Suspect":
|
||||
@@ -44,21 +46,21 @@ public class AlterAttributeAi extends SpellAbilityAi {
|
||||
// is Suspected good or bad?
|
||||
// currently Suspected is better
|
||||
if (!activate) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
case "Saddle":
|
||||
case "Saddled":
|
||||
// AI should not try to Saddle again?
|
||||
if (c.isSaddled()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
@@ -13,8 +14,8 @@ public class AlwaysPlayAi extends SpellAbilityAi {
|
||||
* @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) {
|
||||
return true;
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,6 +3,8 @@ package forge.ai.ability;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -19,24 +21,28 @@ import java.util.Map;
|
||||
|
||||
public class AmassAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army");
|
||||
Card host = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (!aiArmies.isEmpty()) {
|
||||
return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
|
||||
if (aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1))) {
|
||||
// If AI has an Army that can receive counters, play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// AI has Armies but none can receive counters, so don't play
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
|
||||
}
|
||||
}
|
||||
final String type = sa.getParam("Type");
|
||||
StringBuilder sb = new StringBuilder("b_0_0_");
|
||||
sb.append(sa.getOriginalParam("Type").toLowerCase()).append("_army");
|
||||
final String tokenScript = sb.toString();
|
||||
final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army";
|
||||
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
|
||||
|
||||
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
|
||||
|
||||
if (token == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
token.setController(ai, 0);
|
||||
@@ -63,7 +69,11 @@ public class AmassAi extends SpellAbilityAi {
|
||||
//reset static abilities
|
||||
game.getAction().checkStaticAbilities(false);
|
||||
|
||||
return result;
|
||||
if (result) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,8 +92,12 @@ public class AmassAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || checkApiLogic(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -142,130 +142,129 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = aiPlayer.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
if (!sa.metConditions() && sa.getSubAbility() == null) {
|
||||
return false; // what is this for?
|
||||
}
|
||||
|
||||
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
|
||||
// Should I animate a card before i have to sacrifice something better?
|
||||
if (!isAnimatedThisTurn(aiPlayer, source)) {
|
||||
rememberAnimatedThisTurn(aiPlayer, source);
|
||||
return true; // interrupt sacrifice
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
}
|
||||
}
|
||||
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) {
|
||||
return false; // prevent crewing with equal or better creatures
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
}
|
||||
|
||||
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
|
||||
|
||||
sa.setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
}
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
// 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, source)) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && 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)) {
|
||||
return false;
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return false;
|
||||
}
|
||||
// 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 (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||
}
|
||||
return bFlag; // All of the defined stuff is animated, not very useful
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, source);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
AiAbilityDecision decision;
|
||||
if (sa.usesTargeting()) {
|
||||
if(animateTgtAI(sa))
|
||||
return true;
|
||||
else if (!mandatory)
|
||||
return false;
|
||||
else {
|
||||
decision = animateTgtAI(sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
} else if (!mandatory) {
|
||||
return decision;
|
||||
} else {
|
||||
// fallback if animate is mandatory
|
||||
sa.resetTargets();
|
||||
List<Card> list = CardUtil.getValidCardsToTarget(sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return decision;
|
||||
}
|
||||
Card toAnimate = ComputerUtilCard.getWorstAI(list);
|
||||
rememberAnimatedThisTurn(aiPlayer, toAnimate);
|
||||
sa.getTargets().add(toAnimate);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -273,9 +272,14 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
|
||||
}
|
||||
|
||||
private boolean animateTgtAI(final SpellAbility sa) {
|
||||
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
|
||||
if (sa.getMaxTargets() == 0) {
|
||||
// this happens if an optional cost is skipped, e.g. Brave the Wilds
|
||||
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
|
||||
}
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final boolean alwaysActivatePWAbility = sa.isPwAbility()
|
||||
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
|
||||
@@ -287,15 +291,13 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
// list is empty, no possible targets
|
||||
if (list.isEmpty() && !alwaysActivatePWAbility) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// something is used for animate into creature
|
||||
@@ -362,7 +364,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
|
||||
// data is empty, no good targets
|
||||
if (data.isEmpty() && !alwaysActivatePWAbility) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// get the best creature to be animated
|
||||
@@ -385,17 +387,18 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
holdAnimatedTillMain2(ai, worst);
|
||||
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) {
|
||||
releaseHeldTillMain2(ai, worst);
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
}
|
||||
rememberAnimatedThisTurn(ai, worst);
|
||||
sa.getTargets().add(worst);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (logic.equals("SetPT")) {
|
||||
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
|
||||
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
|
||||
// 2. Determine the best target in a smarter way?
|
||||
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
|
||||
Card buffed = becomeAnimated(worst, sa);
|
||||
|
||||
@@ -403,7 +406,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
&& (buffed.getNetPower() - worst.getNetPower() >= 3 || !ComputerUtilCard.doesCreatureAttackAI(ai, worst))) {
|
||||
sa.getTargets().add(worst);
|
||||
rememberAnimatedThisTurn(ai, worst);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +418,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
|
||||
boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
|
||||
if (isValuableAttacker || isValuableBlocker)
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,25 +428,23 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
if(worst != null) {
|
||||
sa.getTargets().add(worst);
|
||||
rememberAnimatedThisTurn(ai, worst);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts") && !list.isEmpty()) {
|
||||
//No logic, but we do have preferences. Pick the best among those?
|
||||
Card best = ComputerUtilCard.getBestAI(list);
|
||||
if(best != null) {
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return true;
|
||||
}
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
// two are the only things
|
||||
// that animate a target. Those can just use AI:RemoveDeck:All until
|
||||
// this can do a reasonably good job of picking a good target
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
public static Card becomeAnimated(final Card card, final SpellAbility sa) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -9,24 +11,30 @@ import forge.game.spellability.SpellAbility;
|
||||
public class AnimateAllAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) {
|
||||
// TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate
|
||||
for (Card c : aiPlayer.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "Always".equals(logic);
|
||||
} // end animateAllCanPlayAI()
|
||||
if ("Always".equals(logic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
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;
|
||||
@@ -16,30 +16,32 @@ import java.util.List;
|
||||
|
||||
public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
//Pulls double duty as the OpenAttraction API. Same logic; usually good to do as long as we have the appropriate cards.
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
CardCollectionView deck = getDeck(ai, sa);
|
||||
|
||||
if(deck.isEmpty())
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
if(!super.canPlayAI(ai, sa))
|
||||
return false;
|
||||
AiAbilityDecision superDecision = super.canPlay(ai, sa);
|
||||
if (!superDecision.willingToPlay())
|
||||
return superDecision;
|
||||
|
||||
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
xPay = Math.max(xPay, deck.size());
|
||||
if (xPay == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) {
|
||||
return getGoodReassembleTarget(ai, sa) != null;
|
||||
if (getGoodReassembleTarget(ai, sa) == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private static CardCollectionView getDeck(Player ai, SpellAbility sa) {
|
||||
@@ -48,11 +50,11 @@ public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
if (xPay == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
@@ -62,7 +64,7 @@ public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
if(target != null)
|
||||
sa.getTargets().add(target);
|
||||
else
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
@@ -84,26 +86,16 @@ public class AssembleContraptionAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
|
||||
if(logic.equals("AtOppEOT"))
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if(getDeck(aiPlayer, sa).isEmpty())
|
||||
return false;
|
||||
|
||||
return super.chkAIDrawback(sa, aiPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if(getDeck(aiPlayer, sa).isEmpty())
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
return super.chkDrawback(sa, aiPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -10,11 +12,11 @@ import java.util.Map;
|
||||
|
||||
public class AssignGroupAi extends SpellAbilityAi {
|
||||
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
|
||||
// otherwise the AI considers the card playable.
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
|
||||
|
||||
@@ -45,24 +45,14 @@ public class AttachAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
|
||||
// and gaining card advantage
|
||||
if (source.hasKeyword("MayFlashSac") && !ai.canCastSorcery()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
|
||||
}
|
||||
|
||||
if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
|
||||
@@ -70,20 +60,16 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
// TODO: Add some extra checks for where the AI may want to cast a replacement aura
|
||||
// on another creature and keep it when the original enchanted creature is useless
|
||||
return false;
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
|
||||
}
|
||||
|
||||
// Attach spells always have a target
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (!attachPreference(sa, tgt, false)) {
|
||||
return false;
|
||||
AiAbilityDecision attachDecision = attachPreference(sa, tgt, false);
|
||||
if (!attachDecision.willingToPlay()) {
|
||||
return attachDecision;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +80,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai)))
|
||||
&& source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
@@ -102,7 +88,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
if (xPay == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
|
||||
sa.setXManaCostPaid(xPay);
|
||||
@@ -112,10 +98,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
|
||||
effectExile.setActivatingPlayer(ai);
|
||||
final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile);
|
||||
return !targets.isEmpty();
|
||||
return !targets.isEmpty() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) {
|
||||
@@ -955,9 +941,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
* @return true, if successful
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
final Card card = sa.getHostCard();
|
||||
// Check if there are any valid targets
|
||||
List<GameObject> targets = new ArrayList<>();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
@@ -969,23 +954,44 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
|
||||
Card newTarget = (Card) targets.get(0);
|
||||
//don't equip human creatures
|
||||
if (newTarget.getController().isOpponentOf(ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
//don't equip a worse creature
|
||||
if (card.isEquipping()) {
|
||||
Card oldTarget = card.getEquipping();
|
||||
if (ComputerUtilCard.evaluateCreature(oldTarget) > ComputerUtilCard.evaluateCreature(newTarget)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean stacking = !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
|
||||
if (!stacking) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
// don't equip creatures that don't gain anything
|
||||
return !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return true;
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player ai) {
|
||||
if (sa.isTrigger() && sa.usesTargeting()) {
|
||||
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
|
||||
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(tgt);
|
||||
}
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
|
||||
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
|
||||
// Living Weapon or similar
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private static boolean isAuraSpell(final SpellAbility sa) {
|
||||
@@ -1005,13 +1011,13 @@ public class AttachAi extends SpellAbilityAi {
|
||||
* the mandatory
|
||||
* @return true, if successful
|
||||
*/
|
||||
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
|
||||
private static AiAbilityDecision attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
|
||||
GameObject o;
|
||||
boolean spellCanTargetPlayer = false;
|
||||
if (isAuraSpell(sa)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (!source.hasKeyword(Keyword.ENCHANT)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
|
||||
String ko = ki.getOriginal();
|
||||
@@ -1036,11 +1042,11 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (o == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
sa.getTargets().add(o);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1692,25 +1698,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return chosen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player ai) {
|
||||
// TODO for targeting optional Halvar trigger, needs to be coordinated with PumpAi to make it playable
|
||||
if (sa.isTrigger() && sa.usesTargeting()) {
|
||||
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
|
||||
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(tgt);
|
||||
}
|
||||
return sa.isTargetNumberValid();
|
||||
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
|
||||
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
|
||||
// Living Weapon or similar
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -11,7 +13,7 @@ import forge.util.MyRandom;
|
||||
|
||||
public class BalanceAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
int diff = 0;
|
||||
Player opp = aiPlayer.getWeakestOpponent();
|
||||
@@ -37,7 +39,7 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
|
||||
if (diff < 0) {
|
||||
// Don't sacrifice permanents even if opponent has a ton of cards in hand
|
||||
return false;
|
||||
return new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final CardCollectionView humHand = opp.getCardsIn(ZoneType.Hand);
|
||||
@@ -45,6 +47,7 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
diff += 0.5 * (humHand.size() - compHand.size());
|
||||
|
||||
// Larger differential == more chance to actually cast this spell
|
||||
return diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
|
||||
boolean willPlay = diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
|
||||
return new AiAbilityDecision(willPlay ? 100 : 0, willPlay ? forge.ai.AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -16,55 +17,51 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
public class BecomesBlockedAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Game game = aiPlayer.getGame();
|
||||
|
||||
if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
|| !game.getPhaseHandler().getPlayerTurn().isOpponentOf(aiPlayer)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
|
||||
sa.resetTargets();
|
||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
return false;
|
||||
}
|
||||
if (choice == null) { // can't find anything left
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// TODO - implement AI
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
boolean chance;
|
||||
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// TODO - implement AI
|
||||
chance = false;
|
||||
|
||||
return chance;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -10,14 +12,13 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BidLifeAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -26,31 +27,31 @@ public class BidLifeAi extends SpellAbilityAi {
|
||||
if (tgt.canTgtCreature()) {
|
||||
List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
Card c = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else if (tgt.getZone().contains(ZoneType.Stack)) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
final SpellAbility topSA = game.getStack().peekAbility();
|
||||
if (!topSA.isCounterableBy(sa) || aiPlayer.equals(topSA.getActivatingPlayer())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (sa.canTargetSpellAbility(topSA)) {
|
||||
sa.getTargets().add(topSA);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -46,9 +48,9 @@ public final class BondAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
} // end bondCanPlayAI()
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
@@ -56,7 +58,7 @@ public final class BondAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
return true;
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialAiLogic;
|
||||
import forge.ai.SpecialCardAi;
|
||||
@@ -21,16 +23,18 @@ public class BranchAi extends SpellAbilityAi {
|
||||
* @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) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("GrislySigil".equals(aiLogic)) {
|
||||
return SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
|
||||
boolean result = SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
|
||||
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
} else if ("BranchCounter".equals(aiLogic)) {
|
||||
return SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); // Bring the Ending, Anticognition (hacky implementation)
|
||||
boolean result = SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa);
|
||||
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
} else if ("TgtAttacker".equals(aiLogic)) {
|
||||
final Combat combat = aiPlayer.getGame().getCombat();
|
||||
if (combat == null || combat.getAttackingPlayer() != aiPlayer) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final CardCollection attackers = combat.getAttackers();
|
||||
@@ -45,16 +49,20 @@ public class BranchAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackers));
|
||||
}
|
||||
|
||||
return sa.isTargetNumberValid();
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// TODO: expand for other cases where the AI is needed to make a decision on a branch
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (decision.willingToPlay() || mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -10,15 +12,15 @@ public class CannotPlayAi extends SpellAbilityAi {
|
||||
* @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) {
|
||||
return false;
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.player.Player;
|
||||
@@ -15,34 +17,36 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
|
||||
* @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) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
// TODO: Extend this if possible for cards that have this as an activated ability
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (logic.equals("WeakestOppExceptCtrl")) {
|
||||
PlayerCollection targetableOpps = aiPlayer.getOpponents();
|
||||
targetableOpps.remove(sa.getHostCard().getController());
|
||||
if (targetableOpps.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -63,4 +67,3 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
|
||||
return (T)weakestTargetableOpp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
@@ -21,7 +18,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Game game = sa.getHostCard().getGame();
|
||||
final SpellAbility topSa = game.getStack().isEmpty() ? null
|
||||
: ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
|
||||
@@ -32,47 +29,50 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
|
||||
// The AI can't otherwise play this ability, but should at least not
|
||||
// miss mandatory activations (e.g. triggers).
|
||||
return sa.isMandatory();
|
||||
if (sa.isMandatory()) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private boolean doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
|
||||
private AiAbilityDecision doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
|
||||
// For cards like Spellskite that retarget spells to itself
|
||||
if (topSa == null) {
|
||||
// nothing on stack, so nothing to target
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
final TargetChoices topTargets = topSa.getTargets();
|
||||
final Card topHost = topSa.getHostCard();
|
||||
|
||||
if (sa.getTargets().size() != 0 && sa.isTrigger()) {
|
||||
if (!sa.getTargets().isEmpty() && sa.isTrigger()) {
|
||||
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
|
||||
// if this does not target at all or already targets host, no need to redirect it again
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
for (Card tgt : topTargets.getTargetCards()) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) {
|
||||
// We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites),
|
||||
// no need to retarget again to another one
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
|
||||
// make sure not to redirect our own abilities
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (!topSa.canTarget(sa.getHostCard())) {
|
||||
// don't try targeting it if we can't legally target the host card with it in the first place
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (!sa.canTarget(topSa)) {
|
||||
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
|
||||
@@ -85,22 +85,22 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
|
||||
&& topTargets.contains(aiPlayer)) {
|
||||
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
Card firstCard = topTargets.getFirstTargetedCard();
|
||||
// if we're not the target don't intervene unless we can steal a buff
|
||||
if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Player firstPlayer = topTargets.getFirstTargetedPlayer();
|
||||
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(topSa);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,30 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
private static CardCollection multipleCardsToChoose = new CardCollection();
|
||||
|
||||
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
||||
if (sa.isHidden()) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)
|
||||
&& !"Battlefield".equals(sa.getParam("Destination")) && !source.isLand()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostDiscard) {
|
||||
CostDiscard cd = (CostDiscard) part;
|
||||
// this is mainly for typecycling
|
||||
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isCraft()) {
|
||||
CardCollection payingCards = new CardCollection();
|
||||
int needed = 0;
|
||||
@@ -77,11 +101,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.getHostCard().removeSVar("AIPreferenceOverride");
|
||||
}
|
||||
|
||||
if (aiLogic.equals("BeforeCombat")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
||||
return false;
|
||||
}
|
||||
} else if (aiLogic.equals("SurpriseBlock")) {
|
||||
if (aiLogic.equals("SurpriseBlock")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
@@ -129,14 +149,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
// Checks for "return true" unlike checkAiLogic()
|
||||
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
multipleCardsToChoose.clear();
|
||||
String aiLogic = sa.getParam("AILogic");
|
||||
if (aiLogic != null) {
|
||||
if (aiLogic.equals("Always")) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||
return doSacAndUpgradeLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||
@@ -156,10 +174,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (aiLogic.equals("MazesEnd")) {
|
||||
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Pongify")) {
|
||||
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
|
||||
if (sa.isTargetNumberValid()) {
|
||||
// Pre-targeted in checkAiLogic
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (aiLogic.equals("ReturnCastable")) {
|
||||
return !sa.getHostCard().getExiledCards().isEmpty()
|
||||
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false);
|
||||
if (!sa.getHostCard().getExiledCards().isEmpty()
|
||||
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
if (sa.isHidden()) {
|
||||
@@ -178,7 +204,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (sa.isHidden()) {
|
||||
return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
|
||||
}
|
||||
@@ -197,7 +223,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) {
|
||||
@@ -206,10 +232,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("Always".equals(aiLogic)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if ("IfNotBuffed".equals(aiLogic)) {
|
||||
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
|
||||
return true; // debuffed by opponent's auras to the level that it becomes useless
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
int delta = 0;
|
||||
for (Card enc : sa.getHostCard().getEnchantedBy()) {
|
||||
@@ -219,9 +245,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
delta++;
|
||||
}
|
||||
}
|
||||
return delta <= 0;
|
||||
if (delta <= 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("SaviorOfOllenbock".equals(aiLogic)) {
|
||||
return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa);
|
||||
if (SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isHidden()) {
|
||||
@@ -250,10 +284,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
@@ -262,80 +294,34 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
try {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// This happens when Origin is something like
|
||||
// "Graveyard,Library" (Doomsday)
|
||||
return false;
|
||||
}
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
}
|
||||
final String destination = sa.getParam("Destination");
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
|
||||
&& !(destination.equals("Battlefield") && !source.isLand())) {
|
||||
return false;
|
||||
if (sa.isNinjutsu()) {
|
||||
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
|
||||
}
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
if (ai.getGame().getCombat() == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
for (final CostPart part : abCost.getCostParts()) {
|
||||
if (part instanceof CostDiscard) {
|
||||
CostDiscard cd = (CostDiscard) part;
|
||||
// this is mainly for typecycling
|
||||
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
|
||||
boolean lowerCMC = false;
|
||||
for (Card attacker : attackers) {
|
||||
if (attacker.getCMC() < source.getCMC()) {
|
||||
lowerCMC = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isNinjutsu()) {
|
||||
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ai.getGame().getCombat() == null) {
|
||||
return false;
|
||||
}
|
||||
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
|
||||
boolean lowerCMC = false;
|
||||
for (Card attacker : attackers) {
|
||||
if (attacker.getCMC() < source.getCMC()) {
|
||||
lowerCMC = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lowerCMC) {
|
||||
return false;
|
||||
}
|
||||
if (!lowerCMC) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// don't play if the conditions aren't met, unless it would trigger a beneficial sub-condition
|
||||
if (!activateForCost && !sa.metConditions()) {
|
||||
final AbilitySub abSub = sa.getSubAbility();
|
||||
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
|
||||
if (!abSub.metConditions()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Iterable<Player> pDefined = Lists.newArrayList(source.getController());
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null && tgt.canTgtPlayer()) {
|
||||
@@ -347,7 +333,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
pDefined = sa.getTargets().getTargetPlayers();
|
||||
} else {
|
||||
@@ -391,12 +377,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!activateForCost && list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if ("Atarka's Command".equals(sourceName)
|
||||
&& (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) {
|
||||
// be strict on playing lands off charms
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
String num = sa.getParamOrDefault("ChangeNum", "1");
|
||||
@@ -404,55 +390,60 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (sa.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
if (xPay == 0) return false;
|
||||
if (xPay == 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
xPay = Math.min(xPay, list.size());
|
||||
sa.setXManaCostPaid(xPay);
|
||||
} else {
|
||||
// Figure out the X amount, bail if it's zero (nothing will change zone).
|
||||
int xValue = AbilityUtils.calculateAmount(source, "X", sa);
|
||||
if (xValue == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sourceName.equals("Temur Sabertooth")) {
|
||||
// activated bounce + pump
|
||||
if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) ||
|
||||
ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility())) {
|
||||
boolean pumpDecision = ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible"));
|
||||
AiAbilityDecision saveDecision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility());
|
||||
if (pumpDecision || saveDecision.willingToPlay()) {
|
||||
for (Card c : list) {
|
||||
if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
return canBouncePermanent(ai, sa, list) != null;
|
||||
if (canBouncePermanent(ai, sa, list) != null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// don't use fetching to top of library/graveyard before main2
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||
&& !sa.hasParam("ActivationPhases")) {
|
||||
if (!destination.equals("Battlefield") && !destination.equals("Hand")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
// Only tutor something in main1 if hand is almost empty
|
||||
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")
|
||||
&& !aiLogic.equals("AnyMainPhase")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,7 +455,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
private static AiAbilityDecision hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -476,11 +467,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (!isCurse && sa.canTarget(aiPlayer)) {
|
||||
sa.getTargets().add(aiPlayer);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -494,23 +485,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a boolean.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
private static AiAbilityDecision hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (sa.getParam("AILogic").equals("Never")) {
|
||||
/*
|
||||
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
|
||||
* Ideally it should look for a good bounce target like "Pacifism"-victims
|
||||
* but there is no simple way to check that. It is preferable for the AI
|
||||
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
|
||||
* (bounce useful permanent).
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<ZoneType> origin = new ArrayList<>();
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
@@ -545,15 +523,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
pDefined = sa.getTargets().getTargetPlayers();
|
||||
|
||||
if (Iterables.isEmpty(pDefined)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
}
|
||||
@@ -567,10 +545,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// *********** Utility functions for Hidden ********************
|
||||
@@ -673,7 +651,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
private static AiAbilityDecision knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Retrieve either this card, or target Cards in Graveyard
|
||||
|
||||
final List<ZoneType> origin = Lists.newArrayList();
|
||||
@@ -685,20 +663,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (!isPreferredTarget(ai, sa, false, false)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
// non-targeted retrieval
|
||||
final List<Card> retrieval = sa.knownDetermineDefined(sa.getParam("Defined"));
|
||||
|
||||
if (retrieval == null || retrieval.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// return this card from graveyard: cards like Hammer of Bogardan
|
||||
@@ -709,7 +683,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// (dying or losing control of)
|
||||
if (origin.contains(ZoneType.Battlefield)) {
|
||||
if (ai.getGame().getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final AbilitySub abSub = sa.getSubAbility();
|
||||
@@ -722,7 +696,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (!(destination.equals(ZoneType.Exile)
|
||||
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
|
||||
&& !destination.equals(ZoneType.Hand)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa);
|
||||
@@ -734,13 +708,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (!contains) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (destination == ZoneType.Battlefield) {
|
||||
if (ComputerUtil.isETBprevented(retrieval.get(0))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// predict whether something may put a ETBing creature below zero toughness
|
||||
@@ -750,7 +724,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
final Card copy = CardCopyService.getLKICopy(c);
|
||||
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
|
||||
if (copy.getNetToughness() <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -764,13 +738,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (nothingWillReturn) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -784,10 +757,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
|
||||
if (aiLogic.equals("SurvivalOfTheFittest")) {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
|
||||
return true;
|
||||
} else if (aiLogic.equals("BeforeCombat")) {
|
||||
return !ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN);
|
||||
}
|
||||
|
||||
if (sa.isHidden()) {
|
||||
@@ -843,16 +818,26 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
private static AiAbilityDecision knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
if ("MimicVat".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
|
||||
if (SpecialCardAi.MimicVat.considerExile(aiPlayer, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return isPreferredTarget(aiPlayer, sa, false, true);
|
||||
if (!isPreferredTarget(aiPlayer, sa, false, true)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
// if we are here, we have a target
|
||||
// so we can play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -904,9 +889,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
|
||||
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
|
||||
}
|
||||
|
||||
if (source.isInZone(ZoneType.Hand)) {
|
||||
list = CardLists.filter(list, CardPredicates.nameNotEquals(source.getName())); // Don't get the same card back.
|
||||
@@ -915,8 +897,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1259,53 +1239,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// if max CMC exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetCMC")) {
|
||||
if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if max power exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetPower")) {
|
||||
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// honor the Same Creature Type restriction
|
||||
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
|
||||
Card firstTarget = sa.getTargetCard();
|
||||
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
|
||||
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
|
||||
// vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving.
|
||||
if (sa.getTargetRestrictions().isSingleZone()) {
|
||||
Card firstTgt = sa.getTargetCard();
|
||||
CardCollection toRemove = new CardCollection();
|
||||
if (firstTgt != null) {
|
||||
for (Card t : sa.getTargets().getTargetCards()) {
|
||||
if (!t.getController().equals(firstTgt.getController())) {
|
||||
toRemove.add(t);
|
||||
}
|
||||
}
|
||||
sa.getTargets().removeAll(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1493,7 +1432,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (choice == null) { // can't find anything left
|
||||
if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
if (sa.getTargets().isEmpty() || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
}
|
||||
@@ -1521,13 +1460,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* a boolean.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
private static AiAbilityDecision knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if ("DeathgorgeScavenger".equals(logic)) {
|
||||
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
|
||||
if (SpecialCardAi.DeathgorgeScavenger.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("ExtraplanarLens".equals(logic)) {
|
||||
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
|
||||
if (SpecialCardAi.ExtraplanarLens.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("ExileCombatThreat".equals(logic)) {
|
||||
return doExileCombatThreatLogic(ai, sa);
|
||||
}
|
||||
@@ -1539,14 +1486,27 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (!list.isEmpty()) {
|
||||
final Card attachedTo = list.get(0);
|
||||
// This code is for the Dragon auras
|
||||
return !attachedTo.getController().isOpponentOf(ai);
|
||||
if (!attachedTo.getController().isOpponentOf(ai)) {
|
||||
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the AI is the controller of the attachedTo card, then it is a valid target.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPreferredTarget(ai, sa, mandatory, true)) {
|
||||
// do nothing
|
||||
} else return isUnpreferredTarget(ai, sa, mandatory);
|
||||
} else {
|
||||
if (isUnpreferredTarget(ai, sa, mandatory)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, Player player, final Player decider) {
|
||||
@@ -1580,7 +1540,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (logic.startsWith("ExilePreference")) {
|
||||
return doExilePreferenceLogic(decider, sa, fetchList);
|
||||
} else if (logic.equals("BounceOwnTrigger")) {
|
||||
return doBounceOwnTriggerLogic(decider, fetchList);
|
||||
return doBounceOwnTriggerLogic(decider, sa, fetchList);
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -1777,7 +1737,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return super.chooseSingleAttackableEntity(ai, sa, options, params);
|
||||
}
|
||||
|
||||
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
|
||||
private AiAbilityDecision doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
|
||||
|
||||
@@ -1796,14 +1756,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestRet);
|
||||
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
|
||||
private AiAbilityDecision doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
String logic = sa.getParam("AILogic");
|
||||
@@ -1811,7 +1771,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (!ph.is(PhaseType.MAIN2)) {
|
||||
// Should be given a chance to cast other spells as well as to use a previously upgraded creature
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
|
||||
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
|
||||
@@ -1850,15 +1810,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (!listGoal.isEmpty()) {
|
||||
// make sure we're upgrading sacCMC->goalCMC
|
||||
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
// no candidates to upgrade
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>)sa.getReplacingObject(AbilityKey.OriginalParams);
|
||||
SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause);
|
||||
@@ -1867,13 +1826,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (Objects.equals(ZoneType.Hand, destination)) {
|
||||
// If the commander is being moved to your hand, don't replace since its easier to cast it again
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
|
||||
if (sa.getHostCard().getName().contains("Squee, the Immortal") &&
|
||||
(destination == ZoneType.Graveyard || destination == ZoneType.Exile)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
|
||||
@@ -1882,28 +1841,38 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin"))
|
||||
&& "Battlefield".equals(causeSub.getParam("Destination"))) {
|
||||
// A blink effect implemented using ChangeZone API
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else // This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to
|
||||
// return the commander to the Command zone.
|
||||
if (subApi == ApiType.DelayedTrigger) {
|
||||
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
|
||||
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
|
||||
// A blink effect implemented using a delayed trigger
|
||||
return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"));
|
||||
if (!"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|
||||
|| !causeSa.getActivatingPlayer().equals(aiPlayer)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|
||||
|| !causeSa.getActivatingPlayer().equals(aiPlayer);
|
||||
}
|
||||
|
||||
// Normally we want the commander back in Command zone to recast him later
|
||||
return true;
|
||||
// Normally we want the commander back in Command zone to recast it later
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
|
||||
public static AiAbilityDecision doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
|
||||
final Combat combat = aiPlayer.getGame().getCombat();
|
||||
|
||||
if (combat == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
Card choice = null;
|
||||
@@ -1938,9 +1907,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (choice != null) {
|
||||
sa.getTargets().add(choice);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) {
|
||||
@@ -2135,16 +2104,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
||||
}
|
||||
|
||||
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
|
||||
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) {
|
||||
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
|
||||
// TODO check for threatened cards
|
||||
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
|
||||
if (!prefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(prefChoices);
|
||||
} else if (!unprefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getWorstAI(unprefChoices);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
|
||||
// some extra benefit like First Responder
|
||||
return ComputerUtilCard.getWorstAI(unprefChoices);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -19,7 +21,7 @@ import java.util.Map;
|
||||
|
||||
public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
// Change Zone All, can be any type moving from one zone to another
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -32,14 +34,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll");
|
||||
|
||||
if (!aiLogicAllowsDiscard) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,31 +61,29 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
|
||||
// Ugin AI: always try to sweep before considering +1
|
||||
if (sourceName.equals("Ugin, the Spirit Dragon")) {
|
||||
return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
|
||||
boolean result = SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
|
||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
if ("LivingDeath".equals(aiLogic)) {
|
||||
// Living Death AI
|
||||
return SpecialCardAi.LivingDeath.consider(ai, sa);
|
||||
} else if ("Timetwister".equals(aiLogic)) {
|
||||
// Timetwister AI
|
||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
||||
} else if ("RetDiscardedThisTurn".equals(aiLogic)) {
|
||||
// e.g. Shadow of the Grave
|
||||
return ai.getDiscardedThisTurn().size() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
boolean result = !ai.getDiscardedThisTurn().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if ("ExileGraveyards".equals(aiLogic)) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
|
||||
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.CREATURES);
|
||||
|
||||
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) {
|
||||
PlayerCollection players = ai.getOpponents();
|
||||
players.add(ai);
|
||||
@@ -98,68 +98,48 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
bestTgt = player;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestTgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestTgt);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// 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 AI:RemoveDeck:All, e.g. Memory Jar)
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
// get the one with the most handsize
|
||||
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Battlefield)) {
|
||||
// this statement is assuming the AI is trying to use this spell offensively
|
||||
// if the AI is using it defensively, then something else needs to occur
|
||||
// if only creatures are affected evaluate both lists and pass only
|
||||
// if human creatures are more valuable
|
||||
if (sa.usesTargeting()) {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
// get the one with the most in graveyard
|
||||
// zone is visible so evaluate which would be hurt the most
|
||||
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
|
||||
|
||||
// set the target
|
||||
if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
computerType = new CardCollection();
|
||||
}
|
||||
|
||||
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
|
||||
int nonCreatureEvalThreshold = 3; // CMC difference
|
||||
if (ai.getController().isAI()) {
|
||||
@@ -181,103 +161,80 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
&& 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;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluateCreatureList(oppType)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // 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) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluatePermanentList(oppType)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Don't cast during main1?
|
||||
if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Graveyard)) {
|
||||
if (sa.usesTargeting()) {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
// get the one with the most in graveyard
|
||||
// zone is visible so evaluate which would be hurt the most
|
||||
Player oppTarget = Collections.max(oppList, AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
} else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) {
|
||||
return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
|
||||
boolean result = (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
|
||||
&& !ComputerUtil.isPlayingReanimator(ai);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Exile)) {
|
||||
if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
|
||||
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
|
||||
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
|
||||
// minimum card advantage unless the hand will be fully reloaded
|
||||
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
|
||||
boolean noDiscard = aiLogic.contains(".noDiscard");
|
||||
|
||||
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
|
||||
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 new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
|
||||
boolean result = (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
// TODO
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (destination.equals(ZoneType.Battlefield)) {
|
||||
if (sa.hasParam("GainControl")) {
|
||||
// Check if the cards are valuable enough
|
||||
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
|
||||
.evaluateCreatureList(oppType)) < 400) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
.evaluatePermanentList(oppType)) < 6) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// don't activate if human gets more back than AI does
|
||||
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(computerType) <= (ComputerUtilCard
|
||||
.evaluateCreatureList(oppType) + 100)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
|
||||
} else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
|
||||
.evaluatePermanentList(oppType) + 2)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance);
|
||||
boolean result = ((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance;
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,11 +249,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -328,127 +285,90 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, final SpellAbility sa, boolean mandatory) {
|
||||
// Change Zone All, can be any type moving from one zone to another
|
||||
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
|
||||
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
|
||||
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
|
||||
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
|
||||
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
|
||||
return ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
|
||||
boolean result = ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
CardCollectionView humanType = ai.getOpponents().getCardsIn(origin);
|
||||
humanType = AbilityUtils.filterListByType(humanType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
CardCollectionView computerType = ai.getCardsIn(origin);
|
||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
// 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()) {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
// get the one with the most handsize
|
||||
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Battlefield)) {
|
||||
// if mandatory, no need to evaluate
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// this statement is assuming the AI is trying to use this spell offensively
|
||||
// if the AI is using it defensively, then something else needs to occur
|
||||
// if only creatures are affected evaluate both lists and pass only
|
||||
// if human creatures are more valuable
|
||||
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(computerType) >= ComputerUtilCard.evaluateCreatureList(humanType)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are more valuable
|
||||
else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
|
||||
return false;
|
||||
} else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Graveyard)) {
|
||||
if (sa.usesTargeting()) {
|
||||
// search targetable Opponents
|
||||
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return sa.isTargetNumberValid();
|
||||
return sa.isTargetNumberValid() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
// get the one with the most in graveyard
|
||||
// zone is visible so evaluate which would be hurt the most
|
||||
Player oppTarget = oppList.max(
|
||||
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
|
||||
|
||||
// set the target
|
||||
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(oppTarget);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Exile)) {
|
||||
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
// currently only exists indirectly (e.g. Summary Dismissal via PlayAi)
|
||||
}
|
||||
|
||||
if (destination.equals(ZoneType.Battlefield)) {
|
||||
// if mandatory, no need to evaluate
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
if (sa.hasParam("GainControl")) {
|
||||
// Check if the cards are valuable enough
|
||||
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
return (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
return (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
boolean result = (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean result = (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
|
||||
.evaluatePermanentList(humanType)) >= 1;
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// don't activate if human gets more back than AI does
|
||||
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
|
||||
return ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are less valuable
|
||||
return ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
|
||||
boolean result = ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean result = ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -18,7 +17,7 @@ import java.util.Map;
|
||||
|
||||
public class CharmAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
|
||||
|
||||
@@ -70,10 +69,10 @@ public class CharmAi extends SpellAbilityAi {
|
||||
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
|
||||
chosenList = chooseOptionsAi(sa, choices, ai, true, num, min);
|
||||
if (chosenList.isEmpty() && min != 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +80,7 @@ public class CharmAi extends SpellAbilityAi {
|
||||
sa.setChosenList(chosenList);
|
||||
|
||||
if (choiceForOpp) {
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.isSpell()) {
|
||||
@@ -89,8 +88,7 @@ public class CharmAi extends SpellAbilityAi {
|
||||
CharmEffect.chainAbilities(sa, chosenList);
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
|
||||
@@ -276,10 +274,10 @@ public class CharmAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
// choices were already targeted
|
||||
if (ab.getRootAbility().getChosenList() != null) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return super.chkDrawbackWithSubs(aiPlayer, ab);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
@@ -26,19 +25,19 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
* The rest of the logic not covered by the canPlayAI template is defined here
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
// search targetable Opponents
|
||||
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
sa.getTargets().add(Iterables.getFirst(oppList, null));
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,23 +134,14 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if (sa.hasParam("AILogic") && !checkAiLogic(ai, sa, sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.equals("AtOppEOT")) {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||
*/
|
||||
|
||||
@@ -22,16 +22,20 @@ import java.util.Map;
|
||||
public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
if (logic.equals("CursedScroll")) {
|
||||
return SpecialCardAi.CursedScroll.consider(ai, sa);
|
||||
if (SpecialCardAi.CursedScroll.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -43,13 +47,13 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("PithingNeedle".equals(aiLogic)) {
|
||||
// Make sure theres something in play worth Needlings.
|
||||
@@ -57,18 +61,27 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa);
|
||||
if (oppPerms.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms);
|
||||
if (card != null) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// 5 percent chance to cast per opposing card with a non mana ability
|
||||
return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size();
|
||||
if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||
|
||||
@@ -11,40 +11,45 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ChooseColorAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
}
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Nykthos, Shrine to Nyx".equals(sourceName)) {
|
||||
return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa);
|
||||
if (SpecialCardAi.NykthosShrineToNyx.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if ("Oona, Queen of the Fae".equals(sourceName)) {
|
||||
if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if ("Addle".equals(sourceName)) {
|
||||
return !ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
|
||||
// TODO Why is this not in the AI logic?
|
||||
// Why are we specifying the weakest opponent?
|
||||
if (!ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("MostExcessOpponentControls")) {
|
||||
@@ -54,10 +59,10 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
|
||||
int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist);
|
||||
if (excess > 4) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("MostProminentInComputerDeck")) {
|
||||
if ("Astral Cornucopia".equals(sourceName)) {
|
||||
// activate in Main 2 hoping that the extra mana surplus will make a difference
|
||||
@@ -65,22 +70,28 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
|
||||
CardPredicates.NONLAND_PERMANENTS);
|
||||
|
||||
return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
|
||||
if (!permanents.isEmpty() && ph.is(PhaseType.MAIN2, ai)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
}
|
||||
} else if (logic.equals("HighestDevotionToColor")) {
|
||||
// currently only works more or less reliably in Main2 to cast own spells
|
||||
if (!ph.is(PhaseType.MAIN2, ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
}
|
||||
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Direction;
|
||||
import forge.game.Game;
|
||||
@@ -18,11 +20,11 @@ public class ChooseDirectionAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
if (logic == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
} else {
|
||||
if ("Aminatou".equals(logic)) {
|
||||
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
|
||||
@@ -33,19 +35,24 @@ public class ChooseDirectionAi extends SpellAbilityAi {
|
||||
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right));
|
||||
int leftValue = Aggregates.sum(left, Card::getCMC);
|
||||
int rightValue = Aggregates.sum(right, Card::getCMC);
|
||||
return aiValue <= leftValue && aiValue <= rightValue;
|
||||
if (aiValue <= leftValue && aiValue <= rightValue) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ChooseEvenOddAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
}
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -19,16 +20,17 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
@@ -20,7 +19,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class ChooseGenericAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
@@ -29,13 +27,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
return true;
|
||||
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
|
||||
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
|
||||
if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) {
|
||||
if (SpellApiToAi.Converter.get(sb).canPlayWithSubs(ai, sb).willingToPlay()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if ("AtOppEOT".equals(aiLogic)) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
|
||||
} else if ("Always".equals(aiLogic)) {
|
||||
return true;
|
||||
}
|
||||
@@ -43,35 +38,46 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
return sa.hasParam("AILogic");
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
// This is equivilant to what was here before but feels bad
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
AiAbilityDecision decision;
|
||||
if (sa.isTrigger()) {
|
||||
decision = doTriggerNoCost(aiPlayer, sa, sa.isMandatory());
|
||||
} else {
|
||||
decision = checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return decision;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) {
|
||||
for (final Player p : aiPlayer.getOpponents()) {
|
||||
if (p.canBeTargetedBy(sa)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return true; // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
|
||||
}
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -262,7 +268,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
List<SpellAbility> filtered = Lists.newArrayList();
|
||||
// filter first for the spells which can be done
|
||||
for (SpellAbility sp : spells) {
|
||||
if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) {
|
||||
if (SpellApiToAi.Converter.get(sp).canPlayWithSubs(player, sp).willingToPlay()) {
|
||||
filtered.add(sp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ChooseNumberAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
} else if (aiLogic.equals("SweepCreatures")) {
|
||||
int maxChoiceLimit = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Max"), sa);
|
||||
int ownCreatureCount = aiPlayer.getCreaturesInPlay().size();
|
||||
@@ -30,17 +27,24 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (refOpp == null) {
|
||||
return false; // no opponent has any creatures
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
int evalAI = ComputerUtilCard.evaluateCreatureList(aiPlayer.getCreaturesInPlay());
|
||||
int evalOpp = ComputerUtilCard.evaluateCreatureList(refOpp.getCreaturesInPlay());
|
||||
|
||||
if (aiPlayer.getLifeLostLastTurn() + aiPlayer.getLifeLostThisTurn() == 0 && evalAI > evalOpp) {
|
||||
return false; // we're not pressured and our stuff seems better, don't do it yet
|
||||
// we're not pressured and our stuff seems better, don't do it yet
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
return ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit);
|
||||
if (ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit)) {
|
||||
// we have more creatures than the opponent, or we have less than the opponent but more than the max choice limit
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// we have less creatures than the opponent and less than the max choice limit
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -49,16 +53,17 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return chance;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
@@ -15,18 +17,18 @@ import java.util.Map;
|
||||
|
||||
public class ChoosePlayerAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
return true;
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return canPlayAI(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -14,7 +11,6 @@ import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -32,21 +28,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
|
||||
// TODO: AI Support! Currently this is copied from AF ChooseCard.
|
||||
// When implementing AI, I believe AI also needs to be made aware of the damage sources chosen
|
||||
// to be prevented (e.g. so the AI doesn't attack with a creature that will not deal any damage
|
||||
// to the player because a CoP was pre-activated on it - unless, of course, there's another
|
||||
// possible reason to attack with that creature).
|
||||
final Card host = sa.getHostCard();
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (abCost != null) {
|
||||
if (!willPayCosts(ai, sa, abCost, source)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -54,7 +42,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
if (sa.hasParam("AILogic")) {
|
||||
@@ -63,11 +51,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
if (!game.getStack().isEmpty()) {
|
||||
final SpellAbility topStack = game.getStack().peekAbility();
|
||||
if (sa.hasParam("Choices") && !topStack.matchesValid(topStack.getHostCard(), sa.getParam("Choices").split(","))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
final ApiType threatApi = topStack.getApi();
|
||||
if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
final Card threatSource = topStack.getHostCard();
|
||||
@@ -79,13 +67,17 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack);
|
||||
return ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0;
|
||||
if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
|
||||
if (sa.hasParam("Choices")) {
|
||||
@@ -98,11 +90,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
}
|
||||
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
|
||||
});
|
||||
return !choices.isEmpty();
|
||||
if (choices.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,23 +21,37 @@ import java.util.Set;
|
||||
|
||||
public class ChooseTypeAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
|
||||
} else if ("MostProminentComputerControls".equals(aiLogic)) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
|
||||
return doMirrorEntityLogic(aiPlayer, sa);
|
||||
if (doMirrorEntityLogic(aiPlayer, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
|
||||
} else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) {
|
||||
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty();
|
||||
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty()
|
||||
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if ("MostProminentOppControls".equals(aiLogic)) {
|
||||
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
|
||||
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty()
|
||||
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return doTriggerAINoCost(aiPlayer, sa, false);
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
}
|
||||
|
||||
private boolean doMirrorEntityLogic(Player aiPlayer, SpellAbility sa) {
|
||||
@@ -101,7 +115,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
boolean isCurse = sa.isCurse();
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -133,16 +147,16 @@ public class ChooseTypeAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!sa.isTargetNumberValid()) {
|
||||
return false; // nothing to target?
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) {
|
||||
if (p.isOpponentOf(ai) && !mandatory && !isCurse) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private String chooseType(SpellAbility sa, CardCollectionView cards) {
|
||||
|
||||
@@ -2,6 +2,8 @@ package forge.ai.ability;
|
||||
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -22,14 +24,15 @@ public class ClashAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
boolean legalAction = true;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
legalAction = selectTarget(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return legalAction;
|
||||
return legalAction ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -39,14 +42,17 @@ public class ClashAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
boolean legalAction = true;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
legalAction = selectTarget(ai, sa);
|
||||
if (!legalAction) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
return legalAction;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -104,7 +110,6 @@ public class ClashAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return sa.getTargets().size() > 0;
|
||||
return !sa.getTargets().isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.SpellApiToAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -12,7 +14,8 @@ import forge.game.trigger.TriggerType;
|
||||
|
||||
public class ClassLevelUpAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
// TODO does leveling up affect combat? Otherwise wait for Main2
|
||||
Card host = sa.getHostCard();
|
||||
final int level = host.getClassLevel() + 1;
|
||||
for (StaticAbility stAb : host.getStaticAbilities()) {
|
||||
@@ -25,12 +28,12 @@ public class ClassLevelUpAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
SpellAbility effect = t.ensureAbility();
|
||||
if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) {
|
||||
return false;
|
||||
if (!SpellApiToAi.Converter.get(effect).doTrigger(aiPlayer, effect, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -18,7 +20,7 @@ import java.util.Map;
|
||||
public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
|
||||
@@ -36,10 +38,6 @@ public class CloneAi extends SpellAbilityAi {
|
||||
// TODO - add some kind of check for during human turn to answer
|
||||
// "Can I use this to block something?"
|
||||
|
||||
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PhaseHandler phase = game.getPhaseHandler();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -66,18 +64,19 @@ public class CloneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!bFlag) { // All of the defined stuff is cloned, not very useful
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
useAbility &= cloneTgtAI(sa);
|
||||
}
|
||||
|
||||
return useAbility;
|
||||
} // end cloneCanPlayAI()
|
||||
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// AI should only activate this during Human's turn
|
||||
boolean chance = true;
|
||||
|
||||
@@ -85,17 +84,22 @@ public class CloneAi extends SpellAbilityAi {
|
||||
chance = cloneTgtAI(sa);
|
||||
}
|
||||
|
||||
return chance;
|
||||
return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
Card host = sa.getHostCard();
|
||||
boolean chance = true;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
chance = cloneTgtAI(sa);
|
||||
} else {
|
||||
if (sa.isReplacementAbility() && host.isCloned()) {
|
||||
// prevent StackOverflow from infinite loop copying another ETB RE
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
if (sa.hasParam("Choices")) {
|
||||
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getParam("Choices"), host.getController(), host, sa);
|
||||
@@ -111,7 +115,11 @@ public class CloneAi extends SpellAbilityAi {
|
||||
// Eventually, we can call the trigger of ETB abilities with
|
||||
// not mandatory as part of the checks to cast something
|
||||
|
||||
return chance || mandatory;
|
||||
if (mandatory || chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +192,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
|
||||
|
||||
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
||||
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
|
||||
: "Permanent.YouDontCtrl+!named" + name + ",Permanent.nonLegendary+!named" + name;
|
||||
|
||||
// TODO: rewrite this block so that this is done somehow more elegantly
|
||||
if (canCloneLegendary) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
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.CardCollection;
|
||||
@@ -14,16 +11,16 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
public class ConniveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
if (!ai.canDraw()) {
|
||||
return false; // can't draw anything
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card host = sa.getHostCard();
|
||||
|
||||
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
|
||||
if (num == 0) {
|
||||
return false; // Won't do anything
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -41,7 +38,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if ((list.isEmpty() && sa.isTargetNumberValid() && !sa.getTargets().isEmpty())) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
@@ -53,7 +50,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return sa.isTargetNumberValid();
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
@@ -66,13 +63,17 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
return !sa.getTargets().isEmpty() && sa.isTargetNumberValid();
|
||||
if (!sa.getTargets().isEmpty() && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!ai.canDraw() && !mandatory) {
|
||||
return false; // can't draw anything
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
boolean preferred = true;
|
||||
@@ -85,7 +86,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (list.isEmpty() && preferred) {
|
||||
@@ -98,14 +99,13 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
// 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()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return sa.isTargetNumberValid();
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
@@ -118,7 +118,10 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(
|
||||
sa.isTargetNumberValid() ? 100 : 0,
|
||||
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
@@ -13,7 +11,6 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
public class ControlExchangeAi extends SpellAbilityAi {
|
||||
|
||||
@@ -21,7 +18,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
Card object1 = null;
|
||||
Card object2 = null;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -41,35 +38,38 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(object2);
|
||||
}
|
||||
if (object1 == null || object2 == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
if (ComputerUtilCard.evaluateCreature(object1) > ComputerUtilCard.evaluateCreature(object2) + 40) {
|
||||
sa.getTargets().add(object1);
|
||||
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (mandatory) {
|
||||
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return decision;
|
||||
} else {
|
||||
if (mandatory) {
|
||||
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid();
|
||||
} else {
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (!sa.usesTargeting()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -90,7 +90,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty())
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
|
||||
Card best = ComputerUtilCard.getBestAI(list);
|
||||
|
||||
@@ -106,7 +106,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
|
||||
// Defined card is better than this one, try to avoid trade
|
||||
if (!best.equals(realBest)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
return doTrigTwoTargetsLogic(aiPlayer, sa, best);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
|
||||
private AiAbilityDecision doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final int creatureThreshold = 100; // TODO: make this configurable from the AI profile
|
||||
final int nonCreatureThreshold = 2;
|
||||
@@ -130,30 +130,30 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
Card aiWorst = ComputerUtilCard.getWorstAI(list);
|
||||
if (aiWorst == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (aiWorst != bestFirstTgt) {
|
||||
if (bestFirstTgt.isCreature() && aiWorst.isCreature()) {
|
||||
if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) {
|
||||
sa.getTargets().add(aiWorst);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
// TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine
|
||||
if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) {
|
||||
sa.getTargets().add(aiWorst);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.clearTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ import java.util.Map;
|
||||
*/
|
||||
public class ControlGainAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
final List<String> lose = Lists.newArrayList();
|
||||
|
||||
if (sa.hasParam("LoseControl")) {
|
||||
@@ -81,22 +81,30 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("AllValid")) {
|
||||
CardCollectionView tgtCards = opponents.getCardsIn(ZoneType.Battlefield);
|
||||
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
|
||||
return !tgtCards.isEmpty();
|
||||
|
||||
if (tgtCards.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
if (sa.hasParam("TargetingPlayer")) {
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
sa.setTargetingPlayer(targetingPlayer);
|
||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
List<Player> oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa));
|
||||
|
||||
if (oppList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (tgt.isRandomTarget()) {
|
||||
@@ -111,12 +119,12 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
if (lose.contains("EOT")
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& !sa.isTrigger()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.hasParam("Defined")) {
|
||||
// no need to target, we'll pick up the target from Defined
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
CardCollection list = opponents.getCardsIn(ZoneType.Battlefield);
|
||||
@@ -165,7 +173,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
int creatures = 0, artifacts = 0, planeswalkers = 0, lands = 0, enchantments = 0;
|
||||
@@ -194,7 +202,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -257,39 +265,41 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(
|
||||
sa.isTargetNumberValid() ? 100 : 0,
|
||||
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
if (sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) {
|
||||
if (sa.hasParam("TargetingPlayer") || (mandatory && !this.canPlay(ai, sa).willingToPlay())) {
|
||||
if (sa.getTargetRestrictions().canOnlyTgtOpponent()) {
|
||||
List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
if (oppList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
sa.getTargets().add(Aggregates.random(oppList));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, final Player ai) {
|
||||
final Game game = ai.getGame();
|
||||
|
||||
// Special card logic that is processed elsewhere
|
||||
@@ -305,7 +315,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
CardCollectionView tgtCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
|
||||
if (tgtCards.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
}
|
||||
final List<String> lose = Lists.newArrayList();
|
||||
@@ -314,10 +324,14 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
lose.addAll(Lists.newArrayList(sa.getParam("LoseControl").split(",")));
|
||||
}
|
||||
|
||||
return !lose.contains("EOT")
|
||||
|| !game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
|
||||
if (lose.contains("EOT")
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
return this.canPlayAI(ai, sa);
|
||||
return this.canPlay(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -41,24 +43,22 @@ import java.util.Map;
|
||||
*/
|
||||
public class ControlGainVariantAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
|
||||
if ("GainControlOwns".equals(logic)) {
|
||||
List<Card> list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), crd -> crd.isCreature() && !crd.getController().equals(crd.getOwner()));
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
for (final Card c : list) {
|
||||
if (ai.equals(c.getController())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,45 +22,52 @@ import java.util.function.Predicate;
|
||||
|
||||
public class CopyPermanentAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("MomirAvatar".equals(aiLogic)) {
|
||||
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
|
||||
} else 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 (ph.is(PhaseType.END_OF_TURN)) {
|
||||
if (ph.getPlayerTurn() == aiPlayer) {
|
||||
// If it's the AI's turn, it can activate at EOT
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If it's not the AI's turn, it can't activate at EOT
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// Not at EOT phase
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
} else if ("DuplicatePerms".equals(aiLogic)) {
|
||||
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if (valid.size() < 2) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
if (sa.hasParam("Defined")) {
|
||||
// If there needs to be an imprinted card, don't activate the ability if nothing was imprinted yet (e.g. Mimic Vat)
|
||||
if (sa.getParam("Defined").equals("Imprinted.ExiledWithSource") && source.getImprintedCards().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isEmbalm() || sa.isEternalize()) {
|
||||
// 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;
|
||||
AiPlayDecision decision = ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa);
|
||||
|
||||
if (decision != AiPlayDecision.WillPlay) {
|
||||
return new AiAbilityDecision(0, decision);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,37 +82,45 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
sa.setTargetingPlayer(targetingPlayer);
|
||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer()) {
|
||||
if (!sa.isCurse()) {
|
||||
if (sa.canTarget(aiPlayer)) {
|
||||
sa.getTargets().add(aiPlayer);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
for (Player p : aiPlayer.getYourTeam()) {
|
||||
if (sa.canTarget(p)) {
|
||||
sa.getTargets().add(p);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
for (Player p : aiPlayer.getOpponents()) {
|
||||
if (sa.canTarget(p)) {
|
||||
sa.getTargets().add(p);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
return doTriggerAINoCost(aiPlayer, sa, false);
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Player activator = sa.getActivatingPlayer();
|
||||
final Game game = host.getGame();
|
||||
@@ -128,13 +143,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
|
||||
//Nothing to target
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate());
|
||||
if (betterList.isEmpty()) {
|
||||
if (!mandatory) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
list = betterList;
|
||||
@@ -146,7 +161,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
if (felidarGuardian.size() > 0) {
|
||||
// can copy a Felidar Guardian and combo off, so let's do it
|
||||
sa.getTargets().add(felidarGuardian.get(0));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,9 +170,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -177,9 +192,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -194,20 +209,22 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host, sa);
|
||||
Collection<Card> betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory);
|
||||
if (betterChoices.isEmpty()) {
|
||||
return mandatory;
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if no targeting, it should always be ok
|
||||
}
|
||||
|
||||
if ("TriggeredCardController".equals(sa.getParam("Controller"))) {
|
||||
Card trigCard = (Card)sa.getTriggeringObject(AbilityKey.Card);
|
||||
if (!mandatory && trigCard != null && trigCard.getController().isOpponentOf(aiPlayer)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -17,14 +17,15 @@ import java.util.Map;
|
||||
public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
Game game = aiPlayer.getGame();
|
||||
int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
|
||||
int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (game.getStack().isEmpty()) {
|
||||
return sa.isMandatory() || "Always".equals(logic);
|
||||
boolean result = sa.isMandatory() || "Always".equals(logic);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final SpellAbility top = game.getStack().peekAbility();
|
||||
@@ -41,47 +42,40 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!MyRandom.percentTrue(chance)
|
||||
&& !"AlwaysIfViable".equals(logic)
|
||||
&& !"OnceIfViable".equals(logic)
|
||||
&& !"Always".equals(logic)
|
||||
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("OnceIfViable".equals(logic)) {
|
||||
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
// Filter AI-specific targets if provided
|
||||
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
|
||||
if (!top.getActivatingPlayer().equals(aiPlayer)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (top.isWrapper() || top.isActivatedAbility()) {
|
||||
// Shouldn't even try with triggered or wrapped abilities at this time, will crash
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (top.getApi() == ApiType.CopySpellAbility) {
|
||||
// Don't try to copy a copy ability, too complex for the AI to handle
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (top.getApi() == ApiType.Mana) {
|
||||
// would lead to Stack Overflow by trying to play this again
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
|
||||
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
|
||||
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
|
||||
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) {
|
||||
// Mana spent is not copied, so these spells generally do nothing when copied.
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
|
||||
// Don't try to copy anything you can't understand how to handle
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle.
|
||||
@@ -99,32 +93,49 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
sa.getTargets().add(top);
|
||||
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, decision);
|
||||
}
|
||||
}
|
||||
|
||||
// the AI should not miss mandatory activations
|
||||
return sa.isMandatory() || "Always".equals(logic);
|
||||
boolean result = sa.isMandatory() || "Always".equals(logic);
|
||||
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (logic.contains("Always")) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
}
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
if (sa.isMandatory()) {
|
||||
return super.chkDrawback(sa, aiPlayer);
|
||||
}
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,7 +149,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
// 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 SpecialCardAi.ChainOfAcid.consider(player, sa).willingToPlay();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,13 +26,11 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
|
||||
public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
boolean toReturn = true;
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final Game game = ai.getGame();
|
||||
@@ -40,22 +38,12 @@ public class CounterAi extends SpellAbilityAi {
|
||||
SpellAbility tgtSA = null;
|
||||
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if ("Force of Will".equals(sourceName)) {
|
||||
if (!SpecialCardAi.ForceOfWill.consider(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,19 +51,19 @@ public class CounterAi extends SpellAbilityAi {
|
||||
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
|
||||
if ((topSA.isSpell() && !topSA.isCounterableBy(sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) {
|
||||
// might as well check for player's friendliness
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
// 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;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (sa.hasParam("CounterNoManaSpell") && topSA.getTotalManaSpent() > 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) {
|
||||
@@ -84,7 +72,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
CostDiscard discardCost = unlessCost.getCostPartByType(CostDiscard.class);
|
||||
if ("Hand".equals(discardCost.getType())) {
|
||||
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,10 +88,11 @@ public class CounterAi extends SpellAbilityAi {
|
||||
tgtCMC += topSA.getPayCosts().getTotalMana().countX() > 0 ? 3 : 0; // TODO: somehow determine the value of X paid and account for it?
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
// This spell doesn't target. Must be a "Coutner All" or "Counter trigger" type of ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
@@ -122,13 +111,13 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (toPay == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
// If this is a reusable Resource, feel free to play it most of the time
|
||||
if (!playReusable(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,15 +136,15 @@ public class CounterAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("Never".equals(logic)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts
|
||||
int minCMC = Integer.parseInt(logic.substring(7));
|
||||
if (tgtCMC < minCMC) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("NullBrooch".equals(logic)) {
|
||||
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,40 +223,40 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (dontCounter) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return doTriggerAINoCost(aiPlayer, sa, true);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return doTriggerNoCost(aiPlayer, sa, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (mandatory && !sa.canAddMoreTarget()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
|
||||
SpellAbility tgtSA = pair.getLeft();
|
||||
|
||||
if (tgtSA == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
sa.getTargets().add(tgtSA);
|
||||
if (!mandatory && !pair.getRight()) {
|
||||
// If not mandatory and not preferred, bail out after setting target
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
@@ -288,14 +277,13 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
if (!mandatory) {
|
||||
if (toPay == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
// If this is a reusable Resource, feel free to play it most
|
||||
// of the time
|
||||
// If this is a reusable Resource, feel free to play it most of the time
|
||||
if (!playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,7 +300,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
// force the Human into making decisions)
|
||||
|
||||
// But really it should be more picky about how it counters things
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public Pair<SpellAbility, Boolean> chooseTargetSpellAbility(Game game, SpellAbility sa, Player ai, boolean mandatory) {
|
||||
@@ -362,11 +350,11 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
// ward or human misplay
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
|
||||
for (SpellAbility toBeCountered : spells) {
|
||||
// ward or human misplay
|
||||
if (!toBeCountered.isCounterableBy(sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -381,7 +369,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// no reason to pay if we don't plan to confirm
|
||||
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
|
||||
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false).willingToPlay()) {
|
||||
return false;
|
||||
}
|
||||
// TODO check hasFizzled
|
||||
|
||||
@@ -45,13 +45,13 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
*
|
||||
* @param list
|
||||
* a {@link forge.CardList} object.
|
||||
* a {@link CardCollectionView} object.
|
||||
* @param type
|
||||
* a {@link java.lang.String} object.
|
||||
* a {@link String} object.
|
||||
* @param amount
|
||||
* a int.
|
||||
* @param newParam TODO
|
||||
* @return a {@link forge.game.card.Card} object.
|
||||
* @param ai a {@link Player} object.
|
||||
* @return a {@link Card} object.
|
||||
*/
|
||||
public static Card chooseCursedTarget(final CardCollectionView list, final String type, final int amount, final Player ai) {
|
||||
Card choice;
|
||||
@@ -65,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
// try to kill the best killable creature, or reduce the best one
|
||||
// but try not to target a Undying Creature
|
||||
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
|
||||
if (killable.size() > 0) {
|
||||
if (!killable.isEmpty()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(killable);
|
||||
} else {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(list);
|
||||
@@ -83,10 +83,10 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
*
|
||||
* @param list
|
||||
* a {@link forge.CardList} object.
|
||||
* a {@link CardCollectionView} object.
|
||||
* @param type
|
||||
* a {@link java.lang.String} object.
|
||||
* @return a {@link forge.game.card.Card} object.
|
||||
* a {@link String} object.
|
||||
* @return a {@link Card} object.
|
||||
*/
|
||||
public static Card chooseBoonTarget(final CardCollectionView list, final String type) {
|
||||
Card choice = null;
|
||||
@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
} else if (type.equals("DIVINITY")) {
|
||||
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
|
||||
} else if (CounterType.get(type).isKeywordCounter()) {
|
||||
} else if (CounterType.getType(type).isKeywordCounter()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
|
||||
} else {
|
||||
// The AI really should put counters on cards that can use it.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
@@ -21,19 +19,25 @@ import java.util.Map;
|
||||
|
||||
public class CountersMoveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
AiAbilityDecision decision = new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!moveTgtAI(ai, sa)) {
|
||||
return false;
|
||||
decision = moveTgtAI(ai, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
|
||||
if (!playReusable(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
if (MyRandom.getRandom().nextFloat() < .8f) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,12 +113,13 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
|
||||
if (!moveTgtAI(ai, sa) && !mandatory) {
|
||||
return false;
|
||||
AiAbilityDecision decision = moveTgtAI(ai, sa);
|
||||
if (!decision.willingToPlay() && !mandatory) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (!sa.isTargetNumberValid() && mandatory) {
|
||||
@@ -122,18 +127,18 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
if (tgtCards.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
final Card card = ComputerUtilCard.getWorstAI(tgtCards);
|
||||
sa.getTargets().add(card);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// no target Probably something like Graft
|
||||
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
final Card host = sa.getHostCard();
|
||||
@@ -145,7 +150,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
|
||||
|
||||
if (srcCards.isEmpty() || destCards.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final Card src = srcCards.get(0);
|
||||
@@ -153,21 +158,21 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
// for such Trigger, do not move counter to another players creature
|
||||
if (!dest.getController().equals(ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (ComputerUtilCard.isUselessCreature(ai, dest)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (dest.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (cType != null) {
|
||||
if (!dest.canReceiveCounters(cType)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
final int amount = calcAmount(sa, cType);
|
||||
int a = src.getCounters(cType);
|
||||
if (a < amount) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final Card srcCopy = CardCopyService.getLKICopy(src);
|
||||
@@ -181,27 +186,31 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
int newEval = ComputerUtilCard.evaluateCreature(srcCopy) + ComputerUtilCard.evaluateCreature(destCopy);
|
||||
|
||||
if (newEval < oldEval) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
// check for some specific AI preferences
|
||||
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
|
||||
return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0;
|
||||
if (!cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
// no target
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return moveTgtAI(ai, sa);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private static int calcAmount(final SpellAbility sa, final CounterType cType) {
|
||||
@@ -226,7 +235,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
return amount;
|
||||
}
|
||||
|
||||
private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
|
||||
private AiAbilityDecision moveTgtAI(final Player ai, final SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
final String type = sa.getParam("CounterType");
|
||||
@@ -244,7 +253,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (destCards.isEmpty()) {
|
||||
// something went wrong
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final Card dest = destCards.get(0);
|
||||
@@ -253,7 +262,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
tgtCards.remove(dest);
|
||||
|
||||
if (cType != null && !dest.canReceiveCounters(cType)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// prefered logic for this: try to steal counter
|
||||
@@ -285,7 +294,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -329,14 +338,14 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else if (sa.getMaxTargets() == 2) {
|
||||
// TODO
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// SA uses target for Defined
|
||||
// Source => Targeted
|
||||
@@ -344,12 +353,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (srcCards.isEmpty()) {
|
||||
// something went wrong
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final Card src = srcCards.get(0);
|
||||
if (cType != null && src.getCounters(cType) <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card lkiWithCounters = CardCopyService.getLKICopy(src);
|
||||
@@ -402,14 +411,14 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
if (!isMandatoryTrigger) {
|
||||
// no good target
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,10 +448,10 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
@@ -21,42 +19,41 @@ import java.util.Map;
|
||||
public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
final CounterType counterType = getCounterType(sa);
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return setTargets(ai, sa);
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
@@ -85,24 +82,27 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!sa.usesTargeting()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
if (setTargets(ai, sa)) {
|
||||
return true;
|
||||
|
||||
AiAbilityDecision decision = setTargets(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
} else if (mandatory) {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
Card safeMatch = list.stream()
|
||||
.filter(CardPredicates.hasCounters().negate())
|
||||
.findFirst().orElse(null);
|
||||
sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch);
|
||||
return true;
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return mandatory;
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private CounterType getCounterType(SpellAbility sa) {
|
||||
@@ -117,7 +117,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean setTargets(Player ai, SpellAbility sa) {
|
||||
private AiAbilityDecision setTargets(Player ai, SpellAbility sa) {
|
||||
final CounterType counterType = getCounterType(sa);
|
||||
|
||||
final Game game = ai.getGame();
|
||||
@@ -154,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (counterType == null || counterType.is(type)) {
|
||||
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
|
||||
addTargetsByCounterType(ai, sa, aiList, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
if (!oppList.isEmpty()) {
|
||||
// not enough targets
|
||||
if (sa.canAddMoreTarget()) {
|
||||
final CounterType type = CounterType.get(CounterEnumType.M1M1);
|
||||
final CounterType type = CounterEnumType.M1M1;
|
||||
if (counterType == null || counterType == type) {
|
||||
addTargetsByCounterType(ai, sa, oppList, type);
|
||||
}
|
||||
@@ -173,10 +173,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
// targeting does failed
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list,
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Map;
|
||||
public class CountersProliferateAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final List<Card> cperms = Lists.newArrayList();
|
||||
boolean allyExpOrEnergy = false;
|
||||
|
||||
@@ -68,25 +68,34 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
}));
|
||||
}
|
||||
|
||||
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
|
||||
if (!cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy) {
|
||||
// AI will play it if there are any counters to proliferate
|
||||
// or if there are no counters, but AI has experience or energy counters
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
boolean chance = true;
|
||||
|
||||
// TODO Make sure Human has poison counters or there are some counters
|
||||
// we want to proliferate
|
||||
return chance;
|
||||
return new AiAbilityDecision(
|
||||
chance ? 100 : 0,
|
||||
chance ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi
|
||||
);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
@@ -101,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Proliferate is always optional for all, no need to select best
|
||||
|
||||
final CounterType poison = CounterType.get(CounterEnumType.POISON);
|
||||
final CounterType poison = CounterEnumType.POISON;
|
||||
|
||||
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
// because countertype can't be chosen anymore, only look for poison counters
|
||||
|
||||
@@ -53,8 +53,7 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
// disable moving counters (unless a specialized AI logic supports it)
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
||||
if (part instanceof CostRemoveCounter remCounter) {
|
||||
final CounterType counterType = remCounter.counter;
|
||||
if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) {
|
||||
return false;
|
||||
@@ -93,12 +92,11 @@ public class CountersPutAi extends CountersAi {
|
||||
return false;
|
||||
}
|
||||
return chance > MyRandom.getRandom().nextFloat();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("LevelUp")) {
|
||||
if (sa.isKeyword(Keyword.LEVEL_UP)) {
|
||||
// creatures enchanted by curse auras have low priority
|
||||
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
for (Card aura : source.getEnchantedBy()) {
|
||||
@@ -119,13 +117,12 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
CardCollection list;
|
||||
Card choice = null;
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
@@ -160,7 +157,7 @@ public class CountersPutAi extends CountersAi {
|
||||
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9));
|
||||
if (!poisonList.isEmpty()) {
|
||||
sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife()));
|
||||
return true;
|
||||
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,12 +168,12 @@ public class CountersPutAi extends CountersAi {
|
||||
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
|
||||
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
|
||||
|
||||
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
|
||||
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
|
||||
|
||||
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
|
||||
if (best != null) {
|
||||
sa.getTargets().add(best);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
|
||||
@@ -196,7 +193,7 @@ public class CountersPutAi extends CountersAi {
|
||||
best = ComputerUtilCard.getBestAI(aiCreat);
|
||||
if (best != null) {
|
||||
sa.getTargets().add(best);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,28 +202,22 @@ public class CountersPutAi extends CountersAi {
|
||||
if (!ai.getCounters().isEmpty()) {
|
||||
if (!eachExisting || ai.getPoisonCounters() < 5) {
|
||||
sa.getTargets().add(ai);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Never".equals(logic)) {
|
||||
return false;
|
||||
} else if ("AlwaysWithNoTgt".equals(logic)) {
|
||||
return true;
|
||||
if ("AlwaysWithNoTgt".equals(logic)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if ("AristocratCounters".equals(logic)) {
|
||||
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa);
|
||||
} else if ("PayEnergy".equals(logic)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if ("PayEnergyConservatively".equals(logic)) {
|
||||
boolean onlyInCombat = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
|
||||
@@ -235,10 +226,10 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
if (playAggro) {
|
||||
// aggro profiles ignore conservative play for this AI logic
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (ph.inCombat() && source != null) {
|
||||
if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
} else if (ai.getGame().getCombat().isBlocking(source)) {
|
||||
// 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(source);
|
||||
@@ -248,28 +239,27 @@ public class CountersPutAi extends CountersAi {
|
||||
int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
|
||||
if (source.getNetToughness() + numActivations > totBlkPower
|
||||
|| source.getNetPower() + numActivations >= totBlkToughness) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
}
|
||||
} 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)) {
|
||||
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
// 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;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (ai.getCounters(CounterEnumType.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;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
} else if (logic.equals("MarkOppCreature")) {
|
||||
if (!ph.is(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
|
||||
Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type));
|
||||
@@ -281,12 +271,12 @@ public class CountersPutAi extends CountersAi {
|
||||
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreat);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (logic.equals("CheckDFC")) {
|
||||
// for cards like Ludevic's Test Subject
|
||||
if (!source.canTransform(null)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (logic.startsWith("MoveCounter")) {
|
||||
return doMoveCounterLogic(ai, sa, ph);
|
||||
@@ -295,8 +285,13 @@ public class CountersPutAi extends CountersAi {
|
||||
if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
// don't use this for mana until after combat
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
return new AiAbilityDecision(25, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
return willActivate;
|
||||
|
||||
if (willActivate) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("ChargeToBestCMC")) {
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
|
||||
@@ -305,15 +300,11 @@ public class CountersPutAi extends CountersAi {
|
||||
return SpecialCardAi.TheOneRing.consider(ai, sa);
|
||||
}
|
||||
|
||||
if (!sa.metConditions() && sa.getSubAbility() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
|
||||
CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility());
|
||||
if (!prot.isEmpty()) {
|
||||
sa.getTargets().add(prot.get(0));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,13 +312,13 @@ public class CountersPutAi extends CountersAi {
|
||||
CardCollection creatsYouCtrl = ai.getCreaturesInPlay();
|
||||
List<Card> leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness);
|
||||
if (leastToughness.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
// TODO If Creature that would be Bolstered for some reason is useless, also return False
|
||||
}
|
||||
|
||||
if (sa.hasParam("Monstrosity") && source.isMonstrous()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// TODO handle proper calculation of X values based on Cost
|
||||
@@ -341,8 +332,8 @@ public class CountersPutAi extends CountersAi {
|
||||
Game game = ai.getGame();
|
||||
Combat combat = game.getCombat();
|
||||
|
||||
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
|
||||
return false;
|
||||
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return doCombatAdaptLogic(source, amount, combat);
|
||||
}
|
||||
@@ -353,7 +344,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
return FightAi.canFight(ai, sa, nPump, nPump);
|
||||
}
|
||||
|
||||
if (amountStr.equals("X")) {
|
||||
@@ -369,12 +360,12 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
// This will "rewind" clockwork cards when they fall to 50% power or below, consider improving
|
||||
if (curCtrs > Math.ceil(maxCtrs / 2.0)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
amount = Math.min(amount, maxCtrs - curCtrs);
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,14 +377,14 @@ public class CountersPutAi extends CountersAi {
|
||||
.mapToInt(Card::getCMC)
|
||||
.max().orElse(0);
|
||||
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't use it if no counters to add
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if ("Polukranos".equals(logic)) {
|
||||
@@ -420,20 +411,14 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
if (!canSurvive) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("AtOppEOT".equals(logic)) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,24 +429,25 @@ public class CountersPutAi extends CountersAi {
|
||||
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
|
||||
// only evaluates case where all tokens are placed on a single target
|
||||
if (sa.getMinTargets() < 2) {
|
||||
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
|
||||
AiAbilityDecision decision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
Card c = sa.getTargetCard();
|
||||
if (sa.getTargets().size() > 1) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(c);
|
||||
}
|
||||
sa.addDividedAllocation(c, amount);
|
||||
return true;
|
||||
} else {
|
||||
if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return false;
|
||||
}
|
||||
return decision;
|
||||
} else if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
CardCollection list;
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
@@ -498,7 +484,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
|
||||
@@ -507,9 +493,9 @@ public class CountersPutAi extends CountersAi {
|
||||
&& sa.isPwAbility()
|
||||
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
|
||||
&& sa.isTargetNumberValid()
|
||||
&& sa.getTargets().size() == 0
|
||||
&& sa.getTargets().isEmpty()
|
||||
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (sourceName.equals("Abzan Charm")) {
|
||||
@@ -531,11 +517,11 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
if (left == 0) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
sa.resetTargets();
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// target loop
|
||||
@@ -543,7 +529,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -575,10 +561,9 @@ public class CountersPutAi extends CountersAi {
|
||||
// check if other choice will already be played
|
||||
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
|
||||
}
|
||||
if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|
||||
if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|
||||
|| ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) {
|
||||
if (abCost == null || abCost == Cost.Zero
|
||||
|| (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
|
||||
if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
// only use at opponent EOT unless it is free
|
||||
choice = chooseBoonTarget(list, type);
|
||||
}
|
||||
@@ -592,7 +577,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -608,66 +593,79 @@ public class CountersPutAi extends CountersAi {
|
||||
choice = null;
|
||||
}
|
||||
if (sa.getTargets().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
// Don't activate Curse abilities on my cards and non-curse abilities
|
||||
// on my opponents
|
||||
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
|
||||
|
||||
// adding counters would cause counter amount to overflow
|
||||
if (Integer.MAX_VALUE - currCounters <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (type.equals("P1P1")) {
|
||||
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
|
||||
// each non +1/+1 counter on the card is a 10% chance of not
|
||||
// activating this ability.
|
||||
|
||||
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
// Instant +1/+1
|
||||
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
|
||||
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
|
||||
return false; // only if next turn and cost is reusable
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// Useless since the card already has the keyword (or for another reason)
|
||||
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
|
||||
return false;
|
||||
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
boolean immediately = ComputerUtil.playImmediately(ai, sa);
|
||||
|
||||
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
|
||||
return false;
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if (immediately) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) {
|
||||
// Don't use non P1P1/M1M1 counters before main 2 if possible
|
||||
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(final SpellAbility sa, Player ai) {
|
||||
boolean chance = true;
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, Player ai) {
|
||||
final Game game = ai.getGame();
|
||||
Card choice = null;
|
||||
final String type = sa.getParam("CounterType");
|
||||
@@ -681,14 +679,12 @@ public class CountersPutAi extends CountersAi {
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = null;
|
||||
|
||||
CardCollection list;
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty() && isMandatoryTrigger) {
|
||||
@@ -701,12 +697,11 @@ public class CountersPutAi extends CountersAi {
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid()
|
||||
|| sa.getTargets().size() == 0) {
|
||||
|| sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
} else {
|
||||
break;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.isCurse()) {
|
||||
@@ -724,9 +719,9 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if ((!sa.isTargetNumberValid()) || (sa.getTargets().size() == 0)) {
|
||||
if ((!sa.isTargetNumberValid()) || (sa.getTargets().isEmpty())) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -741,17 +736,14 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
return chance;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
// boolean chance = true;
|
||||
boolean preferred = true;
|
||||
CardCollection list;
|
||||
final String aiLogic = sa.getParam("AILogic");
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
@@ -770,9 +762,10 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if ("ChargeToBestCMC".equals(aiLogic)) {
|
||||
return doChargeToCMCLogic(ai, sa) || mandatory;
|
||||
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
|
||||
return doChargeToOppCtrlCMCLogic(ai, sa) || mandatory;
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -796,12 +789,11 @@ public class CountersPutAi extends CountersAi {
|
||||
// things like Powder Keg, which are way too complex for the AI
|
||||
}
|
||||
} else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) {
|
||||
// can only target opponent
|
||||
PlayerCollection playerList = new PlayerCollection(IterableUtil.filter(
|
||||
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
|
||||
|
||||
if (playerList.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// try to choose player with less creatures
|
||||
@@ -811,35 +803,34 @@ public class CountersPutAi extends CountersAi {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
} else {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("Fight".equals(logic) || "PowerDmg".equals(logic)) {
|
||||
if ("Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic)) {
|
||||
int nPump = 0;
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
if (FightAi.canFightAi(ai, sa, nPump, nPump)) {
|
||||
return true;
|
||||
AiAbilityDecision decision = FightAi.canFight(ai, sa, nPump, nPump);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
int totalTargets = list.size();
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
Iterable<Card> filteredField;
|
||||
if (sa.isCurse()) {
|
||||
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
filteredField = ai.getCardsIn(ZoneType.Battlefield);
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
int totalTargets = list.size();
|
||||
boolean preferred = true;
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
// When things are mandatory, gotta handle a little differently
|
||||
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (list.isEmpty() && preferred) {
|
||||
@@ -859,10 +850,10 @@ public class CountersPutAi extends CountersAi {
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return sa.isTargetNumberValid();
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card choice = null;
|
||||
Card choice;
|
||||
|
||||
// Choose targets here:
|
||||
if (sa.isCurse()) {
|
||||
@@ -871,33 +862,27 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else {
|
||||
if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
|
||||
sa.addDividedAllocation(choice, left);
|
||||
} else {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
sa.addDividedAllocation(choice, alloc);
|
||||
left -= alloc;
|
||||
}
|
||||
@@ -912,7 +897,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -967,8 +952,8 @@ public class CountersPutAi extends CountersAi {
|
||||
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Bolster does use this
|
||||
// TODO need more or less logic there?
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType m1m1 = CounterEnumType.M1M1;
|
||||
final CounterType p1p1 = CounterEnumType.P1P1;
|
||||
|
||||
// no logic if there is no options or no to choice
|
||||
if (!isOptional && Iterables.size(options) <= 1) {
|
||||
@@ -987,9 +972,7 @@ public class CountersPutAi extends CountersAi {
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||
|
||||
final boolean isCurse = sa.isCurse();
|
||||
|
||||
if (isCurse) {
|
||||
if (sa.isCurse()) {
|
||||
final CardCollection opponents = CardLists.filterControlledBy(options, ai.getOpponents());
|
||||
|
||||
if (!opponents.isEmpty()) {
|
||||
@@ -1086,11 +1069,10 @@ public class CountersPutAi extends CountersAi {
|
||||
Player ai = sa.getActivatingPlayer();
|
||||
GameEntity e = (GameEntity) params.get("Target");
|
||||
// for Card try to select not useless counter
|
||||
if (e instanceof Card) {
|
||||
Card c = (Card) e;
|
||||
if (e instanceof Card c) {
|
||||
if (c.getController().isOpponentOf(ai)) {
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.M1M1;
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, c)) {
|
||||
@@ -1104,15 +1086,14 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e instanceof Player) {
|
||||
Player p = (Player) e;
|
||||
} else if (e instanceof Player p) {
|
||||
if (p.isOpponentOf(ai)) {
|
||||
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
|
||||
return CounterType.get(CounterEnumType.POISON);
|
||||
if (options.contains(CounterEnumType.POISON)) {
|
||||
return CounterEnumType.POISON;
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
|
||||
return CounterType.get(CounterEnumType.EXPERIENCE);
|
||||
if (options.contains(CounterEnumType.EXPERIENCE)) {
|
||||
return CounterEnumType.EXPERIENCE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,7 +1101,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return Iterables.getFirst(options, null);
|
||||
}
|
||||
|
||||
private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
// Spikes (Tempest)
|
||||
|
||||
// Try not to do it unless at the end of opponent's turn or the creature is threatened
|
||||
@@ -1133,7 +1114,7 @@ public class CountersPutAi extends CountersAi {
|
||||
|| (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
|
||||
|
||||
if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
|
||||
CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
|
||||
@@ -1151,45 +1132,45 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
if (bestTgt != null) {
|
||||
sa.getTargets().add(bestTgt);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) {
|
||||
private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) {
|
||||
if (combat.isAttacking(source)) {
|
||||
if (!combat.isBlocked(source)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
for (Card blockedBy : combat.getBlockers(source)) {
|
||||
if (blockedBy.getNetToughness() > source.getNetPower()
|
||||
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
|
||||
if (source.getNetToughness() <= totBlkPower
|
||||
&& source.getNetToughness() + amount > totBlkPower) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
}
|
||||
} else if (combat.isBlocking(source)) {
|
||||
for (Card blocked : combat.getAttackersBlockedBy(source)) {
|
||||
if (blocked.getNetToughness() > source.getNetPower()
|
||||
&& blocked.getNetToughness() <= source.getNetPower() + amount) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
|
||||
}
|
||||
}
|
||||
|
||||
int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower);
|
||||
if (source.getNetToughness() <= totAtkPower
|
||||
&& source.getNetToughness() + amount > totAtkPower) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1200,7 +1181,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return max;
|
||||
}
|
||||
|
||||
private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) {
|
||||
private AiAbilityDecision doChargeToCMCLogic(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
|
||||
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
|
||||
@@ -1215,10 +1196,13 @@ public class CountersPutAi extends CountersAi {
|
||||
optimalCMC = cmc;
|
||||
}
|
||||
}
|
||||
return numCtrs < optimalCMC;
|
||||
if (numCtrs < optimalCMC) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
|
||||
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
|
||||
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
|
||||
@@ -1232,6 +1216,11 @@ public class CountersPutAi extends CountersAi {
|
||||
optimalCMC = cmc;
|
||||
}
|
||||
}
|
||||
return numCtrs < optimalCMC;
|
||||
if (numCtrs < optimalCMC) {
|
||||
// If the AI has less counters than the optimal CMC, it should play the ability.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -15,17 +16,15 @@ import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountersPutAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
List<Card> hList;
|
||||
List<Card> cList;
|
||||
@@ -44,28 +43,9 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
|
||||
}
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,26 +68,23 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
if (curse) {
|
||||
if (type.equals("M1M1")) {
|
||||
final List<Card> killable = CardLists.filter(hList, c -> c.getNetToughness() <= amount);
|
||||
if (!(killable.size() > 2)) {
|
||||
return false;
|
||||
if (killable.size() <= 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// make sure compy doesn't harm his stuff more than human's
|
||||
// stuff
|
||||
if (cList.size() > hList.size()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// human has more things that will benefit, don't play
|
||||
if (hList.size() >= cList.size()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
//Check for cards that could profit from the ability
|
||||
@@ -125,21 +102,21 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (!combatants) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playReusable(ai, sa)) {
|
||||
return chance;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
|
||||
@@ -150,7 +127,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
List<Player> players = Lists.newArrayList();
|
||||
if (!sa.isCurse()) {
|
||||
@@ -168,11 +145,23 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return preferred || mandatory;
|
||||
if (preferred) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
@@ -52,9 +50,13 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
return doTgt(ai, sa, false);
|
||||
if (doTgt(ai, sa, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
@@ -180,11 +182,27 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
return doTgt(ai, sa, mandatory);
|
||||
if (doTgt(ai, sa, mandatory)) {
|
||||
// if we can target, then we can play it
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
// if we can't target, then we can't play it
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
if (mandatory) {
|
||||
// if mandatory, just play it
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// if not mandatory, check if we can play it
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -200,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
Card tgt = (Card) params.get("Target");
|
||||
|
||||
// planeswalker has high priority for loyalty counters
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
}
|
||||
|
||||
if (tgt.getController().isOpponentOf(ai)) {
|
||||
// creatures with BaseToughness below or equal zero might be
|
||||
// killed if their counters are removed
|
||||
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
|
||||
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
if (options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
// this counters are treat first to be removed
|
||||
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
|
||||
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) {
|
||||
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
|
||||
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
|
||||
|
||||
if (maritEmpty) {
|
||||
return CounterType.get(CounterEnumType.ICE);
|
||||
return CounterEnumType.ICE;
|
||||
}
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
}
|
||||
|
||||
// fallback logic, select positive counter to add more
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
@@ -23,14 +25,6 @@ import java.util.function.Predicate;
|
||||
|
||||
public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
}
|
||||
return super.canPlayWithoutRestrict(ai, sa);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -48,24 +42,6 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see
|
||||
* forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player,
|
||||
* forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler,
|
||||
* java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
|
||||
if ("EndOfOpponentsTurn".equals(logic)) {
|
||||
if (!ph.is(PhaseType.END_OF_TURN) || !ph.getNextTurn().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.checkPhaseRestrictions(ai, sa, ph, logic);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -73,7 +49,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
* forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final String type = sa.getParam("CounterType");
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -83,14 +59,14 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (!type.matches("Any") && !type.matches("All")) {
|
||||
final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type));
|
||||
if (currCounters < 1) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
private AiAbilityDecision doTgt(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
|
||||
@@ -103,7 +79,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
@@ -121,7 +97,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardPredicates.hasCounter(CounterEnumType.ICE, 3));
|
||||
if (!depthsList.isEmpty()) {
|
||||
sa.getTargets().add(depthsList.getFirst());
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +110,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!planeswalkerList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (type.matches("Any")) {
|
||||
// variable amount for Hex Parasite
|
||||
@@ -144,7 +120,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
if (manaLeft == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
amount = manaLeft;
|
||||
xPay = true;
|
||||
@@ -166,7 +142,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (xPay) {
|
||||
sa.setXManaCostPaid(ice);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +161,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (xPay) {
|
||||
sa.setXManaCostPaid(best.getCurrentLoyalty());
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// some rules only for amount = 1
|
||||
@@ -202,7 +178,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!aiM1M1List.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// do as P1P1 part
|
||||
@@ -211,7 +187,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!aiUndyingList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// TODO stun counters with canRemoveCounters check
|
||||
@@ -222,7 +198,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardPredicates.hasCounter(CounterEnumType.P1P1));
|
||||
if (!oppP1P1List.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// fallback to remove any counter from opponent
|
||||
@@ -234,7 +210,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
for (final CounterType aType : best.getCounters().keySet()) {
|
||||
if (!ComputerUtil.isNegativeCounter(aType, best)) {
|
||||
sa.getTargets().add(best);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,7 +231,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!aiList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (type.equals("P1P1")) {
|
||||
// no special amount for that one yet
|
||||
@@ -273,7 +249,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
if (!aiList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +263,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!oppList.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
} else if (type.equals("TIME")) {
|
||||
@@ -298,7 +274,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
if (manaLeft == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
amount = manaLeft;
|
||||
xPay = true;
|
||||
@@ -316,7 +292,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (xPay) {
|
||||
sa.setXManaCostPaid(timeCount);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
if (mandatory) {
|
||||
@@ -325,7 +301,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
|
||||
if (!adaptCreats.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// Outlast nice target
|
||||
@@ -336,26 +312,27 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
if (!betterTargets.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
return doTgt(aiPlayer, sa, mandatory);
|
||||
}
|
||||
return mandatory;
|
||||
return mandatory ? new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -369,8 +346,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
GameEntity target = (GameEntity) params.get("Target");
|
||||
CounterType type = (CounterType) params.get("CounterType");
|
||||
|
||||
if (target instanceof Card) {
|
||||
Card targetCard = (Card) target;
|
||||
if (target instanceof Card targetCard) {
|
||||
if (targetCard.getController().isOpponentOf(player)) {
|
||||
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
|
||||
} else {
|
||||
@@ -381,8 +357,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
|
||||
}
|
||||
} else if (target instanceof Player) {
|
||||
Player targetPlayer = (Player) target;
|
||||
} else if (target instanceof Player targetPlayer) {
|
||||
if (targetPlayer.isOpponentOf(player)) {
|
||||
return !type.is(CounterEnumType.POISON) ? max : min;
|
||||
} else {
|
||||
@@ -409,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (targetCard.getController().isOpponentOf(ai)) {
|
||||
// if its a Planeswalker try to remove Loyality first
|
||||
if (targetCard.isPlaneswalker()) {
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
return CounterEnumType.LOYALTY;
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
@@ -417,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.P1P1;
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
|
||||
@@ -5,38 +5,24 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class DamageAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (MyRandom.getRandom().nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) {
|
||||
return false;
|
||||
}
|
||||
// abCost stuff that should probably be centralized...
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// wait until stack is empty (prevents duplicate kills)
|
||||
if (!ai.getGame().getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
|
||||
}
|
||||
|
||||
int x = -1;
|
||||
@@ -51,11 +37,15 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (x == -1) {
|
||||
if (determineOppToKill(ai, sa, source, dmg) != null) {
|
||||
// we already know we can kill a player, so go for it
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// look for other value in this (damaging creatures or
|
||||
// creatures + player, e.g. Pestilence, etc.)
|
||||
return evaluateDamageAll(ai, sa, source, dmg) > 0;
|
||||
if (evaluateDamageAll(ai, sa, source, dmg) > 0) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
int best = -1, best_x = -1;
|
||||
Player bestOpp = determineOppToKill(ai, sa, source, x);
|
||||
@@ -81,9 +71,9 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
sa.setXManaCostPaid(best_x);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +133,9 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
|
||||
// When using Pestilence to hurt players, do it at
|
||||
// the end of the opponent's turn only
|
||||
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|
||||
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
|
||||
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|
||||
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
|
||||
// Need further improvement : if able to kill immediately with repeated activations, do not wait
|
||||
// for phases! Will also need to implement considering repeated activations for killed creatures!
|
||||
// || (ai.sa.getPayCosts(). ??? )
|
||||
@@ -185,7 +175,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String validP = sa.getParamOrDefault("ValidPlayers", "");
|
||||
|
||||
@@ -211,21 +201,21 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
// Don't get yourself killed
|
||||
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// if we can kill human, do it
|
||||
if ((validP.equals("Player") || validP.equals("Opponent") || validP.contains("Targeted"))
|
||||
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) > ComputerUtilCard
|
||||
.evaluateCreatureList(humanList)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +248,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String validP = sa.getParamOrDefault("ValidPlayers", "");
|
||||
|
||||
@@ -287,24 +277,24 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
|
||||
// If it's not mandatory check a few things
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
// Don't get yourself killed
|
||||
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// if we can kill human, do it
|
||||
if ((validP.equals("Player") || validP.contains("Opponent") || validP.contains("Targeted"))
|
||||
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) + 50 >= ComputerUtilCard
|
||||
.evaluateCreatureList(humanList)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import java.util.Map;
|
||||
|
||||
public class DamageDealAi extends DamageAiBase {
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
Card source = sa.getHostCard();
|
||||
@@ -65,15 +65,19 @@ public class DamageDealAi extends DamageAiBase {
|
||||
continue; // in case the calculation gets messed up somewhere
|
||||
}
|
||||
root.setSVar("EnergyToPay", "Number$" + dmg);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
// Life Drain
|
||||
if ("XLifeDrain".equals(logic)) {
|
||||
return doXLifeDrainLogic(ai, sa);
|
||||
if (doXLifeDrainLogic(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// Set PayX here to maximum value.
|
||||
@@ -83,11 +87,15 @@ public class DamageDealAi extends DamageAiBase {
|
||||
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
|
||||
}
|
||||
}
|
||||
return damageTargetAI(ai, sa, dmg, true);
|
||||
if (damageTargetAI(ai, sa, dmg, true)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
@@ -108,7 +116,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
|
||||
boolean isLethal = sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
|
||||
if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,10 +142,10 @@ public class DamageDealAi extends DamageAiBase {
|
||||
if (shouldTgtP(ai, sa, maxDmg, false)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(maxDamaged);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,7 +162,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
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;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,16 +183,24 @@ public class DamageDealAi extends DamageAiBase {
|
||||
* Mostly used to ping the player with remaining counters. The issue with
|
||||
* stacked effects might appear here.
|
||||
*/
|
||||
return damageTargetAI(ai, sa, n, true);
|
||||
if (damageTargetAI(ai, sa, n, true)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Only ping when stack is clear to avoid hassle of evaluating stacked effects
|
||||
* like protection/pumps or over-killing target.
|
||||
*/
|
||||
return ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false);
|
||||
if (ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("NinThePainArtist".equals(logic)) {
|
||||
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
|
||||
@@ -193,11 +209,15 @@ public class DamageDealAi extends DamageAiBase {
|
||||
if (doTarget) {
|
||||
Card tgt = sa.getTargetCard();
|
||||
if (tgt != null) {
|
||||
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
|
||||
if (ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
|
||||
if (sourceName.equals("Sorin, Grim Nemesis")) {
|
||||
@@ -209,35 +229,35 @@ public class DamageDealAi extends DamageAiBase {
|
||||
continue; // in case the calculation gets messed up somewhere
|
||||
}
|
||||
sa.setXManaCostPaid(dmg);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (dmg <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// temporarily disabled until better AI
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Try to chain damage/debuff effects
|
||||
@@ -248,13 +268,13 @@ public class DamageDealAi extends DamageAiBase {
|
||||
int extraDmg = chainDmg.getValue();
|
||||
boolean willTargetIfChained = damageTargetAI(ai, sa, dmg + extraDmg, false);
|
||||
if (!willTargetIfChained) {
|
||||
return false; // won't play it even in chain
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); // won't play it even in chain
|
||||
} else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) {
|
||||
// we're trying to chain a pump spell to a damage spell targeting a player, that won't work
|
||||
// so run an additional check to ensure that we want to cast the current spell separately
|
||||
sa.resetTargets();
|
||||
if (!damageTargetAI(ai, sa, dmg, false)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
// we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
|
||||
@@ -264,7 +284,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
} else if (!damageTargetAI(ai, sa, dmg, false)) {
|
||||
// simple targeting when there is no spell chaining plan
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
|
||||
@@ -288,10 +308,12 @@ public class DamageDealAi extends DamageAiBase {
|
||||
|
||||
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
|
||||
final int cmc = sa.getXManaCostPaid();
|
||||
return ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc));
|
||||
if (!ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -932,14 +954,14 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
int dmg = calculateDamageAmount(sa, source, damage);
|
||||
|
||||
// Remove all damage
|
||||
if (sa.hasParam("Remove")) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
@@ -950,10 +972,18 @@ public class DamageDealAi extends DamageAiBase {
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// If it's not mandatory check a few things
|
||||
return mandatory || damageChooseNontargeted(ai, sa, dmg);
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (damageChooseNontargeted(ai, sa, dmg)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
if (!damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) {
|
||||
@@ -976,7 +1006,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private static int calculateDamageAmount(SpellAbility sa, Card source, String damage) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.player.Player;
|
||||
@@ -14,7 +16,7 @@ public class DamageEachAi extends DamageAiBase {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
@@ -22,30 +24,41 @@ public class DamageEachAi extends DamageAiBase {
|
||||
|
||||
if (sa.usesTargeting() && weakestOpp != null) {
|
||||
if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(weakestOpp);
|
||||
return weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife();
|
||||
if (weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife()) {
|
||||
sa.getTargets().add(weakestOpp);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
|
||||
return shouldTgtP(ai, sa, iDmg, false);
|
||||
|
||||
if (shouldTgtP(ai, sa, iDmg, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// check AI life before playing this drawback?
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -24,18 +20,12 @@ import java.util.List;
|
||||
public class DamagePreventAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Card hostCard = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
boolean chance = false;
|
||||
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
if (!willPayCosts(ai, sa, cost, hostCard)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
// As far as I can tell these Defined Cards will only have one of them
|
||||
@@ -70,7 +60,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
chance = flag;
|
||||
} else { // if nothing on the stack, and it's not declare
|
||||
// blockers. no need to prevent
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} // non-targeted
|
||||
@@ -120,7 +110,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
targetables = CardLists.getTargetableCards(targetables, sa);
|
||||
|
||||
if (targetables.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES);
|
||||
ComputerUtilCard.sortByEvaluateCreature(combatants);
|
||||
@@ -137,11 +127,15 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(hostCard, sa.getParam("Amount"), sa));
|
||||
}
|
||||
|
||||
return chance;
|
||||
if (chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
boolean chance = false;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
@@ -151,7 +145,11 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
chance = preventDamageMandatoryTarget(ai, sa, mandatory);
|
||||
}
|
||||
|
||||
return chance;
|
||||
if (chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -11,24 +13,34 @@ import java.util.Map;
|
||||
|
||||
public class DayTimeAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
|
||||
if ((sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) || sa.getPayCosts().hasManaCost()) {
|
||||
// If it involves a cost that may put us at a disadvantage, better activate before own turn if possible
|
||||
if (!isSorcerySpeed(sa, aiPlayer)) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
} else {
|
||||
return ph.is(PhaseType.MAIN2, aiPlayer); // Give other things a chance to be cast (e.g. Celestus)
|
||||
if (ph.is(PhaseType.MAIN2, aiPlayer)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return true; // TODO: more logic if it's ever a bad idea to trigger this (when non-mandatory)
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -26,27 +23,27 @@ import java.util.List;
|
||||
public class DebuffAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
|
||||
// if there is no target and host card isn't in play, don't activate
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
if (!sa.usesTargeting() && !source.isInPlay()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
// temporarily disabled until AI is improved
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
@@ -58,7 +55,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
// Instant-speed pumps should not be cast outside of combat when the
|
||||
// stack is empty, unless there are specific activation phase requirements
|
||||
if (!isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +63,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
|
||||
final Combat combat = game.getCombat();
|
||||
return cards.stream().anyMatch(c -> {
|
||||
if (cards.stream().anyMatch(c -> {
|
||||
if (c.getController().equals(sa.getActivatingPlayer()) || combat == null)
|
||||
return false;
|
||||
|
||||
@@ -75,21 +72,34 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
}
|
||||
// don't add duplicate negative keywords
|
||||
return sa.hasParam("Keywords") && c.hasAnyKeyword(Arrays.asList(sa.getParam("Keywords").split(" & ")));
|
||||
});
|
||||
})) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
|
||||
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if (!sa.usesTargeting()) {
|
||||
// TODO - copied from AF_Pump.pumpDrawbackAI() - what should be here?
|
||||
} else {
|
||||
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
|
||||
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
} // debuffDrawbackAI()
|
||||
|
||||
/**
|
||||
@@ -234,18 +244,24 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final List<String> kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<>();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
} else {
|
||||
return debuffTgtAI(ai, sa, kws, mandatory);
|
||||
if (debuffTgtAI(ai, sa, kws, mandatory)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,43 +15,56 @@ import forge.game.zone.ZoneType;
|
||||
public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
// TODO: improve ai
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
|
||||
if (trigsa == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (trigsa instanceof AbilitySub) {
|
||||
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
|
||||
} else {
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
|
||||
if (trigsa == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (!sa.hasParam("OptionalDecider")) {
|
||||
return aic.doTrigger(trigsa, true);
|
||||
if (aic.doTrigger(trigsa, true)) {
|
||||
// If the trigger is mandatory, we can play it
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"));
|
||||
if (aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
// Card-specific logic
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
if (logic.equals("SpellCopy")) {
|
||||
@@ -90,9 +103,9 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (count == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (logic.equals("NarsetRebound")) {
|
||||
// should be done in Main2, but it might broke for other cards
|
||||
//if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
@@ -125,10 +138,10 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (count == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (logic.equals("SaveCreature")) {
|
||||
CardCollection ownCreatures = ai.getCreaturesInPlay();
|
||||
|
||||
@@ -142,19 +155,25 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
if (!ownCreatures.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(ownCreatures));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// Generic logic
|
||||
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
|
||||
if (trigsa == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
|
||||
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import forge.util.collect.FCollectionView;
|
||||
|
||||
public class DestroyAi extends SpellAbilityAi {
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@@ -103,36 +103,27 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final boolean noRegen = sa.hasParam("NoRegen");
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
CardCollection list;
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Targeting
|
||||
if (sa.usesTargeting()) {
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
// Assume there where already enough targets chosen by AI Logic Above
|
||||
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// reset targets before AI Logic part
|
||||
sa.resetTargets();
|
||||
int maxTargets;
|
||||
|
||||
if (sa.getRootAbility().costHasManaX()) {
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if (sa.getRootAbility().costHasManaX() ||
|
||||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
|
||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
// need to set XPaid to get the right number for
|
||||
@@ -145,23 +136,22 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
if (maxTargets == 0) {
|
||||
// can't afford X or otherwise target anything
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
|
||||
if (sa.hasParam("TargetingPlayer")) {
|
||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
|
||||
sa.setTargetingPlayer(targetingPlayer);
|
||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
// AI doesn't destroy own cards if it isn't defined in AI logic
|
||||
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
if ("FatalPush".equals(logic)) {
|
||||
final int cmcMax = ai.hasRevolt() ? 4 : 2;
|
||||
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
|
||||
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
|
||||
@@ -206,7 +196,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
// Try to avoid targeting creatures that are dead on board
|
||||
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
// target loop
|
||||
@@ -221,7 +211,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -235,7 +225,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if ("OppDestroyYours".equals(logic)) {
|
||||
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
|
||||
if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else if (CardLists.getNotType(list, "Land").isEmpty()) {
|
||||
@@ -244,7 +234,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) {
|
||||
// Strip Mine, Wasteland - cut short if the relevant logic fails
|
||||
if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -254,14 +244,14 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
//option to hold removal instead only applies for single targeted removal
|
||||
if (!sa.isTrigger() && sa.getMaxTargets() == 1) {
|
||||
if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
// TODO is this good enough? for up to amounts?
|
||||
break;
|
||||
@@ -298,22 +288,22 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
||||
|| ai.getLife() <= 5)) {
|
||||
// Basic ai logic for Lethal Vapors
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if ("Always".equals(logic)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (list.isEmpty()
|
||||
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|
||||
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final boolean noRegen = sa.hasParam("NoRegen");
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -321,7 +311,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
if (list.isEmpty() || list.size() < sa.getMinTargets()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Try to avoid targeting creatures that are dead on board
|
||||
@@ -349,7 +339,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
list.removeAll(preferred);
|
||||
|
||||
if (preferred.isEmpty() && !mandatory) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
@@ -357,12 +347,12 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
if (!sa.isMinTargetChosen()) {
|
||||
if (!mandatory) {
|
||||
sa.resetTargets();
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
Card c = ComputerUtilCard.getBestAI(preferred);
|
||||
@@ -397,9 +387,18 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return sa.isTargetNumberValid();
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
} else {
|
||||
return mandatory;
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,38 +23,23 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return doMassRemovalLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return doMassRemovalLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
|
||||
// AI needs to be expanded, since this function can be pretty complex
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if ("FellTheMighty".equals(aiLogic)) {
|
||||
@@ -64,7 +49,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
return doMassRemovalLogic(ai, sa);
|
||||
}
|
||||
|
||||
public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
public static AiAbilityDecision doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
@@ -72,7 +57,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1);
|
||||
|
||||
if (logic.equals("Always")) {
|
||||
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
String valid = sa.getParamOrDefault("ValidCards", "");
|
||||
@@ -92,7 +77,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
opplist = CardLists.filter(opplist, predicate);
|
||||
ailist = CardLists.filter(ailist, predicate);
|
||||
if (opplist.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
@@ -101,7 +86,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(opponent);
|
||||
ailist.clear();
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,30 +95,35 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.size());
|
||||
int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.size());
|
||||
|
||||
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
|
||||
if (numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (numAiCanSave < ailist.size() && (opplist.size() - numOppsCanSave < ailist.size() - numAiCanSave)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// 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;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
|
||||
}
|
||||
|
||||
// test whether the human can kill the ai next turn
|
||||
@@ -146,39 +136,42 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (!containsAttacker) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai, false);
|
||||
block.assignBlockersForCombat(combat);
|
||||
|
||||
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} // only lands involved
|
||||
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
|
||||
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) {
|
||||
return true;
|
||||
// TODO Should care about any land recursion, not just Crucible of Worlds
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = opponent.getCreaturesInPlay();
|
||||
if (!oppCreatures.isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
// check if the AI would lose more lands than the opponent would
|
||||
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,26 +20,21 @@ import forge.util.TextUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class DigAi 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 ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
Player libraryOwner = ai;
|
||||
|
||||
if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!sa.canTarget(opp)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
@@ -47,29 +42,21 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
// return false if nothing to dig into
|
||||
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
|
||||
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// don't deck yourself
|
||||
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
|
||||
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
|
||||
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final String num = sa.getParam("DigNum");
|
||||
@@ -87,14 +74,14 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()) - manaToSave;
|
||||
if (numCards <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
}
|
||||
}
|
||||
|
||||
if (playReusable(ai, sa)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|
||||
@@ -102,24 +89,24 @@ public class DigAi extends SpellAbilityAi {
|
||||
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
|
||||
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
|
||||
&& !ComputerUtil.activateForCost(sa, ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if ("MadSarkhanDigDmg".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa);
|
||||
}
|
||||
|
||||
return !ComputerUtil.preventRunAwayActivations(sa);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// TODO: improve this check in ways that may be specific to a subability
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
|
||||
@@ -137,12 +124,16 @@ public class DigAi extends SpellAbilityAi {
|
||||
int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]);
|
||||
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, true) - manaToSave;
|
||||
if (numCards <= 0) {
|
||||
return mandatory;
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,15 +194,12 @@ public class DigAi extends SpellAbilityAi {
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
Card topc = player.getZone(ZoneType.Library).get(0);
|
||||
|
||||
// AI actions for individual cards (until this AI can be generalized)
|
||||
if (sa.getHostCard() != null) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
|
||||
// for Explorer's Scope, always put a land on the battlefield tapped
|
||||
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
|
||||
return true;
|
||||
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
}
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
|
||||
// for Explorer's Scope, always put a land on the battlefield tapped
|
||||
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
|
||||
return true;
|
||||
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// looks like perfect code for Delver of Secrets, but what about other cards?
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -14,13 +12,12 @@ import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class DigMultipleAi 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 ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
@@ -29,7 +26,7 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
@@ -37,33 +34,29 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
|
||||
// return false if nothing to dig into
|
||||
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
|
||||
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
|
||||
return false;
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// don't deck yourself
|
||||
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
|
||||
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
|
||||
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (playReusable(ai, sa)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|
||||
@@ -71,14 +64,14 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
|
||||
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
|
||||
&& !ComputerUtil.activateForCost(sa, ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return !ComputerUtil.preventRunAwayActivations(sa);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
@@ -89,7 +82,7 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
@@ -11,7 +9,6 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -19,7 +16,7 @@ import java.util.Map;
|
||||
public class DigUntilAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
double chance = .4; // 40 percent chance with instant speed stuff
|
||||
@@ -42,7 +39,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
// 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;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if ("Land.Basic".equals(sa.getParam("Valid"))
|
||||
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.LANDS_PRODUCING_MANA)) {
|
||||
@@ -52,7 +49,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
// This is important for Replenish/Living Death type decks
|
||||
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +57,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (!sa.canTarget(opp)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
@@ -68,7 +65,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("Valid")) {
|
||||
final String valid = sa.getParam("Valid");
|
||||
if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid, source.getController(), source, sa).isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,7 +77,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
if (root.getXManaCostPaid() == null) {
|
||||
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
if (numCards <= 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
}
|
||||
@@ -88,15 +85,14 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
|
||||
// return false if nothing to dig into
|
||||
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
return randomReturn;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (sa.isCurse()) {
|
||||
@@ -116,7 +112,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -26,31 +26,29 @@ import forge.util.collect.FCollectionView;
|
||||
public class DiscardAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
// temporarily disabled until better AI
|
||||
if (!willPayCosts(ai, sa, abCost, source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("Chandra, Flamecaller".equals(sourceName)) {
|
||||
final int hand = ai.getCardsIn(ZoneType.Hand).size();
|
||||
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
||||
if (MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand))) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (aiLogic.equals("VolrathsShapeshifter")) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
||||
}
|
||||
|
||||
final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0;
|
||||
final boolean humanHasHand = !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
// TODO: Add appropriate restrictions
|
||||
@@ -64,7 +62,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
} else {
|
||||
// defined to the human, so that's fine as long the human has cards
|
||||
if (!humanHasHand) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -78,12 +76,12 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()), ai.getWeakestOpponent()
|
||||
.getCardsIn(ZoneType.Hand).size());
|
||||
if (cardsToDiscard < 1) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.setXManaCostPaid(cardsToDiscard);
|
||||
} else {
|
||||
if (AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa) < 1) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +111,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (numDiscard == 0) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,27 +119,25 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
// Don't use discard abilities before main 2 if possible
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||
&& !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (aiLogic.equals("AnyPhaseIfFavored")) {
|
||||
if (ai.getGame().getCombat() != null) {
|
||||
if (ai.getCardsIn(ZoneType.Hand).size() < ai.getGame().getCombat().getDefenderPlayerByAttacker(source).getCardsIn(ZoneType.Hand).size()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn());
|
||||
|
||||
// some other variables here, like handsize vs. maxHandSize
|
||||
|
||||
return randomReturn;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
||||
@@ -166,7 +162,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
|
||||
@@ -176,7 +172,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
} else if (mandatory && sa.canTarget(ai)) {
|
||||
sa.getTargets().add(ai);
|
||||
} else {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -184,7 +180,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
if ("AtLeast2".equals(sa.getParam("AILogic"))) {
|
||||
final List<Player> players = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
if (players.isEmpty() || players.get(0).getCardsIn(ZoneType.Hand).size() < 2) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,18 +192,22 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
// Drawback AI improvements
|
||||
// if parent draws cards, make sure cards in hand + cards drawn > 0
|
||||
if (sa.usesTargeting()) {
|
||||
return discardTargetAI(ai, sa);
|
||||
if (discardTargetAI(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
// TODO: check for some extra things
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
@@ -16,12 +13,8 @@ import java.util.Map;
|
||||
public class DiscoverAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite loop
|
||||
}
|
||||
|
||||
return true;
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,8 +29,12 @@ public class DiscoverAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
return mandatory || checkApiLogic(ai, sa);
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// assume we are looking to tap human's stuff
|
||||
@@ -25,56 +25,58 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (!defined.contains(opp)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
if (mandatory) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
return defined.contains(opp);
|
||||
if (defined.contains(opp)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
// AI cannot use this properly until he can use SAs during Humans turn
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
boolean randomReturn = true;
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (defined.contains(ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getWeakestOpponent());
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -42,43 +41,41 @@ public class DrawAi extends SpellAbilityAi {
|
||||
* @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (!targetAI(ai, sa, false)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player player = sa.getTargets().getFirstTargetedPlayer();
|
||||
if (player != null && player.isOpponentOf(ai)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Don't tap creatures that may be able to block
|
||||
if (ComputerUtil.waitForBlocking(sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
if (!canLoot(ai, sa)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) {
|
||||
// Canopy lands and other cards that sacrifice themselves to draw cards
|
||||
return ai.getCardsIn(ZoneType.Hand).isEmpty()
|
||||
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile
|
||||
if (ai.getCardsIn(ZoneType.Hand).isEmpty()
|
||||
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5)) {
|
||||
// TODO: make this configurable in the AI profile
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -161,8 +158,6 @@ public class DrawAi extends SpellAbilityAi {
|
||||
// LifeLessThan logic presupposes activation as soon as possible in an
|
||||
// attempt to save the AI from dying
|
||||
return true;
|
||||
} else if (logic.equals("AtOppEOT")) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
} else if (logic.equals("RespondToOwnActivation")) {
|
||||
return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
|
||||
} else if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
||||
@@ -175,8 +170,12 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay());
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
if (targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,7 +369,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
|
||||
// try to make opponent lose to poison
|
||||
// currently only Caress of Phyrexia
|
||||
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (oppA.getPoisonCounters() + numCards > 9) {
|
||||
sa.getTargets().add(oppA);
|
||||
return true;
|
||||
@@ -414,7 +413,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (numCards + ai.getPoisonCounters() >= 8) {
|
||||
aiTarget = false;
|
||||
}
|
||||
@@ -472,7 +471,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// ally would lose because of poison
|
||||
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
|
||||
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -534,12 +533,16 @@ public class DrawAi extends SpellAbilityAi {
|
||||
} // drawTargetAI()
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return targetAI(ai, sa, mandatory);
|
||||
if (targetAI(ai, sa, mandatory)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
||||
@@ -35,7 +35,7 @@ import java.util.Map;
|
||||
|
||||
public class EffectAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai,final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
|
||||
String logic = "";
|
||||
@@ -45,12 +45,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
if (logic.equals("BeginningOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("EndOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
|
||||
@@ -64,20 +59,20 @@ public class EffectAi extends SpellAbilityAi {
|
||||
worthHolding = true;
|
||||
}
|
||||
if (!worthHolding) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
}
|
||||
} else if (logic.equals("RestrictBlocking")) {
|
||||
if (!phase.isPlayerTurn(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)
|
||||
|| phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.getPayCosts().getTotalMana().countX() > 0 && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans.
|
||||
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai, sa.isTrigger()) / 2;
|
||||
if (xPay == 0) { return false; }
|
||||
if (xPay == 0) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
|
||||
sa.setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
@@ -90,23 +85,27 @@ public class EffectAi extends SpellAbilityAi {
|
||||
int potentialDmg = 0;
|
||||
List<Card> currentAttackers = new ArrayList<>();
|
||||
|
||||
if (possibleBlockers.isEmpty()) { return false; }
|
||||
if (possibleBlockers.isEmpty()) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
|
||||
|
||||
for (final Card creat : possibleAttackers) {
|
||||
if (CombatUtil.canAttack(creat, opp) && possibleBlockers.size() > 1) {
|
||||
potentialDmg += creat.getCurrentPower();
|
||||
if (potentialDmg >= oppLife) { return true; }
|
||||
if (potentialDmg >= oppLife) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); }
|
||||
}
|
||||
if (combat != null && combat.isAttacking(creat)) {
|
||||
currentAttackers.add(creat);
|
||||
}
|
||||
}
|
||||
|
||||
return currentAttackers.size() > possibleBlockers.size();
|
||||
if (currentAttackers.size() > possibleBlockers.size()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (logic.equals("Fog")) {
|
||||
FogAi fogAi = new FogAi();
|
||||
if (!fogAi.canPlayAI(ai, sa)) {
|
||||
return false;
|
||||
if (!fogAi.canPlay(ai, sa).willingToPlay()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -124,14 +123,14 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!canTgt) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else {
|
||||
List<Card> list = game.getCombat().getAttackers();
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
Card target = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (target == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(target);
|
||||
}
|
||||
@@ -139,7 +138,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("ChainVeil")) {
|
||||
if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) || ai.getPlaneswalkersInPlay().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("WillCastCreature") && ai.isAI()) {
|
||||
@@ -150,17 +149,17 @@ public class EffectAi extends SpellAbilityAi {
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("Main1")) {
|
||||
if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("Main2")) {
|
||||
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("Evasion")) {
|
||||
if (!phase.isPlayerTurn(ai)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
boolean shouldPlay = false;
|
||||
@@ -185,10 +184,10 @@ public class EffectAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
|
||||
return shouldPlay;
|
||||
return shouldPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
boolean threatened = false;
|
||||
for (final SpellAbilityStackInstance stackInst : game.getStack()) {
|
||||
@@ -204,7 +203,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
randomReturn = threatened;
|
||||
} else if (logic.equals("Prevent")) { // prevent burn spell from opponent
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
final SpellAbility saTop = game.getStack().peekAbility();
|
||||
final Card host = saTop.getHostCard();
|
||||
@@ -215,10 +214,10 @@ public class EffectAi extends SpellAbilityAi {
|
||||
final ApiType type = saTop.getApi();
|
||||
if (type == ApiType.DealDamage || type == ApiType.DamageAll) { // burn spell
|
||||
sa.getTargets().add(saTop);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("NoGain")) {
|
||||
// basic logic to cancel GainLife on stack
|
||||
if (!game.getStack().isEmpty()) {
|
||||
@@ -228,14 +227,14 @@ public class EffectAi extends SpellAbilityAi {
|
||||
while (topStack != null) {
|
||||
if (topStack.getApi() == ApiType.GainLife) {
|
||||
if ("You".equals(topStack.getParam("Defined")) || topStack.isTargeting(activator) || (!topStack.usesTargeting() && !topStack.hasParam("Defined"))) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (topStack.getApi() == ApiType.DealDamage && topStack.getHostCard().hasKeyword(Keyword.LIFELINK)) {
|
||||
Card host = topStack.getHostCard();
|
||||
for (GameEntity target : topStack.getTargets().getTargetEntities()) {
|
||||
if (ComputerUtilCombat.predictDamageTo(target,
|
||||
AbilityUtils.calculateAmount(host, topStack.getParam("NumDmg"), topStack), host, false) > 0) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,11 +248,11 @@ public class EffectAi extends SpellAbilityAi {
|
||||
final Player attackingPlayer = combat.getAttackingPlayer();
|
||||
if (attackingPlayer.isOpponentOf(ai) && attackingPlayer.canGainLife()) {
|
||||
if (ComputerUtilCombat.checkAttackerLifelinkDamage(combat) > 0) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("NonCastCreature")) {
|
||||
// TODO: add support for more cases with more convoluted API setups
|
||||
if (!game.getStack().isEmpty()) {
|
||||
@@ -265,13 +264,13 @@ public class EffectAi extends SpellAbilityAi {
|
||||
boolean reanimator = "true".equalsIgnoreCase(topStack.getSVar("IsReanimatorCard"));
|
||||
if (changeZone && (toBattlefield || reanimator)) {
|
||||
if ("Creature".equals(topStack.getParam("ChangeType")) || topStack.getParamOrDefault("Defined", "").contains("Creature"))
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("Fight")) {
|
||||
return FightAi.canFightAi(ai, sa, 0, 0);
|
||||
return FightAi.canFight(ai, sa, 0,0);
|
||||
} else if (logic.equals("Pump")) {
|
||||
sa.resetTargets();
|
||||
List<Card> options = CardUtil.getValidCardsToTarget(sa);
|
||||
@@ -281,55 +280,55 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
if (!options.isEmpty() && phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("Burn")) {
|
||||
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
|
||||
SpellAbility burn = sa.getSubAbility();
|
||||
return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn);
|
||||
return SpellApiToAi.Converter.get(burn).canPlayWithSubs(ai, burn).willingToPlay() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("YawgmothsWill")) {
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa);
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.startsWith("NeedCreatures")) {
|
||||
// TODO convert to AiCheckSVar
|
||||
if (ai.getCreaturesInPlay().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (logic.contains(":")) {
|
||||
String[] k = logic.split(":");
|
||||
int i = Integer.parseInt(k[1]);
|
||||
return ai.getCreaturesInPlay().size() >= i;
|
||||
return ai.getCreaturesInPlay().size() >= i ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (logic.equals("ReplaySpell")) {
|
||||
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Graveyard), sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (logic.equals("PeaceTalks")) {
|
||||
Player nextPlayer = game.getNextPlayerAfter(ai);
|
||||
|
||||
// If opponent doesn't have creatures, preventing attacks don't mean as much
|
||||
if (nextPlayer.getCreaturesInPlay().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Only cast Peace Talks after you attack just in case you have creatures
|
||||
if (!phase.is(PhaseType.MAIN2)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// Create a pseudo combat and see if my life is in danger
|
||||
return randomReturn;
|
||||
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("Bribe")) {
|
||||
Card host = sa.getHostCard();
|
||||
Combat combat = game.getCombat();
|
||||
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
|
||||
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
|
||||
return true;
|
||||
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
// ideally needs once per combat or something
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("CantRegenerate")) {
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -350,19 +349,19 @@ public class EffectAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
// TODO check Stack for Effects that would destroy the selected card?
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(list));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (sa.getParent() != null) {
|
||||
// sub ability should be okay
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if ("Self".equals(sa.getParam("RememberObjects"))) {
|
||||
// the ones affecting itself are Nimbus cards, were opponent can activate this effect
|
||||
Card host = sa.getHostCard();
|
||||
if (!host.canBeDestroyed()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
|
||||
@@ -370,18 +369,18 @@ public class EffectAi extends SpellAbilityAi {
|
||||
List<ReplacementEffect> repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
|
||||
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
|
||||
if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else { //no AILogic
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if ("False".equals(sa.getParam("Stackable"))) {
|
||||
@@ -390,7 +389,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
name = sa.getHostCard().getName() + "'s Effect";
|
||||
}
|
||||
if (sa.getActivatingPlayer().isCardInCommand(name)) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,20 +405,20 @@ public class EffectAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return canTgt;
|
||||
return canTgt ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (canPlayAI(aiPlayer, sa)) {
|
||||
return true; // if false, fall through further to do the mandatory stuff
|
||||
if (canPlay(aiPlayer, sa).willingToPlay()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +430,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
if (!oppPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
@@ -441,14 +440,14 @@ public class EffectAi extends SpellAbilityAi {
|
||||
if (!aiPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
protected boolean cantRegenerateCheckCombat(Card host) {
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.combat.CombatUtil;
|
||||
@@ -45,19 +43,17 @@ public final class EncodeAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
*
|
||||
* @return a boolean.
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return true;
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -12,18 +14,22 @@ import forge.game.spellability.SpellAbility;
|
||||
public class EndTurnAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory;
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { return false; }
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
|
||||
|
||||
/* (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) {
|
||||
return false;
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayLife;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -22,19 +22,39 @@ public class EndureAi extends SpellAbilityAi {
|
||||
* @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) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return true;
|
||||
// Card-specific logic
|
||||
final String num = sa.getParamOrDefault("Num", "1");
|
||||
if ("X".equals(num) && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
|
||||
if (!aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
int curLife = aiPlayer.getLife();
|
||||
int dangerLife = (((PlayerControllerAi) aiPlayer.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
|
||||
if (curLife <= dangerLife) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
int availableMana = ComputerUtilMana.getAvailableManaEstimate(aiPlayer) - 1;
|
||||
int maxEndureX = Math.min(availableMana, curLife - dangerLife);
|
||||
if (maxEndureX > 0) {
|
||||
sa.setXManaCostPaid(maxEndureX);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
|
||||
@@ -121,7 +141,7 @@ public class EndureAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
@@ -129,12 +149,16 @@ public class EndureAi extends SpellAbilityAi {
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,19 @@ public class ExploreAi extends SpellAbilityAi {
|
||||
* @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) {
|
||||
protected AiAbilityDecision canPlay(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;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
public static boolean shouldPutInGraveyard(Card topCard, Player ai) {
|
||||
@@ -64,19 +64,23 @@ public class ExploreAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ public class FightAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
sa.resetTargets();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// everything is defined or targeted above, can't do anything there unless a specific logic is set
|
||||
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// Get creature lists
|
||||
@@ -42,8 +42,10 @@ public class FightAi extends SpellAbilityAi {
|
||||
// Filter MustTarget requirements
|
||||
StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa);
|
||||
|
||||
if (humCreatures.isEmpty())
|
||||
return false; //prevent IndexOutOfBoundsException on MOJHOSTO variant
|
||||
//prevent IndexOutOfBoundsException on MOJHOSTO variant
|
||||
if (humCreatures.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
// assumes the triggered card belongs to the ai
|
||||
if (sa.hasParam("Defined")) {
|
||||
@@ -54,7 +56,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (fighter1List.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
Card fighter1 = fighter1List.get(0);
|
||||
for (Card humanCreature : humCreatures) {
|
||||
@@ -62,10 +64,11 @@ public class FightAi extends SpellAbilityAi {
|
||||
&& !canKill(humanCreature, fighter1, 0)) {
|
||||
// todo: check min/max targets; see if we picked the best matchup
|
||||
sa.getTargets().add(humanCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false; // bail at this point, otherwise the AI will overtarget and waste the activation
|
||||
// bail at this point, otherwise the AI will overtarget and waste the activation
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
if (sa.hasParam("TargetsFromDifferentZone")) {
|
||||
@@ -77,12 +80,12 @@ public class FightAi extends SpellAbilityAi {
|
||||
// todo: check min/max targets; see if we picked the best matchup
|
||||
sa.getTargets().add(humanCreature);
|
||||
sa.getTargets().add(aiCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
for (Card creature1 : humCreatures) {
|
||||
for (Card creature2 : humCreatures) {
|
||||
@@ -97,42 +100,52 @@ public class FightAi extends SpellAbilityAi {
|
||||
// todo: check min/max targets; see if we picked the best matchup
|
||||
sa.getTargets().add(creature1);
|
||||
sa.getTargets().add(creature2);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||
return true; // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
|
||||
}
|
||||
|
||||
return checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
if (aiLogic.equals("Grothama")) {
|
||||
return mandatory ? true : SpecialCardAi.GrothamaAllDevouring.consider(ai, sa);
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
if (SpecialCardAi.GrothamaAllDevouring.consider(ai, sa)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkApiLogic(ai, sa)) {
|
||||
return true;
|
||||
AiAbilityDecision decision = checkApiLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
if (!mandatory) {
|
||||
return false;
|
||||
return decision;
|
||||
}
|
||||
// if mandatory, we have to play it, so we will try to make a good trade or no trade
|
||||
|
||||
//try to make a good trade or no trade
|
||||
final Card source = sa.getHostCard();
|
||||
List<Card> humCreatures = ai.getOpponents().getCreaturesInPlay();
|
||||
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
||||
if (humCreatures.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
//assumes the triggered card belongs to the ai
|
||||
if (sa.hasParam("Defined")) {
|
||||
@@ -141,19 +154,19 @@ public class FightAi extends SpellAbilityAi {
|
||||
if (canKill(aiCreature, humanCreature, 0)
|
||||
&& ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) {
|
||||
sa.getTargets().add(humanCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
}
|
||||
for (Card humanCreature : humCreatures) {
|
||||
if (!canKill(humanCreature, aiCreature, 0)) {
|
||||
sa.getTargets().add(humanCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
}
|
||||
sa.getTargets().add(humCreatures.get(0));
|
||||
return true;
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +177,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
* @param power bonus to power
|
||||
* @return true if fight effect should be played, false otherwise
|
||||
*/
|
||||
public static boolean canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
public static AiAbilityDecision canFight(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
AbilitySub tgtFight = sa.getSubAbility();
|
||||
@@ -196,7 +209,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
ComputerUtilCard.sortByEvaluateCreature(aiCreatures);
|
||||
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
|
||||
if (humCreatures.isEmpty() || aiCreatures.isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
// Evaluate creature pairs
|
||||
for (Card humanCreature : humCreatures) {
|
||||
@@ -226,7 +239,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
tgtFight.resetTargets();
|
||||
tgtFight.getTargets().add(humanCreature);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else {
|
||||
// Other cards that use AILogic PowerDmg and a single target
|
||||
@@ -236,7 +249,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
tgtFight.resetTargets();
|
||||
tgtFight.getTargets().add(humanCreature);
|
||||
}
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -249,12 +262,12 @@ public class FightAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(aiCreature);
|
||||
tgtFight.resetTargets();
|
||||
tgtFight.getTargets().add(humanCreature);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -10,55 +12,57 @@ import forge.game.spellability.SpellAbility;
|
||||
public class FlipACoinAi extends SpellAbilityAi {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#checkApiLogic(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String ailogic = sa.getParam("AILogic");
|
||||
if (ailogic.equals("Never")) {
|
||||
return false;
|
||||
} else if (ailogic.equals("PhaseOut")) {
|
||||
if (ailogic.equals("PhaseOut")) {
|
||||
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if (ailogic.equals("Bangchuckers")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.resetTargets();
|
||||
for (Player o : ai.getOpponents()) {
|
||||
if (sa.canTarget(o) && o.canLoseLife() && !o.cantLoseForZeroOrLessLife()) {
|
||||
sa.getTargets().add(o);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (ailogic.equals("KillOrcs")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.resetTargets();
|
||||
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
return sa.isTargetNumberValid();
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlayAI(ai, sa);
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -14,26 +16,43 @@ import java.util.Map;
|
||||
|
||||
public class FlipOntoBattlefieldAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
PhaseHandler ph = sa.getHostCard().getGame().getPhaseHandler();
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && sa.getPayCosts().hasManaCost()) {
|
||||
return ph.is(PhaseType.END_OF_TURN);
|
||||
if (ph.is(PhaseType.END_OF_TURN)) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
}
|
||||
|
||||
if ("DamageCreatures".equals(logic)) {
|
||||
int maxToughness = Integer.parseInt(sa.getSubAbility().getParam("NumDmg"));
|
||||
CardCollectionView rightToughness = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), card -> card.getNetToughness() <= maxToughness && card.canBeDestroyed());
|
||||
return !rightToughness.isEmpty();
|
||||
|
||||
if (rightToughness.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
} else {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
return !aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty();
|
||||
if (!aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,36 +22,36 @@ public class FogAi extends SpellAbilityAi {
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
final Card hostCard = sa.getHostCard();
|
||||
final Combat combat = game.getCombat();
|
||||
|
||||
// Don't cast it, if the effect is already in place
|
||||
if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// TODO Test if we can even Fog successfully
|
||||
if (handleMemoryCheck(ai, sa)) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
// Only cast when Stack is empty, so Human uses spells/abilities first
|
||||
if (!game.getStack().isEmpty()) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// TODO Only cast outside of combat if I won't be able to cast inside of combat
|
||||
if (combat == null) {
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
// AI should only activate this during Opponents Declare Blockers phase
|
||||
if (!game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) ||
|
||||
!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
// TODO Be careful of effects that don't let you cast spells during combat
|
||||
return false;
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
int remainingLife = ComputerUtilCombat.lifeThatWouldRemain(ai, combat);
|
||||
@@ -61,28 +61,32 @@ public class FogAi extends SpellAbilityAi {
|
||||
int fogs = countAvailableFogs(ai);
|
||||
if (fogs > 2 && dmg > 2) {
|
||||
// Playing a fog deck. If you got them play them.
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
|
||||
}
|
||||
if (dmg > 2 &&
|
||||
hostCard.hasKeyword(Keyword.BUYBACK) &&
|
||||
CardLists.count(ai.getCardsIn(ZoneType.Battlefield), Card::isLand) > 3) {
|
||||
// Constant mists sacrifices a land to buyback. But if AI is running it, they are probably ok sacrificing some lands
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
|
||||
}
|
||||
|
||||
if ("SeriousDamage".equals(sa.getParam("AILogic"))) {
|
||||
if (dmg > ai.getLife() / 4) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
|
||||
} else if (dmg >= 5) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
|
||||
} else if (ai.getLife() < ai.getStartingLife() / 3) {
|
||||
return true;
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
|
||||
}
|
||||
}
|
||||
// TODO Compare to poison counters?
|
||||
|
||||
// Cast it if life is in danger
|
||||
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
||||
if (ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.Tempo);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleMemoryCheck(Player ai, SpellAbility sa) {
|
||||
@@ -137,7 +141,7 @@ public class FogAi extends SpellAbilityAi {
|
||||
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||
// AI should only activate this during Human's turn
|
||||
boolean chance;
|
||||
final Game game = ai.getGame();
|
||||
@@ -149,11 +153,15 @@ public class FogAi extends SpellAbilityAi {
|
||||
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
||||
}
|
||||
|
||||
return chance;
|
||||
if (chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
final Game game = aiPlayer.getGame();
|
||||
boolean chance;
|
||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getWeakestOpponent())) {
|
||||
@@ -162,6 +170,10 @@ public class FogAi extends SpellAbilityAi {
|
||||
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
||||
}
|
||||
|
||||
return chance || mandatory;
|
||||
if (mandatory || chance) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user