mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 11:18:01 +00:00
Compare commits
1816 Commits
forge-1.6.
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf97aa2fc | ||
|
|
b58a561863 | ||
|
|
06c13804a4 | ||
|
|
bf8e88f76b | ||
|
|
84528b5251 | ||
|
|
f76428b668 | ||
|
|
d8abdf1a2e | ||
|
|
f9f83660f5 | ||
|
|
ddfd97c395 | ||
|
|
f658ba29c9 | ||
|
|
57cf22af43 | ||
|
|
eaea379532 | ||
|
|
2ac3829b6f | ||
|
|
29e3cf4a79 | ||
|
|
d84e2e4e23 | ||
|
|
02b8d39791 | ||
|
|
f7a74edc6e | ||
|
|
8feaf0795f | ||
|
|
87a857220f | ||
|
|
403550af5f | ||
|
|
d6ffdddfc6 | ||
|
|
7663df024c | ||
|
|
704c735bc5 | ||
|
|
ec72e9f7d7 | ||
|
|
f95ada602c | ||
|
|
7c10272d03 | ||
|
|
3c3c64a11a | ||
|
|
722c16e925 | ||
|
|
4a799cf45f | ||
|
|
16acf2d46c | ||
|
|
413c14a1dc | ||
|
|
826f37e4b9 | ||
|
|
39654e498c | ||
|
|
e3432ced3c | ||
|
|
917ae1daab | ||
|
|
a09086bfc2 | ||
|
|
2ac38e6639 | ||
|
|
3dc54790cc | ||
|
|
ce780c5175 | ||
|
|
35a76b4a43 | ||
|
|
999f990870 | ||
|
|
e050a62b0c | ||
|
|
1cd278afcf | ||
|
|
208089a5cd | ||
|
|
0c66c256b3 | ||
|
|
66e8154243 | ||
|
|
ff4db64802 | ||
|
|
4c29790b6c | ||
|
|
3efc4807cf | ||
|
|
a3b543cdd2 | ||
|
|
a12eedfa93 | ||
|
|
a97125d733 | ||
|
|
7b5d2fc053 | ||
|
|
0e587c5b3a | ||
|
|
6ea9a07a3d | ||
|
|
090a47b17f | ||
|
|
f79b326010 | ||
|
|
4c6aa708c1 | ||
|
|
7ac66a298b | ||
|
|
9e1baa9521 | ||
|
|
c1870b4ef8 | ||
|
|
a9b068c89e | ||
|
|
dab7d5030e | ||
|
|
120a314a96 | ||
|
|
bd4305dd13 | ||
|
|
d8b7214a2b | ||
|
|
95122ffcf8 | ||
|
|
49f789c404 | ||
|
|
9cf447d422 | ||
|
|
1dc10b57c6 | ||
|
|
3d11422191 | ||
|
|
fa0fc14c25 | ||
|
|
22cdb7e721 | ||
|
|
d215657641 | ||
|
|
df33b9388f | ||
|
|
ac53f16098 | ||
|
|
36d6342e77 | ||
|
|
72e7822616 | ||
|
|
ea8f990172 | ||
|
|
8003ade509 | ||
|
|
3795232b2c | ||
|
|
dda26098f4 | ||
|
|
ae042d98f4 | ||
|
|
649faa3371 | ||
|
|
cea0c3f628 | ||
|
|
dce2ebbafb | ||
|
|
c80d703acf | ||
|
|
f0418b8d2a | ||
|
|
f363ae0f1e | ||
|
|
24d3801316 | ||
|
|
2a3f2c6dbd | ||
|
|
318a6e2924 | ||
|
|
800311fed8 | ||
|
|
e49d45bf4f | ||
|
|
356bd9c635 | ||
|
|
b400ea6d33 | ||
|
|
7352f9094a | ||
|
|
c90e8bd356 | ||
|
|
9f79073d14 | ||
|
|
249f8a4673 | ||
|
|
fe8ec70159 | ||
|
|
1f5375fed2 | ||
|
|
804d47df7b | ||
|
|
c6dfc1ceb2 | ||
|
|
b3727f6821 | ||
|
|
bc538da8ea | ||
|
|
465e25adff | ||
|
|
809fc513ea | ||
|
|
ea101baf0d | ||
|
|
19f88d6e6d | ||
|
|
7499d05bbc | ||
|
|
de2e3fc070 | ||
|
|
9b85293349 | ||
|
|
bee34c064b | ||
|
|
7ba64ce136 | ||
|
|
d80d2c5cda | ||
|
|
2419b93682 | ||
|
|
740b3f0c49 | ||
|
|
0925379a6d | ||
|
|
0c89087579 | ||
|
|
0565082309 | ||
|
|
cf7fcb07a1 | ||
|
|
ad99d21cdf | ||
|
|
3ad4da4f49 | ||
|
|
62238e0f81 | ||
|
|
2928f157fa | ||
|
|
cacb64b2ca | ||
|
|
1f65c4723d | ||
|
|
c2d4923706 | ||
|
|
5ea07389b4 | ||
|
|
8c56bb51ed | ||
|
|
7c8057dbc6 | ||
|
|
841b9ae668 | ||
|
|
f847a39eaa | ||
|
|
f2eafb99d4 | ||
|
|
847becccfe | ||
|
|
536c2483b0 | ||
|
|
af80228bcd | ||
|
|
334b086efb | ||
|
|
bddce9872f | ||
|
|
3d4e544573 | ||
|
|
7fe68a1c4a | ||
|
|
8693b6ce81 | ||
|
|
200b3f7092 | ||
|
|
69afbde352 | ||
|
|
052fb65518 | ||
|
|
c2ec6978c4 | ||
|
|
941e25d7a7 | ||
|
|
0bddb07ba0 | ||
|
|
869697352d | ||
|
|
9757fdb16b | ||
|
|
4d718177b6 | ||
|
|
2dff30f2cd | ||
|
|
8b5128510b | ||
|
|
1efa8d030b | ||
|
|
c855a107ab | ||
|
|
a379279956 | ||
|
|
3fb38ae682 | ||
|
|
ee29d4289c | ||
|
|
eeccc8490d | ||
|
|
2ae735db74 | ||
|
|
248d6c57c9 | ||
|
|
5eba9bfc14 | ||
|
|
7bc14a526d | ||
|
|
769521c978 | ||
|
|
bc71ef6a18 | ||
|
|
422ab9b835 | ||
|
|
410f9b1acf | ||
|
|
f6b3cb4b53 | ||
|
|
6a72aab106 | ||
|
|
f3695e18ac | ||
|
|
f64846c690 | ||
|
|
79bb27d2a7 | ||
|
|
f0dba0ca0e | ||
|
|
b70627bd3f | ||
|
|
91b894958c | ||
|
|
ef10e83f1c | ||
|
|
e958cb665e | ||
|
|
8413ec92d5 | ||
|
|
92d77b610d | ||
|
|
42f445b1d2 | ||
|
|
ca886f4c0c | ||
|
|
531ac8fc50 | ||
|
|
628c8f5ff1 | ||
|
|
b63037caf3 | ||
|
|
19127cb09d | ||
|
|
004e0a4265 | ||
|
|
9bbc23d550 | ||
|
|
77cea9f74e | ||
|
|
0bc57c145a | ||
|
|
92545309e1 | ||
|
|
bdc2fe8e69 | ||
|
|
1d3c5c85ec | ||
|
|
cd14da0454 | ||
|
|
73ccaac656 | ||
|
|
b19df1be9f | ||
|
|
f10bfe8e8d | ||
|
|
3418fdf822 | ||
|
|
f31098bf15 | ||
|
|
27ec3db33d | ||
|
|
8b618df712 | ||
|
|
a87d428563 | ||
|
|
d778138f92 | ||
|
|
1197b1c59a | ||
|
|
e5cf4e8cea | ||
|
|
4bbfb1852f | ||
|
|
d88ecc8505 | ||
|
|
6d1ef8257f | ||
|
|
5167eabb5c | ||
|
|
c8f08f61e5 | ||
|
|
9ed4822a93 | ||
|
|
0db2fba9fa | ||
|
|
117ee3d7bc | ||
|
|
c30261f9b8 | ||
|
|
5bd0fcf866 | ||
|
|
c888a09a40 | ||
|
|
c88818aa55 | ||
|
|
bafbdc8a5a | ||
|
|
a04c9f144a | ||
|
|
3301d91ae7 | ||
|
|
6c4ed98ad4 | ||
|
|
273894b734 | ||
|
|
2fd3b13d86 | ||
|
|
e3ec921e3d | ||
|
|
8e4ce46891 | ||
|
|
9c50f45866 | ||
|
|
2b3733a752 | ||
|
|
ae709d210e | ||
|
|
8f94bceaf2 | ||
|
|
36fbb71e1d | ||
|
|
464a1fe51c | ||
|
|
4067caa144 | ||
|
|
c576b0f65c | ||
|
|
6156ad254f | ||
|
|
3b0465aafe | ||
|
|
d7da7a3fd1 | ||
|
|
3fb515c73b | ||
|
|
8ef5fbc84b | ||
|
|
51f1c281e7 | ||
|
|
d93e9170ad | ||
|
|
418d4d5019 | ||
|
|
bf6c569c01 | ||
|
|
6e1a994971 | ||
|
|
0ed44d4afb | ||
|
|
92cc1c7718 | ||
|
|
b4347ff53d | ||
|
|
3e2b4418ef | ||
|
|
691264c2c3 | ||
|
|
a21b2acf6f | ||
|
|
54a3052fdb | ||
|
|
33fdd2969a | ||
|
|
2f146c9648 | ||
|
|
d7ead4cc3d | ||
|
|
f5b275d0d1 | ||
|
|
5b18502378 | ||
|
|
792fa2e9ca | ||
|
|
c1f4400441 | ||
|
|
18e813b3ed | ||
|
|
5bc66f4c76 | ||
|
|
df7d5e9c8e | ||
|
|
a9c68d2942 | ||
|
|
541afd807e | ||
|
|
598a391c12 | ||
|
|
a9c536322e | ||
|
|
2635c1798d | ||
|
|
92f1596336 | ||
|
|
9d41132be6 | ||
|
|
5c367ef940 | ||
|
|
423d38c94e | ||
|
|
4ad20fd7d6 | ||
|
|
7cff83168f | ||
|
|
7ea109aaf8 | ||
|
|
beb29502a7 | ||
|
|
1fed0a1264 | ||
|
|
d2182ad49c | ||
|
|
bb01e2a4d4 | ||
|
|
b46c0bb05d | ||
|
|
192ef621f6 | ||
|
|
4a43abc905 | ||
|
|
07dfb2e9df | ||
|
|
2f8beff993 | ||
|
|
72ec9a2b70 | ||
|
|
1ce6979910 | ||
|
|
c0794aff76 | ||
|
|
371ed047c6 | ||
|
|
b4cc04f32b | ||
|
|
42596623a9 | ||
|
|
345c809a23 | ||
|
|
fbe169f06d | ||
|
|
4f5fc25195 | ||
|
|
e1f3fb6ac0 | ||
|
|
9915d3c9ac | ||
|
|
0bbed86241 | ||
|
|
5076d03143 | ||
|
|
52b5ef7647 | ||
|
|
e9f8877dec | ||
|
|
ed87267633 | ||
|
|
e4d469a408 | ||
|
|
5c13e8d9aa | ||
|
|
7afdd4cbf5 | ||
|
|
c155317a6b | ||
|
|
a70513985a | ||
|
|
ffc2324dd5 | ||
|
|
9ef975d467 | ||
|
|
fa9c88c7be | ||
|
|
2d6b9b775e | ||
|
|
8a48371aec | ||
|
|
c891a0fa26 | ||
|
|
99728fd237 | ||
|
|
b76c65441b | ||
|
|
4ca8ec005d | ||
|
|
db3c1b22ce | ||
|
|
b0525bf06b | ||
|
|
d884731691 | ||
|
|
a4e4d089f7 | ||
|
|
0b31ee8562 | ||
|
|
aaf8969af2 | ||
|
|
f7c34aa572 | ||
|
|
16e4c11961 | ||
|
|
116003f189 | ||
|
|
6c87de30dd | ||
|
|
60b2601d8c | ||
|
|
ca37bda739 | ||
|
|
c4698212ce | ||
|
|
0e0d4031c9 | ||
|
|
be1f885341 | ||
|
|
a6c8eedfd4 | ||
|
|
32f9600d51 | ||
|
|
03f99c88ae | ||
|
|
47882191bc | ||
|
|
d7962ae659 | ||
|
|
cbd17b0290 | ||
|
|
839325ab70 | ||
|
|
13dc4304fc | ||
|
|
c49a937fb8 | ||
|
|
36d3905d0e | ||
|
|
9ff748dfd5 | ||
|
|
40878794cf | ||
|
|
a17a69c93a | ||
|
|
469ca98b8a | ||
|
|
330cc1cd3a | ||
|
|
5c61819b25 | ||
|
|
2c31c26cb9 | ||
|
|
a4f490b607 | ||
|
|
36007330e9 | ||
|
|
fba890af81 | ||
|
|
619fee0821 | ||
|
|
4f7d3f4e4e | ||
|
|
ac1f1d2de4 | ||
|
|
5856edd962 | ||
|
|
ac463d081e | ||
|
|
dbd1a872c4 | ||
|
|
ff600d652d | ||
|
|
8a512a13a4 | ||
|
|
a9f1a25da4 | ||
|
|
0fd05f2c36 | ||
|
|
aa0754a53f | ||
|
|
d31d0aa735 | ||
|
|
c92b26928c | ||
|
|
70f5f1be30 | ||
|
|
4ae769506e | ||
|
|
73c6b67830 | ||
|
|
f05963c6ea | ||
|
|
d99b687989 | ||
|
|
d57e4bd077 | ||
|
|
e5c2a18bd6 | ||
|
|
796a2b44c8 | ||
|
|
5e67cf8292 | ||
|
|
5e4880f1d1 | ||
|
|
ea161d83fa | ||
|
|
0f28ca779d | ||
|
|
7711af1214 | ||
|
|
9817eb303b | ||
|
|
21d782b4be | ||
|
|
f14d816e6b | ||
|
|
025ee4df18 | ||
|
|
1b2870a1b2 | ||
|
|
03760521ac | ||
|
|
e4d8983280 | ||
|
|
64431e3661 | ||
|
|
ace56ece7e | ||
|
|
135724fab5 | ||
|
|
5d72b7ce98 | ||
|
|
6697a44259 | ||
|
|
85165850c1 | ||
|
|
3bd24d80c0 | ||
|
|
aab31719d4 | ||
|
|
1ffc84663a | ||
|
|
9f703cf64e | ||
|
|
44b1e2a4e5 | ||
|
|
3e7fa3e440 | ||
|
|
20154d8411 | ||
|
|
26b2a2278e | ||
|
|
886901b48e | ||
|
|
a817967aa0 | ||
|
|
52bbfdb9f4 | ||
|
|
8c74096a7f | ||
|
|
153362393b | ||
|
|
3185d0b38b | ||
|
|
0be8b711ac | ||
|
|
9e60002210 | ||
|
|
fe169f267d | ||
|
|
26c09c3993 | ||
|
|
49f516685f | ||
|
|
89ca710932 | ||
|
|
f1d01341cf | ||
|
|
c30beda09d | ||
|
|
4b4d9da7b1 | ||
|
|
32a94fbb37 | ||
|
|
a589857582 | ||
|
|
f9c20c4a91 | ||
|
|
886d07a4c3 | ||
|
|
b1ca421e52 | ||
|
|
1cb583a3ba | ||
|
|
4683318351 | ||
|
|
e17a4756e7 | ||
|
|
7622f7c550 | ||
|
|
e25332ed27 | ||
|
|
1f270a5231 | ||
|
|
cc32286389 | ||
|
|
d78b2f2fc0 | ||
|
|
a406bc94a8 | ||
|
|
765a743d5b | ||
|
|
40a0d3a3e4 | ||
|
|
b612791fc8 | ||
|
|
c4cc51cf8e | ||
|
|
3494d388bb | ||
|
|
88b887c579 | ||
|
|
061f3ce78f | ||
|
|
944d2475dd | ||
|
|
1d1e57a265 | ||
|
|
522b874f54 | ||
|
|
dea2cfad1c | ||
|
|
1c8369d6fd | ||
|
|
98d8d65687 | ||
|
|
c5c9f5a447 | ||
|
|
c42a3ac6e0 | ||
|
|
be60c4f865 | ||
|
|
fd9bb6f36a | ||
|
|
27118a0f7a | ||
|
|
665d4af5a2 | ||
|
|
1fd2aa17d5 | ||
|
|
bd8db95098 | ||
|
|
fcb3f64d9b | ||
|
|
ee36ab94b6 | ||
|
|
ac86437b35 | ||
|
|
30d8d9f3fe | ||
|
|
f2ace5cd3d | ||
|
|
cc1a71f246 | ||
|
|
b3d8eaee7d | ||
|
|
c70861d7d3 | ||
|
|
4cfa915aec | ||
|
|
2bf31add95 | ||
|
|
61526dea82 | ||
|
|
5c719c27b0 | ||
|
|
342207375e | ||
|
|
ce5b26b0d0 | ||
|
|
6056bfbc49 | ||
|
|
4e925b0385 | ||
|
|
edff4117f5 | ||
|
|
64f2d62605 | ||
|
|
1efe09eeca | ||
|
|
2375176d72 | ||
|
|
3bfc7da93a | ||
|
|
34b4e90d75 | ||
|
|
48ab353521 | ||
|
|
73f872bf6e | ||
|
|
f04c8be999 | ||
|
|
d6eb49db68 | ||
|
|
c03ada1e1c | ||
|
|
9ce3d6182b | ||
|
|
ff3f60cc68 | ||
|
|
89aa4bfab3 | ||
|
|
746e60b517 | ||
|
|
8634cc561f | ||
|
|
d384f0896a | ||
|
|
9f5cc344ad | ||
|
|
45bebdff0c | ||
|
|
59ef9e132a | ||
|
|
af8e9c2466 | ||
|
|
e11163f483 | ||
|
|
d3ff20484d | ||
|
|
76cbbb2bfe | ||
|
|
c51ad4aaa6 | ||
|
|
6d5e56f6bf | ||
|
|
251569e81c | ||
|
|
be2759c354 | ||
|
|
69ee8f3630 | ||
|
|
610991a222 | ||
|
|
f08a0eaffd | ||
|
|
a0baa659ec | ||
|
|
536a19b373 | ||
|
|
27a2d7f642 | ||
|
|
fc29cec8c8 | ||
|
|
5fe19d75a6 | ||
|
|
3252489971 | ||
|
|
f90af4784e | ||
|
|
a2f11fb829 | ||
|
|
b6e6d308a9 | ||
|
|
dd04888106 | ||
|
|
0c397a414e | ||
|
|
2a62001048 | ||
|
|
3132e5dd5b | ||
|
|
8843a12333 | ||
|
|
b2e97ac73b | ||
|
|
11c440209a | ||
|
|
400adeed1c | ||
|
|
abcdc7253e | ||
|
|
b6b8b9440f | ||
|
|
c9a64b8e96 | ||
|
|
327795855a | ||
|
|
b3c5494e74 | ||
|
|
9fc4a247aa | ||
|
|
013cd0a9a8 | ||
|
|
05a9da4eb8 | ||
|
|
81d853ac04 | ||
|
|
af8dfa685a | ||
|
|
946a360652 | ||
|
|
071fe9a910 | ||
|
|
ccb4e87002 | ||
|
|
cd9adb166a | ||
|
|
4f95f2c842 | ||
|
|
7e6771d550 | ||
|
|
0b025fd0ae | ||
|
|
9445204ffc | ||
|
|
0b69923ad7 | ||
|
|
00eb90bcc6 | ||
|
|
8478967c04 | ||
|
|
08f4b5bbe6 | ||
|
|
828a5e7f46 | ||
|
|
2e6151fe25 | ||
|
|
c5fad910ba | ||
|
|
8d7c672b94 | ||
|
|
67de9ba209 | ||
|
|
d3486341f3 | ||
|
|
ac12412ec3 | ||
|
|
73e0a4d35a | ||
|
|
1e09e9e3f8 | ||
|
|
d97265f2f2 | ||
|
|
cd55c4077e | ||
|
|
0a9d5b73d8 | ||
|
|
e5e1de83a1 | ||
|
|
359ab4233b | ||
|
|
715f991d0d | ||
|
|
e00b18bf40 | ||
|
|
77869fd273 | ||
|
|
1f4de4af80 | ||
|
|
6d34e04138 | ||
|
|
1761ccffd0 | ||
|
|
4de53b29f0 | ||
|
|
08c02b7473 | ||
|
|
a648e888c5 | ||
|
|
af94789b7f | ||
|
|
c4520a0ac2 | ||
|
|
75bec0a132 | ||
|
|
b680878558 | ||
|
|
3f95ca2869 | ||
|
|
003d7ac606 | ||
|
|
b61267d3bd | ||
|
|
a6728853db | ||
|
|
d95c46e1b8 | ||
|
|
d3a18c55d3 | ||
|
|
8bffadeb14 | ||
|
|
4abb037081 | ||
|
|
07d6cebba4 | ||
|
|
abd929070e | ||
|
|
ea937a410b | ||
|
|
64159bab6f | ||
|
|
164e6656e8 | ||
|
|
0bac699cbf | ||
|
|
2030ffcea3 | ||
|
|
6433eff8ec | ||
|
|
6c51f7c6bc | ||
|
|
0c82e0614c | ||
|
|
acd30aaf08 | ||
|
|
ce2f75d43d | ||
|
|
3f3875cc0c | ||
|
|
e939218a65 | ||
|
|
d929a8b140 | ||
|
|
3e1aef4a58 | ||
|
|
93dad0bf44 | ||
|
|
d5259e8bfb | ||
|
|
440d7fe2b8 | ||
|
|
0e666f07e9 | ||
|
|
1b542e936b | ||
|
|
13487bbeed | ||
|
|
7a71716c7f | ||
|
|
e72baa9dd4 | ||
|
|
4c9f188b0c | ||
|
|
e78f90bc62 | ||
|
|
19d5e8457f | ||
|
|
0fb0837561 | ||
|
|
6cff21f47b | ||
|
|
477408676d | ||
|
|
97b675bc9f | ||
|
|
65dab56ad0 | ||
|
|
faba97a6f5 | ||
|
|
c10a2e1133 | ||
|
|
83d13cc153 | ||
|
|
bdbdd990a7 | ||
|
|
70e4d80965 | ||
|
|
6ec8b15a34 | ||
|
|
ee21afee61 | ||
|
|
08a08855aa | ||
|
|
95ee7ba2f8 | ||
|
|
cde55f37f3 | ||
|
|
5b147d4ab2 | ||
|
|
058ef0c4c5 | ||
|
|
bb48bd22a3 | ||
|
|
0aecd7068f | ||
|
|
0d4a837be6 | ||
|
|
ac70e6504a | ||
|
|
a64f19a686 | ||
|
|
9a12f8ecab | ||
|
|
37d6e5aa3d | ||
|
|
7cbf5ff74e | ||
|
|
b6ec9728d8 | ||
|
|
b5a167df05 | ||
|
|
f01409ab2c | ||
|
|
a96778a94d | ||
|
|
e9e898da79 | ||
|
|
a8d73f6172 | ||
|
|
84a1d9997b | ||
|
|
5aa489d769 | ||
|
|
fd520f2ea8 | ||
|
|
a1a7cb747b | ||
|
|
3d79e77f47 | ||
|
|
f6c6ecd494 | ||
|
|
52cca9532b | ||
|
|
d412bcc718 | ||
|
|
6cb93e51fa | ||
|
|
84bc9205a9 | ||
|
|
92aea19abd | ||
|
|
5dc94fdf0e | ||
|
|
2089450a1d | ||
|
|
eb92fe944c | ||
|
|
3f426f63a1 | ||
|
|
8c3a96db7f | ||
|
|
edd9142f64 | ||
|
|
424b83ddeb | ||
|
|
610e844747 | ||
|
|
a6fbe153b5 | ||
|
|
0c1108b92d | ||
|
|
684b84690c | ||
|
|
df808f5690 | ||
|
|
d960623dac | ||
|
|
d30c95c58c | ||
|
|
8c1139c151 | ||
|
|
3a6adc5503 | ||
|
|
dbc1556381 | ||
|
|
bd85ed3577 | ||
|
|
1340e43f02 | ||
|
|
aee891cd7e | ||
|
|
9f39c1d305 | ||
|
|
43c23059ba | ||
|
|
12c1caf54d | ||
|
|
3848ba311e | ||
|
|
dcb6cbf5fa | ||
|
|
f70e33a3ea | ||
|
|
857d6f87a0 | ||
|
|
d3444621b9 | ||
|
|
05bfc51a20 | ||
|
|
851c593695 | ||
|
|
8ed711ad34 | ||
|
|
e8eaf60c2c | ||
|
|
df480b67c0 | ||
|
|
31e7316854 | ||
|
|
b183588046 | ||
|
|
2620769b19 | ||
|
|
9f4184974e | ||
|
|
e586debb58 | ||
|
|
eccaf83e7d | ||
|
|
a9a6227b8f | ||
|
|
f16251d046 | ||
|
|
27c508caa0 | ||
|
|
aa10845ef0 | ||
|
|
fe4c836cd2 | ||
|
|
a34afb1a81 | ||
|
|
9ee58451b5 | ||
|
|
faa8ca0b34 | ||
|
|
e084f66354 | ||
|
|
c68887f5f5 | ||
|
|
43862a4b7b | ||
|
|
036d672e7c | ||
|
|
abbbae011f | ||
|
|
bbab6a0d68 | ||
|
|
2e492dfc39 | ||
|
|
3d5606cc7b | ||
|
|
553d059113 | ||
|
|
990345cda0 | ||
|
|
1cbf075036 | ||
|
|
6e518df398 | ||
|
|
8e1af41938 | ||
|
|
da2faaeff4 | ||
|
|
66af4ef831 | ||
|
|
cc0fdb302b | ||
|
|
fd1f686469 | ||
|
|
a45a8a6011 | ||
|
|
a40c362764 | ||
|
|
b3c372a803 | ||
|
|
0ffe6051b6 | ||
|
|
3613cb704e | ||
|
|
0733f7a69a | ||
|
|
d514944657 | ||
|
|
5172aaafdb | ||
|
|
d8ec34aa6f | ||
|
|
0257acce1d | ||
|
|
f7746a0da6 | ||
|
|
52605f6a4a | ||
|
|
d865a8ba0c | ||
|
|
2869337f52 | ||
|
|
083f28c89d | ||
|
|
bd78dff68f | ||
|
|
db291eb9c4 | ||
|
|
f155244997 | ||
|
|
fa8d2f0a25 | ||
|
|
f3b0259884 | ||
|
|
258a9c046e | ||
|
|
97bda7a949 | ||
|
|
6b306860ec | ||
|
|
697c41b0d0 | ||
|
|
ee8ec490d7 | ||
|
|
c1674b568a | ||
|
|
2c7d75996e | ||
|
|
579c08a9e8 | ||
|
|
2ba6e616af | ||
|
|
5d32321aa2 | ||
|
|
ccd50fa98b | ||
|
|
69c9cb1244 | ||
|
|
35e1fca22d | ||
|
|
039dfb63ea | ||
|
|
cf53d03104 | ||
|
|
d4e72f07f9 | ||
|
|
08a6af547c | ||
|
|
dd8a526a30 | ||
|
|
298a6b48ad | ||
|
|
08d305ebfa | ||
|
|
e4f0463299 | ||
|
|
b586182160 | ||
|
|
4051da7ee1 | ||
|
|
1705cc21b4 | ||
|
|
4a3e0b244c | ||
|
|
3855fa3568 | ||
|
|
70b0e7d6c0 | ||
|
|
e40f3cb34d | ||
|
|
434deb92f8 | ||
|
|
d962905799 | ||
|
|
bb10ce78a0 | ||
|
|
0670847700 | ||
|
|
c539acb823 | ||
|
|
f233c2683e | ||
|
|
e5a7f5844c | ||
|
|
db1839c402 | ||
|
|
24b54a0937 | ||
|
|
c3f7640c4d | ||
|
|
dab74b7bba | ||
|
|
c0e9bb223e | ||
|
|
0630658d4d | ||
|
|
3094850579 | ||
|
|
7b29e2e603 | ||
|
|
53784e154d | ||
|
|
20e3bbf288 | ||
|
|
43d016c288 | ||
|
|
320cc45fc8 | ||
|
|
a1831ab338 | ||
|
|
6f57f56d1d | ||
|
|
41062dfc47 | ||
|
|
e6628d4569 | ||
|
|
7c389dbf8e | ||
|
|
651f7cc7cd | ||
|
|
bd70662314 | ||
|
|
9a45a84663 | ||
|
|
5b12d0e3f6 | ||
|
|
d9e1121796 | ||
|
|
b0f37ca217 | ||
|
|
7459f7a9fa | ||
|
|
3f3f2f2c18 | ||
|
|
42f2b0a8e3 | ||
|
|
8326dc1dc7 | ||
|
|
cc68c5cfcb | ||
|
|
9e46cc2b0b | ||
|
|
2fc827fcd7 | ||
|
|
8108bdc91b | ||
|
|
345f6fc2af | ||
|
|
af06cb5cdd | ||
|
|
ba74b333cd | ||
|
|
895d282cc8 | ||
|
|
33831ee7cc | ||
|
|
8aaea65a72 | ||
|
|
e20ba2c0a0 | ||
|
|
e276950caa | ||
|
|
a17663655b | ||
|
|
9bb5a69b56 | ||
|
|
b9afd98189 | ||
|
|
89da789a99 | ||
|
|
f78f8006d6 | ||
|
|
46c79b4213 | ||
|
|
e4fb8c20ac | ||
|
|
5a8762edb1 | ||
|
|
6285c76935 | ||
|
|
77942b632f | ||
|
|
29261c34d5 | ||
|
|
2152e5f731 | ||
|
|
b659833f71 | ||
|
|
f7d626d8c6 | ||
|
|
f1c8ace081 | ||
|
|
655015d33b | ||
|
|
ec24a92987 | ||
|
|
04237c6118 | ||
|
|
86f13e05d2 | ||
|
|
9a6146e1ba | ||
|
|
0a8ce34252 | ||
|
|
18529f47e7 | ||
|
|
98215be0fc | ||
|
|
f2698ef38d | ||
|
|
215ee66a02 | ||
|
|
e055657d13 | ||
|
|
ec7f47dbe7 | ||
|
|
e3e7e4d26a | ||
|
|
118d7d735a | ||
|
|
d7a8534354 | ||
|
|
9fe86bf72a | ||
|
|
1d76f89428 | ||
|
|
fc26af5a89 | ||
|
|
7f85a8f2e1 | ||
|
|
e3ff8029de | ||
|
|
70f5bdd339 | ||
|
|
2ecc36ab12 | ||
|
|
a4c14c6be1 | ||
|
|
6cbda005e8 | ||
|
|
8ea99648f0 | ||
|
|
58a5d9b0e1 | ||
|
|
b969f2eac6 | ||
|
|
5c295e9080 | ||
|
|
64a6c3c5bb | ||
|
|
22cc4c635a | ||
|
|
b5b96f1155 | ||
|
|
06b887cd93 | ||
|
|
c6ef376d15 | ||
|
|
6ef249195e | ||
|
|
f6e99cf748 | ||
|
|
6d39088777 | ||
|
|
e5cb026608 | ||
|
|
ce9dbc2b5f | ||
|
|
bfef677e93 | ||
|
|
0d18a1d19a | ||
|
|
91b3b7194d | ||
|
|
306d515e7f | ||
|
|
0a21e03eac | ||
|
|
e112704a63 | ||
|
|
c0bbf107c6 | ||
|
|
84a6876265 | ||
|
|
add9ffe5d9 | ||
|
|
c71f0b7e39 | ||
|
|
e934071716 | ||
|
|
5693cddc3a | ||
|
|
561d27be0a | ||
|
|
2e19a2a99c | ||
|
|
4055e421bc | ||
|
|
792255b676 | ||
|
|
0ddb8d9644 | ||
|
|
5c29555ae7 | ||
|
|
cf5e6bde9a | ||
|
|
515ddbb28d | ||
|
|
18720e3693 | ||
|
|
6b21664dff | ||
|
|
ca92f90f6d | ||
|
|
8a1ab40f3c | ||
|
|
f738822cce | ||
|
|
2ef900443b | ||
|
|
917c6b7c54 | ||
|
|
966db8af9f | ||
|
|
07b26f03a7 | ||
|
|
d685997040 | ||
|
|
fce6807a3b | ||
|
|
e91b428bd5 | ||
|
|
6581466239 | ||
|
|
380a5bbadd | ||
|
|
4da83ff5f8 | ||
|
|
d5cf8848fa | ||
|
|
4ea6f9dd6a | ||
|
|
d8027b002d | ||
|
|
623cda83d5 | ||
|
|
99b3e4493b | ||
|
|
025a201a7d | ||
|
|
3243555181 | ||
|
|
867eae442b | ||
|
|
e8e80a7ac8 | ||
|
|
480c88a73e | ||
|
|
6f34a42034 | ||
|
|
312421ed28 | ||
|
|
93e81fdc01 | ||
|
|
2d7f0f907b | ||
|
|
6aca67efa6 | ||
|
|
bab2b9f528 | ||
|
|
07886140fb | ||
|
|
90341ee27a | ||
|
|
f78da4f637 | ||
|
|
d3b8ffe328 | ||
|
|
e0b25527b3 | ||
|
|
56798a76c9 | ||
|
|
2faab75bd8 | ||
|
|
d4d7c5b35e | ||
|
|
bf07df8f7b | ||
|
|
3622103a90 | ||
|
|
31680b3849 | ||
|
|
aa7611fceb | ||
|
|
9f0a34455b | ||
|
|
da51f8af37 | ||
|
|
170853f2cc | ||
|
|
df4b625ac4 | ||
|
|
2d6ff3b74c | ||
|
|
a5b3b61052 | ||
|
|
1b1a56e77c | ||
|
|
e18dd07491 | ||
|
|
f94771730e | ||
|
|
cb24df8890 | ||
|
|
499d72d5d6 | ||
|
|
1bab2617b7 | ||
|
|
b5ed2daa81 | ||
|
|
c32cd456b6 | ||
|
|
0f9a71f07d | ||
|
|
a1711daead | ||
|
|
5b6031f41d | ||
|
|
c3787ab02f | ||
|
|
9d1a216a20 | ||
|
|
2603d08aa4 | ||
|
|
990c0afee2 | ||
|
|
1bfd401ed7 | ||
|
|
22414bab7c | ||
|
|
08222b0d5c | ||
|
|
d599d29514 | ||
|
|
e5120c7074 | ||
|
|
68f36bf172 | ||
|
|
ee025f9a13 | ||
|
|
68891d18f4 | ||
|
|
be4b7e7232 | ||
|
|
c3e03c17e8 | ||
|
|
1dc63294b4 | ||
|
|
63191515d3 | ||
|
|
f8b072f14a | ||
|
|
d09862522b | ||
|
|
ce5240223b | ||
|
|
5b8d2cb36a | ||
|
|
d9b35ca1ee | ||
|
|
e8433a1d80 | ||
|
|
092aac82ea | ||
|
|
9039178927 | ||
|
|
a8b2a627f8 | ||
|
|
da8b85decd | ||
|
|
73786bfd94 | ||
|
|
e06ea82f62 | ||
|
|
085732868c | ||
|
|
3f7c512c63 | ||
|
|
28d8b49de7 | ||
|
|
c79d04f29a | ||
|
|
d1ba022ce9 | ||
|
|
d36fb16b12 | ||
|
|
26e94d3ba6 | ||
|
|
d2cd5e7202 | ||
|
|
0bfd5cc292 | ||
|
|
deb25f8299 | ||
|
|
aab28fd3fd | ||
|
|
d9759305a3 | ||
|
|
e015e57dda | ||
|
|
44a2f1880d | ||
|
|
2e3617ad1c | ||
|
|
ae5e7d1e60 | ||
|
|
a31c3dc611 | ||
|
|
2ee3ec303d | ||
|
|
3d8a85a145 | ||
|
|
fc18153f02 | ||
|
|
77c1875b6b | ||
|
|
4388a84787 | ||
|
|
0d3a9c8b16 | ||
|
|
2ca05b5634 | ||
|
|
4569bca39d | ||
|
|
c401a609a4 | ||
|
|
07a71f1059 | ||
|
|
82f6f7d4d5 | ||
|
|
97f68a8247 | ||
|
|
1d9f867c75 | ||
|
|
ebd3c33051 | ||
|
|
00cec4faa0 | ||
|
|
690fa7bf78 | ||
|
|
ace6474157 | ||
|
|
974a017044 | ||
|
|
89917d4f75 | ||
|
|
f651364e00 | ||
|
|
2a8b43b7a3 | ||
|
|
aff991690d | ||
|
|
7f095947ab | ||
|
|
b18b2cff44 | ||
|
|
befd16238b | ||
|
|
20d3e540b0 | ||
|
|
cede2927c5 | ||
|
|
543aae0893 | ||
|
|
e9c55345b8 | ||
|
|
8c7dc08844 | ||
|
|
3ae20885c4 | ||
|
|
09042f1dbf | ||
|
|
6b5c52d2c8 | ||
|
|
e02e2c462b | ||
|
|
30a1ea7a4f | ||
|
|
51a2383574 | ||
|
|
7fbbb9b030 | ||
|
|
f2d88796ed | ||
|
|
1659ca88ca | ||
|
|
fd7ba65203 | ||
|
|
ae66502fd5 | ||
|
|
cc06fb6b40 | ||
|
|
a60c6af30e | ||
|
|
91e7cd6576 | ||
|
|
d717a68445 | ||
|
|
7f4dcf54a7 | ||
|
|
613238e0f9 | ||
|
|
72f7189aeb | ||
|
|
ea2616434c | ||
|
|
0261f25996 | ||
|
|
f5db79ce69 | ||
|
|
236a7c91d5 | ||
|
|
c6bae2116a | ||
|
|
05d42d9518 | ||
|
|
19a6236fa9 | ||
|
|
115dbc8b12 | ||
|
|
03084c3ce3 | ||
|
|
0260afa068 | ||
|
|
2f9044f53f | ||
|
|
f195e85fb8 | ||
|
|
65065343f9 | ||
|
|
50c8014977 | ||
|
|
911d0a4fae | ||
|
|
d1bb81e404 | ||
|
|
76526a0b2d | ||
|
|
a9020d92ca | ||
|
|
74be6e6c14 | ||
|
|
f203168542 | ||
|
|
3c03f5fd71 | ||
|
|
8cfc9d7793 | ||
|
|
ec5c500b8f | ||
|
|
8f7cbeb4ab | ||
|
|
fb32273477 | ||
|
|
1be49e8d8a | ||
|
|
b7edc01952 | ||
|
|
0470878ddf | ||
|
|
d07a65d553 | ||
|
|
58218f10da | ||
|
|
690eebb6a1 | ||
|
|
9bd9936781 | ||
|
|
bf62325329 | ||
|
|
a3775e0b57 | ||
|
|
03c0794b48 | ||
|
|
b5edcaf5ac | ||
|
|
3723a4e656 | ||
|
|
b332038722 | ||
|
|
33df8dd5eb | ||
|
|
7e982b327b | ||
|
|
91585464bd | ||
|
|
3c3f494034 | ||
|
|
87cbc6229a | ||
|
|
bd10364d27 | ||
|
|
36721dde56 | ||
|
|
6a74bd841a | ||
|
|
093b5451c3 | ||
|
|
896dff3fdc | ||
|
|
e8e23603c3 | ||
|
|
426fe0fe40 | ||
|
|
3e2ef43a0a | ||
|
|
a73b480b1f | ||
|
|
4d374dd587 | ||
|
|
3b97f8c396 | ||
|
|
e7a559327c | ||
|
|
ede35abe1a | ||
|
|
edc36f915d | ||
|
|
05a9d457aa | ||
|
|
cc7ce6bae9 | ||
|
|
57e5135346 | ||
|
|
74980f84d5 | ||
|
|
fa7f1cc21a | ||
|
|
8c90fb3c15 | ||
|
|
021a48c070 | ||
|
|
8be583083e | ||
|
|
8d15a7249a | ||
|
|
6259793f39 | ||
|
|
772faf8cd2 | ||
|
|
dd7141aeaa | ||
|
|
521a3f9341 | ||
|
|
452bdd7b4f | ||
|
|
37ebb5731d | ||
|
|
933ce64cce | ||
|
|
b8d0019ece | ||
|
|
4df3da0856 | ||
|
|
59f482e52e | ||
|
|
538ea82899 | ||
|
|
0b7fc67c3f | ||
|
|
7a92712f0b | ||
|
|
6ed9a8b336 | ||
|
|
24df4e78c7 | ||
|
|
a0abaf62b4 | ||
|
|
85ece1ec63 | ||
|
|
ba284aafe3 | ||
|
|
f25ab2f892 | ||
|
|
b026111ef8 | ||
|
|
9850f2e242 | ||
|
|
9313fa9193 | ||
|
|
f19c9183f0 | ||
|
|
5af3384b02 | ||
|
|
46a5512209 | ||
|
|
63c83e97c4 | ||
|
|
cf311fdeb1 | ||
|
|
04ef82bc70 | ||
|
|
7182ad2e9c | ||
|
|
072508e1c1 | ||
|
|
91101a7c40 | ||
|
|
d34fa71797 | ||
|
|
bee7bccc12 | ||
|
|
74eea096b1 | ||
|
|
02c028584b | ||
|
|
2a87d338b4 | ||
|
|
8759532964 | ||
|
|
f332db93ad | ||
|
|
6ffc687174 | ||
|
|
ba6079164c | ||
|
|
372ee945de | ||
|
|
0f0eed3de6 | ||
|
|
9eddb37c3e | ||
|
|
6a262ea604 | ||
|
|
94693f530e | ||
|
|
ee9eced602 | ||
|
|
58fa9b8182 | ||
|
|
89b377ab8f | ||
|
|
1d93fccbb3 | ||
|
|
f8786bcd47 | ||
|
|
8adf2043f7 | ||
|
|
8af3fe3db8 | ||
|
|
09c1db9afe | ||
|
|
2f738ff2b7 | ||
|
|
4429e36c3f | ||
|
|
0b8a3cfcea | ||
|
|
c3d43f6021 | ||
|
|
2a5badbc7c | ||
|
|
3c5f24f99d | ||
|
|
105212a667 | ||
|
|
f5decd2221 | ||
|
|
48bd65cd3b | ||
|
|
90046b11af | ||
|
|
6eede614f8 | ||
|
|
8cd9c5e8a3 | ||
|
|
0ef1019d1c | ||
|
|
f2a55e525b | ||
|
|
0f9eecd821 | ||
|
|
a45848cbe3 | ||
|
|
07af7d7f97 | ||
|
|
87186ca940 | ||
|
|
990bd7e291 | ||
|
|
c39aa35d36 | ||
|
|
3b187d75bb | ||
|
|
84694773a2 | ||
|
|
e8d9083f05 | ||
|
|
25ccebe617 | ||
|
|
c6d3d07b3c | ||
|
|
c6078f9314 | ||
|
|
db5462b79f | ||
|
|
3b8e6a2c3a | ||
|
|
eae7e79ecf | ||
|
|
5264873645 | ||
|
|
6009f097cd | ||
|
|
96265e5ac1 | ||
|
|
5ee33a35fc | ||
|
|
55f3c484f2 | ||
|
|
0bd678318c | ||
|
|
89ab049391 | ||
|
|
aa6e4c5b53 | ||
|
|
ec8a75be52 | ||
|
|
3ae7fe7f04 | ||
|
|
3e4b5830be | ||
|
|
8ea28dad68 | ||
|
|
17c1fc79e1 | ||
|
|
801eaaaf37 | ||
|
|
bfd72d7540 | ||
|
|
1341ef0c3e | ||
|
|
95bda0d4bb | ||
|
|
adf412ea74 | ||
|
|
a00a6734b3 | ||
|
|
e410eab336 | ||
|
|
1367357e68 | ||
|
|
4cb4b3bb19 | ||
|
|
ce92ab77ec | ||
|
|
e365fc60bc | ||
|
|
edb1343676 | ||
|
|
f1426103ff | ||
|
|
7a767327c0 | ||
|
|
e2f9139aa9 | ||
|
|
a80b504379 | ||
|
|
d73b3c44e3 | ||
|
|
c05f54c105 | ||
|
|
c917bafde5 | ||
|
|
b349106f08 | ||
|
|
1c91f345fc | ||
|
|
2b11afbb2d | ||
|
|
e9479dd24f | ||
|
|
d676412291 | ||
|
|
5fd5d032ef | ||
|
|
66ee09f656 | ||
|
|
8f57a55286 | ||
|
|
6498250114 | ||
|
|
bf7b9a7057 | ||
|
|
8de7099c0f | ||
|
|
c2b3c4919d | ||
|
|
90b5531041 | ||
|
|
e57de3268f | ||
|
|
b9e68834de | ||
|
|
5352514c2b | ||
|
|
aa58b3df35 | ||
|
|
dfe5a90b06 | ||
|
|
893486eb93 | ||
|
|
bdc684d2d1 | ||
|
|
80bc435df6 | ||
|
|
00cbfa45d7 | ||
|
|
5c18270826 | ||
|
|
aa86ef631b | ||
|
|
c67a2bd458 | ||
|
|
f796ec32a7 | ||
|
|
9ce43ea7a6 | ||
|
|
19702794e4 | ||
|
|
9015bfcac3 | ||
|
|
ad13ef9187 | ||
|
|
4a1b147a25 | ||
|
|
5970908a3f | ||
|
|
e3d37ca831 | ||
|
|
4c9afb8c80 | ||
|
|
253049ec5a | ||
|
|
c1d7f86542 | ||
|
|
aa44fba1e5 | ||
|
|
5dbf2b0ff5 | ||
|
|
8fcbaef064 | ||
|
|
08c3509823 | ||
|
|
d9961bc1a0 | ||
|
|
9fd8c1f546 | ||
|
|
d265102f31 | ||
|
|
063a50b14a | ||
|
|
ebb06b34b0 | ||
|
|
bbc3a75e4a | ||
|
|
86c5ef363e | ||
|
|
daefcab8f5 | ||
|
|
3d5f9beac5 | ||
|
|
0b01855bf4 | ||
|
|
cc78904582 | ||
|
|
306b4652eb | ||
|
|
721a629ba8 | ||
|
|
4f00aeeed7 | ||
|
|
88ca671965 | ||
|
|
7c3cfe1326 | ||
|
|
735f1007cf | ||
|
|
66df063e2a | ||
|
|
e7547905a3 | ||
|
|
206b01376d | ||
|
|
e50ef893e8 | ||
|
|
496cc470e2 | ||
|
|
b82516ba4d | ||
|
|
bbe7ff3c9b | ||
|
|
d33770f107 | ||
|
|
6523500d09 | ||
|
|
1a7a5fe6a8 | ||
|
|
9426c531e3 | ||
|
|
35a6977df8 | ||
|
|
c06280a1ea | ||
|
|
8b3ff137d1 | ||
|
|
0fa826f926 | ||
|
|
5c7b40248c | ||
|
|
7a3a8b0490 | ||
|
|
78d13749d5 | ||
|
|
1143a43d30 | ||
|
|
3cdf553142 | ||
|
|
f9569895b4 | ||
|
|
905b469515 | ||
|
|
aac3443b78 | ||
|
|
bd9b05463e | ||
|
|
bb82c6fa02 | ||
|
|
7232f9446a | ||
|
|
639f2b8e02 | ||
|
|
7d860542b1 | ||
|
|
7593dad6c8 | ||
|
|
ecc4834110 | ||
|
|
2e1adc6f9b | ||
|
|
18a0866735 | ||
|
|
4036fc44e3 | ||
|
|
9f7bbd6efc | ||
|
|
1ff11a09ed | ||
|
|
be3d4723ef | ||
|
|
190703d74b | ||
|
|
eac361d892 | ||
|
|
a772cf9910 | ||
|
|
77eab9e6f8 | ||
|
|
c4fc168369 | ||
|
|
41409fb5c1 | ||
|
|
71eb88eb7b | ||
|
|
dcbde94d78 | ||
|
|
11bbcf5b42 | ||
|
|
badf68b80a | ||
|
|
01e1c2ab0a | ||
|
|
bef51732e0 | ||
|
|
77046cf38a | ||
|
|
2a623957c3 | ||
|
|
65322925a4 | ||
|
|
346c4db3d9 | ||
|
|
7212698863 | ||
|
|
1779544d70 | ||
|
|
0270420da8 | ||
|
|
a1461851ee | ||
|
|
eaabad923e | ||
|
|
a9e12f5aca | ||
|
|
0637d5511d | ||
|
|
27682b2d07 | ||
|
|
0982852563 | ||
|
|
8147a315c2 | ||
|
|
fdf787e9b4 | ||
|
|
01d2338388 | ||
|
|
1dfec40f49 | ||
|
|
a82602faae | ||
|
|
c2aac371ae | ||
|
|
9aa6e0d39a | ||
|
|
3c177c4ebf | ||
|
|
d8b090bae2 | ||
|
|
928792bea7 | ||
|
|
99775194fd | ||
|
|
beb73828e0 | ||
|
|
87dc72b874 | ||
|
|
528ae68282 | ||
|
|
a44031cd39 | ||
|
|
af922d5171 | ||
|
|
bd6599a309 | ||
|
|
2e062b1ad6 | ||
|
|
5fc6654045 | ||
|
|
32c3494098 | ||
|
|
379461820b | ||
|
|
424cb7a9f2 | ||
|
|
b41e2b6f51 | ||
|
|
84e73f6f52 | ||
|
|
39fb42db10 | ||
|
|
85c18ce07b | ||
|
|
3fe548d8b7 | ||
|
|
eb32222d39 | ||
|
|
e70d7ab5c1 | ||
|
|
a3da735087 | ||
|
|
8c12b5a47e | ||
|
|
65c27c6c93 | ||
|
|
4c31e1a4b5 | ||
|
|
7be776d417 | ||
|
|
1a93e8f331 | ||
|
|
b6107f79dd | ||
|
|
fb0acb6a8b | ||
|
|
240a898fe5 | ||
|
|
3aad5916f5 | ||
|
|
c2a7f19d38 | ||
|
|
0a5fe2e2af | ||
|
|
f6e5256ae4 | ||
|
|
46aefe6b8a | ||
|
|
af7ef111b4 | ||
|
|
c918743187 | ||
|
|
817a691248 | ||
|
|
04e33448e0 | ||
|
|
aa4624e344 | ||
|
|
1b7a819a2b | ||
|
|
096036f2a4 | ||
|
|
b488ff053a | ||
|
|
04f24faf32 | ||
|
|
c180343853 | ||
|
|
da0739ab21 | ||
|
|
aa1d2a1879 | ||
|
|
9d9ef0069a | ||
|
|
50790b94a3 | ||
|
|
59b7f9c775 | ||
|
|
0f83f23052 | ||
|
|
1da95433ae | ||
|
|
d0579b5f75 | ||
|
|
1d19f56de8 | ||
|
|
3688e13137 | ||
|
|
45d71d0c5f | ||
|
|
d8be84b617 | ||
|
|
f7c4fe8e91 | ||
|
|
9005598f90 | ||
|
|
5e508770ef | ||
|
|
8331f92775 | ||
|
|
4c3c7e3807 | ||
|
|
1e39e3e815 | ||
|
|
5f6519d4c6 | ||
|
|
e93c35bcae | ||
|
|
a9d522d1d9 | ||
|
|
44ac42a4b5 | ||
|
|
78a60c750a | ||
|
|
6012b06a38 | ||
|
|
3ff370a903 | ||
|
|
6147d64a74 | ||
|
|
91b839dd26 | ||
|
|
cdb6d10519 | ||
|
|
586abb5466 | ||
|
|
710ac14202 | ||
|
|
a8735d1b4c | ||
|
|
f9ede752f2 | ||
|
|
5bdb205168 | ||
|
|
b606b939a6 | ||
|
|
0e0b12dcc0 | ||
|
|
b9e7b1f29b | ||
|
|
f930f2aa80 | ||
|
|
8698f7e4ab | ||
|
|
40b62c09f7 | ||
|
|
8ddc3ae172 | ||
|
|
b3e2fb4046 | ||
|
|
42558f2bd4 | ||
|
|
1e23b0a17e | ||
|
|
5b6e0d5c10 | ||
|
|
a53de75873 | ||
|
|
b3dc4671d0 | ||
|
|
ef4dd57032 | ||
|
|
ad1e17f329 | ||
|
|
2ba53e2dab | ||
|
|
909b8e7127 | ||
|
|
8280401e10 | ||
|
|
6096fc1c92 | ||
|
|
0378d54acc | ||
|
|
d33b642b8e | ||
|
|
27d68bcf45 | ||
|
|
b0122a8e38 | ||
|
|
96cbb63c84 | ||
|
|
bd16899c56 | ||
|
|
b874a2b76d | ||
|
|
aaf16273a9 | ||
|
|
f83e03e775 | ||
|
|
5328e443db | ||
|
|
ed49943039 | ||
|
|
220fdcdc75 | ||
|
|
a1b2a5149d | ||
|
|
3090a991a6 | ||
|
|
e0cae27f2f | ||
|
|
21c7c74b31 | ||
|
|
441693ac36 | ||
|
|
77910d82fc | ||
|
|
05258a5b48 | ||
|
|
387abd1606 | ||
|
|
3e4efc22a6 | ||
|
|
2851e51af8 | ||
|
|
ad8ad35c03 | ||
|
|
20d27bb9b3 | ||
|
|
30e878cbf9 | ||
|
|
374f6fcc1b | ||
|
|
4ea7d68bff | ||
|
|
88491f61e2 | ||
|
|
abe710f3ab | ||
|
|
3e4e87db23 | ||
|
|
e7ca1a13a6 | ||
|
|
aca78410f6 | ||
|
|
989331cbb4 | ||
|
|
7a8267a772 | ||
|
|
37ca383360 | ||
|
|
90eb55fbf8 | ||
|
|
e96c503b11 | ||
|
|
99ce6cf29d | ||
|
|
e14952607d | ||
|
|
de3efa0fe7 | ||
|
|
9af123523a | ||
|
|
7bc5adbde2 | ||
|
|
5287978e2f | ||
|
|
d364cc0f51 | ||
|
|
e7c30da23c | ||
|
|
1e6467cf80 | ||
|
|
12452c9b2a | ||
|
|
92b985d24d | ||
|
|
a0f640c739 | ||
|
|
46ecbc9c42 | ||
|
|
96f97e41e1 | ||
|
|
5726f05531 | ||
|
|
b389209c05 | ||
|
|
a82b3fd88a | ||
|
|
04a125d20f | ||
|
|
fc2e93a57f | ||
|
|
cfe9c17b58 | ||
|
|
b24f31f98c | ||
|
|
5263edc3e2 | ||
|
|
99b80cccad | ||
|
|
9794c5a188 | ||
|
|
2bd19736f4 | ||
|
|
13376d0ced | ||
|
|
1aa8475295 | ||
|
|
4bffe22f68 | ||
|
|
094941ab8c | ||
|
|
1a1bcc8d5c | ||
|
|
e77eb8a563 | ||
|
|
c063ccfa02 | ||
|
|
b1ccb5c3de | ||
|
|
29b4704c4b | ||
|
|
931d7d55dd | ||
|
|
2df0403c94 | ||
|
|
9cd878bad1 | ||
|
|
4c816eabc1 | ||
|
|
d28cbf3863 | ||
|
|
6269719b70 | ||
|
|
8a347e71ed | ||
|
|
9c44662f2e | ||
|
|
884291a8a3 | ||
|
|
97428909a4 | ||
|
|
a0bb52ff5f | ||
|
|
1a766c3ccc | ||
|
|
19bea70dc0 | ||
|
|
322a7020e3 | ||
|
|
242444ecc9 | ||
|
|
4820ebba84 | ||
|
|
7658481db2 | ||
|
|
f41644bc97 | ||
|
|
2163eb5910 | ||
|
|
ffbaf1e54f | ||
|
|
76ac47bbc6 | ||
|
|
d499f1af65 | ||
|
|
a249eeab22 | ||
|
|
712e7dbeb8 | ||
|
|
22571acd0c | ||
|
|
eba8d0bd80 | ||
|
|
4b52dea65a | ||
|
|
a5eeaae0d0 | ||
|
|
0d85eb6b90 | ||
|
|
3abb226b5c | ||
|
|
d38fdfb291 | ||
|
|
a63361302d | ||
|
|
069dbce000 | ||
|
|
5970c575c8 | ||
|
|
6ba659ff55 | ||
|
|
2c9f0e3c7d | ||
|
|
06359b9d06 | ||
|
|
7f31fd5092 | ||
|
|
01cdd82cc5 | ||
|
|
c6094cf3ee | ||
|
|
2ba7da0486 | ||
|
|
dcc0295b4b | ||
|
|
8632b0ffec | ||
|
|
bc0b69857a | ||
|
|
198d48fd84 | ||
|
|
e85d6bebd0 | ||
|
|
0679d9a30f | ||
|
|
b0584d4499 | ||
|
|
51bfc37a27 | ||
|
|
c979b27653 | ||
|
|
184f0fad99 | ||
|
|
f9b88c2738 | ||
|
|
f47a679dda | ||
|
|
e5e56b5260 | ||
|
|
c62f6b41d2 | ||
|
|
6b32c4997d | ||
|
|
cb89c96dac | ||
|
|
b31ca263fd | ||
|
|
a0451cfab0 | ||
|
|
4baecd9f79 | ||
|
|
381851aba7 | ||
|
|
23d3d5973b | ||
|
|
fcdf9cf2ec | ||
|
|
9831236d79 | ||
|
|
c644d6bf2f | ||
|
|
d82b008c33 | ||
|
|
d53f9e99af | ||
|
|
870b168cea | ||
|
|
094e2478f9 | ||
|
|
110b9f5058 | ||
|
|
d959753641 | ||
|
|
027ecf29ae | ||
|
|
d4392635bf | ||
|
|
c72370eff0 | ||
|
|
6b799be7dd | ||
|
|
244d5bba47 | ||
|
|
d539ddf018 | ||
|
|
f279792273 | ||
|
|
aa4a793566 | ||
|
|
1aaa2340f8 | ||
|
|
329247391b | ||
|
|
3fc5daef01 | ||
|
|
e087d04c17 | ||
|
|
a3a713b608 | ||
|
|
98251a8631 | ||
|
|
b147a16487 | ||
|
|
411b86197a | ||
|
|
8acab6b662 | ||
|
|
dba550550c | ||
|
|
74cfc733dd | ||
|
|
23981accd1 | ||
|
|
209f34124e | ||
|
|
14cff4facf | ||
|
|
dac089b0fb | ||
|
|
52aeda7faf | ||
|
|
5273f5c9b6 | ||
|
|
d2a30ea049 | ||
|
|
716ba3eea8 | ||
|
|
5cece53e50 | ||
|
|
ba73f8a323 | ||
|
|
81379a49a7 | ||
|
|
77e4862b3e | ||
|
|
1a3bd2aa74 | ||
|
|
9997b4a2de | ||
|
|
74b12606ef | ||
|
|
14d5034b8e | ||
|
|
8b282818e9 | ||
|
|
48a4f8ba67 | ||
|
|
1dd048eed4 | ||
|
|
e398d6af75 | ||
|
|
39b1c1f7e4 | ||
|
|
ff78b384ac | ||
|
|
d45a6f94f5 | ||
|
|
48fe8d5762 | ||
|
|
36b493f03a | ||
|
|
670ccaf891 | ||
|
|
d16a48a1a2 | ||
|
|
3f4eedbeab | ||
|
|
ef3dd4a833 | ||
|
|
fee074db42 | ||
|
|
5463af7f60 | ||
|
|
b776cd4a91 | ||
|
|
45945e839f | ||
|
|
043ad7e3aa | ||
|
|
d04e186dea | ||
|
|
4253365e03 | ||
|
|
07437b7880 | ||
|
|
5ddd007f67 | ||
|
|
814978a178 | ||
|
|
5350e77125 | ||
|
|
22e2e32377 | ||
|
|
58b2c36498 | ||
|
|
0e4af7dbcf | ||
|
|
3aba1c5ccf | ||
|
|
44af80f336 | ||
|
|
4591f5b372 | ||
|
|
2270b2a224 | ||
|
|
4eb68aae77 | ||
|
|
c92e3cca6b | ||
|
|
13ba0121d7 | ||
|
|
be4943a9d6 | ||
|
|
9484e3b52d | ||
|
|
ef731703e8 | ||
|
|
11b86806de | ||
|
|
b872f59a01 | ||
|
|
b2fc1cc1f2 | ||
|
|
6907c9c550 | ||
|
|
93701585f8 | ||
|
|
50e596e63a | ||
|
|
51ece30c34 | ||
|
|
110a078074 | ||
|
|
85b222d9e2 | ||
|
|
c974d4f30a | ||
|
|
75916a21a6 | ||
|
|
8e2fc40105 | ||
|
|
f81bd857ed | ||
|
|
e7ac05db75 | ||
|
|
eb3f526a96 | ||
|
|
371b0bdce1 | ||
|
|
06e70e7476 | ||
|
|
a748fe6610 | ||
|
|
f43dd4d3dc | ||
|
|
9c01b18922 | ||
|
|
932fd032e8 | ||
|
|
c392deaf38 | ||
|
|
c575c1bea3 | ||
|
|
38ec5efa32 | ||
|
|
70764e73c3 | ||
|
|
03fd7fba79 | ||
|
|
33d56a287b | ||
|
|
c3523f93aa | ||
|
|
26f08c7a30 | ||
|
|
a3266cda45 | ||
|
|
80052fabe8 | ||
|
|
3819a845f0 | ||
|
|
d66c0f2d61 | ||
|
|
0bf9da71ab | ||
|
|
a07ff51c68 | ||
|
|
48faabee49 | ||
|
|
d2f9eeab12 | ||
|
|
c8ba4f0379 | ||
|
|
fba0fe1866 | ||
|
|
9685b92bc9 | ||
|
|
3ae9779eec | ||
|
|
ebbdc46305 | ||
|
|
4fb1c866da | ||
|
|
431d5ef8a8 | ||
|
|
174d1f7838 | ||
|
|
b57ecea305 | ||
|
|
64f39dcb79 | ||
|
|
86149145f6 | ||
|
|
f3c41e9a66 | ||
|
|
2a6723f8db | ||
|
|
2bb8d8b52a | ||
|
|
ac86cf19a0 | ||
|
|
ae29fef6d4 | ||
|
|
54486cb5be | ||
|
|
22e41df8ea | ||
|
|
d6dfc2ffb6 | ||
|
|
cb9d0ce3ed | ||
|
|
cef5117e29 | ||
|
|
466af94499 | ||
|
|
ab9dea6930 | ||
|
|
cc2c585a55 | ||
|
|
f9b1a59368 | ||
|
|
23cbe917c2 | ||
|
|
971f9694da | ||
|
|
32f057fa26 | ||
|
|
8c7edaaca4 | ||
|
|
205323200a | ||
|
|
9b880eb669 | ||
|
|
3ce53cedc8 | ||
|
|
37e44968dc | ||
|
|
4fb58bc005 | ||
|
|
b24832dd84 | ||
|
|
57aa32f0c1 | ||
|
|
60c20cc628 | ||
|
|
eaae999878 | ||
|
|
f0af71d8c0 | ||
|
|
b65fed7a0d | ||
|
|
364205cd9b | ||
|
|
081ea769a1 | ||
|
|
7fef69635f | ||
|
|
e3c3315873 | ||
|
|
bb0218d3c6 | ||
|
|
4e3e721c5a | ||
|
|
79321a0ffb | ||
|
|
230644141a | ||
|
|
480fa113b7 | ||
|
|
9016d937a3 | ||
|
|
a361576f8a | ||
|
|
bee695afd4 | ||
|
|
f3fcdf808f | ||
|
|
8e469493ac | ||
|
|
872df9ee70 | ||
|
|
69806f1260 | ||
|
|
0a24feab13 | ||
|
|
e40766583e | ||
|
|
fd3fb5041b | ||
|
|
8ffcaf328e | ||
|
|
5381e54469 | ||
|
|
f571685648 | ||
|
|
701d363e3c | ||
|
|
afa697031c | ||
|
|
7239c98a4c | ||
|
|
48c0297fe7 | ||
|
|
a02be14f1a | ||
|
|
a0b8c758b4 | ||
|
|
97a8029f74 | ||
|
|
4d6781b26b | ||
|
|
b2d24725de | ||
|
|
b04ac67860 | ||
|
|
61784fd48d | ||
|
|
f29738a96b | ||
|
|
cb313ed513 | ||
|
|
e23656a2cd | ||
|
|
8a39ff469f | ||
|
|
89c369189a | ||
|
|
1108c92beb | ||
|
|
e43cfce016 | ||
|
|
5931b53896 | ||
|
|
020b31cb6f | ||
|
|
8fc41c4c45 | ||
|
|
d9b3c2c15c | ||
|
|
10a9825f82 | ||
|
|
2bbe168200 | ||
|
|
d6e8a96b19 | ||
|
|
c34fae302e | ||
|
|
7957135979 | ||
|
|
c9330d0b7f | ||
|
|
9df5fc12cd | ||
|
|
dd64685049 | ||
|
|
eb54b90001 | ||
|
|
b1ba5790cc | ||
|
|
56d79f36dc | ||
|
|
bbcf7b638f | ||
|
|
94cc314738 | ||
|
|
39eb6482e3 | ||
|
|
85e66ebd8a | ||
|
|
322b7084ea | ||
|
|
8ac2c0462a | ||
|
|
24e2e29894 | ||
|
|
e00aeb39e5 | ||
|
|
6ec2849bd5 | ||
|
|
698c9d2923 | ||
|
|
82f379ecbe | ||
|
|
a502ceea53 | ||
|
|
53e4b39066 | ||
|
|
fceea79323 | ||
|
|
6adf49de45 | ||
|
|
0b85346ac4 | ||
|
|
53f0544da8 | ||
|
|
839ced1b32 | ||
|
|
b6fcbba57d | ||
|
|
b7f220d02b | ||
|
|
398ae8946c | ||
|
|
65357e1441 | ||
|
|
0a7f579bd8 | ||
|
|
2e0d2bb5e5 | ||
|
|
0612d447dc | ||
|
|
fd7e19d339 | ||
|
|
7f8dec161d | ||
|
|
26f50d109a | ||
|
|
acfdf23c22 | ||
|
|
a15588120b | ||
|
|
da28816967 | ||
|
|
53bf9922ca | ||
|
|
363ff9610d | ||
|
|
772c9bc77d | ||
|
|
c1f10c32c0 | ||
|
|
0a8c36e086 | ||
|
|
ae35e4b589 | ||
|
|
92a760541b | ||
|
|
3c67546f4a | ||
|
|
695670cc96 | ||
|
|
0f42aa4ec7 | ||
|
|
8aca69f845 | ||
|
|
63be38a3c3 | ||
|
|
e7f6e5c740 | ||
|
|
2ae8d49ec8 | ||
|
|
f9e987e933 | ||
|
|
8e9c76a9e8 | ||
|
|
ad52b60798 | ||
|
|
012cc28f8a | ||
|
|
1668717cf8 |
21682
.gitattributes
vendored
21682
.gitattributes
vendored
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.1</version>
|
||||
<version>1.6.6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package forge;
|
||||
package forge.ai;
|
||||
|
||||
public enum AIOption {
|
||||
USE_SIMULATION;
|
||||
@@ -35,11 +35,13 @@ import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.combat.GlobalAttackRestrictions;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Expressions;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
@@ -243,6 +245,19 @@ public class AiAttackController {
|
||||
return false;
|
||||
}
|
||||
|
||||
public final static Card getCardCanBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) {
|
||||
final List<Card> attackerList = new ArrayList<Card>(attackers);
|
||||
if (!c.isCreature()) {
|
||||
return null;
|
||||
}
|
||||
for (final Card attacker : attackerList) {
|
||||
if (CombatUtil.canBlock(attacker, c, nextTurn)) {
|
||||
return attacker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// this checks to make sure that the computer player doesn't lose when the human player attacks
|
||||
// this method is used by getAttackers()
|
||||
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
|
||||
@@ -362,8 +377,12 @@ public class AiAttackController {
|
||||
blockersLeft--;
|
||||
continue;
|
||||
}
|
||||
totalAttack += ComputerUtilCombat.damageIfUnblocked(attacker, ai, null, false);
|
||||
totalPoison += ComputerUtilCombat.poisonIfUnblocked(attacker, ai);
|
||||
|
||||
// Test for some special triggers that can change the creature in combat
|
||||
Card effectiveAttacker = ComputerUtilCombat.applyPotentialAttackCloneTriggers(attacker);
|
||||
|
||||
totalAttack += ComputerUtilCombat.damageIfUnblocked(effectiveAttacker, ai, null, false);
|
||||
totalPoison += ComputerUtilCombat.poisonIfUnblocked(effectiveAttacker, ai);
|
||||
}
|
||||
|
||||
if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) {
|
||||
@@ -424,16 +443,45 @@ public class AiAttackController {
|
||||
|
||||
final Player opp = this.defendingOpponent;
|
||||
|
||||
for (Card attacker : attackers) {
|
||||
if (!CombatUtil.canBeBlocked(attacker, this.blockers, null)
|
||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||
unblockedAttackers.add(attacker);
|
||||
// if true, the AI will attempt to identify which blockers will already be taken,
|
||||
// thus attempting to predict how many creatures with evasion can actively block
|
||||
boolean predictEvasion = false;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
if (aic.getBooleanProperty(AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION)) {
|
||||
predictEvasion = true;
|
||||
}
|
||||
}
|
||||
|
||||
CardCollection accountedBlockers = new CardCollection(this.blockers);
|
||||
CardCollection categorizedAttackers = new CardCollection();
|
||||
|
||||
if (predictEvasion) {
|
||||
// split categorizedAttackers such that the ones with evasion come first and
|
||||
// can be properly accounted for. Note that at this point the attackers need
|
||||
// to be sorted by power already (see the Collections.sort call above).
|
||||
categorizedAttackers.addAll(ComputerUtilCombat.categorizeAttackersByEvasion(this.attackers));
|
||||
} else {
|
||||
categorizedAttackers.addAll(this.attackers);
|
||||
}
|
||||
|
||||
for (Card attacker : categorizedAttackers) {
|
||||
if (!CombatUtil.canBeBlocked(attacker, accountedBlockers, null)
|
||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||
unblockedAttackers.add(attacker);
|
||||
} else {
|
||||
if (predictEvasion) {
|
||||
List<Card> potentialBestBlockers = CombatUtil.getPotentialBestBlockers(attacker, accountedBlockers, null);
|
||||
accountedBlockers.removeAll(potentialBestBlockers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remainingAttackers.removeAll(unblockedAttackers);
|
||||
|
||||
for (Card blocker : this.blockers) {
|
||||
if (blocker.hasKeyword("CARDNAME can block any number of creatures.")
|
||||
|| blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) {
|
||||
|| blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
|
||||
for (Card attacker : this.attackers) {
|
||||
if (CombatUtil.canBlock(attacker, blocker)) {
|
||||
remainingAttackers.remove(attacker);
|
||||
@@ -449,7 +497,7 @@ public class AiAttackController {
|
||||
if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) {
|
||||
break;
|
||||
}
|
||||
if (blocker.hasKeyword("CARDNAME can block an additional creature.")) {
|
||||
if (blocker.hasKeyword("CARDNAME can block an additional creature each combat.")) {
|
||||
blockedAttackers.add(remainingAttackers.get(0));
|
||||
remainingAttackers.remove(0);
|
||||
maxBlockersAfterCrew--;
|
||||
@@ -498,11 +546,16 @@ public class AiAttackController {
|
||||
}
|
||||
Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0));
|
||||
|
||||
final GameEntity entity = ai.getMustAttackEntity();
|
||||
// Attempt to see if there's a defined entity that must be attacked strictly this turn...
|
||||
GameEntity entity = ai.getMustAttackEntityThisTurn();
|
||||
if (entity == null) {
|
||||
// ...or during the attacking creature controller's turn
|
||||
entity = ai.getMustAttackEntity();
|
||||
}
|
||||
if (null != entity) {
|
||||
int n = defs.indexOf(entity);
|
||||
if (-1 == n) {
|
||||
System.out.println("getMustAttackEntity() returned something not in defenders.");
|
||||
System.out.println("getMustAttackEntity() or getMustAttackEntityThisTurn() returned something not in defenders.");
|
||||
return prefDefender;
|
||||
} else {
|
||||
return entity;
|
||||
@@ -544,10 +597,21 @@ public class AiAttackController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Aggro options
|
||||
boolean playAggro = false;
|
||||
int chanceToAttackToTrade = 0;
|
||||
boolean tradeIfTappedOut = false;
|
||||
int extraChanceIfOppHasMana = 0;
|
||||
boolean tradeIfLowerLifePressure = false;
|
||||
if (ai.getController().isAI()) {
|
||||
playAggro = ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true");
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
playAggro = aic.getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
chanceToAttackToTrade = aic.getIntProperty(AiProps.CHANCE_TO_ATTACK_INTO_TRADE);
|
||||
tradeIfTappedOut = aic.getBooleanProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
|
||||
extraChanceIfOppHasMana = aic.getIntProperty(AiProps.CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA);
|
||||
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
|
||||
}
|
||||
|
||||
final boolean bAssault = this.doAssault(ai);
|
||||
// TODO: detect Lightmine Field by presence of a card with a specific trigger
|
||||
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
|
||||
@@ -558,7 +622,11 @@ public class AiAttackController {
|
||||
|
||||
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
||||
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
|
||||
final int attackMax = restrict.getMax();
|
||||
int attackMax = restrict.getMax();
|
||||
if (attackMax == -1) {
|
||||
// check with the local limitations vs. the chosen defender
|
||||
attackMax = ComputerUtilCombat.getMaxAttackersFor(defender);
|
||||
}
|
||||
|
||||
if (attackMax == 0) {
|
||||
// can't attack anymore
|
||||
@@ -581,7 +649,8 @@ public class AiAttackController {
|
||||
&& isEffectiveAttacker(ai, attacker, combat)) {
|
||||
mustAttack = true;
|
||||
} else {
|
||||
for (String s : attacker.getKeywords()) {
|
||||
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||
String s = inst.getOriginal();
|
||||
if (s.equals("CARDNAME attacks each turn if able.")
|
||||
|| s.startsWith("CARDNAME attacks specific player each combat if able")
|
||||
|| s.equals("CARDNAME attacks each combat if able.")) {
|
||||
@@ -590,7 +659,7 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mustAttack || attacker.getController().getMustAttackEntity() != null) {
|
||||
if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) {
|
||||
combat.addAttacker(attacker, defender);
|
||||
attackersLeft.remove(attacker);
|
||||
numForcedAttackers++;
|
||||
@@ -711,7 +780,20 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
|
||||
for (final Card pCard : this.oppList) {
|
||||
boolean predictEvasion = (ai.getController().isAI()
|
||||
&& ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION));
|
||||
|
||||
CardCollection categorizedOppList = new CardCollection();
|
||||
if (predictEvasion) {
|
||||
// If predicting evasion, make sure that attackers with evasion are considered first
|
||||
// (to avoid situations where the AI would predict his non-flyers to be blocked with
|
||||
// flying creatures and then believe that flyers will necessarily be left unblocked)
|
||||
categorizedOppList.addAll(ComputerUtilCombat.categorizeAttackersByEvasion(this.oppList));
|
||||
} else {
|
||||
categorizedOppList.addAll(this.oppList);
|
||||
}
|
||||
|
||||
for (final Card pCard : categorizedOppList) {
|
||||
// if the creature can attack next turn add it to counter attackers list
|
||||
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
|
||||
nextTurnAttackers.add(pCard);
|
||||
@@ -719,8 +801,13 @@ public class AiAttackController {
|
||||
humanForces += 1; // player forces they might use to attack
|
||||
}
|
||||
// increment player forces that are relevant to an attritional attack - includes walls
|
||||
if (canBlockAnAttacker(pCard, candidateAttackers, true)) {
|
||||
|
||||
Card potentialOppBlocker = getCardCanBlockAnAttacker(pCard, candidateAttackers, true);
|
||||
if (potentialOppBlocker != null) {
|
||||
humanForcesForAttritionalAttack += 1;
|
||||
if (predictEvasion) {
|
||||
candidateAttackers.remove(potentialOppBlocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,8 +934,18 @@ public class AiAttackController {
|
||||
if (ratioDiff > 0 && doAttritionalAttack) {
|
||||
this.aiAggression = 5; // attack at all costs
|
||||
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|
||||
|| (playAggro && humanLifeToDamageRatio > 1)) {
|
||||
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
|
||||
this.aiAggression = 4; // attack expecting to trade or damage player.
|
||||
} else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
|
||||
&& defendingOpponent != null
|
||||
&& ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent)
|
||||
&& ai.getLife() > defendingOpponent.getLife()
|
||||
&& !ComputerUtilCombat.lifeInDanger(ai, combat)
|
||||
&& (ComputerUtilMana.getAvailableManaEstimate(ai) > 0) || tradeIfTappedOut
|
||||
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
|
||||
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
|
||||
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
|
||||
this.aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
|
||||
} else if (ratioDiff >= 0 && this.attackers.size() > 1) {
|
||||
this.aiAggression = 3; // attack expecting to make good trades or damage player.
|
||||
} else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
|
||||
@@ -995,8 +1092,10 @@ public class AiAttackController {
|
||||
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|
||||
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
||||
if (!hasCombatEffect) {
|
||||
for (String keyword : attacker.getKeywords()) {
|
||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
||||
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||
String keyword = inst.getOriginal();
|
||||
if (keyword.equals("Wither") || keyword.equals("Infect")
|
||||
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
|
||||
hasCombatEffect = true;
|
||||
break;
|
||||
}
|
||||
@@ -1029,7 +1128,8 @@ public class AiAttackController {
|
||||
if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) {
|
||||
canKillAllDangerous = false;
|
||||
} else {
|
||||
for (String keyword : defender.getKeywords()) {
|
||||
for (KeywordInterface inst : defender.getKeywords()) {
|
||||
String keyword = inst.getOriginal();
|
||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
||||
canKillAllDangerous = false;
|
||||
break;
|
||||
@@ -1037,12 +1137,28 @@ public class AiAttackController {
|
||||
// and combat will have negative effects
|
||||
}
|
||||
}
|
||||
|
||||
if (canKillAllDangerous
|
||||
&& !hasAttackEffect && !hasCombatEffect
|
||||
&& (this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0)) {
|
||||
if (ai.getController().isAI()) {
|
||||
if (((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK)) {
|
||||
// We can't kill a blocker, there is no reason to attack unless we can cripple a
|
||||
// blocker or gain life from attacking or we have some kind of another attack/combat effect,
|
||||
// or if we can deal damage to the opponent via the sheer number of potential attackers
|
||||
// (note that the AI will sometimes still miscount here, and thus attack into a block,
|
||||
// because there is no way to check which attackers are actually guaranteed to attack at this point)
|
||||
canKillAllDangerous = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!attacker.hasKeyword("vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||
if (!attacker.hasKeyword("Vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||
canKillAllDangerous = false;
|
||||
canBeKilled = true;
|
||||
canBeKilledByOne = true;
|
||||
@@ -1147,18 +1263,51 @@ public class AiAttackController {
|
||||
}
|
||||
if (sa.usesTargeting()) {
|
||||
sa.setActivatingPlayer(c.getController());
|
||||
if (CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).isEmpty()) {
|
||||
List<Card> validTargets = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
|
||||
if (validTargets.isEmpty()) {
|
||||
missTarget = true;
|
||||
break;
|
||||
} else if (sa.isCurse() && CardLists.filter(validTargets,
|
||||
CardPredicates.isControlledByAnyOf(c.getController().getOpponents())).isEmpty()) {
|
||||
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
|
||||
missTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (missTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (random.nextBoolean()) {
|
||||
// A specific AI condition for Exert: if specified on the card, the AI will always
|
||||
// exert creatures that meet this condition
|
||||
if (c.hasSVar("AIExertCondition")) {
|
||||
if (!c.getSVar("AIExertCondition").isEmpty()) {
|
||||
final String needsToExert = c.getSVar("AIExertCondition");
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
String sVar = needsToExert.split(" ")[0];
|
||||
String comparator = needsToExert.split(" ")[1];
|
||||
String compareTo = comparator.substring(2);
|
||||
try {
|
||||
x = Integer.parseInt(sVar);
|
||||
} catch (final NumberFormatException e) {
|
||||
x = CardFactoryUtil.xCount(c, c.getSVar(sVar));
|
||||
}
|
||||
try {
|
||||
y = Integer.parseInt(compareTo);
|
||||
} catch (final NumberFormatException e) {
|
||||
y = CardFactoryUtil.xCount(c, c.getSVar(compareTo));
|
||||
}
|
||||
if (Expressions.compare(x, comparator, y)) {
|
||||
shouldExert = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldExert && random.nextBoolean()) {
|
||||
// TODO Improve when the AI wants to use Exert powers
|
||||
shouldExert = true;
|
||||
}
|
||||
|
||||
@@ -19,26 +19,20 @@ package forge.ai;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.CardTraitBase;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -89,8 +83,10 @@ public class AiBlockController {
|
||||
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
||||
final List<Card> blockers = new ArrayList<>();
|
||||
|
||||
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
|
||||
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
|
||||
for (final Card b : blockersLeft) {
|
||||
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false)) {
|
||||
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, true)) {
|
||||
blockers.add(b);
|
||||
}
|
||||
}
|
||||
@@ -102,8 +98,10 @@ public class AiBlockController {
|
||||
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
||||
final List<Card> blockers = new ArrayList<>();
|
||||
|
||||
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
|
||||
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
|
||||
for (final Card b : blockersLeft) {
|
||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false)) {
|
||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, true)) {
|
||||
blockers.add(b);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +149,7 @@ public class AiBlockController {
|
||||
for (final Card c : attackers) {
|
||||
sortedAttackers.add(c);
|
||||
}
|
||||
} else {
|
||||
} else if (defender instanceof Player && defender.equals(ai)){
|
||||
firstAttacker = combat.getAttackersOf(defender);
|
||||
}
|
||||
}
|
||||
@@ -417,7 +415,8 @@ public class AiBlockController {
|
||||
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
|
||||
return false;
|
||||
}
|
||||
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
|
||||
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
|
||||
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker) || randomTrade;
|
||||
}
|
||||
});
|
||||
if (usableBlockers.size() < 2) {
|
||||
@@ -526,7 +525,56 @@ public class AiBlockController {
|
||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||
}
|
||||
|
||||
private void makeGangNonLethalBlocks(final Combat combat) {
|
||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||
List<Card> blockers;
|
||||
|
||||
// Try to block a Menace attacker with two blockers, neither of which will die
|
||||
for (final Card attacker : attackersLeft) {
|
||||
if (!attacker.hasKeyword("Menace") && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
|
||||
List<Card> usableBlockers;
|
||||
final List<Card> blockGang = new ArrayList<>();
|
||||
int absorbedDamage; // The amount of damage needed to kill the first blocker
|
||||
|
||||
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.getNetToughness() > attacker.getNetCombatDamage();
|
||||
}
|
||||
});
|
||||
if (usableBlockers.size() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Card leader = ComputerUtilCard.getWorstCreatureAI(usableBlockers);
|
||||
blockGang.add(leader);
|
||||
usableBlockers.remove(leader);
|
||||
absorbedDamage = ComputerUtilCombat.getEnoughDamageToKill(leader, attacker.getNetCombatDamage(), attacker, true);
|
||||
|
||||
// consider a double block
|
||||
for (final Card blocker : usableBlockers) {
|
||||
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(blocker, attacker.getNetCombatDamage(), attacker, true);
|
||||
// only do it if neither blocking creature will die
|
||||
if (absorbedDamage > attacker.getNetCombatDamage() && absorbedDamage2 > attacker.getNetCombatDamage()) {
|
||||
currentAttackers.remove(attacker);
|
||||
combat.addBlocker(attacker, blocker);
|
||||
if (CombatUtil.canBlock(attacker, leader, combat)) {
|
||||
combat.addBlocker(attacker, leader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||
}
|
||||
|
||||
// Bad Trade Blocks (should only be made if life is in danger)
|
||||
// Random Trade Blocks (performed randomly if enabled in profile and only when in favorable conditions)
|
||||
/**
|
||||
* <p>
|
||||
* makeTradeBlocks.
|
||||
@@ -546,18 +594,31 @@ public class AiBlockController {
|
||||
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
|
||||
if (!killingBlockers.isEmpty() && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
|
||||
|
||||
if (!killingBlockers.isEmpty()) {
|
||||
final Card blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers);
|
||||
boolean doTrade = false;
|
||||
|
||||
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
// Always trade when life in danger
|
||||
doTrade = true;
|
||||
} else {
|
||||
// Randomly trade creatures with lower power and [hopefully] worse abilities, if enabled in profile
|
||||
doTrade = wouldLikeToRandomlyTrade(attacker, blocker, combat);
|
||||
}
|
||||
|
||||
if (doTrade) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
currentAttackers.remove(attacker);
|
||||
}
|
||||
}
|
||||
}
|
||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||
}
|
||||
|
||||
@@ -760,6 +821,105 @@ public class AiBlockController {
|
||||
}
|
||||
}
|
||||
|
||||
private void makeChumpBlocksToSavePW(Combat combat) {
|
||||
if (ComputerUtilCombat.lifeInDanger(ai, combat) || ai.getLife() <= ai.getStartingLife() / 5) {
|
||||
// most likely not worth trying to protect planeswalkers when at threateningly low life or in
|
||||
// dangerous combat which threatens lethal or severe damage to face
|
||||
return;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
|
||||
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
|
||||
|
||||
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
|
||||
// detect how much damage is threatened to each of the planeswalkers, see which ones would be
|
||||
// worth protecting according to the AI profile properties
|
||||
CardCollection threatenedPWs = new CardCollection();
|
||||
for (final Card attacker : attackers) {
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card) {
|
||||
if (!onlyIfLethal) {
|
||||
threatenedPWs.add((Card) def);
|
||||
} else {
|
||||
int damageToPW = 0;
|
||||
for (final Card pwatkr : combat.getAttackersOf(def)) {
|
||||
if (!combat.isBlocked(pwatkr)) {
|
||||
damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true);
|
||||
}
|
||||
}
|
||||
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= ((Card) def).getCounters(CounterType.LOYALTY)) {
|
||||
threatenedPWs.add((Card) def);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CardCollection pwsWithChumpBlocks = new CardCollection();
|
||||
CardCollection chosenChumpBlockers = new CardCollection();
|
||||
CardCollection chumpPWDefenders = CardLists.filter(new CardCollection(this.blockersLeft), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken
|
||||
: evalThresholdNonToken);
|
||||
}
|
||||
});
|
||||
CardLists.sortByPowerAsc(chumpPWDefenders);
|
||||
if (!chumpPWDefenders.isEmpty()) {
|
||||
for (final Card attacker : attackers) {
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card && threatenedPWs.contains((Card) def)) {
|
||||
if (attacker.hasKeyword("Trample")) {
|
||||
// don't bother trying to chump a trampling creature
|
||||
continue;
|
||||
}
|
||||
if (!combat.getBlockers(attacker).isEmpty()) {
|
||||
// already blocked by something, no need to chump
|
||||
continue;
|
||||
}
|
||||
Card blockerDecided = null;
|
||||
for (final Card blocker : chumpPWDefenders) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
pwsWithChumpBlocks.add((Card) combat.getDefenderByAttacker(attacker));
|
||||
chosenChumpBlockers.add(blocker);
|
||||
blockerDecided = blocker;
|
||||
blockersLeft.remove(blocker);
|
||||
break;
|
||||
}
|
||||
}
|
||||
chumpPWDefenders.remove(blockerDecided);
|
||||
}
|
||||
}
|
||||
// check to see if we managed to cover all the blockers of the planeswalker; if not, bail
|
||||
for (final Card pw : pwsWithChumpBlocks) {
|
||||
CardCollection pwAttackers = combat.getAttackersOf(pw);
|
||||
CardCollection pwDefenders = new CardCollection();
|
||||
boolean isFullyBlocked = true;
|
||||
if (!pwAttackers.isEmpty()) {
|
||||
int damageToPW = 0;
|
||||
for (Card pwAtk : pwAttackers) {
|
||||
if (!combat.getBlockers(pwAtk).isEmpty()) {
|
||||
pwDefenders.addAll(combat.getBlockers(pwAtk));
|
||||
} else {
|
||||
isFullyBlocked = false;
|
||||
damageToPW += ComputerUtilCombat.predictDamageTo((Card) pw, pwAtk.getNetCombatDamage(), pwAtk, true);
|
||||
}
|
||||
}
|
||||
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) {
|
||||
for (Card chump : pwDefenders) {
|
||||
if (chosenChumpBlockers.contains(chump)) {
|
||||
combat.removeFromCombat(chump);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
|
||||
|
||||
final List<Card> oldBlockers = combat.getAllBlockers();
|
||||
@@ -824,7 +984,7 @@ public class AiBlockController {
|
||||
List<Card> chumpBlockers;
|
||||
|
||||
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
|
||||
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true")) {
|
||||
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
|
||||
diff = 0;
|
||||
}
|
||||
|
||||
@@ -856,9 +1016,8 @@ public class AiBlockController {
|
||||
// When the AI holds some Fog effect, don't bother about lifeInDanger
|
||||
if (!ComputerUtil.hasAFogEffect(ai)) {
|
||||
lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
|
||||
if (lifeInDanger) {
|
||||
makeTradeBlocks(combat); // choose necessary trade blocks
|
||||
}
|
||||
|
||||
// if life is still in danger
|
||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
makeChumpBlocks(combat); // choose necessary chump blocks
|
||||
@@ -927,6 +1086,7 @@ public class AiBlockController {
|
||||
|
||||
// assign blockers that have to block
|
||||
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able.");
|
||||
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
|
||||
// if an attacker with lure attacks - all that can block
|
||||
for (final Card blocker : blockersLeft) {
|
||||
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
|
||||
@@ -940,7 +1100,8 @@ public class AiBlockController {
|
||||
for (final Card blocker : blockers) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
|
||||
&& (CombatUtil.mustBlockAnAttacker(blocker, combat)
|
||||
|| blocker.hasKeyword("CARDNAME blocks each turn if able."))) {
|
||||
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|
||||
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
if (blocker.getMustBlockCards() != null) {
|
||||
int mustBlockAmt = blocker.getMustBlockCards().size();
|
||||
@@ -956,6 +1117,18 @@ public class AiBlockController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
|
||||
// unless life is low enough to be more worried about saving preserving the life total
|
||||
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
makeChumpBlocksToSavePW(combat);
|
||||
}
|
||||
|
||||
// if there are still blockers left, see if it's possible to block Menace creatures with
|
||||
// non-lethal blockers that won't kill the attacker but won't die to it as well
|
||||
makeGangNonLethalBlocks(combat);
|
||||
|
||||
//Check for validity of blocks in case something slipped through
|
||||
for (Card attacker : attackers) {
|
||||
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
|
||||
@@ -1056,4 +1229,91 @@ public class AiBlockController {
|
||||
return first;
|
||||
}
|
||||
|
||||
private boolean wouldLikeToRandomlyTrade(Card attacker, Card blocker, Combat combat) {
|
||||
// Determines if the AI would like to randomly trade its blocker for the attacker in given combat
|
||||
boolean enableRandomTrades = false;
|
||||
boolean randomTradeIfBehindOnBoard = false;
|
||||
boolean randomTradeIfCreatInHand = false;
|
||||
int chanceModForEmbalm = 0;
|
||||
int chanceToTradeToSaveWalker = 0;
|
||||
int chanceToTradeDownToSaveWalker = 0;
|
||||
int minRandomTradeChance = 0;
|
||||
int maxRandomTradeChance = 0;
|
||||
int maxCreatDiff = 0;
|
||||
int maxCreatDiffWithRepl = 0;
|
||||
int aiCreatureCount = 0;
|
||||
int oppCreatureCount = 0;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
|
||||
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
|
||||
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
|
||||
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
|
||||
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
|
||||
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
|
||||
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
|
||||
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
|
||||
}
|
||||
|
||||
if (!enableRandomTrades) {
|
||||
return false;
|
||||
}
|
||||
|
||||
aiCreatureCount = ComputerUtil.countUsefulCreatures(ai);
|
||||
|
||||
if (!attackersLeft.isEmpty()) {
|
||||
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
|
||||
}
|
||||
|
||||
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
|
||||
// Temporarily controlled object - don't trade with it
|
||||
// TODO: find a more reliable way to figure out that control will be reestablished next turn
|
||||
return false;
|
||||
}
|
||||
|
||||
int numSteps = ai.getStartingLife() - 5; // e.g. 15 steps between 5 life and 20 life
|
||||
float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps;
|
||||
int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep));
|
||||
if (chance > maxRandomTradeChance) {
|
||||
chance = maxRandomTradeChance;
|
||||
}
|
||||
|
||||
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false);
|
||||
int evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
|
||||
boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
|
||||
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
|
||||
|
||||
if (atkEmbalm && !blkEmbalm) {
|
||||
// The opponent will eventually get his creature back, while the AI won't
|
||||
chance = Math.max(0, chance - chanceModForEmbalm);
|
||||
}
|
||||
|
||||
if (blocker.isFaceDown() && blocker.getState(CardStateName.Original).getType().isCreature()) {
|
||||
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it
|
||||
// in relation to the original state, not to the Morph state
|
||||
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true);
|
||||
}
|
||||
int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
|
||||
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();
|
||||
boolean creatureParityOrAllowedDiff = aiCreatureCount
|
||||
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
|
||||
boolean wantToTradeWithCreatInHand = randomTradeIfCreatInHand
|
||||
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).isEmpty()
|
||||
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
||||
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
||||
&& combat.getDefenderByAttacker(attacker) instanceof Card
|
||||
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
|
||||
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
||||
|
||||
if (((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
||||
&& powerParityOrHigher
|
||||
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
|
||||
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +41,23 @@ import java.util.Set;
|
||||
public class AiCardMemory {
|
||||
|
||||
private final Set<Card> memMandatoryAttackers;
|
||||
private final Set<Card> memTrickAttackers;
|
||||
private final Set<Card> memHeldManaSources;
|
||||
private final Set<Card> memHeldManaSourcesForCombat;
|
||||
private final Set<Card> memAttachedThisTurn;
|
||||
private final Set<Card> memAnimatedThisTurn;
|
||||
private final Set<Card> memBouncedThisTurn;
|
||||
private final Set<Card> memActivatedThisTurn;
|
||||
|
||||
public AiCardMemory() {
|
||||
this.memMandatoryAttackers = new HashSet<>();
|
||||
this.memHeldManaSources = new HashSet<>();
|
||||
this.memHeldManaSourcesForCombat = new HashSet<>();
|
||||
this.memAttachedThisTurn = new HashSet<>();
|
||||
this.memAnimatedThisTurn = new HashSet<>();
|
||||
this.memBouncedThisTurn = new HashSet<>();
|
||||
this.memActivatedThisTurn = new HashSet<>();
|
||||
this.memTrickAttackers = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,10 +67,13 @@ public class AiCardMemory {
|
||||
*/
|
||||
public enum MemorySet {
|
||||
MANDATORY_ATTACKERS,
|
||||
HELD_MANA_SOURCES,
|
||||
TRICK_ATTACKERS,
|
||||
HELD_MANA_SOURCES_FOR_MAIN2,
|
||||
HELD_MANA_SOURCES_FOR_DECLBLK,
|
||||
ATTACHED_THIS_TURN,
|
||||
ANIMATED_THIS_TURN,
|
||||
BOUNCED_THIS_TURN,
|
||||
ACTIVATED_THIS_TURN,
|
||||
//REVEALED_CARDS // stub, not linked to AI code yet
|
||||
}
|
||||
|
||||
@@ -72,14 +81,20 @@ public class AiCardMemory {
|
||||
switch (set) {
|
||||
case MANDATORY_ATTACKERS:
|
||||
return memMandatoryAttackers;
|
||||
case HELD_MANA_SOURCES:
|
||||
case TRICK_ATTACKERS:
|
||||
return memTrickAttackers;
|
||||
case HELD_MANA_SOURCES_FOR_MAIN2:
|
||||
return memHeldManaSources;
|
||||
case HELD_MANA_SOURCES_FOR_DECLBLK:
|
||||
return memHeldManaSourcesForCombat;
|
||||
case ATTACHED_THIS_TURN:
|
||||
return memAttachedThisTurn;
|
||||
case ANIMATED_THIS_TURN:
|
||||
return memAnimatedThisTurn;
|
||||
case BOUNCED_THIS_TURN:
|
||||
return memBouncedThisTurn;
|
||||
case ACTIVATED_THIS_TURN:
|
||||
return memActivatedThisTurn;
|
||||
//case REVEALED_CARDS:
|
||||
// return memRevealedCards;
|
||||
default:
|
||||
@@ -249,10 +264,13 @@ public class AiCardMemory {
|
||||
*/
|
||||
public void clearAllRemembered() {
|
||||
clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
|
||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES);
|
||||
clearMemorySet(MemorySet.TRICK_ATTACKERS);
|
||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
|
||||
clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
|
||||
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
|
||||
clearMemorySet(MemorySet.BOUNCED_THIS_TURN);
|
||||
clearMemorySet(MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
|
||||
// Static functions to simplify access to AI card memory of a given AI player.
|
||||
@@ -265,6 +283,9 @@ public class AiCardMemory {
|
||||
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
|
||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
|
||||
}
|
||||
public static boolean isRememberedCardByName(Player ai, String name, MemorySet set) {
|
||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCardByName(name, set);
|
||||
}
|
||||
public static void clearMemorySet(Player ai, MemorySet set) {
|
||||
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
|
||||
}
|
||||
|
||||
@@ -17,14 +17,6 @@
|
||||
*/
|
||||
package forge.ai;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.esotericsoftware.minlog.Log;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
@@ -32,52 +24,31 @@ import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.ability.ChangeZoneAi;
|
||||
import forge.ai.ability.ExploreAi;
|
||||
import forge.ai.simulation.SpellAbilityPicker;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.deck.CardPool;
|
||||
import forge.deck.Deck;
|
||||
import forge.deck.DeckSection;
|
||||
import forge.game.CardTraitBase;
|
||||
import forge.game.CardTraitPredicates;
|
||||
import forge.game.Direction;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.*;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.SpellApiBased;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.replacement.ReplaceMoved;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.OptionalCost;
|
||||
import forge.game.spellability.Spell;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityCondition;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.spellability.SpellPermanent;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
@@ -88,6 +59,10 @@ import forge.util.Expressions;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* AiController class.
|
||||
@@ -605,11 +580,32 @@ public class AiController {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void reserveManaSourcesForMain2(SpellAbility sa) {
|
||||
public void reserveManaSources(SpellAbility sa) {
|
||||
reserveManaSources(sa, PhaseType.MAIN2);
|
||||
}
|
||||
|
||||
public void reserveManaSources(SpellAbility sa, PhaseType phaseType) {
|
||||
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
|
||||
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
||||
|
||||
AiCardMemory.MemorySet memSet;
|
||||
|
||||
switch (phaseType) {
|
||||
case MAIN2:
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||
break;
|
||||
case COMBAT_DECLARE_BLOCKERS:
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
|
||||
break;
|
||||
default:
|
||||
System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
|
||||
+ phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
|
||||
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
|
||||
break;
|
||||
}
|
||||
|
||||
for (Card c : manaSources) {
|
||||
AiCardMemory.rememberCard(player, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
|
||||
AiCardMemory.rememberCard(player, c, memSet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,12 +720,31 @@ public class AiController {
|
||||
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
|
||||
|
||||
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return 1;
|
||||
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// deprioritize pump spells with pure energy cost (can be activated last,
|
||||
// since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler)
|
||||
int a2 = 0, b2 = 0;
|
||||
if (a.getApi() == ApiType.Pump && a.getPayCosts() != null && a.getPayCosts().getCostEnergy() != null) {
|
||||
if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
a2 = a.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (b.getApi() == ApiType.Pump && b.getPayCosts() != null && b.getPayCosts().getCostEnergy() != null) {
|
||||
if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
b2 = b.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (a2 == 0 && b2 > 0) {
|
||||
return -1;
|
||||
} else if (b2 == 0 && a2 > 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cast 0 mana cost spells first (might be a Mox)
|
||||
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
|
||||
return -1;
|
||||
@@ -737,9 +752,9 @@ public class AiController {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return -1;
|
||||
} else if (b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
} else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -752,8 +767,14 @@ public class AiController {
|
||||
private int getSpellAbilityPriority(SpellAbility sa) {
|
||||
int p = 0;
|
||||
Card source = sa.getHostCard();
|
||||
final Player ai = source.getController();
|
||||
final Player ai = source == null ? sa.getActivatingPlayer() : source.getController();
|
||||
if (ai == null) {
|
||||
System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa);
|
||||
return 0;
|
||||
}
|
||||
final boolean noCreatures = ai.getCreaturesInPlay().isEmpty();
|
||||
|
||||
if (source != null) {
|
||||
// puts creatures in front of spells
|
||||
if (source.isCreature()) {
|
||||
p += 1;
|
||||
@@ -762,19 +783,14 @@ public class AiController {
|
||||
if (source.isEquipment() && noCreatures) {
|
||||
p -= 9;
|
||||
}
|
||||
// use Surge and Prowl costs when able to
|
||||
if (sa.isSurged() ||
|
||||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
||||
p += 9;
|
||||
}
|
||||
// 1. increase chance of using Surge effects
|
||||
// 2. non-surged versions are usually inefficient
|
||||
if (sa.getHostCard().getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||
p -= 9;
|
||||
}
|
||||
// move snap-casted spells to front
|
||||
if (source.isInZone(ZoneType.Graveyard)) {
|
||||
if(sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
|
||||
if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
|
||||
p += 50;
|
||||
}
|
||||
}
|
||||
@@ -782,6 +798,17 @@ public class AiController {
|
||||
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
|
||||
p -= 9;
|
||||
}
|
||||
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
|
||||
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||
}
|
||||
}
|
||||
|
||||
// use Surge and Prowl costs when able to
|
||||
if (sa.isSurged() ||
|
||||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
||||
p += 9;
|
||||
}
|
||||
// sort planeswalker abilities with most costly first
|
||||
if (sa.getRestrictions().isPwAbility()) {
|
||||
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
||||
@@ -801,11 +828,6 @@ public class AiController {
|
||||
p -= 9;
|
||||
}
|
||||
|
||||
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
|
||||
p -= (((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||
}
|
||||
|
||||
// try to cast mana ritual spells before casting spells to maximize potential mana
|
||||
if ("ManaRitual".equals(sa.getParam("AILogic"))) {
|
||||
p += 9;
|
||||
@@ -840,7 +862,36 @@ public class AiController {
|
||||
sourceCard = sa.getHostCard();
|
||||
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) {
|
||||
min = 1;
|
||||
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
|
||||
}
|
||||
|
||||
if (sa.hasParam("AnyNumber")) {
|
||||
if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) {
|
||||
CardCollection discards = new CardCollection();
|
||||
final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand);
|
||||
final int numLandsOTB = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||
int numOppInHand = 0;
|
||||
for (Player p : player.getGame().getPlayers()) {
|
||||
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
|
||||
numOppInHand = p.getCardsIn(ZoneType.Hand).size();
|
||||
}
|
||||
}
|
||||
for (Card c : inHand) {
|
||||
if (c.hasSVar("DoNotDiscardIfAble") || c.hasSVar("IsReanimatorCard")) { continue; }
|
||||
if (c.isCreature() && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getSpellPermanent(), player)) {
|
||||
discards.add(c);
|
||||
}
|
||||
if ((c.isLand() && numLandsOTB >= 5) || (c.getFirstSpellAbility() != null && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getFirstSpellAbility(), player))) {
|
||||
if (discards.size() + 1 <= numOppInHand) {
|
||||
discards.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return discards;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// look for good discards
|
||||
@@ -858,6 +909,9 @@ public class AiController {
|
||||
}
|
||||
if (prefCard == null) {
|
||||
prefCard = ComputerUtil.getCardPreference(player, sourceCard, "DiscardCost", validCards);
|
||||
if (prefCard != null && prefCard.hasSVar("DoNotDiscardIfAble")) {
|
||||
prefCard = null;
|
||||
}
|
||||
}
|
||||
if (prefCard != null) {
|
||||
discardList.add(prefCard);
|
||||
@@ -894,13 +948,52 @@ public class AiController {
|
||||
if (numLandsInHand > 0) {
|
||||
numLandsAvailable++;
|
||||
}
|
||||
|
||||
//Discard unplayable card
|
||||
if (validCards.get(0).getCMC() > numLandsAvailable) {
|
||||
discardList.add(validCards.get(0));
|
||||
validCards.remove(validCards.get(0));
|
||||
boolean discardedUnplayable = false;
|
||||
for (int j = 0; j < validCards.size(); j++) {
|
||||
if (validCards.get(j).getCMC() > numLandsAvailable && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
|
||||
discardList.add(validCards.get(j));
|
||||
validCards.remove(validCards.get(j));
|
||||
discardedUnplayable = true;
|
||||
break;
|
||||
} else if (validCards.get(j).getCMC() <= numLandsAvailable) {
|
||||
// cut short to avoid looping over cards which are guaranteed not to fit the criteria
|
||||
break;
|
||||
}
|
||||
else { //Discard worst card
|
||||
}
|
||||
|
||||
if (!discardedUnplayable) {
|
||||
// discard worst card
|
||||
Card worst = ComputerUtilCard.getWorstAI(validCards);
|
||||
if (worst == null) {
|
||||
// there were only instants and sorceries, and maybe cards that are not good to discard, so look
|
||||
// for more discard options
|
||||
worst = ComputerUtilCard.getCheapestSpellAI(validCards);
|
||||
}
|
||||
if (worst == null && !validCards.isEmpty()) {
|
||||
// still nothing chosen, so choose the first thing that works, trying not to make DoNotDiscardIfAble
|
||||
// discards
|
||||
for (Card c : validCards) {
|
||||
if (!c.hasSVar("DoNotDiscardIfAble")) {
|
||||
worst = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Only DoNotDiscardIfAble cards? If we have a duplicate for something, discard it
|
||||
if (worst == null) {
|
||||
for (Card c : validCards) {
|
||||
if (CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c.getName())).size() > 1) {
|
||||
worst = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (worst == null) {
|
||||
// Otherwise just grab a random card and discard it
|
||||
worst = Aggregates.random(validCards);
|
||||
}
|
||||
}
|
||||
}
|
||||
discardList.add(worst);
|
||||
validCards.remove(worst);
|
||||
}
|
||||
@@ -1059,6 +1152,20 @@ public class AiController {
|
||||
}
|
||||
|
||||
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
|
||||
CardCollection playBeforeLand = CardLists.filter(player.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return "true".equalsIgnoreCase(card.getSVar("PlayBeforeLandDrop"));
|
||||
}
|
||||
});
|
||||
|
||||
if (!playBeforeLand.isEmpty()) {
|
||||
SpellAbility wantToPlayBeforeLand = chooseSpellAbilityToPlayFromList(ComputerUtilAbility.getSpellAbilities(playBeforeLand, player), false);
|
||||
if (wantToPlayBeforeLand != null) {
|
||||
return singleSpellAbilityList(wantToPlayBeforeLand);
|
||||
}
|
||||
}
|
||||
|
||||
if (landsWannaPlay != null) {
|
||||
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
|
||||
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
|
||||
@@ -1066,6 +1173,7 @@ public class AiController {
|
||||
Card land = chooseBestLandToPlay(landsWannaPlay);
|
||||
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|
||||
|| player.cantLoseForZeroOrLessLife() ) {
|
||||
if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
|
||||
game.PLAY_LAND_SURROGATE.setHostCard(land);
|
||||
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||
abilities.add(game.PLAY_LAND_SURROGATE);
|
||||
@@ -1073,10 +1181,114 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return singleSpellAbilityList(getSpellAbilityToPlay());
|
||||
}
|
||||
|
||||
private boolean isSafeToHoldLandDropForMain2(Card landToPlay) {
|
||||
if (!MyRandom.percentTrue(getIntProperty(AiProps.HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED))) {
|
||||
// check against the chance specified in the profile
|
||||
return false;
|
||||
}
|
||||
if (game.getPhaseHandler().getTurn() <= 2) {
|
||||
// too obvious when doing it on the very first turn of the game
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand),
|
||||
Predicates.not(CardPredicates.Presets.LANDS));
|
||||
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
|
||||
if (CardLists.filter(otb, Predicates.not(CardPredicates.Presets.LANDS)).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: improve the detection of taplands
|
||||
boolean isTapLand = false;
|
||||
for (ReplacementEffect repl : landToPlay.getReplacementEffects()) {
|
||||
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
|
||||
isTapLand = true;
|
||||
}
|
||||
}
|
||||
|
||||
int totalCMCInHand = Aggregates.sum(inHand, CardPredicates.Accessors.fnGetCmc);
|
||||
int minCMCInHand = Aggregates.min(inHand, CardPredicates.Accessors.fnGetCmc);
|
||||
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
|
||||
|
||||
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && !isTapLand;
|
||||
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
|
||||
|
||||
boolean hasRelevantAbsOTB = !CardLists.filter(otb, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
boolean isTapLand = false;
|
||||
for (ReplacementEffect repl : card.getReplacementEffects()) {
|
||||
// TODO: improve the detection of taplands
|
||||
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
|
||||
isTapLand = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (SpellAbility sa : card.getSpellAbilities()) {
|
||||
if (sa.getPayCosts() != null && sa.isAbility()
|
||||
&& sa.getPayCosts().getCostMana() != null
|
||||
&& sa.getPayCosts().getCostMana().getMana().getCMC() > 0
|
||||
&& (!sa.getPayCosts().hasTapCost() || !isTapLand)
|
||||
&& (!sa.hasParam("ActivationZone") || sa.getParam("ActivationZone").contains("Battlefield"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}).isEmpty();
|
||||
|
||||
boolean hasLandBasedEffect = !CardLists.filter(otb, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
for (Trigger t : card.getTriggers()) {
|
||||
Map<String, String> params = t.getMapParams();
|
||||
if ("ChangesZone".equals(params.get("Mode"))
|
||||
&& params.containsKey("ValidCard")
|
||||
&& !params.get("ValidCard").contains("nonLand")
|
||||
&& ((params.get("ValidCard").contains("Land")) || (params.get("ValidCard").contains("Permanent")))
|
||||
&& "Battlefield".equals(params.get("Destination"))) {
|
||||
// Landfall and other similar triggers
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (String sv : card.getSVars().keySet()) {
|
||||
String varValue = card.getSVar(sv);
|
||||
if (varValue.startsWith("Count$Valid") || sv.equals("BuffedBy")) {
|
||||
if (varValue.contains("Land") || varValue.contains("Plains") || varValue.contains("Forest")
|
||||
|| varValue.contains("Mountain") || varValue.contains("Island") || varValue.contains("Swamp")
|
||||
|| varValue.contains("Wastes")) {
|
||||
// In presence of various cards that get buffs like "equal to the number of lands you control",
|
||||
// safer for our AI model to just play the land earlier rather than make a blunder
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}).isEmpty();
|
||||
|
||||
// TODO: add prediction for effects that will untap a tapland as it enters the battlefield
|
||||
if (!canCastWithLandDrop && cantCastAnythingNow && !hasLandBasedEffect && (!hasRelevantAbsOTB || isTapLand)) {
|
||||
// Hopefully there's not much to do with the extra mana immediately, can wait for Main 2
|
||||
return true;
|
||||
}
|
||||
if ((predictedMana <= totalCMCInHand && canCastWithLandDrop) || (hasRelevantAbsOTB && !isTapLand) || hasLandBasedEffect) {
|
||||
// Might need an extra land to cast something, or for some kind of an ETB ability with a cost or an
|
||||
// alternative cost (if we cast it in Main 1), or to use an activated ability on the battlefield
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private final SpellAbility getSpellAbilityToPlay() {
|
||||
// if top of stack is owned by me
|
||||
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
|
||||
@@ -1306,7 +1518,7 @@ public class AiController {
|
||||
+ MyRandom.getRandom().nextInt(3);
|
||||
return Math.max(remaining, min) / 2;
|
||||
} else if ("LowestLoseLife".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getOpponent().getLife())) + 1;
|
||||
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, ComputerUtil.getOpponentFor(player).getLife())) + 1;
|
||||
} else if ("HighestGetCounter".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(3);
|
||||
} else if (source.hasSVar("EnergyToPay")) {
|
||||
@@ -1428,6 +1640,24 @@ public class AiController {
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// Special case for Bow to My Command which simulates a complex tap cost via ChooseCard
|
||||
// TODO: consider enhancing support for tapXType<Any/...> in UnlessCost to get rid of this hack
|
||||
if ("BowToMyCommand".equals(sa.getParam("AILogic"))) {
|
||||
if (!sa.getHostCard().getZone().is(ZoneType.Command)) {
|
||||
// Make sure that other opponents do not tap for an already abandoned scheme
|
||||
result.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
int totPower = 0;
|
||||
for (Card p : result) {
|
||||
totPower += p.getNetPower();
|
||||
}
|
||||
if (totPower >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,8 +1783,12 @@ public class AiController {
|
||||
if (useSimulation) {
|
||||
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
if (sa.getApi() == ApiType.Explore) {
|
||||
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
|
||||
} else {
|
||||
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
}
|
||||
|
||||
public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {
|
||||
// list is only one or empty, no need to filter
|
||||
|
||||
@@ -373,6 +373,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostPutCardToLib cost) {
|
||||
if (cost.payCostFromSource()) {
|
||||
return PaymentDecision.card(source);
|
||||
}
|
||||
Integer c = cost.convertAmount();
|
||||
final Game game = player.getGame();
|
||||
CardCollection chosen = new CardCollection();
|
||||
@@ -469,7 +472,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
CardCollectionView totap;
|
||||
if (isVehicle) {
|
||||
totalP = type.split("withTotalPowerGE")[1];
|
||||
type = type.replace("+withTotalPowerGE" + totalP, "");
|
||||
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
|
||||
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped);
|
||||
} else {
|
||||
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
|
||||
|
||||
@@ -21,6 +21,7 @@ import forge.LobbyPlayer;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.FileUtil;
|
||||
|
||||
import forge.util.TextUtil;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import java.io.File;
|
||||
@@ -52,7 +53,7 @@ public class AiProfileUtil {
|
||||
* @return the full relative path and file name for the given profile.
|
||||
*/
|
||||
private static String buildFileName(final String profileName) {
|
||||
return String.format("%s/%s%s", AI_PROFILE_DIR, profileName, AI_PROFILE_EXT);
|
||||
return TextUtil.concatNoSpace(AI_PROFILE_DIR, "/", profileName, AI_PROFILE_EXT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,20 +29,51 @@ public enum AiProps { /** */
|
||||
DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */
|
||||
MULLIGAN_THRESHOLD ("5"), /** */
|
||||
PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"),
|
||||
HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED ("0"), /** */
|
||||
HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS ("true"), /** */
|
||||
CHEAT_WITH_MANA_ON_SHUFFLE ("false"),
|
||||
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
|
||||
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
|
||||
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
|
||||
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
|
||||
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
|
||||
PLAY_AGGRO ("false"), /** */
|
||||
MIN_SPELL_CMC_TO_COUNTER ("0"),
|
||||
PLAY_AGGRO ("false"),
|
||||
CHANCE_TO_ATTACK_INTO_TRADE ("40"), /** */
|
||||
RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE ("true"), /** */
|
||||
ATTACK_INTO_TRADE_WHEN_TAPPED_OUT ("false"), /** */
|
||||
CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA ("0"), /** */
|
||||
TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK ("true"), /** */
|
||||
TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK ("false"), /** */
|
||||
CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK ("30"), /** */
|
||||
ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK ("true"), /** */
|
||||
RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS ("false"), /** */
|
||||
MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE ("1"), /** */
|
||||
ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT ("true"), /** */
|
||||
MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL ("1"), /** */
|
||||
MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK ("30"), /** */
|
||||
MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK ("70"), /** */
|
||||
CHANCE_DECREASE_TO_TRADE_VS_EMBALM ("30"), /** */
|
||||
CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER ("70"), /** */
|
||||
CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER ("0"), /** */
|
||||
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
||||
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
||||
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
||||
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
||||
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
||||
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
||||
CHANCE_TO_COUNTER_CMC_3 ("100"), /** */
|
||||
ALWAYS_COUNTER_OTHER_COUNTERSPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_DAMAGE_SPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS ("true"), /** */
|
||||
ALWAYS_COUNTER_REMOVAL_SPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
|
||||
ALWAYS_COUNTER_AURAS ("true"), /** */
|
||||
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
|
||||
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("false"), /** */
|
||||
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
|
||||
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
|
||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
|
||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
|
||||
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
|
||||
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
||||
USE_BERSERK_AGGRESSIVELY ("false"), /** */
|
||||
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
|
||||
@@ -50,7 +81,31 @@ public enum AiProps { /** */
|
||||
STRIPMINE_MIN_LANDS_FOR_NO_TIMING_CHECK ("3"), /** */
|
||||
STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK ("6"), /** */
|
||||
STRIPMINE_MAX_LANDS_TO_ATTEMPT_MANALOCKING ("3"), /** */
|
||||
STRIPMINE_HIGH_PRIORITY_ON_SKIPPED_LANDDROP ("false"); /** */
|
||||
STRIPMINE_HIGH_PRIORITY_ON_SKIPPED_LANDDROP ("false"),
|
||||
TOKEN_GENERATION_ABILITY_CHANCE ("80"), /** */
|
||||
TOKEN_GENERATION_ALWAYS_IF_FROM_PLANESWALKER ("true"), /** */
|
||||
TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS ("true"), /** */
|
||||
SCRY_NUM_LANDS_TO_STILL_NEED_MORE ("4"), /** */
|
||||
SCRY_NUM_LANDS_TO_NOT_NEED_MORE ("7"), /** */
|
||||
SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES ("4"), /** */
|
||||
SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC ("3"), /** */
|
||||
SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE ("160"), /** */
|
||||
SCRY_EVALTHR_CMC_THRESHOLD ("3"), /** */
|
||||
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
|
||||
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
|
||||
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
|
||||
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
|
||||
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
|
||||
CONSERVATIVE_ENERGY_PAYMENT_ONLY_DEFENSIVELY ("true"), /** */
|
||||
BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF ("200"), /** */
|
||||
BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF ("200"), /** */
|
||||
BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF ("3"), /** */
|
||||
BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF ("3"), /** */
|
||||
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
|
||||
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
|
||||
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE ("2"); /** */
|
||||
// Experimental features, must be removed after extensive testing and, ideally, defaulting
|
||||
// <-- There are no experimental options here -->
|
||||
|
||||
private final String strDefaultVal;
|
||||
|
||||
|
||||
@@ -17,23 +17,9 @@
|
||||
*/
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import forge.ai.ability.ProtectAi;
|
||||
import forge.ai.ability.TokenAi;
|
||||
import forge.card.CardType;
|
||||
@@ -46,32 +32,17 @@ import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -79,7 +50,11 @@ import forge.game.zone.Zone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollection;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -99,15 +74,27 @@ public class ComputerUtil {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
if (sa.isSpell() && !source.isCopiedSpell()) {
|
||||
if (source.getType().hasStringType("Arcane")) {
|
||||
sa = AbilityUtils.addSpliceEffects(sa);
|
||||
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
|
||||
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
|
||||
// otherwise the AI will fail to add the card to stack and that'll knock it out of the game
|
||||
sa.resetTargets();
|
||||
if (((PlayerControllerAi) ai.getController()).getAi().canPlaySa(sa) != AiPlayDecision.WillPlay) {
|
||||
// for whatever reason the AI doesn't want to play the thing with the spliced subs anymore,
|
||||
// proceeding past this point may result in an illegal play
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
source.setCastSA(sa);
|
||||
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
||||
sa.setLastStateGraveyard(game.getLastStateGraveyard());
|
||||
sa.setHostCard(game.getAction().moveToStack(source, sa));
|
||||
}
|
||||
|
||||
if (source.getType().hasStringType("Arcane")) {
|
||||
sa = AbilityUtils.addSpliceEffects(sa);
|
||||
}
|
||||
}
|
||||
sa.resetPaidHash();
|
||||
|
||||
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
|
||||
CharmEffect.makeChoices(sa);
|
||||
@@ -192,7 +179,7 @@ public class ComputerUtil {
|
||||
if (unless != null && !unless.endsWith(">")) {
|
||||
final int amount = AbilityUtils.calculateAmount(source, unless, sa);
|
||||
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size();
|
||||
|
||||
// If the Unless isn't enough, this should be less likely to be used
|
||||
if (amount > usableManaSources) {
|
||||
@@ -310,18 +297,20 @@ public class ComputerUtil {
|
||||
|
||||
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
|
||||
final Game game = ai.getGame();
|
||||
String prefDef = "";
|
||||
if (activate != null) {
|
||||
prefDef = activate.getSVar("AIPreference");
|
||||
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
|
||||
for (String prefGroup : prefGroups) {
|
||||
final String[] prefValid = prefGroup.trim().split("\\$");
|
||||
if (prefValid[0].equals(pref)) {
|
||||
final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","), activate.getController(), activate, null);
|
||||
if (prefValid[0].equals(pref) && !prefValid[1].startsWith("Special:")) {
|
||||
CardCollection overrideList = null;
|
||||
|
||||
if (activate.hasSVar("AIPreferenceOverride")) {
|
||||
overrideList = CardLists.getValidCards(typeList, activate.getSVar("AIPreferenceOverride"), activate.getController(), activate, null);
|
||||
}
|
||||
|
||||
for (String validItem : prefValid[1].split(",")) {
|
||||
final CardCollection prefList = CardLists.getValidCards(typeList, validItem, activate.getController(), activate, null);
|
||||
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold");
|
||||
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold");
|
||||
|
||||
@@ -345,12 +334,13 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (!prefList.isEmpty()) {
|
||||
if (!prefList.isEmpty() || (overrideList != null && !overrideList.isEmpty())) {
|
||||
return ComputerUtilCard.getWorstAI(overrideList == null ? prefList : overrideList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pref.contains("SacCost")) {
|
||||
// search for permanents with SacMe. priority 1 is the lowest, priority 5 the highest
|
||||
for (int ip = 0; ip < 6; ip++) {
|
||||
@@ -411,6 +401,11 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// Survival of the Fittest logic
|
||||
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
|
||||
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
|
||||
}
|
||||
|
||||
// Discard lands
|
||||
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
|
||||
if (!landsInHand.isEmpty()) {
|
||||
@@ -707,6 +702,8 @@ public class ComputerUtil {
|
||||
return sacrificed; // sacrifice none
|
||||
}
|
||||
}
|
||||
boolean exceptSelf = "ExceptSelf".equals(source.getParam("AILogic"));
|
||||
boolean removedSelf = false;
|
||||
|
||||
if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) {
|
||||
if (source.hasParam("Exploit")) {
|
||||
@@ -764,11 +761,22 @@ public class ComputerUtil {
|
||||
|
||||
final int max = Math.min(remaining.size(), amount);
|
||||
|
||||
if (exceptSelf) {
|
||||
removedSelf = remaining.remove(source.getHostCard());
|
||||
}
|
||||
|
||||
for (int i = 0; i < max; i++) {
|
||||
Card c = chooseCardToSacrifice(remaining, ai, destroy);
|
||||
remaining.remove(c);
|
||||
if (c != null) {
|
||||
sacrificed.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (sacrificed.isEmpty() && removedSelf) {
|
||||
sacrificed.add(source.getHostCard());
|
||||
}
|
||||
|
||||
return sacrificed;
|
||||
}
|
||||
|
||||
@@ -848,11 +856,11 @@ public class ComputerUtil {
|
||||
continue; // Won't play ability
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c, sa)) {
|
||||
continue; // Won't play ability
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c)) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c, sa)) {
|
||||
continue; // Won't play ability
|
||||
}
|
||||
}
|
||||
@@ -868,7 +876,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
} catch (final Exception ex) {
|
||||
throw new RuntimeException(String.format("There is an error in the card code for %s:%s", c.getName(), ex.getMessage()), ex);
|
||||
throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -907,7 +915,7 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
} catch (final Exception ex) {
|
||||
throw new RuntimeException(String.format("There is an error in the card code for %s:%s", c.getName(), ex.getMessage()), ex);
|
||||
throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -926,7 +934,7 @@ public class ComputerUtil {
|
||||
return true;
|
||||
} else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
|
||||
//Only play these main1 when the opponent has creatures (stealing and giving them haste)
|
||||
if (!card.getController().getOpponent().getCreaturesInPlay().isEmpty()) {
|
||||
if (!ai.getOpponents().getCreaturesInPlay().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
|
||||
@@ -934,6 +942,15 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// try not to cast Raid creatures in main 1 if an attack is likely
|
||||
if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword("Haste")) {
|
||||
for (Card potentialAtkr: ai.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (card.getManaCost().isZero()) {
|
||||
return true;
|
||||
}
|
||||
@@ -973,7 +990,7 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ai.getOpponent())) {
|
||||
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) {
|
||||
return true;
|
||||
}
|
||||
if (card.isCreature()) {
|
||||
@@ -993,7 +1010,7 @@ public class ComputerUtil {
|
||||
} // BuffedBy
|
||||
|
||||
// get all cards the human controls with AntiBuffedBy
|
||||
final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
for (Card buffedcard : antibuffed) {
|
||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||
@@ -1033,11 +1050,11 @@ public class ComputerUtil {
|
||||
return ret;
|
||||
} else {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
Combat combat = new Combat(ai.getOpponent());
|
||||
CardCollectionView attackers = ai.getOpponent().getCreaturesInPlay();
|
||||
Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
|
||||
CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||
for (Card att : attackers) {
|
||||
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
||||
combat.addAttacker(att, att.getController().getOpponent());
|
||||
combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
|
||||
}
|
||||
}
|
||||
AiBlockController aiBlock = new AiBlockController(ai);
|
||||
@@ -1101,7 +1118,7 @@ public class ComputerUtil {
|
||||
return (sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
|| !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases"));
|
||||
}
|
||||
@@ -1111,6 +1128,10 @@ public class ComputerUtil {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
|
||||
if (source != null && "ALWAYS".equals(source.getSVar("PlayMain1"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
@@ -1145,7 +1166,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
// get all cards the human controls with AntiBuffedBy
|
||||
final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
for (Card buffedcard : antibuffed) {
|
||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||
@@ -1331,7 +1352,7 @@ public class ComputerUtil {
|
||||
if (tgt == null) {
|
||||
continue;
|
||||
}
|
||||
final Player enemy = ai.getOpponent();
|
||||
final Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
if (!sa.canTarget(enemy)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1436,17 +1457,26 @@ public class ComputerUtil {
|
||||
objects = canBeTargeted;
|
||||
}
|
||||
|
||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
||||
toughness = saviour.hasParam("NumDef") ?
|
||||
AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("NumDef"), saviour) : 0;
|
||||
final List<String> keywords = saviour.hasParam("KW") ?
|
||||
Arrays.asList(saviour.getParam("KW").split(" & ")) : new ArrayList<String>();
|
||||
SpellAbility saviorWithSubs = saviour;
|
||||
ApiType saviorWithSubsApi = saviorWithSubs == null ? null : saviorWithSubs.getApi();
|
||||
while (saviorWithSubs != null) {
|
||||
ApiType curApi = saviorWithSubs.getApi();
|
||||
if (curApi == ApiType.Pump || curApi == ApiType.PumpAll) {
|
||||
toughness = saviorWithSubs.hasParam("NumDef") ?
|
||||
AbilityUtils.calculateAmount(saviorWithSubs.getHostCard(), saviorWithSubs.getParam("NumDef"), saviour) : 0;
|
||||
final List<String> keywords = saviorWithSubs.hasParam("KW") ?
|
||||
Arrays.asList(saviorWithSubs.getParam("KW").split(" & ")) : new ArrayList<String>();
|
||||
if (keywords.contains("Indestructible")) {
|
||||
grantIndestructible = true;
|
||||
}
|
||||
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
|
||||
grantShroud = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Consider pump in subabilities, e.g. Bristling Hydra hexproof subability
|
||||
saviorWithSubs = saviorWithSubs.getSubAbility();
|
||||
|
||||
}
|
||||
|
||||
if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
|
||||
@@ -1590,7 +1620,8 @@ public class ComputerUtil {
|
||||
&& (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
|
||||
&& !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone
|
||||
|| saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null)) {
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null
|
||||
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
@@ -1604,7 +1635,9 @@ public class ComputerUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviorWithSubsApi == ApiType.Pump
|
||||
|| saviorWithSubsApi == ApiType.PumpAll) {
|
||||
if ((tgt == null && !grantIndestructible)
|
||||
|| (!grantShroud && !grantIndestructible)) {
|
||||
continue;
|
||||
@@ -1857,6 +1890,27 @@ public class ComputerUtil {
|
||||
public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) {
|
||||
boolean bottom = false;
|
||||
|
||||
// AI profile-based toggles
|
||||
int maxLandsToScryLandsToTop = 3;
|
||||
int minLandsToScryLandsAway = 8;
|
||||
int minCreatsToScryCreatsAway = 5;
|
||||
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
|
||||
int lowCMCThreshold = 3;
|
||||
int maxCreatsToScryLowCMCAway = 3;
|
||||
boolean uncastablesToBottom = false;
|
||||
int uncastableCMCThreshold = 1;
|
||||
if (player.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||
maxLandsToScryLandsToTop = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||
minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
|
||||
minCreatsToScryCreatsAway = aic.getIntProperty(AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
|
||||
minCreatEvalThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
|
||||
lowCMCThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
|
||||
maxCreatsToScryLowCMCAway = aic.getIntProperty(AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
|
||||
uncastablesToBottom = aic.getBooleanProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
|
||||
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
|
||||
}
|
||||
|
||||
CardCollectionView allCards = player.getAllCards();
|
||||
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
|
||||
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
|
||||
@@ -1871,8 +1925,9 @@ public class ComputerUtil {
|
||||
CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player)));
|
||||
int numCards = allCreatures.size();
|
||||
|
||||
if (landsOTB.size() < 3 && landsInHand.isEmpty()) {
|
||||
if ((!c.isLand() && !manaArts.contains(c.getName())) || c.getManaAbilities().isEmpty()) {
|
||||
if (landsOTB.size() < maxLandsToScryLandsToTop && landsInHand.isEmpty()) {
|
||||
if ((!c.isLand() && !manaArts.contains(c.getName()))
|
||||
|| (c.getManaAbilities().isEmpty() && !c.hasABasicLandType())) {
|
||||
// scry away non-lands and non-manaproducing lands in situations when the land count
|
||||
// on the battlefield is low, to try to improve the mana base early
|
||||
bottom = true;
|
||||
@@ -1880,7 +1935,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
if (c.isLand()) {
|
||||
if (landsOTB.size() >= 8) {
|
||||
if (landsOTB.size() >= minLandsToScryLandsAway) {
|
||||
// probably enough lands not to urgently need another one, so look for more gas instead
|
||||
bottom = true;
|
||||
} else if (landsInHand.size() >= Math.max(cardsInHand.size() / 2, 2)) {
|
||||
@@ -1898,16 +1953,15 @@ public class ComputerUtil {
|
||||
} else if (c.isCreature()) {
|
||||
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
|
||||
int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0;
|
||||
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
|
||||
int maxControlledCMC = Aggregates.max(creaturesOTB, CardPredicates.Accessors.fnGetCmc);
|
||||
|
||||
if (ComputerUtilCard.evaluateCreature(c) < avgCreatureValue) {
|
||||
if (creaturesOTB.size() > 5) {
|
||||
if (creaturesOTB.size() > minCreatsToScryCreatsAway) {
|
||||
// if there are more than five creatures and the creature is question is below average for
|
||||
// the deck, scry it to the bottom
|
||||
bottom = true;
|
||||
} else if (creaturesOTB.size() > 3 && c.getCMC() <= 3
|
||||
&& maxControlledCMC >= 4 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
|
||||
} else if (creaturesOTB.size() > maxCreatsToScryLowCMCAway && c.getCMC() <= lowCMCThreshold
|
||||
&& maxControlledCMC >= lowCMCThreshold + 1 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
|
||||
// if we are already at a stage when we have 4+ CMC creatures on the battlefield,
|
||||
// probably worth it to scry away very low value creatures with low CMC
|
||||
bottom = true;
|
||||
@@ -1915,6 +1969,15 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (uncastablesToBottom && !c.isLand()) {
|
||||
int cmc = c.isSplitCard() ? Math.min(c.getCMC(Card.SplitCMCMode.LeftSplitCMC), c.getCMC(Card.SplitCMCMode.RightSplitCMC))
|
||||
: c.getCMC();
|
||||
int maxCastable = ComputerUtilMana.getAvailableManaEstimate(player, false) + landsInHand.size();
|
||||
if (cmc - maxCastable >= uncastableCMCThreshold) {
|
||||
bottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
return bottom;
|
||||
}
|
||||
|
||||
@@ -2047,7 +2110,7 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
else if (logic.equals("ChosenLandwalk")) {
|
||||
for (Card c : ai.getOpponent().getLandsInPlay()) {
|
||||
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
|
||||
for (String t : c.getType()) {
|
||||
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
|
||||
chosen = t;
|
||||
@@ -2065,7 +2128,7 @@ public class ComputerUtil {
|
||||
else if (kindOfType.equals("Land")) {
|
||||
if (logic != null) {
|
||||
if (logic.equals("ChosenLandwalk")) {
|
||||
for (Card c : ai.getOpponent().getLandsInPlay()) {
|
||||
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
|
||||
for (String t : c.getType().getLandTypes()) {
|
||||
if (!invalidTypes.contains(t)) {
|
||||
chosen = t;
|
||||
@@ -2098,7 +2161,7 @@ public class ComputerUtil {
|
||||
case "Torture":
|
||||
return "Torture";
|
||||
case "GraceOrCondemnation":
|
||||
return ai.getCreaturesInPlay().size() > ai.getOpponent().getCreaturesInPlay().size() ? "Grace"
|
||||
return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace"
|
||||
: "Condemnation";
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists
|
||||
@@ -2683,4 +2746,89 @@ public class ComputerUtil {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public static final Player getOpponentFor(final Player player) {
|
||||
// This method is deprecated and currently functions as a synonym for player.getWeakestOpponent
|
||||
// until it can be replaced everywhere in the code.
|
||||
|
||||
// Consider replacing calls to this method either with a multiplayer-friendly determination of
|
||||
// opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent
|
||||
// where that is applicable and makes sense from the point of view of multiplayer AI logic.
|
||||
Player opponent = player.getWeakestOpponent();
|
||||
if (opponent != null) {
|
||||
return opponent;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No opponents left ingame for " + player);
|
||||
}
|
||||
|
||||
public static int countUsefulCreatures(Player p) {
|
||||
CardCollection creats = p.getCreaturesInPlay();
|
||||
int count = 0;
|
||||
|
||||
for (Card c : creats) {
|
||||
if (!ComputerUtilCard.isUselessCreature(p, c)) {
|
||||
count ++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public static boolean isPlayingReanimator(final Player ai) {
|
||||
// TODO: either add SVars to other reanimator cards, or improve the prediction so that it avoids using a SVar
|
||||
// at all but detects this effect from SA parameters (preferred, but difficult)
|
||||
CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
|
||||
CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library});
|
||||
|
||||
Predicate<Card> markedAsReanimator = new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return "true".equalsIgnoreCase(card.getSVar("IsReanimatorCard"));
|
||||
}
|
||||
};
|
||||
|
||||
int numInHand = CardLists.filter(inHand, markedAsReanimator).size();
|
||||
int numInDeck = CardLists.filter(inDeck, markedAsReanimator).size();
|
||||
|
||||
return numInHand > 0 || numInDeck >= 3;
|
||||
}
|
||||
|
||||
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||
final Card source = sa.getHostCard();
|
||||
if (source == null) { return srcList; }
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection list;
|
||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||
int value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy(false)) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return ComputerUtilCard.evaluateCreature(c) > totalValue + 30;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
} else {
|
||||
return srcList;
|
||||
}
|
||||
}
|
||||
|
||||
return srcList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
@@ -17,7 +19,6 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.zone.ZoneType;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class ComputerUtilAbility {
|
||||
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
||||
@@ -78,7 +79,7 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
|
||||
public static List<SpellAbility> getSpellAbilities(final CardCollectionView l, final Player player) {
|
||||
final List<SpellAbility> spellAbilities = new ArrayList<SpellAbility>();
|
||||
final List<SpellAbility> spellAbilities = Lists.newArrayList();
|
||||
for (final Card c : l) {
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
spellAbilities.add(sa);
|
||||
@@ -93,7 +94,7 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
|
||||
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
||||
final List<SpellAbility> newAbilities = new ArrayList<SpellAbility>();
|
||||
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
||||
for (SpellAbility sa : originList) {
|
||||
sa.setActivatingPlayer(player);
|
||||
//add alternative costs as additional spell abilities
|
||||
@@ -101,7 +102,7 @@ public class ComputerUtilAbility {
|
||||
newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
|
||||
}
|
||||
|
||||
final List<SpellAbility> result = new ArrayList<SpellAbility>();
|
||||
final List<SpellAbility> result = Lists.newArrayList();
|
||||
for (SpellAbility sa : newAbilities) {
|
||||
sa.setActivatingPlayer(player);
|
||||
result.addAll(GameActionUtil.getOptionalCosts(sa));
|
||||
@@ -132,6 +133,42 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
|
||||
public static String getAbilitySourceName(SpellAbility sa) {
|
||||
return sa.getOriginalHost() != null ? sa.getOriginalHost().getName() : sa.getHostCard() != null ? sa.getHostCard().getName() : "";
|
||||
final Card c = getAbilitySource(sa);
|
||||
return c != null ? c.getName() : "";
|
||||
}
|
||||
|
||||
public static CardCollection getCardsTargetedWithApi(Player ai, CardCollection cardList, SpellAbility sa, ApiType api) {
|
||||
// Returns a collection of cards which have already been targeted with the given API either in the parent ability,
|
||||
// in the sub ability, or by something on stack. If "sa" is specified, the parent and sub abilities of this SA will
|
||||
// be checked for targets. If "sa" is null, only the stack instances will be checked.
|
||||
CardCollection targeted = new CardCollection();
|
||||
if (sa != null) {
|
||||
SpellAbility saSub = sa.getRootAbility();
|
||||
while (saSub != null) {
|
||||
if (saSub.getApi() == api && saSub.getTargets() != null) {
|
||||
for (Card c : cardList) {
|
||||
if (saSub.getTargets().getTargetCards().contains(c)) {
|
||||
// Was already targeted with this API in a parent or sub SA
|
||||
targeted.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
saSub = saSub.getSubAbility();
|
||||
}
|
||||
}
|
||||
for (SpellAbilityStackInstance si : ai.getGame().getStack()) {
|
||||
SpellAbility ab = si.getSpellAbility(false);
|
||||
if (ab != null && ab.getApi() == api && si.getTargetChoices() != null) {
|
||||
for (Card c : cardList) {
|
||||
// TODO: somehow ensure that the detected SA won't be countered
|
||||
if (si.getTargetChoices().getTargetCards().contains(c)) {
|
||||
// Was already targeted by a spell ability instance on stack
|
||||
targeted.add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return targeted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.card.CardType;
|
||||
@@ -29,19 +18,13 @@ import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordCollection;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -55,7 +38,12 @@ import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperCard;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public class ComputerUtilCard {
|
||||
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
|
||||
@@ -364,7 +352,12 @@ public class ComputerUtilCard {
|
||||
}
|
||||
|
||||
if (hasEnchantmants || hasArtifacts) {
|
||||
final List<Card> ae = CardLists.filter(list, Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS));
|
||||
final List<Card> ae = CardLists.filter(list, Predicates.and(Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return !card.hasSVar("DoNotDiscardIfAble");
|
||||
}
|
||||
}));
|
||||
return getCheapestPermanentAI(ae, null, false);
|
||||
}
|
||||
|
||||
@@ -377,6 +370,32 @@ public class ComputerUtilCard {
|
||||
return getCheapestPermanentAI(list, null, false);
|
||||
}
|
||||
|
||||
public static final Card getCheapestSpellAI(final Iterable<Card> list) {
|
||||
if (!Iterables.isEmpty(list)) {
|
||||
CardCollection cc = CardLists.filter(new CardCollection(list),
|
||||
Predicates.or(CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||
Collections.sort(cc, CardLists.CmcComparatorInv);
|
||||
|
||||
if (cc.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Card cheapest = cc.getLast();
|
||||
if (cheapest.hasSVar("DoNotDiscardIfAble")) {
|
||||
for (int i = cc.size() - 1; i >= 0; i--) {
|
||||
if (!cc.get(i).hasSVar("DoNotDiscardIfAble")) {
|
||||
cheapest = cc.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cheapest;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
|
||||
@Override
|
||||
public int compare(final Card a, final Card b) {
|
||||
@@ -399,6 +418,10 @@ public class ComputerUtilCard {
|
||||
return creatureEvaluator.evaluateCreature(c);
|
||||
}
|
||||
|
||||
public static int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
|
||||
return creatureEvaluator.evaluateCreature(c, considerPT, considerCMC);
|
||||
}
|
||||
|
||||
public static int evaluatePermanentList(final CardCollectionView list) {
|
||||
int value = 0;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
@@ -482,7 +505,7 @@ public class ComputerUtilCard {
|
||||
*/
|
||||
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
||||
AiBlockController aiBlk = new AiBlockController(ai);
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
Combat combat = new Combat(opp);
|
||||
//Use actual attackers if available, else consider all possible attackers
|
||||
Combat currentCombat = ai.getGame().getCombat();
|
||||
@@ -845,9 +868,10 @@ public class ComputerUtilCard {
|
||||
List<String> chosen = new ArrayList<String>();
|
||||
Player ai = sa.getActivatingPlayer();
|
||||
final Game game = ai.getGame();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.hasParam("AILogic")) {
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
if (logic.equals("MostProminentInHumanDeck")) {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
|
||||
}
|
||||
@@ -873,7 +897,7 @@ public class ComputerUtilCard {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
}
|
||||
else if (logic.equals("MostProminentHumanControls")) {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getOpponent().getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(opp.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
}
|
||||
else if (logic.equals("MostProminentPermanent")) {
|
||||
chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||
@@ -897,7 +921,7 @@ public class ComputerUtilCard {
|
||||
String bestColor = Constant.GREEN;
|
||||
for (byte color : MagicColor.WUBRG) {
|
||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = opp.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||
@@ -934,7 +958,7 @@ public class ComputerUtilCard {
|
||||
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final PhaseType phaseType = ph.getPhase();
|
||||
@@ -1174,6 +1198,18 @@ public class ComputerUtilCard {
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
final Combat combat = phase.getCombat();
|
||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||
final boolean loseCardAtEOT = "Sacrifice".equals(sa.getParam("AtEOT")) || "Exile".equals(sa.getParam("AtEOT"))
|
||||
|| "Destroy".equals(sa.getParam("AtEOT")) || "ExileCombat".equals(sa.getParam("AtEOT"));
|
||||
|
||||
boolean combatTrick = false;
|
||||
boolean holdCombatTricks = false;
|
||||
int chanceToHoldCombatTricks = -1;
|
||||
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||
}
|
||||
|
||||
if (!c.canBeTargetedBy(sa)) {
|
||||
return false;
|
||||
@@ -1186,11 +1222,11 @@ public class ComputerUtilCard {
|
||||
/* -- currently disabled until better conditions are devised and the spell prediction is made smarter --
|
||||
// Determine if some mana sources need to be held for the future spell to cast in Main 2 before determining whether to pump.
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
|
||||
if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
|
||||
// only hold mana sources once
|
||||
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Pump);
|
||||
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
||||
aic.reserveManaSourcesForMain2(futureSpell);
|
||||
aic.reserveManaSources(futureSpell);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1204,8 +1240,8 @@ public class ComputerUtilCard {
|
||||
return true;
|
||||
}
|
||||
|
||||
// buff attacker/blocker using triggered pump
|
||||
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
// buff attacker/blocker using triggered pump (unless it's lethal and we don't want to be reckless)
|
||||
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !loseCardAtEOT) {
|
||||
if (phase.isPlayerTurn(ai)) {
|
||||
if (CombatUtil.canAttack(c)) {
|
||||
return true;
|
||||
@@ -1217,7 +1253,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
}
|
||||
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
|
||||
List<Card> oppCreatures = opp.getCreaturesInPlay();
|
||||
float chance = 0;
|
||||
@@ -1234,6 +1270,28 @@ public class ComputerUtilCard {
|
||||
threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
|
||||
}
|
||||
chance += threat;
|
||||
|
||||
// -- Hold combat trick (the AI will try to delay the pump until Declare Blockers) --
|
||||
// Enable combat trick mode only in case it's a pure buff spell in hand with no keywords or with Trample,
|
||||
// First Strike, or Double Strike, otherwise the AI is unlikely to cast it or it's too late to
|
||||
// cast it during Declare Blockers, thus ruining its attacker
|
||||
if (holdCombatTricks && sa.getApi() == ApiType.Pump
|
||||
&& sa.hasParam("NumAtt") && sa.getHostCard() != null
|
||||
&& sa.getHostCard().getZone() != null && sa.getHostCard().getZone().is(ZoneType.Hand)
|
||||
&& c.getNetPower() > 0 // too obvious if attacking with a 0-power creature
|
||||
&& sa.getHostCard().isInstant() // only do it for instant speed spells in hand
|
||||
&& ComputerUtilMana.hasEnoughManaSourcesToCast(sa, ai)) {
|
||||
combatTrick = true;
|
||||
|
||||
final List<String> kws = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
|
||||
: Lists.<String>newArrayList();
|
||||
for (String kw : kws) {
|
||||
if (!kw.equals("Trample") && !kw.equals("First Strike") && !kw.equals("Double Strike")) {
|
||||
combatTrick = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//2. grant haste
|
||||
@@ -1261,7 +1319,7 @@ public class ComputerUtilCard {
|
||||
boolean pumpedWillDie = false;
|
||||
final boolean isAttacking = combat.isAttacking(c);
|
||||
|
||||
if (isBerserk && isAttacking) { pumpedWillDie = true; }
|
||||
if ((isBerserk && isAttacking) || loseCardAtEOT) { pumpedWillDie = true; }
|
||||
|
||||
if (isAttacking) {
|
||||
pumpedCombat.addAttacker(pumped, opp);
|
||||
@@ -1319,6 +1377,16 @@ public class ComputerUtilCard {
|
||||
if (combat.isAttacking(c) && opp.getLife() > 0) {
|
||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
|
||||
int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true);
|
||||
int poisonOrig = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0;
|
||||
int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
|
||||
|
||||
// predict Infect
|
||||
if (pumpedDmg == 0 && c.hasKeyword("Infect")) {
|
||||
if (poisonPumped > poisonOrig) {
|
||||
pumpedDmg = poisonPumped;
|
||||
}
|
||||
}
|
||||
|
||||
if (combat.isBlocked(c)) {
|
||||
if (!c.hasKeyword("Trample")) {
|
||||
dmg = 0;
|
||||
@@ -1331,9 +1399,12 @@ public class ComputerUtilCard {
|
||||
pumpedDmg = 0;
|
||||
}
|
||||
}
|
||||
if (pumpedDmg >= opp.getLife()) {
|
||||
if (pumpedDmg > dmg) {
|
||||
if ((!c.hasKeyword("Infect") && pumpedDmg >= opp.getLife())
|
||||
|| (c.hasKeyword("Infect") && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// try to determine if pumping a creature for more power will give lethal on board
|
||||
// considering all unblocked creatures after the blockers are already declared
|
||||
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
|
||||
@@ -1395,14 +1466,47 @@ public class ComputerUtilCard {
|
||||
|
||||
if (isBerserk) {
|
||||
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
||||
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
|
||||
if (ai.getController() instanceof PlayerControllerAi) {
|
||||
boolean aggr = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY);
|
||||
boolean aggr = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
|
||||
|| sa.hasParam("AtEOT");
|
||||
if (!aggr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean wantToHoldTrick = holdCombatTricks;
|
||||
if (chanceToHoldCombatTricks >= 0) {
|
||||
// Obey the chance specified in the AI profile for holding combat tricks
|
||||
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
|
||||
} else {
|
||||
// Use standard considerations dependent solely on the buff chance determined above
|
||||
wantToHoldTrick &= MyRandom.getRandom().nextFloat() < chance;
|
||||
}
|
||||
|
||||
boolean isHeldCombatTrick = combatTrick && wantToHoldTrick;
|
||||
|
||||
if (isHeldCombatTrick) {
|
||||
if (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.TRICK_ATTACKERS)) {
|
||||
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
|
||||
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
|
||||
// an army of opposing blockers with only one combat trick in hand)
|
||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
|
||||
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
|
||||
// the combat trick
|
||||
if (ai.getController().isAI()) {
|
||||
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
|
||||
// the AI overextending the attack
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return MyRandom.getRandom().nextFloat() < chance;
|
||||
}
|
||||
|
||||
@@ -1417,8 +1521,7 @@ public class ComputerUtilCard {
|
||||
* @return
|
||||
*/
|
||||
public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
|
||||
final Card c, final int toughness, final int power,
|
||||
final List<String> keywords) {
|
||||
final Card c, int toughness, int power, final List<String> keywords) {
|
||||
Card pumped = CardFactory.copyCard(c, true);
|
||||
pumped.setSickness(c.hasSickness());
|
||||
final long timestamp = c.getGame().getNextTimestamp();
|
||||
@@ -1431,8 +1534,25 @@ public class ComputerUtilCard {
|
||||
}
|
||||
}
|
||||
|
||||
// Berserk (and other similar cards)
|
||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||
final int berserkPower = isBerserk ? c.getCurrentPower() : 0;
|
||||
int berserkPower = 0;
|
||||
if (isBerserk && sa.hasSVar("X")) {
|
||||
if ("Targeted$CardPower".equals(sa.getSVar("X"))) {
|
||||
berserkPower = c.getCurrentPower();
|
||||
} else {
|
||||
berserkPower = AbilityUtils.calculateAmount(sa.getHostCard(), "X", sa);
|
||||
}
|
||||
}
|
||||
|
||||
// Electrostatic Pummeler
|
||||
for (SpellAbility ab : c.getSpellAbilities()) {
|
||||
if ("Pummeler".equals(ab.getParam("AILogic"))) {
|
||||
Pair<Integer, Integer> newPT = SpecialCardAi.ElectrostaticPummeler.getPumpedPT(ai, power, toughness);
|
||||
power = newPT.getLeft();
|
||||
toughness = newPT.getRight();
|
||||
}
|
||||
}
|
||||
|
||||
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
|
||||
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
|
||||
@@ -1446,19 +1566,21 @@ public class ComputerUtilCard {
|
||||
if (c.isTapped()) {
|
||||
pumped.setTapped(true);
|
||||
}
|
||||
final List<String> copiedKeywords = pumped.getKeywords();
|
||||
List<String> toCopy = new ArrayList<String>();
|
||||
for (String kw : c.getKeywords()) {
|
||||
if (!copiedKeywords.contains(kw)) {
|
||||
if (kw.startsWith("HIDDEN")) {
|
||||
pumped.addHiddenExtrinsicKeyword(kw);
|
||||
|
||||
KeywordCollection copiedKeywords = new KeywordCollection();
|
||||
copiedKeywords.insertAll(pumped.getKeywords());
|
||||
List<KeywordInterface> toCopy = Lists.newArrayList();
|
||||
for (KeywordInterface k : c.getKeywords()) {
|
||||
if (!copiedKeywords.contains(k.getOriginal())) {
|
||||
if (k.getHidden()) {
|
||||
pumped.addHiddenExtrinsicKeyword(k);
|
||||
} else {
|
||||
toCopy.add(kw);
|
||||
toCopy.add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
|
||||
pumped.addChangedCardKeywords(toCopy, new ArrayList<String>(), false, timestamp2);
|
||||
pumped.addChangedCardKeywordsInternal(toCopy, Lists.<KeywordInterface>newArrayList(), false, timestamp2, true);
|
||||
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
|
||||
return pumped;
|
||||
}
|
||||
@@ -1612,4 +1734,55 @@ public class ComputerUtilCard {
|
||||
|
||||
return maxEnergyCost;
|
||||
}
|
||||
|
||||
public static CardCollection prioritizeCreaturesWorthRemovingNow(final Player ai, CardCollection oppCards, final boolean temporary) {
|
||||
if (!CardLists.getNotType(oppCards, "Creature").isEmpty()) {
|
||||
// non-creatures were passed, nothing to do here
|
||||
return oppCards;
|
||||
}
|
||||
|
||||
boolean enablePriorityRemoval = false;
|
||||
boolean priorityRemovalOnlyInDanger = false;
|
||||
int priorityRemovalThreshold = 0;
|
||||
int lifeInDanger = 5;
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
enablePriorityRemoval = aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
|
||||
priorityRemovalThreshold = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
|
||||
priorityRemovalOnlyInDanger = aic.getBooleanProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
|
||||
lifeInDanger = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
|
||||
}
|
||||
|
||||
if (!enablePriorityRemoval) {
|
||||
// Nothing to do here, the profile does not allow prioritizing
|
||||
return oppCards;
|
||||
}
|
||||
|
||||
CardCollection aiCreats = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||
if (temporary) {
|
||||
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
|
||||
oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED);
|
||||
}
|
||||
|
||||
CardCollection priorityCards = new CardCollection();
|
||||
for (Card atk : oppCards) {
|
||||
if (isUselessCreature(atk.getController(), atk)) {
|
||||
continue;
|
||||
}
|
||||
for (Card blk : aiCreats) {
|
||||
if (!CombatUtil.canBlock(atk, blk, true)) {
|
||||
boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
|
||||
if (!priorityRemovalOnlyInDanger || threat) {
|
||||
priorityCards.add(atk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!priorityCards.isEmpty() && priorityCards.size() <= priorityRemovalThreshold) {
|
||||
return priorityCards;
|
||||
}
|
||||
|
||||
return oppCards;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,16 +32,11 @@ import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
@@ -53,6 +48,7 @@ import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
|
||||
@@ -109,7 +105,8 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final String keyword : atacker.getKeywords()) {
|
||||
for (final KeywordInterface inst : atacker.getKeywords()) {
|
||||
final String keyword = inst.getOriginal();
|
||||
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
|
||||
final String defined = keyword.split(":")[1];
|
||||
final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0);
|
||||
@@ -390,6 +387,18 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
|
||||
// Special cases:
|
||||
// AI can't lose in combat in presence of Worship (with creatures)
|
||||
if (!CardLists.filter(otb, CardPredicates.nameEquals("Worship")).isEmpty() && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// AI can't lose in combat in presence of Elderscale Wurm (at 7 life or more)
|
||||
if (!CardLists.filter(otb, CardPredicates.nameEquals("Elderscale Wurm")).isEmpty() && ai.getLife() >= 7) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// check for creatures that must be blocked
|
||||
final List<Card> attackers = combat.getAttackersOf(ai);
|
||||
|
||||
@@ -401,9 +410,20 @@ public class ComputerUtilCombat {
|
||||
|
||||
if (blockers.isEmpty()) {
|
||||
if (!attacker.getSVar("MustBeBlocked").equals("")) {
|
||||
boolean cond = false;
|
||||
String condVal = attacker.getSVar("MustBeBlocked");
|
||||
boolean isAttackingPlayer = combat.getDefenderByAttacker(attacker) instanceof Player;
|
||||
|
||||
cond |= "true".equalsIgnoreCase(condVal);
|
||||
cond |= "attackingplayer".equalsIgnoreCase(condVal) && isAttackingPlayer;
|
||||
cond |= "attackingplayerconservative".equalsIgnoreCase(condVal) && isAttackingPlayer
|
||||
&& ai.getCreaturesInPlay().size() >= 3 && ai.getCreaturesInPlay().size() > attacker.getController().getCreaturesInPlay().size();
|
||||
|
||||
if (cond) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (threateningCommanders.contains(attacker)) {
|
||||
return true;
|
||||
}
|
||||
@@ -683,12 +703,21 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static boolean attackerWouldBeDestroyed(Player ai, final Card attacker, Combat combat) {
|
||||
final List<Card> blockers = combat.getBlockers(attacker);
|
||||
int firstStrikeBlockerDmg = 0;
|
||||
|
||||
for (final Card defender : blockers) {
|
||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
|
||||
&& !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
|
||||
return true;
|
||||
}
|
||||
if (defender.hasKeyword("First Strike") || defender.hasKeyword("Double Strike")) {
|
||||
firstStrikeBlockerDmg += defender.getNetCombatDamage();
|
||||
}
|
||||
}
|
||||
|
||||
// Consider first strike and double strike
|
||||
if (attacker.hasKeyword("First Strike") || attacker.hasKeyword("Double Strike")) {
|
||||
return firstStrikeBlockerDmg >= ComputerUtilCombat.getDamageToKill(attacker);
|
||||
}
|
||||
|
||||
return ComputerUtilCombat.totalDamageOfBlockers(attacker, blockers) >= ComputerUtilCombat.getDamageToKill(attacker);
|
||||
@@ -781,7 +810,7 @@ public class ComputerUtilCombat {
|
||||
if (validBlocked.contains(".withLesserPower")) {
|
||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||
// ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
|
||||
validBlocked = validBlocked.replace(".withLesserPower", "");
|
||||
validBlocked = TextUtil.fastReplace(validBlocked, ".withLesserPower", "");
|
||||
if (defender.getCurrentPower() <= attacker.getCurrentPower()) {
|
||||
return false;
|
||||
}
|
||||
@@ -795,7 +824,7 @@ public class ComputerUtilCombat {
|
||||
if (validBlocker.contains(".withLesserPower")) {
|
||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
|
||||
validBlocker = validBlocker.replace(".withLesserPower", "");
|
||||
validBlocker = TextUtil.fastReplace(validBlocker, ".withLesserPower", "");
|
||||
if (defender.getCurrentPower() >= attacker.getCurrentPower()) {
|
||||
return false;
|
||||
}
|
||||
@@ -854,9 +883,12 @@ public class ComputerUtilCombat {
|
||||
public static int predictPowerBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
|
||||
int power = 0;
|
||||
|
||||
// Apparently, Flanking is predicted below from a trigger, so using the code below results in double
|
||||
// application of power bonus. A bit more testing may be needed though, so commenting out for now.
|
||||
/*
|
||||
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
||||
power -= attacker.getAmountOfKeyword("Flanking");
|
||||
}
|
||||
}*/
|
||||
|
||||
// Serene Master switches power with attacker
|
||||
if (blocker.getName().equals("Serene Master")) {
|
||||
@@ -874,8 +906,6 @@ public class ComputerUtilCombat {
|
||||
power -= attacker.getNetCombatDamage();
|
||||
}
|
||||
|
||||
power += blocker.getKeywordMagnitude("Bushido");
|
||||
|
||||
final Game game = attacker.getGame();
|
||||
// look out for continuous static abilities that only care for blocking
|
||||
// creatures
|
||||
@@ -889,7 +919,7 @@ public class ComputerUtilCombat {
|
||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = params.get("Affected").replace("blocking", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "blocking", "Creature");
|
||||
if (!blocker.isValid(valid, card.getController(), card, null)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1024,7 +1054,6 @@ public class ComputerUtilCombat {
|
||||
toughness += attacker.getNetToughness() - blocker.getNetToughness();
|
||||
}
|
||||
|
||||
toughness += blocker.getKeywordMagnitude("Bushido");
|
||||
final Game game = attacker.getGame();
|
||||
final FCollection<Trigger> theTriggers = new FCollection<Trigger>();
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1176,10 +1205,12 @@ public class ComputerUtilCombat {
|
||||
* a {@link forge.game.combat.Combat} object.
|
||||
* @return a int.
|
||||
*/
|
||||
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat , boolean withoutAbilities) {
|
||||
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat, boolean withoutAbilities) {
|
||||
return predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat, boolean withoutAbilities, boolean withoutCombatStaticAbilities) {
|
||||
int power = 0;
|
||||
|
||||
power += attacker.getKeywordMagnitude("Bushido");
|
||||
//check Exalted only for the first attacker
|
||||
if (combat != null && combat.getAttackers().isEmpty()) {
|
||||
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1216,6 +1247,7 @@ public class ComputerUtilCombat {
|
||||
|
||||
// look out for continuous static abilities that only care for attacking
|
||||
// creatures
|
||||
if (!withoutCombatStaticAbilities) {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
@@ -1226,7 +1258,7 @@ public class ComputerUtilCombat {
|
||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1241,6 +1273,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Trigger trigger : theTriggers) {
|
||||
final Map<String, String> trigParams = trigger.getMapParams();
|
||||
@@ -1305,9 +1338,13 @@ public class ComputerUtilCombat {
|
||||
} else {
|
||||
String bonus = new String(source.getSVar(att));
|
||||
if (bonus.contains("TriggerCount$NumBlockers")) {
|
||||
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
|
||||
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
|
||||
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
} else if (bonus.contains("TriggeredAttacker$CardPower")) { // e.g. Arahbo, Roar of the World
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardPower", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetPower())));
|
||||
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
|
||||
}
|
||||
power += CardFactoryUtil.xCount(source, bonus);
|
||||
|
||||
@@ -1372,6 +1409,10 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
|
||||
, boolean withoutAbilities) {
|
||||
return predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
|
||||
, boolean withoutAbilities, boolean withoutCombatStaticAbilities) {
|
||||
int toughness = 0;
|
||||
|
||||
//check Exalted only for the first attacker
|
||||
@@ -1394,12 +1435,12 @@ public class ComputerUtilCombat {
|
||||
theTriggers.addAll(card.getTriggers());
|
||||
}
|
||||
if (blocker != null) {
|
||||
toughness += attacker.getKeywordMagnitude("Bushido");
|
||||
theTriggers.addAll(blocker.getTriggers());
|
||||
}
|
||||
|
||||
// look out for continuous static abilities that only care for attacking
|
||||
// creatures
|
||||
if (!withoutCombatStaticAbilities) {
|
||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
@@ -1408,7 +1449,7 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
|
||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1422,7 +1463,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
|
||||
final String valid = params.get("Affected").replace("untapped", "Creature");
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)
|
||||
|| attacker.hasKeyword("Vigilance")) {
|
||||
continue;
|
||||
@@ -1434,6 +1475,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final Trigger trigger : theTriggers) {
|
||||
final Map<String, String> trigParams = trigger.getMapParams();
|
||||
@@ -1517,9 +1559,9 @@ public class ComputerUtilCombat {
|
||||
} else {
|
||||
String bonus = new String(source.getSVar(def));
|
||||
if (bonus.contains("TriggerCount$NumBlockers")) {
|
||||
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
|
||||
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
|
||||
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
}
|
||||
toughness += CardFactoryUtil.xCount(source, bonus);
|
||||
}
|
||||
@@ -1674,6 +1716,10 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
|
||||
final boolean withoutAbilities) {
|
||||
return canDestroyAttacker(ai, attacker, blocker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
|
||||
final boolean withoutAbilities, final boolean withoutAttackerStaticAbilities) {
|
||||
// Can activate transform ability
|
||||
if (!withoutAbilities) {
|
||||
attacker = canTransform(attacker);
|
||||
@@ -1725,10 +1771,10 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
if (attacker.toughnessAssignsDamage()) {
|
||||
attackerDamage = attacker.getNetToughness()
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
} else {
|
||||
attackerDamage = attacker.getNetPower()
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
}
|
||||
|
||||
int possibleDefenderPrevention = 0;
|
||||
@@ -1741,11 +1787,16 @@ public class ComputerUtilCombat {
|
||||
// consider Damage Prevention/Replacement
|
||||
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
|
||||
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
|
||||
if (!attacker.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
|
||||
if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
||||
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
if (blocker.hasKeyword("Double Strike")) {
|
||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||
@@ -1914,6 +1965,10 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
|
||||
final boolean withoutAbilities) {
|
||||
return canDestroyBlocker(ai, blocker, attacker, combat, withoutAbilities, false);
|
||||
}
|
||||
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
|
||||
final boolean withoutAbilities, final boolean withoutAttackerStaticAbilities) {
|
||||
// Can activate transform ability
|
||||
if (!withoutAbilities) {
|
||||
attacker = canTransform(attacker);
|
||||
@@ -1947,10 +2002,10 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
if (attacker.toughnessAssignsDamage()) {
|
||||
attackerDamage = attacker.getNetToughness()
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
} else {
|
||||
attackerDamage = attacker.getNetPower()
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
}
|
||||
|
||||
int possibleDefenderPrevention = 0;
|
||||
@@ -1975,7 +2030,7 @@ public class ComputerUtilCombat {
|
||||
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
||||
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
if (attacker.hasKeyword("Double Strike")) {
|
||||
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
|
||||
@@ -2192,6 +2247,7 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public final static int getDamageToKill(final Card c) {
|
||||
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
|
||||
|
||||
if ((killDamage > c.getPreventNextDamageTotalShields())
|
||||
&& c.hasSVar("DestroyWhenDamaged")) {
|
||||
killDamage = 1 + c.getPreventNextDamageTotalShields();
|
||||
@@ -2431,6 +2487,95 @@ public class ComputerUtilCombat {
|
||||
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
|
||||
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
|
||||
}
|
||||
|
||||
public static int getMaxAttackersFor(final GameEntity defender) {
|
||||
if (defender instanceof Player) {
|
||||
for (final Card card : ((Player) defender).getCardsIn(ZoneType.Battlefield)) {
|
||||
if (card.hasKeyword("No more than one creature can attack you each combat.")) {
|
||||
return 1;
|
||||
} else if (card.hasKeyword("No more than two creatures can attack you each combat.")) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<Card> categorizeAttackersByEvasion(List<Card> attackers) {
|
||||
List<Card> categorizedAttackers = Lists.newArrayList();
|
||||
|
||||
CardCollection withEvasion = new CardCollection();
|
||||
CardCollection withoutEvasion = new CardCollection();
|
||||
|
||||
for (Card atk : attackers) {
|
||||
boolean hasProtection = false;
|
||||
for (KeywordInterface inst : atk.getKeywords()) {
|
||||
String kw = inst.getOriginal();
|
||||
if (kw.startsWith("Protection")) {
|
||||
hasProtection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (atk.hasKeyword("Flying") || atk.hasKeyword("Shadow")
|
||||
|| atk.hasKeyword("Horsemanship") || (atk.hasKeyword("Fear")
|
||||
|| atk.hasKeyword("Intimidate") || atk.hasKeyword("Skulk") || hasProtection)) {
|
||||
withEvasion.add(atk);
|
||||
} else {
|
||||
withoutEvasion.add(atk);
|
||||
}
|
||||
}
|
||||
|
||||
// attackers that can only be blocked by cards with specific keywords or color, etc.
|
||||
// (maybe will need to split into 2 or 3 tiers depending on importance)
|
||||
categorizedAttackers.addAll(withEvasion);
|
||||
// all other attackers that have no evasion
|
||||
// (Menace and other abilities that limit blocking by amount of blockers is likely handled
|
||||
// elsewhere, but that needs testing and possibly fine-tuning).
|
||||
categorizedAttackers.addAll(withoutEvasion);
|
||||
|
||||
return categorizedAttackers;
|
||||
}
|
||||
|
||||
public static Card applyPotentialAttackCloneTriggers(Card attacker) {
|
||||
// This method returns the potentially cloned card if the creature turns into something else during the attack
|
||||
// (currently looks for the creature with maximum raw power since that's what the AI usually judges by when
|
||||
// deciding whether the creature is worth blocking).
|
||||
// If the creature doesn't change into anything, returns the original creature.
|
||||
if (attacker == null) { return null; }
|
||||
Card attackerAfterTrigs = attacker;
|
||||
|
||||
// Test for some special triggers that can change the creature in combat
|
||||
for (Trigger t : attacker.getTriggers()) {
|
||||
if (t.getMode() == TriggerType.Attacks && t.hasParam("Execute")) {
|
||||
if (!attacker.hasSVar(t.getParam("Execute"))) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility exec = AbilityFactory.getAbility(attacker, t.getParam("Execute"));
|
||||
if (exec != null) {
|
||||
if (exec.getApi() == ApiType.Clone && "Self".equals(exec.getParam("CloneTarget"))
|
||||
&& exec.hasParam("ValidTgts") && exec.getParam("ValidTgts").contains("Creature")
|
||||
&& exec.getParam("ValidTgts").contains("attacking")) {
|
||||
// Tilonalli's Skinshifter and potentially other similar cards that can clone other stuff
|
||||
// while attacking
|
||||
if (exec.getParam("ValidTgts").contains("nonLegendary") && attacker.getType().isLegendary()) {
|
||||
continue;
|
||||
}
|
||||
int maxPwr = 0;
|
||||
for (Card c : attacker.getController().getCreaturesInPlay()) {
|
||||
if (c.getNetPower() > maxPwr || (c.getNetPower() == maxPwr && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(attackerAfterTrigs))) {
|
||||
maxPwr = c.getNetPower();
|
||||
attackerAfterTrigs = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attackerAfterTrigs;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.ai.ability.AnimateAi;
|
||||
import forge.card.ColorSet;
|
||||
import forge.game.GameActionUtil;
|
||||
@@ -27,6 +21,10 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public class ComputerUtilCost {
|
||||
@@ -90,6 +88,15 @@ public class ComputerUtilCost {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove X counters - set ChosenX to max possible value here, the SAs should correct that
|
||||
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
|
||||
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
|
||||
final String sVar = sa.getSVar(remCounter.getAmount());
|
||||
if (sVar.equals("XChoice")) {
|
||||
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
|
||||
}
|
||||
}
|
||||
|
||||
// check the sa what the PaymentDecision is.
|
||||
// ignore Loyality abilities with Zero as Cost
|
||||
if (sa != null && !CounterType.LOYALTY.equals(type)) {
|
||||
@@ -231,13 +238,15 @@ public class ComputerUtilCost {
|
||||
* the source
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean checkCreatureSacrificeCost(final Player ai, final Cost cost, final Card source) {
|
||||
public static boolean checkCreatureSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
if (sac.payCostFromSource() && source.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
@@ -247,10 +256,19 @@ public class ComputerUtilCost {
|
||||
continue;
|
||||
}
|
||||
|
||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(","), source.getController(), source, null);
|
||||
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
|
||||
final CardCollection sacList = new CardCollection();
|
||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
||||
|
||||
int count = 0;
|
||||
while (count < amount) {
|
||||
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
|
||||
if (prefCard == null) {
|
||||
return false;
|
||||
}
|
||||
sacList.add(prefCard);
|
||||
typeList.remove(prefCard);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -267,13 +285,14 @@ public class ComputerUtilCost {
|
||||
* is the gain important enough?
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final boolean important) {
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility, final boolean important) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
final String type = sac.getType();
|
||||
|
||||
@@ -286,10 +305,20 @@ public class ComputerUtilCost {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
final CardCollection sacList = new CardCollection();
|
||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
||||
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
|
||||
|
||||
int count = 0;
|
||||
while (count < amount) {
|
||||
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
|
||||
if (prefCard == null) {
|
||||
return false;
|
||||
}
|
||||
sacList.add(prefCard);
|
||||
typeList.remove(prefCard);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -336,7 +365,7 @@ public class ComputerUtilCost {
|
||||
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
|
||||
String type = part.getType();
|
||||
String totalP = type.split("withTotalPowerGE")[1];
|
||||
type = type.replace("+withTotalPowerGE" + totalP, "");
|
||||
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
|
||||
CardCollection exclude = CardLists.getValidCards(
|
||||
new CardCollection(ai.getCardsIn(ZoneType.Battlefield)), type.split(";"),
|
||||
source.getController(), source, sa);
|
||||
@@ -364,8 +393,8 @@ public class ComputerUtilCost {
|
||||
* the source
|
||||
* @return true, if successful
|
||||
*/
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source) {
|
||||
return checkSacrificeCost(ai, cost, source, true);
|
||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
|
||||
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -561,12 +590,12 @@ public class ComputerUtilCost {
|
||||
|
||||
return checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& checkDamageCost(payer, cost, source, 4)
|
||||
&& (isMine || checkSacrificeCost(payer, cost, source))
|
||||
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || checkDiscardCost(payer, cost, source))
|
||||
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
|
||||
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
|
||||
&& (!source.getName().equals("Chain of Vapor") || (payer.getOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||
&& (!source.getName().equals("Chain of Vapor") || (ComputerUtil.getOpponentFor(payer).getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||
}
|
||||
|
||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package forge.ai;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.*;
|
||||
import forge.ai.ability.AnimateAi;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
@@ -18,11 +14,7 @@ import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostAdjustment;
|
||||
@@ -41,7 +33,6 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
@@ -203,7 +194,8 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sa.getHostCard() != null && sa.getApi() == ApiType.Animate) {
|
||||
if (sa.getHostCard() != null) {
|
||||
if (sa.getApi() == ApiType.Animate) {
|
||||
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
|
||||
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
||||
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard()
|
||||
@@ -213,31 +205,61 @@ public class ComputerUtilMana {
|
||||
|
||||
// If a manland was previously animated this turn, do not tap it to animate another manland
|
||||
if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
|
||||
&& ai.getController() instanceof PlayerControllerAi
|
||||
&& ai.getController().isAI()
|
||||
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
|
||||
continue;
|
||||
}
|
||||
} else if (sa.getApi() == ApiType.Pump) {
|
||||
if ((sa.getHostCard().isInstant() || sa.getHostCard().isSorcery())
|
||||
&& ma.getHostCard().isCreature()
|
||||
&& ai.getController().isAI()
|
||||
&& ma.getPayCosts().hasTapCost()
|
||||
&& sa.getTargets().getTargetCards().contains(ma.getHostCard())) {
|
||||
// do not activate pump instants/sorceries targeting creatures by tapping targeted
|
||||
// creatures for mana (for example, Servant of the Conduit)
|
||||
continue;
|
||||
}
|
||||
} else if (sa.getApi() == ApiType.Attach
|
||||
&& "AvoidPayingWithAttachTarget".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
|
||||
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
|
||||
// be tapped to pay its own cost if there's another untapped land like that available
|
||||
if (ma.getHostCard().equals(sa.getTargetCard())) {
|
||||
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)).size() > 1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SpellAbility paymentChoice = ma;
|
||||
|
||||
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
|
||||
// to attempt to make the spell uncounterable when possible.
|
||||
if ((toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)
|
||||
&& ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
||||
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
||||
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
|
||||
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
|
||||
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
|
||||
continue;
|
||||
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
|
||||
for (SpellAbility ab : saList) {
|
||||
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
||||
return ab;
|
||||
if (!ab.getHostCard().isTapped()) {
|
||||
paymentChoice = ab;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String typeRes = cost.getSourceRestriction();
|
||||
if (StringUtils.isNotBlank(typeRes) && !ma.getHostCard().getType().hasStringType(typeRes)) {
|
||||
if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (canPayShardWithSpellAbility(toPay, ai, ma, sa, checkCosts)) {
|
||||
return ma;
|
||||
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts)) {
|
||||
return paymentChoice;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -491,6 +513,16 @@ public class ComputerUtilMana {
|
||||
// extraMana, sa.getHostCard(), sa.toUnsuppressedString(), StringUtils.join(paymentPlan, "\n\t"));
|
||||
// }
|
||||
|
||||
// See if it's possible to pay with something that was left in the mana pool in corner cases,
|
||||
// e.g. Gemstone Caverns with a Luck counter on it generating colored mana (which fails to be
|
||||
// processed correctly on a per-ability basis, leaving floating mana in the pool)
|
||||
if (!cost.isPaid() && !manapool.isEmpty()) {
|
||||
for (byte color : MagicColor.WUBRGC) {
|
||||
manapool.tryPayCostWithColor(color, sa, cost);
|
||||
}
|
||||
}
|
||||
|
||||
// The cost is still unpaid, so refund the mana and report
|
||||
if (!cost.isPaid()) {
|
||||
refundMana(manaSpentToPay, ai, sa);
|
||||
if (test) {
|
||||
@@ -802,7 +834,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// isManaSourceReserved returns true if sourceCard is reserved as a mana source for payment
|
||||
// for the future spell to be cast in Mana 2. However, if "sa" (the spell ability that is
|
||||
// for the future spell to be cast in another phase. However, if "sa" (the spell ability that is
|
||||
// being considered for casting) is high priority, then mana source reservation will be
|
||||
// ignored.
|
||||
private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) {
|
||||
@@ -816,8 +848,21 @@ public class ComputerUtilMana {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
|
||||
|
||||
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
||||
|
||||
// For combat tricks, always obey mana reservation
|
||||
if (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP) {
|
||||
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
|
||||
}
|
||||
else {
|
||||
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) {
|
||||
// This mana source is held elsewhere for a combat trick.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a low priority spell (it's explicitly marked so elsewhere in the AI with a SVar), always
|
||||
// obey mana reservations; otherwise, obey mana reservations depending on the "chance to reserve"
|
||||
// obey mana reservations for Main 2; otherwise, obey mana reservations depending on the "chance to reserve"
|
||||
// AI profile variable.
|
||||
if (sa.getSVar("LowPriorityAI").equals("")) {
|
||||
if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) {
|
||||
@@ -825,16 +870,16 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
||||
if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) {
|
||||
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
|
||||
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
}
|
||||
else {
|
||||
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
|
||||
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
|
||||
// This mana source is held elsewhere for a Main Phase 2 spell.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1092,8 +1137,65 @@ public class ComputerUtilMana {
|
||||
return cost;
|
||||
}
|
||||
|
||||
// This method can be used to estimate the total amount of mana available to the player,
|
||||
// including the mana available in that player's mana pool
|
||||
public static int getAvailableManaEstimate(final Player p) {
|
||||
return getAvailableManaEstimate(p, true);
|
||||
}
|
||||
|
||||
public static int getAvailableManaEstimate(final Player p, final boolean checkPlayable) {
|
||||
int availableMana = 0;
|
||||
|
||||
final CardCollectionView list = new CardCollection(p.getCardsIn(ZoneType.Battlefield));
|
||||
final List<Card> srcs = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return !c.getManaAbilities().isEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
int maxProduced = 0;
|
||||
int producedWithCost = 0;
|
||||
boolean hasSourcesWithNoManaCost = false;
|
||||
|
||||
for (Card src : srcs) {
|
||||
maxProduced = 0;
|
||||
|
||||
for (SpellAbility ma : src.getManaAbilities()) {
|
||||
ma.setActivatingPlayer(p);
|
||||
if (!checkPlayable || ma.canPlay()) {
|
||||
int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
|
||||
int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length;
|
||||
int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma);
|
||||
|
||||
int producedTotal = producedMana * producedAmount - costsToActivate;
|
||||
|
||||
if (costsToActivate > 0) {
|
||||
producedWithCost += producedTotal;
|
||||
} else if (!hasSourcesWithNoManaCost) {
|
||||
hasSourcesWithNoManaCost = true;
|
||||
}
|
||||
|
||||
if (producedTotal > maxProduced) {
|
||||
maxProduced = producedTotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableMana += maxProduced;
|
||||
}
|
||||
|
||||
availableMana += p.getManaPool().totalMana();
|
||||
|
||||
if (producedWithCost > 0 && !hasSourcesWithNoManaCost) {
|
||||
availableMana -= producedWithCost; // probably can't activate them, no other mana available
|
||||
}
|
||||
|
||||
return availableMana;
|
||||
}
|
||||
|
||||
//This method is currently used by AI to estimate available mana
|
||||
public static CardCollection getAvailableMana(final Player ai, final boolean checkPlayable) {
|
||||
public static CardCollection getAvailableManaSources(final Player ai, final boolean checkPlayable) {
|
||||
final CardCollectionView list = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand));
|
||||
final List<Card> manaSources = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -1200,7 +1302,7 @@ public class ComputerUtilMana {
|
||||
System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources);
|
||||
}
|
||||
return sortedManaSources;
|
||||
} // getAvailableMana()
|
||||
} // getAvailableManaSources()
|
||||
|
||||
//This method is currently used by AI to estimate mana available
|
||||
private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final Player ai, boolean checkPlayable) {
|
||||
@@ -1221,7 +1323,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// Loop over all current available mana sources
|
||||
for (final Card sourceCard : getAvailableMana(ai, checkPlayable)) {
|
||||
for (final Card sourceCard : getAvailableManaSources(ai, checkPlayable)) {
|
||||
if (DEBUG_MANA_PAYMENT) {
|
||||
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor sourceCard = " + sourceCard);
|
||||
}
|
||||
@@ -1264,7 +1366,7 @@ public class ComputerUtilMana {
|
||||
Card crd = replacementEffect.getHostCard();
|
||||
String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement"));
|
||||
if (repType.contains("Chosen")) {
|
||||
repType = repType.replace("Chosen", MagicColor.toShortString(crd.getChosenColor()));
|
||||
repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor()));
|
||||
}
|
||||
mp.setManaReplaceType(repType);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ package forge.ai;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
@@ -19,13 +24,18 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
}
|
||||
|
||||
public int evaluateCreature(final Card c) {
|
||||
return evaluateCreature(c, true, true);
|
||||
}
|
||||
|
||||
public int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
|
||||
int value = 80;
|
||||
if (!c.isToken()) {
|
||||
value += addValue(20, "non-token"); // tokens should be worth less than actual cards
|
||||
}
|
||||
int power = getEffectivePower(c);
|
||||
final int toughness = getEffectiveToughness(c);
|
||||
for (String keyword : c.getKeywords()) {
|
||||
for (KeywordInterface kw : c.getKeywords()) {
|
||||
String keyword = kw.getOriginal();
|
||||
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|
||||
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
@@ -34,9 +44,13 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (considerPT) {
|
||||
value += addValue(power * 15, "power");
|
||||
value += addValue(toughness * 10, "toughness: " + toughness);
|
||||
}
|
||||
if (considerCMC) {
|
||||
value += addValue(c.getCMC() * 5, "cmc");
|
||||
}
|
||||
|
||||
// Evasion keywords
|
||||
if (c.hasKeyword("Flying")) {
|
||||
@@ -91,6 +105,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(power * 15, "infect");
|
||||
}
|
||||
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
|
||||
value += addValue(c.getKeywordMagnitude("Afflict") * 5, "afflict");
|
||||
}
|
||||
|
||||
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
|
||||
@@ -99,6 +114,14 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
|
||||
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
|
||||
|
||||
// Keywords that may produce temporary or permanent buffs over time
|
||||
if (c.hasKeyword("Prowess")) {
|
||||
value += addValue(5, "prowess");
|
||||
}
|
||||
if (c.hasKeyword("Outlast")) {
|
||||
value += addValue(10, "outlast");
|
||||
}
|
||||
|
||||
// Defensive Keywords
|
||||
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
||||
value += addValue(5, "reach");
|
||||
@@ -184,7 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(10, "sa: " + sa);
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
@@ -203,9 +226,44 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
if (!c.getEncodedCards().isEmpty()) {
|
||||
value += addValue(24, "encoded");
|
||||
}
|
||||
|
||||
// card-specific evaluation modifier
|
||||
if (c.hasSVar("AIEvaluationModifier")) {
|
||||
int mod = AbilityUtils.calculateAmount(c, c.getSVar("AIEvaluationModifier"), null);
|
||||
value += mod;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private int evaluateSpellAbility(SpellAbility sa) {
|
||||
// Pump abilities
|
||||
if (sa.getApi() == ApiType.Pump) {
|
||||
// Pump abilities that grant +X/+X to the card
|
||||
if ("+X".equals(sa.getParam("NumAtt"))
|
||||
&& "+X".equals(sa.getParam("NumDef"))
|
||||
&& !sa.usesTargeting()
|
||||
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
|
||||
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
// Electrostatic Pummeler, can be expanded for similar cards
|
||||
int initPower = getEffectivePower(sa.getHostCard());
|
||||
int pumpedPower = initPower;
|
||||
int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY);
|
||||
if (energy > 0) {
|
||||
int numActivations = energy / 3;
|
||||
for (int i = 0; i < numActivations; i++) {
|
||||
pumpedPower *= 2;
|
||||
}
|
||||
return (pumpedPower - initPower) * 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// default value
|
||||
return 10;
|
||||
}
|
||||
|
||||
protected int addValue(int value, String text) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import forge.StaticData;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
@@ -21,16 +13,29 @@ import forge.game.ability.effects.DetachedCardEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.event.GameEventAttackersDeclared;
|
||||
import forge.game.event.GameEventCombatChanged;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.PlayerZone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperCard;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
public abstract class GameState {
|
||||
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
|
||||
@@ -48,17 +53,40 @@ public abstract class GameState {
|
||||
private String humanCounters = "";
|
||||
private String computerCounters = "";
|
||||
|
||||
private boolean puzzleCreatorState = false;
|
||||
|
||||
private final Map<ZoneType, String> humanCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
|
||||
private final Map<ZoneType, String> aiCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
|
||||
|
||||
private final Map<Integer, Card> idToCard = new HashMap<>();
|
||||
private final Map<Card, Integer> cardToAttachId = new HashMap<>();
|
||||
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
||||
private final Map<Card, String> cardToChosenType = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
|
||||
private final Map<Card, String> cardToNamedCard = new HashMap<>();
|
||||
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
|
||||
private final Map<Card, Card> cardAttackMap = new HashMap<>();
|
||||
|
||||
private final Map<Card, String> cardToScript = new HashMap<>();
|
||||
|
||||
private final Map<String, String> abilityString = new HashMap<>();
|
||||
|
||||
private final Set<Card> cardsReferencedByID = new HashSet<>();
|
||||
private final Set<Card> cardsWithoutETBTrigs = new HashSet<>();
|
||||
|
||||
private String tChangePlayer = "NONE";
|
||||
private String tChangePhase = "NONE";
|
||||
|
||||
private String precastHuman = null;
|
||||
private String precastAI = null;
|
||||
|
||||
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
|
||||
private final int TARGET_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
|
||||
private final int TARGET_HUMAN = -2;
|
||||
private final int TARGET_AI = -3;
|
||||
|
||||
public GameState() {
|
||||
}
|
||||
|
||||
@@ -67,18 +95,31 @@ public abstract class GameState {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(String.format("humanlife=%d\n", humanLife));
|
||||
sb.append(String.format("ailife=%d\n", computerLife));
|
||||
|
||||
if (puzzleCreatorState) {
|
||||
// append basic puzzle metadata if we're dumping from the puzzle creator screen
|
||||
sb.append("[metadata]\n");
|
||||
sb.append("Name:New Puzzle\n");
|
||||
sb.append("URL:https://www.cardforge.org\n");
|
||||
sb.append("Goal:Win\n");
|
||||
sb.append("Turns:1\n");
|
||||
sb.append("Difficulty:Easy\n");
|
||||
sb.append("Description:Win this turn.\n");
|
||||
sb.append("[state]\n");
|
||||
}
|
||||
|
||||
sb.append(TextUtil.concatNoSpace("humanlife=", String.valueOf(humanLife), "\n"));
|
||||
sb.append(TextUtil.concatNoSpace("ailife=", String.valueOf(computerLife), "\n"));
|
||||
|
||||
if (!humanCounters.isEmpty()) {
|
||||
sb.append(String.format("humancounters=%s\n", humanCounters));
|
||||
sb.append(TextUtil.concatNoSpace("humancounters=", humanCounters, "\n"));
|
||||
}
|
||||
if (!computerCounters.isEmpty()) {
|
||||
sb.append(String.format("aicounters=%s\n", computerCounters));
|
||||
sb.append(TextUtil.concatNoSpace("aicounters=", computerCounters, "\n"));
|
||||
}
|
||||
|
||||
sb.append(String.format("activeplayer=%s\n", tChangePlayer));
|
||||
sb.append(String.format("activephase=%s\n", tChangePhase));
|
||||
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
|
||||
sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
|
||||
appendCards(humanCardTexts, "human", sb);
|
||||
appendCards(aiCardTexts, "ai", sb);
|
||||
return sb.toString();
|
||||
@@ -86,13 +127,13 @@ public abstract class GameState {
|
||||
|
||||
private void appendCards(Map<ZoneType, String> cardTexts, String categoryPrefix, StringBuilder sb) {
|
||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
||||
sb.append(String.format("%s%s=%s\n", categoryPrefix, ZONES.get(kv.getKey()), kv.getValue()));
|
||||
sb.append(TextUtil.concatNoSpace(categoryPrefix, ZONES.get(kv.getKey()), "=", kv.getValue(), "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
public void initFromGame(Game game) throws Exception {
|
||||
FCollectionView<Player> players = game.getPlayers();
|
||||
// Can only serialized a two player game with one AI and one human.
|
||||
// Can only serialize a two player game with one AI and one human.
|
||||
if (players.size() != 2) {
|
||||
throw new Exception("Game not supported");
|
||||
}
|
||||
@@ -110,12 +151,52 @@ public abstract class GameState {
|
||||
tChangePhase = game.getPhaseHandler().getPhase().toString();
|
||||
aiCardTexts.clear();
|
||||
humanCardTexts.clear();
|
||||
|
||||
// Mark the cards that need their ID remembered for various reasons
|
||||
cardsReferencedByID.clear();
|
||||
for (ZoneType zone : ZONES.keySet()) {
|
||||
for (Card card : game.getCardsIn(zone)) {
|
||||
if (card.getExiledWith() != null) {
|
||||
// Remember the ID of the card that exiled this card
|
||||
cardsReferencedByID.add(card.getExiledWith());
|
||||
}
|
||||
if (zone == ZoneType.Battlefield) {
|
||||
if (!card.getEnchantedBy(false).isEmpty()
|
||||
|| !card.getEquippedBy(false).isEmpty()
|
||||
|| !card.getFortifiedBy(false).isEmpty()) {
|
||||
// Remember the ID of cards that have attachments
|
||||
cardsReferencedByID.add(card);
|
||||
}
|
||||
}
|
||||
for (Object o : card.getRemembered()) {
|
||||
// Remember the IDs of remembered cards
|
||||
if (o instanceof Card) {
|
||||
cardsReferencedByID.add((Card)o);
|
||||
}
|
||||
}
|
||||
for (Card i : card.getImprintedCards()) {
|
||||
// Remember the IDs of imprinted cards
|
||||
cardsReferencedByID.add(i);
|
||||
}
|
||||
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
|
||||
// Remember the IDs of attacked planeswalkers
|
||||
GameEntity def = game.getCombat().getDefenderByAttacker(card);
|
||||
if (def instanceof Card) {
|
||||
cardsReferencedByID.add((Card)def);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ZoneType zone : ZONES.keySet()) {
|
||||
// Init texts to empty, so that restoring will clear the state
|
||||
// if the zone had no cards in it (e.g. empty hand).
|
||||
aiCardTexts.put(zone, "");
|
||||
humanCardTexts.put(zone, "");
|
||||
for (Card card : game.getCardsIn(zone)) {
|
||||
if (card.getName().equals("Puzzle Goal") && card.getOracleText().contains("New Puzzle")) {
|
||||
puzzleCreatorState = true;
|
||||
}
|
||||
if (card instanceof DetachedCardEffect) {
|
||||
continue;
|
||||
}
|
||||
@@ -130,7 +211,7 @@ public abstract class GameState {
|
||||
newText.append(";");
|
||||
}
|
||||
if (c.isToken()) {
|
||||
newText.append("t:" + new CardFactory.TokenInfo(c).toString());
|
||||
newText.append("t:" + new TokenInfo(c).toString());
|
||||
} else {
|
||||
if (c.getPaperCard() == null) {
|
||||
return;
|
||||
@@ -140,6 +221,11 @@ public abstract class GameState {
|
||||
if (c.isCommander()) {
|
||||
newText.append("|IsCommander");
|
||||
}
|
||||
|
||||
if (cardsReferencedByID.contains(c)) {
|
||||
newText.append("|Id:").append(c.getId());
|
||||
}
|
||||
|
||||
if (zoneType == ZoneType.Battlefield) {
|
||||
if (c.isTapped()) {
|
||||
newText.append("|Tapped");
|
||||
@@ -147,6 +233,16 @@ public abstract class GameState {
|
||||
if (c.isSick()) {
|
||||
newText.append("|SummonSick");
|
||||
}
|
||||
if (c.isRenowned()) {
|
||||
newText.append("|Renowned");
|
||||
}
|
||||
if (c.isMonstrous()) {
|
||||
newText.append("|Monstrous:");
|
||||
newText.append(c.getMonstrosityNum());
|
||||
}
|
||||
if (c.isPhasedOut()) {
|
||||
newText.append("|PhasedOut");
|
||||
}
|
||||
if (c.isFaceDown()) {
|
||||
newText.append("|FaceDown");
|
||||
if (c.isManifested()) {
|
||||
@@ -155,11 +251,10 @@ public abstract class GameState {
|
||||
}
|
||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||
newText.append("|Transformed");
|
||||
}
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
if (!counters.isEmpty()) {
|
||||
newText.append("|Counters:");
|
||||
newText.append(countersToString(counters));
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
newText.append("|Flipped");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||
newText.append("|Meld");
|
||||
}
|
||||
if (c.getEquipping() != null) {
|
||||
newText.append("|Attaching:").append(c.getEquipping().getId());
|
||||
@@ -169,10 +264,69 @@ public abstract class GameState {
|
||||
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
||||
}
|
||||
|
||||
if (!c.getEnchantedBy(false).isEmpty() || !c.getEquippedBy(false).isEmpty() || !c.getFortifiedBy(false).isEmpty()) {
|
||||
newText.append("|Id:").append(c.getId());
|
||||
if (c.getDamage() > 0) {
|
||||
newText.append("|Damage:").append(c.getDamage());
|
||||
}
|
||||
|
||||
if (!c.getChosenColor().isEmpty()) {
|
||||
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
|
||||
}
|
||||
if (!c.getChosenType().isEmpty()) {
|
||||
newText.append("|ChosenType:").append(c.getChosenType());
|
||||
}
|
||||
if (!c.getNamedCard().isEmpty()) {
|
||||
newText.append("|NamedCard:").append(c.getNamedCard());
|
||||
}
|
||||
|
||||
List<String> rememberedCardIds = Lists.newArrayList();
|
||||
for (Object obj : c.getRemembered()) {
|
||||
if (obj instanceof Card) {
|
||||
int id = ((Card)obj).getId();
|
||||
rememberedCardIds.add(String.valueOf(id));
|
||||
}
|
||||
}
|
||||
if (!rememberedCardIds.isEmpty()) {
|
||||
newText.append("|RememberedCards:").append(TextUtil.join(rememberedCardIds, ","));
|
||||
}
|
||||
|
||||
List<String> imprintedCardIds = Lists.newArrayList();
|
||||
for (Card impr : c.getImprintedCards()) {
|
||||
int id = impr.getId();
|
||||
imprintedCardIds.add(String.valueOf(id));
|
||||
}
|
||||
if (!imprintedCardIds.isEmpty()) {
|
||||
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneType == ZoneType.Exile) {
|
||||
if (c.getExiledWith() != null) {
|
||||
newText.append("|ExiledWith:").append(c.getExiledWith().getId());
|
||||
}
|
||||
if (c.isFaceDown()) {
|
||||
newText.append("|FaceDown"); // Exiled face down
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Exile) {
|
||||
// A card can have counters on the battlefield and in exile (e.g. exiled by Mairsil, the Pretender)
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
if (!counters.isEmpty()) {
|
||||
newText.append("|Counters:");
|
||||
newText.append(countersToString(counters));
|
||||
}
|
||||
}
|
||||
|
||||
if (c.getGame().getCombat() != null) {
|
||||
if (c.getGame().getCombat().isAttacking(c)) {
|
||||
newText.append("|Attacking");
|
||||
GameEntity def = c.getGame().getCombat().getDefenderByAttacker(c);
|
||||
if (def instanceof Card) {
|
||||
newText.append(":" + def.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cardTexts.put(zoneType, newText.toString());
|
||||
}
|
||||
|
||||
@@ -186,7 +340,7 @@ public abstract class GameState {
|
||||
}
|
||||
|
||||
first = false;
|
||||
counterString.append(String.format("%s=%d", kv.getKey().toString(), kv.getValue()));
|
||||
counterString.append(TextUtil.concatNoSpace(kv.getKey().toString(), "=", String.valueOf(kv.getValue())));
|
||||
}
|
||||
return counterString.toString();
|
||||
}
|
||||
@@ -298,6 +452,12 @@ public abstract class GameState {
|
||||
abilityString.put(categoryName.substring("ability".length()), categoryValue);
|
||||
}
|
||||
|
||||
else if (categoryName.endsWith("precast")) {
|
||||
if (isHuman)
|
||||
precastHuman = categoryValue;
|
||||
else
|
||||
precastAI = categoryValue;
|
||||
}
|
||||
else {
|
||||
System.out.println("Unknown key: " + categoryName);
|
||||
}
|
||||
@@ -318,6 +478,13 @@ public abstract class GameState {
|
||||
|
||||
idToCard.clear();
|
||||
cardToAttachId.clear();
|
||||
cardToRememberedId.clear();
|
||||
cardToExiledWithId.clear();
|
||||
markedDamage.clear();
|
||||
cardToChosenClrs.clear();
|
||||
cardToChosenType.clear();
|
||||
cardToScript.clear();
|
||||
cardAttackMap.clear();
|
||||
|
||||
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
|
||||
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
|
||||
@@ -331,22 +498,369 @@ public abstract class GameState {
|
||||
if (!computerCounters.isEmpty()) {
|
||||
applyCountersToGameEntity(ai, computerCounters);
|
||||
}
|
||||
|
||||
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
|
||||
|
||||
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
||||
game.getTriggerHandler().setSuppressAllTriggers(true);
|
||||
|
||||
setupPlayerState(humanLife, humanCardTexts, human);
|
||||
setupPlayerState(computerLife, aiCardTexts, ai);
|
||||
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
|
||||
handleCardAttachments();
|
||||
handleChosenEntities();
|
||||
handleRememberedEntities();
|
||||
handleScriptExecution(game);
|
||||
handlePrecastSpells(game);
|
||||
handleMarkedDamage();
|
||||
|
||||
game.getTriggerHandler().setSuppressAllTriggers(false);
|
||||
|
||||
// Combat only works for 1v1 matches for now (which are the only matches dev mode supports anyway)
|
||||
// Note: triggers may fire during combat declarations ("whenever X attacks, ...", etc.)
|
||||
if (newPhase == PhaseType.COMBAT_DECLARE_ATTACKERS || newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS) {
|
||||
boolean toDeclareBlockers = newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS;
|
||||
handleCombat(game, newPlayerTurn, newPlayerTurn.getSingleOpponent(), toDeclareBlockers);
|
||||
}
|
||||
|
||||
game.getStack().setResolving(false);
|
||||
|
||||
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
|
||||
}
|
||||
|
||||
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
|
||||
// First we need to ensure that all attackers are declared in the Declare Attackers step,
|
||||
// even if proceeding straight to Declare Blockers
|
||||
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, attackingPlayer);
|
||||
|
||||
if (game.getPhaseHandler().getCombat() == null) {
|
||||
game.getPhaseHandler().setCombat(new Combat(attackingPlayer));
|
||||
game.updateCombatForView();
|
||||
}
|
||||
|
||||
Combat combat = game.getPhaseHandler().getCombat();
|
||||
for (Entry<Card, Card> attackMap : cardAttackMap.entrySet()) {
|
||||
Card attacker = attackMap.getKey();
|
||||
Card attacked = attackMap.getValue();
|
||||
|
||||
combat.addAttacker(attacker, attacked == null ? defendingPlayer : attacked);
|
||||
}
|
||||
|
||||
// Run the necessary combat events and triggers to set things up correctly as if the
|
||||
// attack was actually declared by the attacking player
|
||||
Multimap<GameEntity, Card> attackersMap = ArrayListMultimap.create();
|
||||
for (GameEntity ge : combat.getDefenders()) {
|
||||
attackersMap.putAll(ge, combat.getAttackersOf(ge));
|
||||
}
|
||||
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
|
||||
|
||||
if (!combat.getAttackers().isEmpty()) {
|
||||
List<GameEntity> attackedTarget = Lists.newArrayList();
|
||||
for (final Card c : combat.getAttackers()) {
|
||||
attackedTarget.add(combat.getDefenderByAttacker(c));
|
||||
}
|
||||
final Map<String, Object> runParams = Maps.newHashMap();
|
||||
runParams.put("Attackers", combat.getAttackers());
|
||||
runParams.put("AttackingPlayer", combat.getAttackingPlayer());
|
||||
runParams.put("AttackedTarget", attackedTarget);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
|
||||
}
|
||||
|
||||
for (final Card c : combat.getAttackers()) {
|
||||
CombatUtil.checkDeclaredAttacker(game, c, combat);
|
||||
}
|
||||
|
||||
game.getTriggerHandler().resetActiveTriggers();
|
||||
game.updateCombatForView();
|
||||
game.fireEvent(new GameEventCombatChanged());
|
||||
|
||||
// Gracefully proceed to Declare Blockers, giving priority to the defending player,
|
||||
// but only if the stack is empty (otherwise the game will crash).
|
||||
game.getStack().addAllTriggeredAbilitiesToStack();
|
||||
if (toDeclareBlockers && game.getStack().isEmpty()) {
|
||||
game.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRememberedEntities() {
|
||||
// Remembered: X
|
||||
for (Entry<Card, List<String>> rememberedEnts : cardToRememberedId.entrySet()) {
|
||||
Card c = rememberedEnts.getKey();
|
||||
List<String> ids = rememberedEnts.getValue();
|
||||
|
||||
for (String id : ids) {
|
||||
Card tgt = idToCard.get(Integer.parseInt(id));
|
||||
c.addRemembered(tgt);
|
||||
}
|
||||
}
|
||||
|
||||
// Imprinting: X
|
||||
for (Entry<Card, List<String>> imprintedCards : cardToImprintedId.entrySet()) {
|
||||
Card c = imprintedCards.getKey();
|
||||
List<String> ids = imprintedCards.getValue();
|
||||
|
||||
for (String id : ids) {
|
||||
Card tgt = idToCard.get(Integer.parseInt(id));
|
||||
c.addImprintedCard(tgt);
|
||||
}
|
||||
}
|
||||
|
||||
// Exiled with X
|
||||
for (Entry<Card, String> rememberedEnts : cardToExiledWithId.entrySet()) {
|
||||
Card c = rememberedEnts.getKey();
|
||||
String id = rememberedEnts.getValue();
|
||||
|
||||
Card exiledWith = idToCard.get(Integer.parseInt(id));
|
||||
c.setExiledWith(exiledWith);
|
||||
}
|
||||
}
|
||||
|
||||
private int parseTargetInScript(final String tgtDef) {
|
||||
int tgtID = TARGET_NONE;
|
||||
|
||||
if (tgtDef.equalsIgnoreCase("human")) {
|
||||
tgtID = TARGET_HUMAN;
|
||||
} else if (tgtDef.equalsIgnoreCase("ai")) {
|
||||
tgtID = TARGET_AI;
|
||||
} else {
|
||||
tgtID = Integer.parseInt(tgtDef);
|
||||
}
|
||||
|
||||
return tgtID;
|
||||
}
|
||||
|
||||
private void handleScriptedTargetingForSA(final Game game, final SpellAbility sa, int tgtID) {
|
||||
Player human = game.getPlayers().get(0);
|
||||
Player ai = game.getPlayers().get(1);
|
||||
|
||||
if (tgtID != TARGET_NONE) {
|
||||
switch (tgtID) {
|
||||
case TARGET_HUMAN:
|
||||
sa.getTargets().add(human);
|
||||
break;
|
||||
case TARGET_AI:
|
||||
sa.getTargets().add(ai);
|
||||
break;
|
||||
default:
|
||||
sa.getTargets().add(idToCard.get(tgtID));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleScriptExecution(final Game game) {
|
||||
for (Entry<Card, String> scriptPtr : cardToScript.entrySet()) {
|
||||
Card c = scriptPtr.getKey();
|
||||
String sPtr = scriptPtr.getValue();
|
||||
|
||||
executeScript(game, c, sPtr);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeScript(Game game, Card c, String sPtr) {
|
||||
int tgtID = TARGET_NONE;
|
||||
if (sPtr.contains("->")) {
|
||||
String tgtDef = sPtr.substring(sPtr.lastIndexOf("->") + 2);
|
||||
|
||||
tgtID = parseTargetInScript(tgtDef);
|
||||
sPtr = sPtr.substring(0, sPtr.lastIndexOf("->"));
|
||||
}
|
||||
|
||||
SpellAbility sa = null;
|
||||
if (StringUtils.isNumeric(sPtr)) {
|
||||
int numSA = Integer.parseInt(sPtr);
|
||||
if (c.getSpellAbilities().size() >= numSA) {
|
||||
sa = c.getSpellAbilities().get(numSA);
|
||||
} else {
|
||||
System.err.println("ERROR: Unable to find SA with index " + numSA + " on card " + c + " to execute!");
|
||||
}
|
||||
} else {
|
||||
// Special handling for keyworded abilities
|
||||
if (sPtr.startsWith("KW#")) {
|
||||
String kwName = sPtr.substring(3);
|
||||
FCollectionView<SpellAbility> saList = c.getSpellAbilities();
|
||||
|
||||
if (kwName.equals("Awaken") || kwName.equals("AwakenOnly")) {
|
||||
// AwakenOnly only creates the Awaken effect, while Awaken precasts the whole spell with Awaken
|
||||
for (SpellAbility ab : saList) {
|
||||
if (ab.getDescription().startsWith("Awaken")) {
|
||||
ab.setActivatingPlayer(c.getController());
|
||||
ab.getSubAbility().setActivatingPlayer(c.getController());
|
||||
// target for Awaken is set in its first subability
|
||||
handleScriptedTargetingForSA(game, ab.getSubAbility(), tgtID);
|
||||
sa = kwName.equals("AwakenOnly") ? ab.getSubAbility() : ab;
|
||||
}
|
||||
}
|
||||
if (sa == null) {
|
||||
System.err.println("ERROR: Could not locate keyworded ability Awaken in card " + c + " to execute!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SVar-based script execution
|
||||
String svarValue = "";
|
||||
|
||||
if (sPtr.startsWith("CustomScript:")) {
|
||||
// A custom line defined in the game state file
|
||||
svarValue = sPtr.substring(sPtr.indexOf(":") + 1);
|
||||
} else {
|
||||
// A SVar from the card script file
|
||||
if (!c.hasSVar(sPtr)) {
|
||||
System.err.println("ERROR: Unable to find SVar " + sPtr + " on card " + c + " + to execute!");
|
||||
return;
|
||||
}
|
||||
|
||||
svarValue = c.getSVar(sPtr);
|
||||
|
||||
if (tgtID != TARGET_NONE && svarValue.contains("| Defined$")) {
|
||||
// We want a specific target, so try to undefine a predefined target if possible
|
||||
svarValue = TextUtil.fastReplace(svarValue, "| Defined$", "| Undefined$");
|
||||
if (tgtID == TARGET_HUMAN || tgtID == TARGET_AI) {
|
||||
svarValue += " | ValidTgts$ Player";
|
||||
} else {
|
||||
svarValue += " | ValidTgts$ Card";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa = AbilityFactory.getAbility(svarValue, c);
|
||||
if (sa == null) {
|
||||
System.err.println("ERROR: Unable to generate ability for SVar " + svarValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.setActivatingPlayer(c.getController());
|
||||
handleScriptedTargetingForSA(game, sa, tgtID);
|
||||
|
||||
sa.resolve();
|
||||
|
||||
// resolve subabilities
|
||||
SpellAbility subSa = sa.getSubAbility();
|
||||
while (subSa != null) {
|
||||
subSa.resolve();
|
||||
subSa = subSa.getSubAbility();
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePrecastSpells(final Game game) {
|
||||
Player human = game.getPlayers().get(0);
|
||||
Player ai = game.getPlayers().get(1);
|
||||
|
||||
if (precastHuman != null) {
|
||||
String[] spellList = TextUtil.split(precastHuman, ';');
|
||||
for (String spell : spellList) {
|
||||
precastSpellFromCard(spell, human, game);
|
||||
}
|
||||
}
|
||||
if (precastAI != null) {
|
||||
String[] spellList = TextUtil.split(precastAI, ';');
|
||||
for (String spell : spellList) {
|
||||
precastSpellFromCard(spell, ai, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void precastSpellFromCard(String spellDef, final Player activator, final Game game) {
|
||||
int tgtID = TARGET_NONE;
|
||||
String scriptID = "";
|
||||
|
||||
if (spellDef.contains(":")) {
|
||||
// targeting via -> will be handled in executeScript
|
||||
scriptID = spellDef.substring(spellDef.indexOf(":") + 1).trim();
|
||||
spellDef = spellDef.substring(0, spellDef.indexOf(":")).trim();
|
||||
} else if (spellDef.contains("->")) {
|
||||
String tgtDef = spellDef.substring(spellDef.indexOf("->") + 2).trim();
|
||||
tgtID = parseTargetInScript(tgtDef);
|
||||
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
|
||||
}
|
||||
|
||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
|
||||
|
||||
if (pc == null) {
|
||||
System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
|
||||
return;
|
||||
}
|
||||
|
||||
Card c = Card.fromPaperCard(pc, activator);
|
||||
SpellAbility sa = null;
|
||||
|
||||
if (!scriptID.isEmpty()) {
|
||||
executeScript(game, c, scriptID);
|
||||
return;
|
||||
}
|
||||
|
||||
sa = c.getFirstSpellAbility();
|
||||
sa.setActivatingPlayer(activator);
|
||||
|
||||
handleScriptedTargetingForSA(game, sa, tgtID);
|
||||
|
||||
sa.resolve();
|
||||
}
|
||||
|
||||
private void handleMarkedDamage() {
|
||||
for (Entry<Card, Integer> entry : markedDamage.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
Integer dmg = entry.getValue();
|
||||
|
||||
c.setDamage(dmg);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChosenEntities() {
|
||||
// TODO: the AI still gets to choose something (and the notification box pops up) before the
|
||||
// choice is overwritten here. Somehow improve this so that there is at least no notification
|
||||
// about the choice that will be force-changed anyway.
|
||||
|
||||
// Chosen colors
|
||||
for (Entry<Card, List<String>> entry : cardToChosenClrs.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
List<String> colors = entry.getValue();
|
||||
|
||||
c.setChosenColors(colors);
|
||||
}
|
||||
|
||||
// Chosen type
|
||||
for (Entry<Card, String> entry : cardToChosenType.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
c.setChosenType(entry.getValue());
|
||||
}
|
||||
|
||||
// Named card
|
||||
for (Entry<Card, String> entry : cardToNamedCard.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
c.setNamedCard(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCardAttachments() {
|
||||
// Unattach all permanents first
|
||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||
Card attachedTo = idToCard.get(entry.getValue());
|
||||
|
||||
attachedTo.unEnchantAllCards();
|
||||
attachedTo.unEquipAllCards();
|
||||
for (Card c : attachedTo.getFortifiedBy(true)) {
|
||||
attachedTo.unFortifyCard(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach permanents by ID
|
||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||
Card attachedTo = idToCard.get(entry.getValue());
|
||||
Card attacher = entry.getKey();
|
||||
|
||||
if (attacher.isEquipment()) {
|
||||
attacher.equipCard(attachedTo);
|
||||
} else if (attacher.isAura()) {
|
||||
attacher.enchantEntity(attachedTo);
|
||||
} else if (attacher.isFortified()) {
|
||||
attacher.fortifyCard(attachedTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
|
||||
//entity.setCounters(new HashMap<CounterType, Integer>());
|
||||
entity.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
||||
String[] allCounterStrings = counterString.split(",");
|
||||
for (final String counterPair : allCounterStrings) {
|
||||
String[] pair = counterPair.split("=", 2);
|
||||
@@ -356,7 +870,6 @@ public abstract class GameState {
|
||||
|
||||
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
|
||||
// Lock check static as we setup player state
|
||||
final Game game = p.getGame();
|
||||
|
||||
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
||||
@@ -384,11 +897,17 @@ public abstract class GameState {
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
// Note: Not clearCounters() since we want to keep the counters
|
||||
// var as-is.
|
||||
c.setCounters(new HashMap<CounterType, Integer>());
|
||||
p.getZone(ZoneType.Hand).add(c);
|
||||
c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
||||
if (c.isAura()) {
|
||||
p.getGame().getAction().moveToPlay(c, null);
|
||||
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
||||
// (will be overridden later, so the actual value shouldn't matter)
|
||||
c.setEnchanting(c);
|
||||
}
|
||||
|
||||
if (cardsWithoutETBTrigs.contains(c)) {
|
||||
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null);
|
||||
} else {
|
||||
p.getZone(ZoneType.Hand).add(c);
|
||||
p.getGame().getAction().moveToPlay(c, null);
|
||||
}
|
||||
|
||||
@@ -401,25 +920,6 @@ public abstract class GameState {
|
||||
}
|
||||
}
|
||||
|
||||
game.getTriggerHandler().suppressMode(TriggerType.Unequip);
|
||||
|
||||
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
|
||||
Card attachedTo = idToCard.get(entry.getValue());
|
||||
Card attacher = entry.getKey();
|
||||
|
||||
attachedTo.unEnchantAllCards();
|
||||
attachedTo.unEquipAllCards();
|
||||
|
||||
if (attacher.isEquipment()) {
|
||||
attacher.equipCard(attachedTo);
|
||||
} else if (attacher.isAura()) {
|
||||
attacher.enchantEntity(attachedTo);
|
||||
} else if (attacher.isFortified()) {
|
||||
attacher.fortifyCard(attachedTo);
|
||||
}
|
||||
}
|
||||
|
||||
game.getTriggerHandler().clearSuppression(TriggerType.Unequip);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,10 +949,16 @@ public abstract class GameState {
|
||||
Card c;
|
||||
boolean hasSetCurSet = false;
|
||||
if (cardinfo[0].startsWith("t:")) {
|
||||
// TODO Make sure Game State conversion works with new tokens
|
||||
String tokenStr = cardinfo[0].substring(2);
|
||||
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
|
||||
c = new TokenInfo(tokenStr).makeOneToken(player);
|
||||
} else {
|
||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode);
|
||||
if (pc == null) {
|
||||
System.err.println("ERROR: Tried to create a non-existent card named " + cardinfo[0] + " (set: " + (setCode == null ? "any" : setCode) + ") when loading game state!");
|
||||
continue;
|
||||
}
|
||||
|
||||
c = Card.fromPaperCard(pc, player);
|
||||
if (setCode != null) {
|
||||
hasSetCurSet = true;
|
||||
@@ -463,6 +969,13 @@ public abstract class GameState {
|
||||
for (final String info : cardinfo) {
|
||||
if (info.startsWith("Tapped")) {
|
||||
c.tap();
|
||||
} else if (info.startsWith("Renowned")) {
|
||||
c.setRenowned(true);
|
||||
} else if (info.startsWith("Monstrous:")) {
|
||||
c.setMonstrous(true);
|
||||
c.setMonstrosityNum(Integer.parseInt(info.substring((info.indexOf(':') + 1))));
|
||||
} else if (info.startsWith("PhasedOut")) {
|
||||
c.setPhasedOut(true);
|
||||
} else if (info.startsWith("Counters:")) {
|
||||
applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("SummonSick")) {
|
||||
@@ -474,6 +987,10 @@ public abstract class GameState {
|
||||
}
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
} else if (info.startsWith("Flipped")) {
|
||||
c.setState(CardStateName.Flipped, true);
|
||||
} else if (info.startsWith("Meld")) {
|
||||
c.setState(CardStateName.Meld, true);
|
||||
} else if (info.startsWith("IsCommander")) {
|
||||
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
|
||||
c.setCommander(true);
|
||||
@@ -488,6 +1005,32 @@ public abstract class GameState {
|
||||
} else if (info.startsWith("Ability:")) {
|
||||
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
|
||||
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
|
||||
} else if (info.startsWith("Damage:")) {
|
||||
int dmg = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
||||
markedDamage.put(c, dmg);
|
||||
} else if (info.startsWith("ChosenColor:")) {
|
||||
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("ChosenType:")) {
|
||||
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("NamedCard:")) {
|
||||
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("ExecuteScript:")) {
|
||||
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("RememberedCards:")) {
|
||||
cardToRememberedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("Imprinting:")) {
|
||||
cardToImprintedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("ExiledWith:")) {
|
||||
cardToExiledWithId.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("Attacking")) {
|
||||
if (info.contains(":")) {
|
||||
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
||||
cardAttackMap.put(c, idToCard.get(id));
|
||||
} else {
|
||||
cardAttackMap.put(c, null);
|
||||
}
|
||||
} else if (info.equals("NoETBTrigs")) {
|
||||
cardsWithoutETBTrigs.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package forge.ai;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import forge.AIOption;
|
||||
import forge.LobbyPlayer;
|
||||
import forge.game.Game;
|
||||
import forge.game.player.IGameEntitiesFactory;
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import com.esotericsoftware.minlog.Log;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
@@ -14,9 +7,9 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import forge.LobbyPlayer;
|
||||
import forge.ai.ability.ProtectAi;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.ICardFace;
|
||||
import forge.card.MagicColor;
|
||||
@@ -32,21 +25,14 @@ import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPartMana;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.mana.Mana;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.DelayedReveal;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerController;
|
||||
import forge.game.player.PlayerView;
|
||||
import forge.game.player.*;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperCard;
|
||||
@@ -55,6 +41,12 @@ import forge.util.ITriggerEvent;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
@@ -115,7 +107,23 @@ public class PlayerControllerAi extends PlayerController {
|
||||
if (ability.getApi() != null) {
|
||||
switch (ability.getApi()) {
|
||||
case ChooseNumber:
|
||||
return ability.getActivatingPlayer().isOpponentOf(player) ? 0 : ComputerUtilMana.determineLeftoverMana(ability, player);
|
||||
Player payingPlayer = ability.getActivatingPlayer();
|
||||
String logic = ability.getParamOrDefault("AILogic", "");
|
||||
boolean anyController = logic.equals("MaxForAnyController");
|
||||
|
||||
if (logic.startsWith("PowerLeakMaxMana.") && ability.getHostCard().isEnchantingCard()) {
|
||||
// For cards like Power Leak, the payer will be the owner of the enchanted card
|
||||
// TODO: is there any way to generalize this and avoid a special exclusion?
|
||||
payingPlayer = ability.getHostCard().getEnchantingCard().getController();
|
||||
}
|
||||
|
||||
int number = ComputerUtilMana.determineLeftoverMana(ability, player);
|
||||
|
||||
if (logic.startsWith("MaxMana.") || logic.startsWith("PowerLeakMaxMana.")) {
|
||||
number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1)));
|
||||
}
|
||||
|
||||
return payingPlayer.isOpponentOf(player) && !anyController ? 0 : number;
|
||||
case BidLife:
|
||||
return 0;
|
||||
default:
|
||||
@@ -180,7 +188,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
@Override
|
||||
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
|
||||
final SpellAbility sa = wrapper.getWrappedAbility();
|
||||
final Trigger regtrig = wrapper.getTrigger();
|
||||
//final Trigger regtrig = wrapper.getTrigger();
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
|
||||
return true;
|
||||
}
|
||||
@@ -282,8 +290,43 @@ public class PlayerControllerAi extends PlayerController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone) {
|
||||
//TODO Add logic for AI ordering here
|
||||
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
|
||||
//TODO Add more logic for AI ordering here
|
||||
|
||||
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
|
||||
if (destinationZone == ZoneType.Graveyard) {
|
||||
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
|
||||
return card.getName().equals("Volrath's Shapeshifter")
|
||||
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
|
||||
}
|
||||
}).isEmpty()) {
|
||||
int bestValue = 0;
|
||||
Card bestCreature = null;
|
||||
for (Card c : cards) {
|
||||
int curValue = ComputerUtilCard.evaluateCreature(c);
|
||||
if (c.isCreature() && curValue > bestValue) {
|
||||
bestValue = curValue;
|
||||
bestCreature = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCreature != null) {
|
||||
CardCollection reordered = new CardCollection();
|
||||
for (Card c : cards) {
|
||||
if (!c.equals(bestCreature)) {
|
||||
reordered.add(c);
|
||||
}
|
||||
}
|
||||
reordered.add(bestCreature);
|
||||
return reordered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: return with the same order as was passed into this method
|
||||
return cards;
|
||||
}
|
||||
|
||||
@@ -421,6 +464,24 @@ public class PlayerControllerAi extends PlayerController {
|
||||
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
|
||||
ability.setActivatingPlayer(c.getController());
|
||||
|
||||
// FIXME: This is a hack to check if the AI can play the "exile from library" pay costs (Cumulative Upkeep,
|
||||
// e.g. Thought Lash). We have to do it and bail early if the AI can't pay, because otherwise the AI will
|
||||
// pay the cost partially, which should not be possible
|
||||
int nExileLib = 0;
|
||||
List<CostPart> parts = CostAdjustment.adjust(cost, sa).getCostParts();
|
||||
for (final CostPart part : parts) {
|
||||
if (part instanceof CostExile) {
|
||||
CostExile exile = (CostExile) part;
|
||||
if (exile.from == ZoneType.Library) {
|
||||
nExileLib += exile.convertAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nExileLib > c.getController().getCardsIn(ZoneType.Library).size()) {
|
||||
return false;
|
||||
}
|
||||
// - End of hack for Exile a card from library Cumulative Upkeep -
|
||||
|
||||
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
|
||||
ComputerUtil.playNoStack(c.getController(), ability, game);
|
||||
return true;
|
||||
@@ -493,7 +554,32 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Boolean defaultVal) {
|
||||
switch(kindOfChoice) {
|
||||
case TapOrUntap: return true;
|
||||
case UntapOrLeaveTapped: return defaultVal != null && defaultVal.booleanValue();
|
||||
case UntapOrLeaveTapped:
|
||||
Card source = sa.getHostCard();
|
||||
if (source != null && source.hasSVar("AIUntapPreference")) {
|
||||
switch (source.getSVar("AIUntapPreference")) {
|
||||
case "Always":
|
||||
return true;
|
||||
case "Never":
|
||||
return false;
|
||||
case "BetterTgtThanRemembered":
|
||||
if (source.getRememberedCount() > 0) {
|
||||
Card rem = (Card) source.getFirstRemembered();
|
||||
if (!rem.getZone().is(ZoneType.Battlefield)) {
|
||||
return true;
|
||||
}
|
||||
for (Card c : source.getController().getCreaturesInPlay()) {
|
||||
if (c != rem && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(rem) + 30) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return defaultVal != null && defaultVal.booleanValue();
|
||||
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
|
||||
case LeftOrRight: return brains.chooseDirection(sa);
|
||||
default:
|
||||
@@ -844,21 +930,40 @@ public class PlayerControllerAi extends PlayerController {
|
||||
@Override
|
||||
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
|
||||
CardCollectionView oppLibrary = ComputerUtil.getOpponentFor(player).getCardsIn(ZoneType.Library);
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParam("AILogic");
|
||||
|
||||
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
// If any Conspiracies are present, try not to choose the same name twice
|
||||
// (otherwise the AI will spam the same name)
|
||||
for (Card consp : player.getCardsIn(ZoneType.Command)) {
|
||||
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
String chosenName = consp.getNamedCard();
|
||||
if (!chosenName.isEmpty()) {
|
||||
aiLibrary = CardLists.filter(aiLibrary, Predicates.not(CardPredicates.nameEquals(chosenName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("MostProminentInComputerDeck")) {
|
||||
return ComputerUtilCard.getMostProminentCardName(player.getCardsIn(ZoneType.Library));
|
||||
return ComputerUtilCard.getMostProminentCardName(aiLibrary);
|
||||
} else if (logic.equals("MostProminentInHumanDeck")) {
|
||||
return ComputerUtilCard.getMostProminentCardName(player.getOpponent().getCardsIn(ZoneType.Library));
|
||||
return ComputerUtilCard.getMostProminentCardName(oppLibrary);
|
||||
} else if (logic.equals("MostProminentCreatureInComputerDeck")) {
|
||||
CardCollectionView cards = CardLists.getValidCards(player.getCardsIn(ZoneType.Library), "Creature", player, sa.getHostCard());
|
||||
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard());
|
||||
return ComputerUtilCard.getMostProminentCardName(cards);
|
||||
} else if (logic.equals("BestCreatureInComputerDeck")) {
|
||||
return ComputerUtilCard.getBestCreatureAI(player.getCardsIn(ZoneType.Library)).getName();
|
||||
return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
|
||||
} else if (logic.equals("RandomInComputerDeck")) {
|
||||
return Aggregates.random(player.getCardsIn(ZoneType.Library)).getName();
|
||||
return Aggregates.random(aiLibrary).getName();
|
||||
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
|
||||
CardCollectionView cards = CardLists.getValidCards(player.getCardsIn(ZoneType.Library), "Card.Instant,Card.Sorcery", player, sa.getHostCard());
|
||||
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard());
|
||||
return ComputerUtilCard.getMostProminentCardName(cards);
|
||||
} else if (logic.equals("CursedScroll")) {
|
||||
return SpecialCardAi.CursedScroll.chooseCard(player, sa);
|
||||
}
|
||||
} else {
|
||||
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
|
||||
|
||||
@@ -17,28 +17,20 @@
|
||||
*/
|
||||
package forge.ai;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
@@ -46,8 +38,19 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellPermanent;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.maps.LinkedHashMapToAmount;
|
||||
import forge.util.maps.MapToAmount;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Special logic for individual cards
|
||||
@@ -73,8 +76,8 @@ public class SpecialCardAi {
|
||||
|
||||
// Black Lotus and Lotus Bloom
|
||||
public static class BlackLotus {
|
||||
public static boolean consider(Player ai, SpellAbility sa, ManaCostBeingPaid cost) {
|
||||
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
public static boolean consider(final Player ai, final SpellAbility sa, final ManaCostBeingPaid cost) {
|
||||
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
int numManaSrcs = manaSources.size();
|
||||
|
||||
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN,
|
||||
@@ -100,49 +103,25 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Bonds of Faith
|
||||
public static class BondsOfFaith {
|
||||
public static Card getBestAttachTarget(final Player ai, SpellAbility sa, List<Card> list) {
|
||||
Card chosen = null;
|
||||
// Chain of Acid
|
||||
public static class ChainOfAcid {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.Presets.LANDS);
|
||||
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
|
||||
Predicates.not(CardPredicates.Presets.CREATURES));
|
||||
|
||||
List<Card> aiHumans = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
// Don't buff opponent's humans
|
||||
if (!c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return c.getType().hasCreatureType("Human");
|
||||
}
|
||||
});
|
||||
List<Card> oppNonHumans = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
// Don't debuff AI's own non-humans
|
||||
if (c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return !c.getType().hasCreatureType("Human") && !ComputerUtilCard.isUselessCreature(ai, c);
|
||||
}
|
||||
});
|
||||
|
||||
if (!aiHumans.isEmpty() && !oppNonHumans.isEmpty()) {
|
||||
Card bestAi = ComputerUtilCard.getBestCreatureAI(aiHumans);
|
||||
Card bestOpp = ComputerUtilCard.getBestCreatureAI(oppNonHumans);
|
||||
chosen = ComputerUtilCard.evaluateCreature(bestAi) > ComputerUtilCard.evaluateCreature(bestOpp) ? bestAi : bestOpp;
|
||||
} else if (!aiHumans.isEmpty()) {
|
||||
chosen = ComputerUtilCard.getBestCreatureAI(aiHumans);
|
||||
} else if (!oppNonHumans.isEmpty()) {
|
||||
chosen = ComputerUtilCard.getBestCreatureAI(oppNonHumans);
|
||||
}
|
||||
|
||||
return chosen;
|
||||
// TODO: improve this logic (currently the AI has difficulty evaluating non-creature permanents,
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Chain of Smog
|
||||
public static class ChainOfSmog {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean 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
|
||||
@@ -165,11 +144,66 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Cursed Scroll
|
||||
public static class CursedScroll {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
CardCollectionView hand = ai.getCardsIn(ZoneType.Hand);
|
||||
if (hand.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For now, see if all cards in hand have the same name, and then proceed if true
|
||||
return CardLists.filter(hand, CardPredicates.nameEquals(hand.getFirst().getName())).size() == hand.size();
|
||||
}
|
||||
|
||||
public static String chooseCard(final Player ai, final SpellAbility sa) {
|
||||
int maxCount = 0;
|
||||
Card best = null;
|
||||
CardCollectionView hand = ai.getCardsIn(ZoneType.Hand);
|
||||
|
||||
for (Card c : ai.getCardsIn(ZoneType.Hand)) {
|
||||
int count = CardLists.filter(hand, CardPredicates.nameEquals(c.getName())).size();
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
|
||||
return best.getName();
|
||||
}
|
||||
}
|
||||
|
||||
// Deathgorge Scavenger
|
||||
public static class DeathgorgeScavenger {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Card worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
|
||||
Card worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
|
||||
if (worstCreat == null) {
|
||||
worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
|
||||
}
|
||||
if (worstNonCreat == null) {
|
||||
worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (worstCreat != null && ai.getLife() <= ai.getStartingLife() / 4) {
|
||||
sa.getTargets().add(worstCreat);
|
||||
} else if (worstNonCreat != null && ai.getGame().getCombat() != null
|
||||
&& ai.getGame().getCombat().isAttacking(sa.getHostCard())) {
|
||||
sa.getTargets().add(worstNonCreat);
|
||||
} else if (worstCreat != null) {
|
||||
sa.getTargets().add(worstCreat);
|
||||
}
|
||||
|
||||
return sa.getTargets().getNumTargeted() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Desecration Demon
|
||||
public static class DesecrationDemon {
|
||||
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
|
||||
|
||||
public static boolean considerSacrificingCreature(Player ai, SpellAbility sa) {
|
||||
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
|
||||
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"))));
|
||||
boolean hasUsefulBlocker = false;
|
||||
|
||||
@@ -193,7 +227,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Donate
|
||||
public static class Donate {
|
||||
public static boolean considerTargetingOpponent(Player ai, SpellAbility sa) {
|
||||
public static boolean 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) {
|
||||
@@ -229,7 +263,7 @@ public class SpecialCardAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean considerDonatingPermanent(Player ai, SpellAbility sa) {
|
||||
public static boolean 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();
|
||||
@@ -243,9 +277,199 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Electrostatic Pummeler
|
||||
public static class ElectrostaticPummeler {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
Game game = ai.getGame();
|
||||
Combat combat = game.getCombat();
|
||||
Pair<Integer, Integer> predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
|
||||
|
||||
// Try to save the Pummeler from death by pumping it if it's threatened with a damage spell
|
||||
if (ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source)) {
|
||||
SpellAbility saTop = game.getStack().peekAbility();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not activate if damage will be prevented
|
||||
if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Activate Electrostatic Pummeler's pump only as a combat trick
|
||||
if (game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN)) {
|
||||
if (predictOverwhelmingDamage(ai, sa)) {
|
||||
// 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;
|
||||
}
|
||||
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isBlocking = combat.isBlocking(source);
|
||||
boolean cantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
|
||||
|
||||
CardCollection opposition = isBlocking ? combat.getAttackersBlockedBy(source) : combat.getBlockers(source);
|
||||
int oppP = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetAttack);
|
||||
int oppT = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetNetToughness);
|
||||
|
||||
boolean oppHasFirstStrike = false;
|
||||
boolean oppCantDie = true;
|
||||
boolean unblocked = opposition.isEmpty();
|
||||
boolean canTrample = source.hasKeyword("Trample");
|
||||
|
||||
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
|
||||
int loyalty = ((Card)combat.getDefenderByAttacker(source)).getCounters(CounterType.LOYALTY);
|
||||
int totalDamageToPW = 0;
|
||||
for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
|
||||
if (combat.isUnblocked(atk)) {
|
||||
totalDamageToPW += atk.getNetCombatDamage();
|
||||
}
|
||||
}
|
||||
if (totalDamageToPW >= oppT + loyalty) {
|
||||
// Already enough damage to take care of the planeswalker
|
||||
return false;
|
||||
}
|
||||
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
|
||||
// Can pump to kill the planeswalker, go for it
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (Card c : opposition) {
|
||||
if (c.hasKeyword("First Strike") || c.hasKeyword("Double Strike")) {
|
||||
oppHasFirstStrike = true;
|
||||
}
|
||||
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
|
||||
oppCantDie = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlocking) {
|
||||
int oppLife = combat.getDefendingPlayerRelatedTo(source).getLife();
|
||||
if (((unblocked || canTrample) && (predictedPT.getLeft() - oppT > oppLife / 2))
|
||||
|| (canTrample && predictedPT.getLeft() - oppT > 0 && predictedPT.getRight() > oppP)) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
|
||||
// Can't survive first strike or double strike, don't pump
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
|
||||
// Already enough to kill the blockers and survive, don't overpump
|
||||
return false;
|
||||
}
|
||||
if (oppCantDie && !source.hasKeyword("Trample") && !source.hasKeyword("Wither")
|
||||
&& !source.hasKeyword("Infect") && predictedPT.getLeft() <= oppT) {
|
||||
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
int oppLife = ai.getWeakestOpponent().getLife();
|
||||
CardCollection oppInPlay = ai.getWeakestOpponent().getCreaturesInPlay();
|
||||
CardCollection potentialBlockers = new CardCollection();
|
||||
|
||||
for (Card b : oppInPlay) {
|
||||
if (CombatUtil.canBlock(sa.getHostCard(), b)) {
|
||||
potentialBlockers.add(b);
|
||||
}
|
||||
}
|
||||
|
||||
Pair<Integer, Integer> predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
|
||||
int oppT = Aggregates.sum(potentialBlockers, CardPredicates.Accessors.fnGetNetToughness);
|
||||
|
||||
if (potentialBlockers.isEmpty() || (sa.getHostCard().hasKeyword("Trample") && predictedPT.getLeft() - oppT >= oppLife)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Pair<Integer, Integer> getPumpedPT(Player ai, int power, int toughness) {
|
||||
int energy = ai.getCounters(CounterType.ENERGY);
|
||||
if (energy > 0) {
|
||||
int numActivations = energy / 3;
|
||||
for (int i = 0; i < numActivations; i++) {
|
||||
power *= 2;
|
||||
toughness *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return Pair.of(power, toughness);
|
||||
}
|
||||
}
|
||||
|
||||
// Extraplanar Lens
|
||||
public static class ExtraplanarLens {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Card bestBasic = null;
|
||||
Card bestBasicSelfOnly = null;
|
||||
|
||||
CardCollection aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
CardCollection oppLands = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
|
||||
int bestCount = 0;
|
||||
int bestSelfOnlyCount = 0;
|
||||
for (String landType : MagicColor.Constant.BASIC_LANDS) {
|
||||
CardCollection landsOfType = CardLists.filter(aiLands, CardPredicates.nameEquals(landType));
|
||||
CardCollection oppLandsOfType = CardLists.filter(oppLands, CardPredicates.nameEquals(landType));
|
||||
|
||||
int numCtrl = CardLists.filter(aiLands, CardPredicates.nameEquals(landType)).size();
|
||||
if (numCtrl > bestCount) {
|
||||
bestCount = numCtrl;
|
||||
bestBasic = ComputerUtilCard.getWorstLand(landsOfType);
|
||||
}
|
||||
if (numCtrl > bestSelfOnlyCount && numCtrl > 1 && oppLandsOfType.isEmpty() && bestBasicSelfOnly == null) {
|
||||
bestSelfOnlyCount = numCtrl;
|
||||
bestBasicSelfOnly = ComputerUtilCard.getWorstLand(landsOfType);
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (bestBasicSelfOnly != null) {
|
||||
sa.getTargets().add(bestBasicSelfOnly);
|
||||
return true;
|
||||
} else if (bestBasic != null) {
|
||||
sa.getTargets().add(bestBasic);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Force of Will
|
||||
public static class ForceOfWill {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
CardCollection blueCards = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.isColor(MagicColor.BLUE));
|
||||
|
||||
boolean isExileMode = false;
|
||||
@@ -272,8 +496,9 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Guilty Conscience
|
||||
public static class GuiltyConscience {
|
||||
public static Card getBestAttachTarget(final Player ai, SpellAbility sa, List<Card> list) {
|
||||
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
|
||||
Card chosen = null;
|
||||
|
||||
List<Card> aiStuffies = CardLists.filter(list, new Predicate<Card>() {
|
||||
@@ -308,10 +533,148 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Living Death (and possibly other similar cards using AILogic LivingDeath)
|
||||
// Intuition (and any other card that might potentially let you pick N cards from the library,
|
||||
// one of which will then be picked for you by the opponent)
|
||||
public static class Intuition {
|
||||
public static CardCollection considerMultiple(final Player ai, final SpellAbility sa) {
|
||||
if (ai.getController().isAI()) {
|
||||
if (!((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.INTUITION_ALTERNATIVE_LOGIC)) {
|
||||
return new CardCollection(); // fall back to standard ChangeZoneAi considerations
|
||||
}
|
||||
}
|
||||
|
||||
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("ChangeNum"), sa);
|
||||
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
|
||||
Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
|
||||
Collections.sort(lib, CardLists.CmcComparatorInv);
|
||||
|
||||
// Additional cards which are difficult to auto-classify but which are generally good to Intuition for
|
||||
List<String> highPriorityNamedCards = Lists.newArrayList("Accumulated Knowledge", "Take Inventory");
|
||||
|
||||
// figure out how many of each card we have in deck
|
||||
MapToAmount<String> cardAmount = new LinkedHashMapToAmount<>();
|
||||
for (Card c : lib) {
|
||||
cardAmount.add(c.getName());
|
||||
}
|
||||
|
||||
// Trix: see if we can complete the combo (if it looks like we might win shortly or if we need to get a Donate stat)
|
||||
boolean donateComboMightWin = false;
|
||||
int numIllusionsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Illusions of Grandeur")).size();
|
||||
if (ai.getOpponentsSmallestLifeTotal() < 20 || numIllusionsOTB > 0) {
|
||||
donateComboMightWin = true;
|
||||
int numIllusionsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals("Illusions of Grandeur")).size();
|
||||
int numDonateInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals("Donate")).size();
|
||||
int numIllusionsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.nameEquals("Illusions of Grandeur")).size();
|
||||
int numDonateInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.nameEquals("Donate")).size();
|
||||
CardCollection comboList = new CardCollection();
|
||||
if ((numIllusionsInHand > 0 || numIllusionsOTB > 0) && numDonateInHand == 0 && numDonateInLib >= 3) {
|
||||
for (Card c : lib) {
|
||||
if (c.getName().equals("Donate")) {
|
||||
comboList.add(c);
|
||||
}
|
||||
}
|
||||
return comboList;
|
||||
} else if (numDonateInHand > 0 && numIllusionsInHand == 0 && numIllusionsInLib >= 3) {
|
||||
for (Card c : lib) {
|
||||
if (c.getName().equals("Illusions of Grandeur")) {
|
||||
comboList.add(c);
|
||||
}
|
||||
}
|
||||
return comboList;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a priority list for cards that we have no more than 4 of and that are not lands
|
||||
CardCollection libPriorityList = new CardCollection();
|
||||
CardCollection libHighPriorityList = new CardCollection();
|
||||
CardCollection libLowPriorityList = new CardCollection();
|
||||
List<String> processed = Lists.newArrayList();
|
||||
for (int i = 4; i > 0; i--) {
|
||||
for (Card c : lib) {
|
||||
if (!donateComboMightWin && (c.getName().equals("Illusions of Grandeur") || c.getName().equals("Donate"))) {
|
||||
// Probably not worth putting two of the combo pieces into the graveyard
|
||||
// since one Illusions-Donate is likely to not be enough
|
||||
continue;
|
||||
}
|
||||
if (cardAmount.get(c.getName()) == i && !c.isLand() && !processed.contains(c.getName())) {
|
||||
// if it's a card that is generally good to place in the graveyard, also add it
|
||||
// to the mix
|
||||
boolean canRetFromGrave = false;
|
||||
String name = c.getName().replace(',', ';');
|
||||
for (Trigger t : c.getTriggers()) {
|
||||
SpellAbility ab = null;
|
||||
if (t.hasParam("Execute")) {
|
||||
ab = AbilityFactory.getAbility(c.getSVar(t.getParam("Execute")), c);
|
||||
}
|
||||
if (ab == null) { continue; }
|
||||
|
||||
if (ab.getApi() == ApiType.ChangeZone
|
||||
&& "Self".equals(ab.getParam("Defined"))
|
||||
&& "Graveyard".equals(ab.getParam("Origin"))
|
||||
&& "Battlefield".equals(ab.getParam("Destination"))) {
|
||||
canRetFromGrave = true;
|
||||
}
|
||||
if (ab.getApi() == ApiType.ChangeZoneAll
|
||||
&& TextUtil.concatNoSpace("Creature.named", name).equals(ab.getParam("ChangeType"))
|
||||
&& "Graveyard".equals(ab.getParam("Origin"))
|
||||
&& "Battlefield".equals(ab.getParam("Destination"))) {
|
||||
canRetFromGrave = true;
|
||||
}
|
||||
}
|
||||
boolean isGoodToPutInGrave = c.hasSVar("DiscardMe") || canRetFromGrave
|
||||
|| (ComputerUtil.isPlayingReanimator(ai) && c.isCreature());
|
||||
|
||||
for (Card c1 : lib) {
|
||||
if (c1.getName().equals(c.getName())) {
|
||||
if (CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName())).isEmpty()
|
||||
&& ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
|
||||
// Try not to search for things we already have in hand or that we can't cast
|
||||
libPriorityList.add(c1);
|
||||
} else {
|
||||
libLowPriorityList.add(c1);
|
||||
}
|
||||
if (isGoodToPutInGrave || highPriorityNamedCards.contains(c.getName())) {
|
||||
libHighPriorityList.add(c1);
|
||||
}
|
||||
}
|
||||
}
|
||||
processed.add(c.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're playing Reanimator, we're really interested just in the highest CMC spells, not the
|
||||
// ones we necessarily have multiples of
|
||||
if (ComputerUtil.isPlayingReanimator(ai)) {
|
||||
Collections.sort(libHighPriorityList, CardLists.CmcComparatorInv);
|
||||
}
|
||||
|
||||
// Otherwise, try to grab something that is hopefully decent to grab, in priority order
|
||||
CardCollection chosen = new CardCollection();
|
||||
if (libHighPriorityList.size() >= changeNum) {
|
||||
for (int i = 0; i < changeNum; i++) {
|
||||
chosen.add(libHighPriorityList.get(i));
|
||||
}
|
||||
} else if (libPriorityList.size() >= changeNum) {
|
||||
for (int i = 0; i < changeNum; i++) {
|
||||
chosen.add(libPriorityList.get(i));
|
||||
}
|
||||
} else if (libLowPriorityList.size() >= changeNum) {
|
||||
for (int i = 0; i < changeNum; i++) {
|
||||
chosen.add(libLowPriorityList.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
return chosen;
|
||||
}
|
||||
}
|
||||
|
||||
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
|
||||
public static class LivingDeath {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
int aiBattlefieldPower = 0, aiGraveyardPower = 0;
|
||||
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value
|
||||
|
||||
CardCollection aiCreaturesInGY = CardLists.filter(ai.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES);
|
||||
|
||||
if (aiCreaturesInGY.isEmpty()) {
|
||||
@@ -348,13 +711,94 @@ public class SpecialCardAi {
|
||||
}
|
||||
|
||||
// if we get more value out of this than our opponent does (hopefully), go for it
|
||||
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower);
|
||||
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold);
|
||||
}
|
||||
}
|
||||
|
||||
// Mairsil, the Pretender
|
||||
public static class MairsilThePretender {
|
||||
// Scan the fetch list for a card with at least one activated ability.
|
||||
// TODO: can be improved to a full consider(sa, ai) logic which would scan the graveyard first and hand last
|
||||
public static Card considerCardFromList(final CardCollection fetchList) {
|
||||
for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) {
|
||||
for (SpellAbility ab : c.getSpellAbilities()) {
|
||||
if (ab.isAbility() && !ab.isTrigger()) {
|
||||
Player controller = c.getController();
|
||||
boolean wasCaged = false;
|
||||
for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile),
|
||||
CardPredicates.hasCounter(CounterType.CAGE))) {
|
||||
if (c.getName().equals(caged.getName())) {
|
||||
wasCaged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasCaged) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mimic Vat
|
||||
public static class MimicVat {
|
||||
public static boolean considerExile(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Card exiledWith = source.getImprintedCards().isEmpty() ? null : source.getImprintedCards().getFirst();
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
final Card tgt = defined.isEmpty() ? null : defined.get(0);
|
||||
|
||||
return exiledWith == null || (tgt != null && ComputerUtilCard.evaluateCreature(tgt) > ComputerUtilCard.evaluateCreature(exiledWith));
|
||||
}
|
||||
|
||||
public static boolean 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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
|| (ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && ai.getGame().getCombat() != null
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
// Momir Vig, Simic Visionary Avatar
|
||||
public static class MomirVigAvatar {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
|
||||
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
|
||||
// Some basic strategy for Momir
|
||||
if (tokenSize < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokenSize > 11) {
|
||||
tokenSize = 11;
|
||||
}
|
||||
|
||||
source.setSVar("PayX", Integer.toString(tokenSize));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Necropotence
|
||||
public static class Necropotence {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Game game = ai.getGame();
|
||||
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
||||
int maxHandSize = ai.getMaxHandSize();
|
||||
@@ -407,9 +851,34 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Null Brooch
|
||||
public static class NullBrooch {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
// TODO: improve the detection of Ensnaring Bridge type effects ("GTX", "X" need generalization)
|
||||
boolean hasEnsnaringBridgeEffect = false;
|
||||
for (Card otb : ai.getCardsIn(ZoneType.Battlefield)) {
|
||||
for (StaticAbility stab : otb.getStaticAbilities()) {
|
||||
if ("CARDNAME can't attack.".equals(stab.getParam("AddHiddenKeyword"))
|
||||
&& "Creature.powerGTX".equals(stab.getParam("Affected"))
|
||||
&& "Count$InYourHand".equals(otb.getSVar("X"))) {
|
||||
hasEnsnaringBridgeEffect = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Maybe use it for some important high-impact spells even if there are more cards in hand?
|
||||
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && !hasEnsnaringBridgeEffect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Nykthos, Shrine to Nyx
|
||||
public static class NykthosShrineToNyx {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Game game = ai.getGame();
|
||||
PhaseHandler ph = game.getPhaseHandler();
|
||||
if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
@@ -428,7 +897,7 @@ public class SpecialCardAi {
|
||||
final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
|
||||
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai);
|
||||
|
||||
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableMana(ai, true), CardPredicates.Presets.UNTAPPED).size();
|
||||
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableManaSources(ai, true), CardPredicates.Presets.UNTAPPED).size();
|
||||
|
||||
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
|
||||
ManaCost cost = testSa.getPayCosts().getTotalMana();
|
||||
@@ -468,7 +937,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Phyrexian Dreadnought
|
||||
public static class PhyrexianDreadnought {
|
||||
public static CardCollection reviseCreatureSacList(Player ai, SpellAbility sa, CardCollection choices) {
|
||||
public static CardCollection reviseCreatureSacList(final Player ai, final SpellAbility sa, final CardCollection choices) {
|
||||
choices.sort(Collections.reverseOrder(ComputerUtilCard.EvaluateCreatureComparator));
|
||||
int power = 0;
|
||||
List<Card> toKeep = Lists.newArrayList();
|
||||
@@ -492,11 +961,11 @@ public class SpecialCardAi {
|
||||
|
||||
// Sarkhan the Mad
|
||||
public static class SarkhanTheMad {
|
||||
public static boolean considerDig(Player ai, SpellAbility sa) {
|
||||
public static boolean considerDig(final Player ai, final SpellAbility sa) {
|
||||
return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1;
|
||||
}
|
||||
|
||||
public static boolean considerMakeDragon(Player ai, SpellAbility sa) {
|
||||
public static boolean 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, new Predicate<Card>() {
|
||||
@@ -513,7 +982,7 @@ public class SpecialCardAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean considerUltimate(Player ai, SpellAbility sa, Player weakestOpp) {
|
||||
public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) {
|
||||
int minLife = weakestOpp.getLife();
|
||||
|
||||
int dragonPower = 0;
|
||||
@@ -526,9 +995,123 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Survival of the Fittest
|
||||
public static class SurvivalOfTheFittest {
|
||||
public static Card considerDiscardTarget(final Player ai) {
|
||||
// The AI here only checks the number of available creatures of various CMC, which is equivalent to knowing
|
||||
// your deck composition and checking (and counting) the cards in other zones so you know what you have left
|
||||
// in the library. As such, this does not cause unfair advantage, at least unless there are cards that are
|
||||
// face down (on the battlefield or in exile). Might need some kind of an update to consider hidden information
|
||||
// like that properly (probably by adding all those cards to the evaluation mix so the AI doesn't "know" which
|
||||
// ones are already face down in play and which are still in the library)
|
||||
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
|
||||
CardCollectionView creatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES);
|
||||
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
|
||||
if (creatsInHand.isEmpty() || creatsInLib.isEmpty()) { return null; }
|
||||
|
||||
int numManaSrcs = ComputerUtilMana.getAvailableManaEstimate(ai, false)
|
||||
+ Math.min(1, manaSrcsInHand.size());
|
||||
|
||||
// Cards in library that are either below/at (preferred) or above the max CMC affordable by the AI
|
||||
// (the latter might happen if we're playing a Reanimator deck with lots of fatties)
|
||||
CardCollection atTargetCMCInLib = CardLists.filter(creatsInLib, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilMana.hasEnoughManaSourcesToCast(card.getSpellPermanent(), ai);
|
||||
}
|
||||
});
|
||||
if (atTargetCMCInLib.isEmpty()) {
|
||||
atTargetCMCInLib = CardLists.filter(creatsInLib, CardPredicates.greaterCMC(numManaSrcs));
|
||||
}
|
||||
Collections.sort(atTargetCMCInLib, CardLists.CmcComparatorInv);
|
||||
if (atTargetCMCInLib.isEmpty()) {
|
||||
// Nothing to aim for?
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cards in hand that are below the max CMC affordable by the AI
|
||||
CardCollection belowMaxCMC = CardLists.filter(creatsInHand, CardPredicates.lessCMC(numManaSrcs - 1));
|
||||
Collections.sort(belowMaxCMC, Collections.reverseOrder(CardLists.CmcComparatorInv));
|
||||
|
||||
// Cards in hand that are above the max CMC affordable by the AI
|
||||
CardCollection aboveMaxCMC = CardLists.filter(creatsInHand, CardPredicates.greaterCMC(numManaSrcs + 1));
|
||||
Collections.sort(aboveMaxCMC, CardLists.CmcComparatorInv);
|
||||
|
||||
Card maxCMC = !aboveMaxCMC.isEmpty() ? aboveMaxCMC.getFirst() : null;
|
||||
Card minCMC = !belowMaxCMC.isEmpty() ? belowMaxCMC.getFirst() : null;
|
||||
Card bestInLib = !atTargetCMCInLib.isEmpty() ? atTargetCMCInLib.getFirst() : null;
|
||||
|
||||
int maxCMCdiff = 0;
|
||||
if (maxCMC != null) {
|
||||
maxCMCdiff = maxCMC.getCMC() - numManaSrcs; // how far are we from viably casting it?
|
||||
}
|
||||
|
||||
// We have something too fat to viably cast in the nearest future, discard it hoping to
|
||||
// grab something more immediately valuable (or maybe we're playing Reanimator and we want
|
||||
// it to be in the graveyard anyway)
|
||||
if (maxCMCdiff >= 3) {
|
||||
return maxCMC;
|
||||
}
|
||||
// We have a card in hand that is worse than the one in library, so discard the worst card
|
||||
if (maxCMCdiff <= 0 && minCMC != null
|
||||
&& ComputerUtilCard.evaluateCreature(bestInLib) > ComputerUtilCard.evaluateCreature(minCMC)) {
|
||||
return minCMC;
|
||||
}
|
||||
// We have a card in the library that is closer to being castable than the one in hand, and
|
||||
// no options with smaller CMC, so discard the one that is harder to cast for the one that is
|
||||
// easier to cast right now, but only if the best card in the library is at least CMC 3
|
||||
// (probably not worth it to grab low mana cost cards this way)
|
||||
if (maxCMC != null && maxCMC.getCMC() < bestInLib.getCMC() && bestInLib.getCMC() >= 3) {
|
||||
return maxCMC;
|
||||
}
|
||||
// We appear to be playing Reanimator (or we have a reanimator card in hand already), so it's
|
||||
// worth to fill the graveyard now
|
||||
if (ComputerUtil.isPlayingReanimator(ai) && !creatsInLib.isEmpty()) {
|
||||
CardCollection creatsInHandByCMC = new CardCollection(creatsInHand);
|
||||
Collections.sort(creatsInHandByCMC, CardLists.CmcComparatorInv);
|
||||
return creatsInHandByCMC.getFirst();
|
||||
}
|
||||
|
||||
// probably nothing that is worth changing, so bail
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Card considerCardToGet(final Player ai, final SpellAbility sa) {
|
||||
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
|
||||
if (creatsInLib.isEmpty()) { return null; }
|
||||
|
||||
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
int numManaSrcs = ComputerUtilMana.getAvailableManaEstimate(ai, false)
|
||||
+ Math.min(1, manaSrcsInHand.size());
|
||||
|
||||
CardCollection atTargetCMCInLib = CardLists.filter(creatsInLib, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilMana.hasEnoughManaSourcesToCast(card.getSpellPermanent(), ai);
|
||||
}
|
||||
});
|
||||
if (atTargetCMCInLib.isEmpty()) {
|
||||
atTargetCMCInLib = CardLists.filter(creatsInLib, CardPredicates.greaterCMC(numManaSrcs));
|
||||
}
|
||||
Collections.sort(atTargetCMCInLib, CardLists.CmcComparatorInv);
|
||||
|
||||
Card bestInLib = atTargetCMCInLib != null ? atTargetCMCInLib.getFirst() : null;
|
||||
|
||||
if (bestInLib == null && ComputerUtil.isPlayingReanimator(ai)) {
|
||||
// For Reanimator, we don't mind grabbing the biggest thing possible to recycle it again with SotF later.
|
||||
CardCollection creatsInLibByCMC = new CardCollection(creatsInLib);
|
||||
Collections.sort(creatsInLibByCMC, CardLists.CmcComparatorInv);
|
||||
return creatsInLibByCMC.getFirst();
|
||||
}
|
||||
|
||||
return bestInLib;
|
||||
}
|
||||
}
|
||||
|
||||
// Timetwister
|
||||
public static class Timetwister {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
int maxOppHandSize = 0;
|
||||
|
||||
@@ -550,8 +1133,55 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Volrath's Shapeshifter
|
||||
public static class VolrathsShapeshifter {
|
||||
public static boolean 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;
|
||||
}
|
||||
|
||||
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
|
||||
Card topGY = null;
|
||||
Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand));
|
||||
int numCreatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).size();
|
||||
|
||||
if (!aiGY.isEmpty()) {
|
||||
topGY = ai.getCardsIn(ZoneType.Graveyard).get(0);
|
||||
}
|
||||
|
||||
if (creatHand != null) {
|
||||
if (topGY == null
|
||||
|| !topGY.isCreature()
|
||||
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
|
||||
if (numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) {
|
||||
Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand));
|
||||
if (creatHand != null) {
|
||||
CardCollection cc = new CardCollection();
|
||||
cc.add(creatHand);
|
||||
return cc;
|
||||
}
|
||||
|
||||
// Should ideally never get here
|
||||
System.err.println("Volrath's Shapeshifter AI: Could not find a discard target despite the previous confirmation to proceed!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Ugin, the Spirit Dragon
|
||||
public static class UginTheSpiritDragon {
|
||||
public static boolean considerPWAbilityPriority(Player ai, SpellAbility sa, ZoneType origin, CardCollectionView oppType, CardCollectionView computerType) {
|
||||
public static boolean considerPWAbilityPriority(final Player ai, final SpellAbility sa, final ZoneType origin, CardCollectionView oppType, CardCollectionView computerType) {
|
||||
Card source = sa.getHostCard();
|
||||
Game game = source.getGame();
|
||||
|
||||
@@ -610,7 +1240,7 @@ public class SpecialCardAi {
|
||||
|
||||
// Yawgmoth's Bargain
|
||||
public static class YawgmothsBargain {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
Game game = ai.getGame();
|
||||
PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
@@ -651,12 +1281,16 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Yawgmoth's Will (can potentially be expanded for other broadly similar effects too)
|
||||
// Yawgmoth's Will and other cards with similar effect, e.g. Magus of the Will
|
||||
public static class YawgmothsWill {
|
||||
public static boolean consider(Player ai, SpellAbility sa) {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
CardCollectionView cardsInGY = ai.getCardsIn(ZoneType.Graveyard);
|
||||
if (cardsInGY.size() == 0) {
|
||||
return false;
|
||||
} else if (ai.getGame().getPhaseHandler().getPlayerTurn() != ai) {
|
||||
// The AI is not very good at deciding for what to viably do during the opp's turn when this
|
||||
// comes from an instant speed effect (e.g. Magus of the Will)
|
||||
return false;
|
||||
}
|
||||
|
||||
int minManaAdj = 2; // we want the AI to have some spare mana for possible other spells to cast
|
||||
@@ -673,8 +1307,8 @@ public class SpecialCardAi {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ComputerUtilAbility.getAbilitySourceName(ab).equals(ComputerUtilAbility.getAbilitySourceName(sa))
|
||||
|| ab.hasParam("AINoRecursiveCheck")) {
|
||||
if ((ComputerUtilAbility.getAbilitySourceName(ab).equals(ComputerUtilAbility.getAbilitySourceName(sa))
|
||||
&& !(ab instanceof SpellPermanent)) || ab.hasParam("AINoRecursiveCheck")) {
|
||||
// prevent infinitely recursing abilities that are susceptible to reentry
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.ICardFace;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostParser;
|
||||
@@ -25,6 +20,10 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityCondition;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base class for API-specific AI logic
|
||||
* <p>
|
||||
@@ -103,6 +102,12 @@ public abstract class SpellAbilityAi {
|
||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||
*/
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (aiLogic.equals("CheckCondition")) {
|
||||
SpellAbility saCopy = sa.copy();
|
||||
saCopy.setActivatingPlayer(ai);
|
||||
return saCopy.getConditions().areMet(saCopy);
|
||||
}
|
||||
|
||||
return !("Never".equals(aiLogic));
|
||||
}
|
||||
|
||||
@@ -118,7 +123,7 @@ public abstract class SpellAbilityAi {
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.ability.*;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.util.ReflectionUtil;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public enum SpellApiToAi {
|
||||
Converter;
|
||||
|
||||
@@ -25,12 +24,14 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.Animate, AnimateAi.class)
|
||||
.put(ApiType.AnimateAll, AnimateAllAi.class)
|
||||
.put(ApiType.Attach, AttachAi.class)
|
||||
.put(ApiType.Ascend, AlwaysPlayAi.class)
|
||||
.put(ApiType.Balance, BalanceAi.class)
|
||||
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
|
||||
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
|
||||
.put(ApiType.BidLife, BidLifeAi.class)
|
||||
.put(ApiType.Bond, BondAi.class)
|
||||
.put(ApiType.Branch, AlwaysPlayAi.class)
|
||||
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
|
||||
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
||||
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
||||
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
|
||||
@@ -71,12 +72,14 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.ExchangeControlVariant, CannotPlayAi.class)
|
||||
.put(ApiType.ExchangePower, PowerExchangeAi.class)
|
||||
.put(ApiType.ExchangeZone, ZoneExchangeAi.class)
|
||||
.put(ApiType.Explore, ExploreAi.class)
|
||||
.put(ApiType.Fight, FightAi.class)
|
||||
.put(ApiType.FlipACoin, FlipACoinAi.class)
|
||||
.put(ApiType.Fog, FogAi.class)
|
||||
.put(ApiType.GainControl, ControlGainAi.class)
|
||||
.put(ApiType.GainLife, LifeGainAi.class)
|
||||
.put(ApiType.GainOwnership, CannotPlayAi.class)
|
||||
.put(ApiType.GameDrawn, CannotPlayAi.class)
|
||||
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
|
||||
.put(ApiType.Goad, GoadAi.class)
|
||||
.put(ApiType.Haunt, HauntAi.class)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -21,7 +22,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Random r = MyRandom.getRandom();
|
||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
@@ -46,7 +47,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -87,7 +88,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
public class AlwaysPlayAi extends SpellAbilityAi {
|
||||
@@ -13,4 +14,9 @@ public class AlwaysPlayAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import forge.ai.AiCardMemory;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardType;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -38,6 +25,10 @@ import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -86,13 +77,13 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
num = (num == null) ? "1" : num;
|
||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
||||
ai.getOpponent(), topStack.getHostCard(), topStack);
|
||||
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), topStack);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
|
||||
Card animatedCopy = becomeAnimated(source, sa);
|
||||
list.add(animatedCopy);
|
||||
list = CardLists.getValidCards(list, valid.split(","), ai.getOpponent(), topStack.getHostCard(),
|
||||
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(),
|
||||
topStack);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||
@@ -172,11 +163,17 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
|
||||
if (sa.hasParam("Crew") && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return false;
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
@@ -186,9 +183,21 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return false;
|
||||
}
|
||||
this.rememberAnimatedThisTurn(aiPlayer, c);
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
||||
if (!sa.getHostCard().isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(sa.getHostCard()))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bFlag) {
|
||||
this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||
}
|
||||
return bFlag; // All of the defined stuff is animated, not very useful
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
@@ -251,13 +260,8 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// need to targetable
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
// try to look for AI targets if able
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection prefList = CardLists.getValidCards(list, sa.getParam("AITgts").split(","), ai, source, sa);
|
||||
if(!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
||||
list = prefList;
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||
|
||||
// list is empty, no possible targets
|
||||
if (list.isEmpty()) {
|
||||
@@ -301,8 +305,9 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// if its player turn,
|
||||
// check if its Permanent or that creature would attack
|
||||
if (ph.isPlayerTurn(ai)) {
|
||||
if (!sa.hasParam("Permanent") &&
|
||||
!ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)) {
|
||||
if (!sa.hasParam("Permanent")
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
|
||||
&& !sa.hasParam("UntilHostLeavesPlay")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
@@ -24,7 +24,10 @@ import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
@@ -38,7 +41,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
@@ -250,6 +253,18 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mandatory) {
|
||||
if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) {
|
||||
// try to identify if this thing can actually tap
|
||||
for (SpellAbility ab : c.getAllSpellAbilities()) {
|
||||
if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!c.isEnchanted()) {
|
||||
return true;
|
||||
}
|
||||
@@ -478,7 +493,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if ("Guilty Conscience".equals(sourceName)) {
|
||||
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
|
||||
} else if ("Bonds of Faith".equals(sourceName)) {
|
||||
chosen = SpecialCardAi.BondsOfFaith.getBestAttachTarget(ai, sa, list);
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
|
||||
} else if ("Clutch of Undeath".equals(sourceName)) {
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
|
||||
}
|
||||
|
||||
// If Mandatory (brought directly into play without casting) gotta
|
||||
@@ -490,6 +507,44 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return chosen;
|
||||
}
|
||||
|
||||
private static Card attachAIInstantReequipPreference(final SpellAbility sa, final Card attachSource) {
|
||||
// e.g. Cranial Plating
|
||||
PhaseHandler ph = attachSource.getGame().getPhaseHandler();
|
||||
Combat combat = attachSource.getGame().getCombat();
|
||||
Card equipped = sa.getHostCard().getEquipping();
|
||||
if (equipped == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int powerBuff = 0;
|
||||
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
|
||||
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
|
||||
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null);
|
||||
}
|
||||
}
|
||||
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
|
||||
int damage = 0;
|
||||
for (Card c : combat.getUnblockedAttackers()) {
|
||||
damage += ComputerUtilCombat.predictDamageTo(combat.getDefenderPlayerByAttacker(equipped), c.getNetCombatDamage(), c, true);
|
||||
}
|
||||
if (combat.isBlocked(equipped)) {
|
||||
for (Card atk : combat.getAttackers()) {
|
||||
if (!combat.isBlocked(atk) && !ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null).contains(atk)) {
|
||||
if (ComputerUtilCombat.predictDamageTo(combat.getDefenderPlayerByAttacker(atk),
|
||||
atk.getNetCombatDamage(), atk, true) > 0) {
|
||||
if (damage + powerBuff >= combat.getDefenderPlayerByAttacker(atk).getLife()) {
|
||||
sa.resetTargets(); // this is needed to avoid bugs with adding two targets to a single SA
|
||||
return atk; // lethal damage, we can win right now, so why not?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Should generalize this code a bit since they all have similar structures
|
||||
/**
|
||||
* Attach ai control preference.
|
||||
@@ -580,8 +635,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
|
||||
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||
|
||||
String kws = stabMap.get("AddKeyword");
|
||||
if (kws != null) {
|
||||
@@ -702,30 +757,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* parseSVar TODO - flesh out javadoc for this method.
|
||||
*
|
||||
* @param hostCard
|
||||
* the Card with the SVar on it
|
||||
* @param amount
|
||||
* a String
|
||||
* @return the calculated number
|
||||
*/
|
||||
public static int parseSVar(final Card hostCard, final String amount) {
|
||||
int num = 0;
|
||||
if (amount == null) {
|
||||
return num;
|
||||
}
|
||||
|
||||
try {
|
||||
num = Integer.valueOf(amount);
|
||||
} catch (final NumberFormatException e) {
|
||||
num = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(amount).split("\\$")[1]);
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach preference.
|
||||
*
|
||||
@@ -874,8 +905,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
|
||||
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||
|
||||
grantingAbilities |= stabMap.containsKey("AddAbility");
|
||||
|
||||
@@ -1046,9 +1077,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
CardCollection prefList = list;
|
||||
if (sa.hasParam("AITgts")) {
|
||||
prefList = CardLists.getValidCards(list, sa.getParam("AITgts"), sa.getActivatingPlayer(), attachSource);
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
prefList = ComputerUtil.filterAITgts(sa, aiPlayer, (CardCollection)list, true);
|
||||
|
||||
Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic"));
|
||||
|
||||
@@ -1061,6 +1092,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ("InstantReequipPowerBuff".equals(sa.getParam("AILogic"))) {
|
||||
return c;
|
||||
}
|
||||
|
||||
boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping());
|
||||
|
||||
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) {
|
||||
@@ -1077,10 +1112,10 @@ public class AttachAi extends SpellAbilityAi {
|
||||
// make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around
|
||||
boolean decideMoveFromUseless = uselessCreature && aic.getBooleanProperty(AiProps.PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS);
|
||||
|
||||
if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) {
|
||||
if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
|
||||
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach);
|
||||
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
||||
aic.reserveManaSourcesForMain2(futureSpell);
|
||||
aic.reserveManaSources(futureSpell);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1137,6 +1172,12 @@ public class AttachAi extends SpellAbilityAi {
|
||||
prefList = CardLists.filterControlledBy(list, prefPlayer);
|
||||
}
|
||||
|
||||
// AI logic types that do not require a prefList and that evaluate the
|
||||
// usefulness of attach action autonomously
|
||||
if ("InstantReequipPowerBuff".equals(logic)) {
|
||||
return attachAIInstantReequipPreference(sa, attachSource);
|
||||
}
|
||||
|
||||
// If there are no preferred cards, and not mandatory bail out
|
||||
if (prefList.isEmpty()) {
|
||||
return chooseUnpreferred(mandatory, list);
|
||||
@@ -1287,9 +1328,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) {
|
||||
return false;
|
||||
}
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) {
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
|
||||
if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.")
|
||||
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) {
|
||||
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
|
||||
return false;
|
||||
}
|
||||
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
|
||||
@@ -1386,6 +1427,43 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Card doPumpOrCurseAILogic(final Player ai, final SpellAbility sa, final List<Card> list, final String type) {
|
||||
Card chosen = null;
|
||||
|
||||
List<Card> aiType = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
// Don't buff opponent's creatures of given type
|
||||
if (!c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return c.getType().hasCreatureType(type);
|
||||
}
|
||||
});
|
||||
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
// Don't debuff AI's own creatures not of given type
|
||||
if (c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
|
||||
}
|
||||
});
|
||||
|
||||
if (!aiType.isEmpty() && !oppNonType.isEmpty()) {
|
||||
Card bestAi = ComputerUtilCard.getBestCreatureAI(aiType);
|
||||
Card bestOpp = ComputerUtilCard.getBestCreatureAI(oppNonType);
|
||||
chosen = ComputerUtilCard.evaluateCreature(bestAi) > ComputerUtilCard.evaluateCreature(bestOpp) ? bestAi : bestOpp;
|
||||
} else if (!aiType.isEmpty()) {
|
||||
chosen = ComputerUtilCard.getBestCreatureAI(aiType);
|
||||
} else if (!oppNonType.isEmpty()) {
|
||||
chosen = ComputerUtilCard.getBestCreatureAI(oppNonType);
|
||||
}
|
||||
|
||||
return chosen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -16,7 +17,7 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
|
||||
int diff = 0;
|
||||
// TODO Add support for multiplayer logic
|
||||
final Player opp = aiPlayer.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
||||
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canTgtCreature()) {
|
||||
List<Card> list = CardLists.getTargetableCards(aiPlayer.getOpponent().getCardsIn(ZoneType.Battlefield), sa);
|
||||
List<Card> list = CardLists.getTargetableCards(ComputerUtil.getOpponentFor(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
|
||||
@@ -36,10 +36,10 @@ public final class BondAi extends SpellAbilityAi {
|
||||
* <p>
|
||||
* bondCanPlayAI.
|
||||
* </p>
|
||||
* @param aiPlayer
|
||||
* a {@link forge.game.player.Player} object.
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
*
|
||||
* @return a boolean.
|
||||
*/
|
||||
@@ -53,4 +53,9 @@ public final class BondAi extends SpellAbilityAi {
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||
return ComputerUtilCard.getBestCreatureAI(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
@@ -22,11 +12,7 @@ import forge.game.GameObject;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
@@ -40,6 +26,9 @@ import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ChangeZoneAi extends SpellAbilityAi {
|
||||
/*
|
||||
@@ -49,6 +38,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* too much: blink/bounce/exile/tutor/Raise Dead/Surgical Extraction/......
|
||||
*/
|
||||
|
||||
// multipleCardsToChoose is used by Intuition and can be adapted to be used by other
|
||||
// cards where multiple cards are fetched at once and they need to be coordinated
|
||||
private static CardCollection multipleCardsToChoose = new CardCollection();
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
|
||||
@@ -64,23 +57,47 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
} else if (aiLogic.equals("PriorityOptionalCost")) {
|
||||
boolean highPriority = false;
|
||||
// if we have more than one of these in hand, might not be worth waiting for optional cost payment on the additional copy
|
||||
highPriority |= CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(sa.getHostCard().getName())).size() > 1;
|
||||
// if we are in danger in combat, no need to wait to pay the optional cost
|
||||
highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat());
|
||||
|
||||
if (!highPriority) {
|
||||
if (Iterables.isEmpty(sa.getOptionalCosts())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.checkAiLogic(ai, sa, aiLogic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||
// Checks for "return true" unlike checkAiLogic()
|
||||
|
||||
multipleCardsToChoose.clear();
|
||||
String aiLogic = sa.getParam("AILogic");
|
||||
if (aiLogic != null) {
|
||||
if (aiLogic.equals("Always")) {
|
||||
return true;
|
||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||
return this.doSacAndUpgradeLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||
return this.doSacAndReturnFromGraveLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Necropotence")) {
|
||||
return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("SameName")) { // Declaration in Stone
|
||||
return this.doSameNameLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("ReanimateAll")) {
|
||||
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Intuition")) {
|
||||
// This logic only fills the multiple cards array, the decision to play is made
|
||||
// separately in hiddenOriginCanPlayAI later.
|
||||
multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
if (isHidden(sa)) {
|
||||
@@ -133,6 +150,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return doReturnCommanderLogic(sa, aiPlayer);
|
||||
}
|
||||
|
||||
if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
|
||||
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
|
||||
return true; // debuffed by opponent's auras to the level that it becomes useless
|
||||
}
|
||||
int delta = 0;
|
||||
for (Card enc : sa.getHostCard().getEnchantedBy(false)) {
|
||||
if (enc.getController().isOpponentOf(aiPlayer)) {
|
||||
delta--;
|
||||
} else {
|
||||
delta++;
|
||||
}
|
||||
}
|
||||
return delta <= 0;
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
return hiddenTriggerAI(aiPlayer, sa, mandatory);
|
||||
}
|
||||
@@ -166,7 +198,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
ZoneType origin = null;
|
||||
final Player opponent = ai.getOpponent();
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
@@ -182,7 +214,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
|
||||
&& !(destination.equals("Battlefield") && !source.isLand())) {
|
||||
return false;
|
||||
}
|
||||
@@ -367,7 +399,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Player opp = aiPlayer.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
if (tgt != null && tgt.canTgtPlayer()) {
|
||||
boolean isCurse = sa.isCurse();
|
||||
if (isCurse && sa.canTarget(opp)) {
|
||||
@@ -428,7 +460,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
Iterable<Player> pDefined;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if ((tgt != null) && tgt.canTgtPlayer()) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.isCurse()) {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
@@ -547,8 +579,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static Card chooseCreature(final Player ai, CardCollection list) {
|
||||
// Creating a new combat for testing purposes.
|
||||
Combat combat = new Combat(ai.getOpponent());
|
||||
for (Card att : ai.getOpponent().getCreaturesInPlay()) {
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
Combat combat = new Combat(opponent);
|
||||
for (Card att : opponent.getCreaturesInPlay()) {
|
||||
combat.addAttacker(att, ai);
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai);
|
||||
@@ -647,6 +680,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (destination == ZoneType.Battlefield) {
|
||||
// predict whether something may put a ETBing creature below zero toughness
|
||||
// (e.g. Reassembing Skeleton + Elesh Norn, Grand Cenobite)
|
||||
for (final Card c : retrieval) {
|
||||
if (c.isCreature()) {
|
||||
final Card copy = CardUtil.getLKICopy(c);
|
||||
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
|
||||
if (copy.getNetToughness() <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
@@ -666,6 +713,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
return true;
|
||||
}
|
||||
@@ -720,6 +773,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||
if ("MimicVat".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
return true;
|
||||
}
|
||||
@@ -764,9 +821,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, source, sa);
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
if (sa.hasParam("AITgts")) {
|
||||
list = CardLists.getValidCards(list, sa.getParam("AITgts"), ai, source);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (source.isInZone(ZoneType.Hand)) {
|
||||
list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(source.getName()))); // Don't get the same card back.
|
||||
}
|
||||
@@ -828,7 +894,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
&& !currCombat.getBlockers(attacker).isEmpty()) {
|
||||
ComputerUtilCard.sortByEvaluateCreature(blockers);
|
||||
Combat combat = new Combat(ai);
|
||||
combat.addAttacker(attacker, ai.getOpponent());
|
||||
combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
|
||||
for (Card blocker : blockers) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
}
|
||||
@@ -974,6 +1040,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!sa.hasParam("AITgtOwnCards")) {
|
||||
list = CardLists.filterControlledBy(list, ai.getOpponents());
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -988,6 +1056,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
});
|
||||
}
|
||||
|
||||
// See if maybe there's a special priority applicable for this, in case the opponent
|
||||
// has dangerous unblockables in play
|
||||
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Only care about combatants during combat
|
||||
if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) {
|
||||
CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null);
|
||||
@@ -1000,6 +1075,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the opponent can save a creature from bounce/blink/whatever by paying
|
||||
// the Unless cost (for example, Erratic Portal)
|
||||
list.removeAll(getSafeTargetsIfUnlessCostPaid(ai, sa, list));
|
||||
|
||||
if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1077,8 +1156,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
} else {
|
||||
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
||||
boolean aiTgtsOK = false;
|
||||
if (sa.hasParam("AIMinTgts")) {
|
||||
int minTgts = Integer.parseInt(sa.getParam("AIMinTgts"));
|
||||
if (sa.getTargets().getNumTargeted() >= minTgts) {
|
||||
aiTgtsOK = true;
|
||||
}
|
||||
}
|
||||
if (!aiTgtsOK) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1284,6 +1372,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
if ("DeathgorgeScavenger".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
|
||||
} else if ("ExtraplanarLens".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
|
||||
}
|
||||
|
||||
if (sa.getTargetRestrictions() == null) {
|
||||
// Just in case of Defined cases
|
||||
if (!mandatory && sa.hasParam("AttachedTo")) {
|
||||
@@ -1313,12 +1407,22 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("NeverBounceItself".equals(logic)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (fetchList.contains(source)) {
|
||||
if (fetchList.contains(source) && (fetchList.size() > 1 && !sa.getTriggeringAbility().isMandatory())) {
|
||||
// For cards that should never be bounced back to hand with their own [e.g. triggered] abilities, such as guild lands.
|
||||
fetchList.remove(source);
|
||||
}
|
||||
} else if ("WorstCard".equals(logic)) {
|
||||
return ComputerUtilCard.getWorstAI(fetchList);
|
||||
} else if ("Mairsil".equals(logic)) {
|
||||
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
|
||||
} else if ("SurvivalOfTheFittest".equals(logic)) {
|
||||
return SpecialCardAi.SurvivalOfTheFittest.considerCardToGet(decider, sa);
|
||||
} else if ("Intuition".equals(logic)) {
|
||||
if (!multipleCardsToChoose.isEmpty()) {
|
||||
Card choice = multipleCardsToChoose.get(0);
|
||||
multipleCardsToChoose.remove(0);
|
||||
return choice;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -1459,7 +1563,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return Iterables.getFirst(options, null);
|
||||
}
|
||||
|
||||
private boolean doSacAndUpgradeLogic(final Player ai, SpellAbility sa) {
|
||||
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
|
||||
|
||||
CardCollection listToSac = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.restriction(definedSac.split(","), ai, source, sa));
|
||||
listToSac.sort(Collections.reverseOrder(CardLists.CmcComparatorInv));
|
||||
|
||||
CardCollection listToRet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Presets.CREATURES);
|
||||
listToRet.sort(CardLists.CmcComparatorInv);
|
||||
|
||||
if (!listToSac.isEmpty() && !listToRet.isEmpty()) {
|
||||
Card worstSac = listToSac.getFirst();
|
||||
Card bestRet = listToRet.getFirst();
|
||||
|
||||
if (bestRet.getCMC() > worstSac.getCMC()
|
||||
&& ComputerUtilCard.evaluateCreature(bestRet) > ComputerUtilCard.evaluateCreature(worstSac)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestRet);
|
||||
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
String logic = sa.getParam("AILogic");
|
||||
@@ -1604,7 +1734,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// A blink effect implemented using ChangeZone API
|
||||
return false;
|
||||
} else if (subApi == ApiType.DelayedTrigger) {
|
||||
SpellAbility exec = causeSub.getAdditonalAbility("Execute");
|
||||
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
|
||||
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
|
||||
if ("Exile".equals(exec.getParam("Origin")) && "Battlefield".equals(exec.getParam("Destination"))) {
|
||||
// A blink effect implemented using a delayed trigger
|
||||
@@ -1623,6 +1753,45 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
|
||||
// Determines if the controller of each potential target can negate the ChangeZone effect
|
||||
// by paying the Unless cost. Returns the list of targets that can be saved that way.
|
||||
final Card source = sa.getHostCard();
|
||||
final CardCollection canBeSaved = new CardCollection();
|
||||
|
||||
for (Card potentialTgt : potentialTgts) {
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
|
||||
if (unlessCost != null && !unlessCost.endsWith(">")) {
|
||||
Player opp = potentialTgt.getController();
|
||||
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||
|
||||
int toPay = 0;
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
setPayX = true;
|
||||
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
} else {
|
||||
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
|
||||
}
|
||||
|
||||
if (toPay == 0) {
|
||||
canBeSaved.add(potentialTgt);
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
canBeSaved.add(potentialTgt);
|
||||
}
|
||||
|
||||
if (setPayX) {
|
||||
source.setSVar("PayX", Integer.toString(toPay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return canBeSaved;
|
||||
}
|
||||
|
||||
private static void rememberBouncedThisTurn(Player ai, Card c) {
|
||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiPlayerPredicates;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Random;
|
||||
|
||||
public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
@@ -46,9 +37,13 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
|
||||
boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
|
||||
|
||||
if (!aiLogicAllowsDiscard) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Random r = MyRandom.getRandom();
|
||||
// prevent run-away activations - first time will always return true
|
||||
@@ -73,21 +68,55 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
|
||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||
|
||||
// Living Death AI
|
||||
if ("LivingDeath".equals(sa.getParam("AILogic"))) {
|
||||
// Living Death AI
|
||||
return SpecialCardAi.LivingDeath.consider(ai, sa);
|
||||
} else if ("Timetwister".equals(sa.getParam("AILogic"))) {
|
||||
// Timetwister AI
|
||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
||||
} else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
|
||||
// e.g. Shadow of the Grave
|
||||
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
} else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
|
||||
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
|
||||
|
||||
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
|
||||
PlayerCollection players = new PlayerCollection();
|
||||
players.addAll(ai.getOpponents());
|
||||
players.add(ai);
|
||||
int maxSize = 1;
|
||||
for (Player player : players) {
|
||||
Player bestTgt = null;
|
||||
if (player.canBeTargetedBy(sa)) {
|
||||
CardCollectionView cardsGY = CardLists.filter(player.getCardsIn(ZoneType.Graveyard),
|
||||
CardPredicates.Presets.CREATURES);
|
||||
if (cardsGY.size() > maxSize) {
|
||||
maxSize = cardsGY.size();
|
||||
bestTgt = player;
|
||||
}
|
||||
}
|
||||
|
||||
// Timetwister AI
|
||||
if ("Timetwister".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
||||
if (bestTgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestTgt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO improve restrictions on when the AI would want to use this
|
||||
// spBounceAll has some AI we can compare to.
|
||||
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
||||
if (!sa.usesTargeting()) {
|
||||
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar, Timetwister)
|
||||
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar)
|
||||
return true;
|
||||
} else {
|
||||
// search targetable Opponents
|
||||
@@ -130,15 +159,39 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
}
|
||||
computerType = new CardCollection();
|
||||
}
|
||||
|
||||
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
|
||||
int nonCreatureEvalThreshold = 3; // CMC difference
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
if (destination == ZoneType.Hand) {
|
||||
creatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF);
|
||||
nonCreatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF);
|
||||
} else {
|
||||
creatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF);
|
||||
nonCreatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF);
|
||||
}
|
||||
}
|
||||
|
||||
// mass zone change for creatures: if in dire danger, do it; otherwise, only do it if the opponent's
|
||||
// creatures are better in value
|
||||
if ((CardLists.getNotType(oppType, "Creature").size() == 0)
|
||||
&& (CardLists.getNotType(computerType, "Creature").size() == 0)) {
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + 200) >= ComputerUtilCard
|
||||
if (game.getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
|
||||
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
// Life is in serious danger, return all creatures from the battlefield to wherever
|
||||
// so they don't deal lethal damage
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluateCreatureList(oppType)) {
|
||||
return false;
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
} // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human
|
||||
// permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + 3) >= ComputerUtilCard
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||
.evaluatePermanentList(oppType)) {
|
||||
return false;
|
||||
}
|
||||
@@ -180,6 +233,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
// minimum card advantage unless the hand will be fully reloaded
|
||||
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
|
||||
|
||||
if (numExiledWithSrc > curHandSize) {
|
||||
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
|
||||
// Try to gain some card advantage if the card will die anyway
|
||||
// TODO: ideally, should evaluate the hand value and not discard good hands to it
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
@@ -231,8 +292,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
* </p>
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
* @param aiPlayer
|
||||
* a {@link forge.game.player.Player} object.
|
||||
*
|
||||
* @return a boolean.
|
||||
*/
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
@@ -18,6 +10,9 @@ import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class CharmAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||
@@ -124,6 +119,8 @@ public class CharmAi extends SpellAbilityAi {
|
||||
|
||||
private List<AbilitySub> chooseTriskaidekaphobia(List<AbilitySub> choices, final Player ai) {
|
||||
List<AbilitySub> chosenList = Lists.newArrayList();
|
||||
if (choices == null || choices.isEmpty()) { return chosenList; }
|
||||
|
||||
AbilitySub gain = choices.get(0);
|
||||
AbilitySub lose = choices.get(1);
|
||||
FCollection<Player> opponents = ai.getOpponents();
|
||||
|
||||
@@ -6,6 +6,8 @@ import java.util.List;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
@@ -16,6 +18,7 @@ import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
@@ -122,7 +125,7 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
}
|
||||
} else if (aiLogic.equals("Duneblast")) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = ai.getOpponent().getCreaturesInPlay();
|
||||
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
|
||||
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
|
||||
|
||||
@@ -228,6 +231,13 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
Collections.reverse(creats);
|
||||
choice = creats.get(0);
|
||||
}
|
||||
} else if ("NegativePowerFirst".equals(logic)) {
|
||||
Card lowest = Aggregates.itemWithMin(options, CardPredicates.Accessors.fnGetNetPower);
|
||||
if (lowest.getNetPower() <= 0) {
|
||||
choice = lowest;
|
||||
} else {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(options);
|
||||
}
|
||||
} else if ("TangleWire".equals(logic)) {
|
||||
CardCollectionView betterList = CardLists.filter(options, new Predicate<Card>() {
|
||||
@Override
|
||||
|
||||
@@ -6,10 +6,7 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardRules;
|
||||
import forge.card.CardSplitType;
|
||||
@@ -17,7 +14,6 @@ import forge.card.CardStateName;
|
||||
import forge.card.ICardFace;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
@@ -36,29 +32,16 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
if (logic.equals("MomirAvatar")) {
|
||||
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
|
||||
// Some basic strategy for Momir
|
||||
if (tokenSize < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tokenSize > 11) {
|
||||
tokenSize = 11;
|
||||
}
|
||||
|
||||
source.setSVar("PayX", Integer.toString(tokenSize));
|
||||
return SpecialCardAi.MomirVigAvatar.consider(ai, sa);
|
||||
} else if (logic.equals("CursedScroll")) {
|
||||
return SpecialCardAi.CursedScroll.consider(ai, sa);
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
@@ -78,6 +61,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||
|
||||
return ComputerUtilCard.getBestAI(options);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
@@ -53,7 +52,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("Addle".equals(sourceName)) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ai.getOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -62,7 +61,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
||||
if (logic.equals("MostExcessOpponentControls")) {
|
||||
for (byte color : MagicColor.WUBRG) {
|
||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView opplist = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||
|
||||
@@ -196,7 +196,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// milling against Tamiyo is pointless
|
||||
if (owner.isCardInCommand("Tamiyo, the Moon Sage emblem")) {
|
||||
if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) {
|
||||
return allow;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -16,8 +17,9 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(aiPlayer.getOpponent())) {
|
||||
sa.getTargets().add(aiPlayer.getOpponent());
|
||||
Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
@@ -51,7 +52,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -63,8 +64,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(ai.getOpponent())) {
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -15,6 +13,9 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@@ -131,13 +132,19 @@ public class CloneAi extends SpellAbilityAi {
|
||||
* cloneTgtAI.
|
||||
* </p>
|
||||
*
|
||||
* @param af
|
||||
* a {@link forge.game.ability.AbilityFactory} object.
|
||||
* @param sa
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a boolean.
|
||||
*/
|
||||
private boolean cloneTgtAI(final SpellAbility sa) {
|
||||
// Specific logic for cards
|
||||
if ("CloneAttacker".equals(sa.getParam("AILogic"))) {
|
||||
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default:
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
// two are the only things
|
||||
// that clone a target. Those can just use SVar:RemAIDeck:True until
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
|
||||
CardCollection list =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on
|
||||
// purpose
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@@ -227,6 +227,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
||||
}
|
||||
|
||||
if (t != null) {
|
||||
if (t.isCreature())
|
||||
creatures--;
|
||||
if (t.isPlaneswalker())
|
||||
@@ -237,6 +238,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
artifacts--;
|
||||
if (t.isEnchantment())
|
||||
enchantments--;
|
||||
}
|
||||
|
||||
if (!sa.canTarget(t)) {
|
||||
list.remove(t);
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CopyPermanentAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Card source = sa.getHostCard();
|
||||
// TODO - I'm sure someone can do this AI better
|
||||
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("MimicVat".equals(aiLogic)) {
|
||||
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
|
||||
} else if ("AtEOT".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN);
|
||||
} else if ("AtOppEOT".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
import java.util.List;
|
||||
@@ -25,9 +26,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
|
||||
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
|
||||
|
||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
return super.chkAIDrawback(sa, aiPlayer);
|
||||
@@ -37,5 +39,17 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
||||
return spells.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
|
||||
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(player, sa);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiProps;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import java.util.Iterator;
|
||||
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
@@ -20,11 +11,14 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
@@ -43,7 +37,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
@@ -66,6 +60,8 @@ public class CounterAi extends SpellAbilityAi {
|
||||
// might as well check for player's friendliness
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
|
||||
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|
||||
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
|
||||
return false;
|
||||
@@ -75,6 +71,12 @@ public class CounterAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("OppDiscardsHand".equals(sa.getParam("AILogic"))) {
|
||||
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (sa.canTargetSpellAbility(topSA)) {
|
||||
sa.getTargets().add(topSA);
|
||||
@@ -93,8 +95,9 @@ public class CounterAi extends SpellAbilityAi {
|
||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||
|
||||
if (unlessCost != null && !unlessCost.endsWith(">")) {
|
||||
// Is this Usable Mana Sources? Or Total Available Mana?
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
||||
Player opp = tgtSA.getActivatingPlayer();
|
||||
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||
|
||||
int toPay = 0;
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
@@ -137,6 +140,10 @@ public class CounterAi extends SpellAbilityAi {
|
||||
if (tgtCMC < minCMC) {
|
||||
return false;
|
||||
}
|
||||
} else if ("NullBrooch".equals(logic)) {
|
||||
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,14 +153,31 @@ public class CounterAi extends SpellAbilityAi {
|
||||
boolean ctrCmc0ManaPerms = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS);
|
||||
boolean ctrDamageSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_DAMAGE_SPELLS);
|
||||
boolean ctrRemovalSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_REMOVAL_SPELLS);
|
||||
boolean ctrPumpSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_PUMP_SPELLS);
|
||||
boolean ctrAuraSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_AURAS);
|
||||
boolean ctrOtherCounters = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_OTHER_COUNTERSPELLS);
|
||||
int ctrChanceCMC1 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_1);
|
||||
int ctrChanceCMC2 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_2);
|
||||
int ctrChanceCMC3 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_3);
|
||||
String ctrNamed = aic.getProperty(AiProps.ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS);
|
||||
boolean dontCounter = false;
|
||||
|
||||
if (tgtCMC == 1 && !MyRandom.percentTrue(ctrChanceCMC1)) {
|
||||
dontCounter = true;
|
||||
} else if (tgtCMC == 2 && !MyRandom.percentTrue(ctrChanceCMC2)) {
|
||||
dontCounter = true;
|
||||
} else if (tgtCMC == 3 && !MyRandom.percentTrue(ctrChanceCMC3)) {
|
||||
dontCounter = true;
|
||||
}
|
||||
|
||||
if (tgtSA != null && tgtCMC < aic.getIntProperty(AiProps.MIN_SPELL_CMC_TO_COUNTER)) {
|
||||
boolean dontCounter = true;
|
||||
dontCounter = true;
|
||||
Card tgtSource = tgtSA.getHostCard();
|
||||
if ((tgtSource != null && tgtCMC == 0 && tgtSource.isPermanent() && !tgtSource.getManaAbilities().isEmpty() && ctrCmc0ManaPerms)
|
||||
|| (tgtSA.getApi() == ApiType.DealDamage || tgtSA.getApi() == ApiType.LoseLife || tgtSA.getApi() == ApiType.DamageAll && ctrDamageSpells)
|
||||
|| (tgtSA.getApi() == ApiType.Counter && ctrOtherCounters)
|
||||
|| ((tgtSA.getApi() == ApiType.Pump || tgtSA.getApi() == ApiType.PumpAll) && ctrPumpSpells)
|
||||
|| (tgtSA.getApi() == ApiType.Attach && ctrAuraSpells)
|
||||
|| (tgtSA.getApi() == ApiType.Destroy || tgtSA.getApi() == ApiType.DestroyAll || tgtSA.getApi() == ApiType.Sacrifice
|
||||
|| tgtSA.getApi() == ApiType.SacrificeAll && ctrRemovalSpells)) {
|
||||
dontCounter = false;
|
||||
@@ -167,15 +191,19 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// should always counter CMC 1 with Mental Misstep despite a possible limitation by minimum CMC
|
||||
if (tgtCMC == 1 && "Mental Misstep".equals(source.getName())) {
|
||||
// should not refrain from countering a CMC X spell if that's the only CMC
|
||||
// counterable with that particular counterspell type (e.g. Mental Misstep vs. CMC 1 spells)
|
||||
if (sa.getParamOrDefault("ValidTgts", "").startsWith("Card.cmcEQ")) {
|
||||
int validTgtCMC = AbilityUtils.calculateAmount(source, sa.getParam("ValidTgts").substring(10), sa);
|
||||
if (tgtCMC == validTgtCMC) {
|
||||
dontCounter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dontCounter) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -212,8 +240,9 @@ public class CounterAi extends SpellAbilityAi {
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
if (unlessCost != null) {
|
||||
// Is this Usable Mana Sources? Or Total Available Mana?
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
||||
Player opp = tgtSA.getActivatingPlayer();
|
||||
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||
|
||||
int toPay = 0;
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -24,6 +16,9 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountersMoveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
@@ -109,6 +104,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
// Make sure that removing the last counter doesn't kill the creature
|
||||
if ("Self".equals(sa.getParam("Source"))) {
|
||||
if (host != null && host.getNetToughness() - 1 <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -188,6 +189,13 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
if (newEval < oldEval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for some specific AI preferences
|
||||
if (src.hasStartOfKeyword("Graft") && "DontMoveCounterIfLethal".equals(src.getSVar("AIGraftPreference"))) {
|
||||
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// no target
|
||||
return true;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
@@ -20,6 +14,7 @@ import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -32,6 +27,11 @@ import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class CountersPutAi extends SpellAbilityAi {
|
||||
|
||||
/*
|
||||
@@ -225,13 +225,42 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
|
||||
boolean onlyInCombat = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
|
||||
boolean onlyDefensive = ai.getController().isAI()
|
||||
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_DEFENSIVELY);
|
||||
|
||||
if (playAggro) {
|
||||
// aggro profiles ignore conservative play for this AI logic
|
||||
return true;
|
||||
} else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
return true;
|
||||
} else if (ai.getGame().getCombat() != null && sa.getHostCard() != null) {
|
||||
if (ai.getGame().getCombat().isAttacking(sa.getHostCard())) {
|
||||
if (ai.getGame().getCombat().isAttacking(sa.getHostCard()) && !onlyDefensive) {
|
||||
return true;
|
||||
} else if (ai.getGame().getCombat().isBlocking(sa.getHostCard())) {
|
||||
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
|
||||
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(sa.getHostCard());
|
||||
int totBlkPower = Aggregates.sum(blocked, CardPredicates.Accessors.fnGetNetPower);
|
||||
int totBlkToughness = Aggregates.min(blocked, CardPredicates.Accessors.fnGetNetToughness);
|
||||
|
||||
int numActivations = ai.getCounters(CounterType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
|
||||
if (sa.getHostCard().getNetToughness() + numActivations > totBlkPower
|
||||
|| sa.getHostCard().getNetPower() + numActivations >= totBlkToughness) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (sa.getSubAbility() != null
|
||||
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
|
||||
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
|
||||
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ANIMATED_THIS_TURN)) {
|
||||
// Bristling Hydra: save from death using a ping activation
|
||||
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return true;
|
||||
}
|
||||
} else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
// outside of combat, this logic only works if the relevant AI profile option is enabled
|
||||
// and if there is enough energy saved
|
||||
if (!onlyInCombat) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -273,7 +302,8 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
}
|
||||
|
||||
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
|
||||
if (amountStr.equals("X")) {
|
||||
if (source.getSVar(amountStr).equals("Count$xPaid")) {
|
||||
// By default, set PayX here to maximum value (used for most SAs of this type).
|
||||
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
|
||||
@@ -289,10 +319,19 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
amount = Math.min(amount, maxCtrs - curCtrs);
|
||||
if (amount <= 0) { return false; }
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
source.setSVar("PayX", Integer.toString(amount));
|
||||
} else if ("ExiledCreatureFromGraveCMC".equals(sa.getParam("AILogic"))) {
|
||||
// e.g. Necropolis
|
||||
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
|
||||
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't use it if no counters to add
|
||||
@@ -326,7 +365,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
|
||||
if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && !ph.isPlayerTurn(ai)) {
|
||||
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -373,6 +412,13 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
});
|
||||
|
||||
if (abCost.hasSpecificCostType(CostSacrifice.class)) {
|
||||
Card sacTarget = ComputerUtil.getCardPreference(ai, source, "SacCost", list);
|
||||
// this card is planned to be sacrificed during cost payment, so don't target it
|
||||
// (otherwise the AI can cheat by activating this SA and not paying the sac cost, e.g. Extruder)
|
||||
list.remove(sacTarget);
|
||||
}
|
||||
|
||||
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -491,7 +537,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
|
||||
boolean immediately = ComputerUtil.playImmediately(ai, sa);
|
||||
|
||||
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, immediately)) {
|
||||
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -670,6 +716,13 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
preferred = false;
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
// Still an empty list, but we have to choose something (mandatory); expand targeting to
|
||||
// include AI's own cards to see if there's anything targetable (e.g. Plague Belcher).
|
||||
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
preferred = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (list.isEmpty()) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -35,10 +37,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
final String type = sa.getParam("CounterType");
|
||||
final String amountStr = sa.getParam("CounterNum");
|
||||
final String valid = sa.getParam("ValidCards");
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final boolean curse = sa.isCurse();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
hList = CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
hList = CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
|
||||
if (abCost != null) {
|
||||
@@ -51,13 +54,23 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("AtEOTOrBlock")) {
|
||||
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return false;
|
||||
}
|
||||
} else if (logic.equals("AtOppEOT")) {
|
||||
if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (tgt != null) {
|
||||
Player pl = curse ? ai.getOpponent() : ai;
|
||||
Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
|
||||
sa.getTargets().add(pl);
|
||||
|
||||
hList = CardLists.filterControlledBy(hList, pl);
|
||||
@@ -138,6 +151,33 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return player.getCreaturesInPlay().size() >= player.getOpponent().getCreaturesInPlay().size();
|
||||
return player.getCreaturesInPlay().size() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (sa.usesTargeting()) {
|
||||
List<Player> players = Lists.newArrayList();
|
||||
if (!sa.isCurse()) {
|
||||
players.add(aiPlayer);
|
||||
}
|
||||
players.addAll(aiPlayer.getOpponents());
|
||||
players.addAll(aiPlayer.getAllies());
|
||||
if (sa.isCurse()) {
|
||||
players.add(aiPlayer);
|
||||
}
|
||||
|
||||
for (final Player p : players) {
|
||||
if (p.canBeTargetedBy(sa) && sa.canTarget(p)) {
|
||||
boolean preferred = false;
|
||||
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(p);
|
||||
return preferred || mandatory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mandatory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,20 @@
|
||||
*/
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerController.BinaryChoiceType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* AbilityFactory_PutOrRemoveCountersAi class.
|
||||
@@ -75,13 +70,8 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa);
|
||||
if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
||||
list = prefList;
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||
|
||||
if (sa.hasParam("CounterType")) {
|
||||
// currently only Jhoira's Timebug
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
@@ -10,12 +8,7 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -23,6 +16,9 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CountersRemoveAi extends SpellAbilityAi {
|
||||
|
||||
/*
|
||||
@@ -102,13 +98,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa);
|
||||
if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
||||
list = prefList;
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||
|
||||
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
|
||||
|
||||
@@ -174,10 +165,11 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Get rid of Planeswalkers:
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
list = game.getPlayers().getCardsIn(ZoneType.Battlefield);
|
||||
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
|
||||
|
||||
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS,
|
||||
CardCollection planeswalkerList = CardLists.filter(list,
|
||||
Predicates.and(CardPredicates.Presets.PLANEWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
|
||||
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
|
||||
|
||||
if (!planeswalkerList.isEmpty()) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -18,7 +18,14 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
|
||||
int restDamage = d;
|
||||
final Game game = comp.getGame();
|
||||
final Player enemy = comp.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(comp);
|
||||
boolean dmgByCardsInHand = false;
|
||||
|
||||
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
|
||||
sa.getHostCard().getSVar(sa.getParam("NumDmg")).equals("TargetedPlayer$CardsInHand")) {
|
||||
dmgByCardsInHand = true;
|
||||
}
|
||||
|
||||
if (!sa.canTarget(enemy)) {
|
||||
return false;
|
||||
}
|
||||
@@ -70,10 +77,13 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
value = 1.0f * restDamage / enemy.getLife();
|
||||
}
|
||||
} else {
|
||||
if (phase.isPlayerTurn(enemy) && phase.is(PhaseType.END_OF_TURN)) {
|
||||
if (phase.isPlayerTurn(enemy)) {
|
||||
if (phase.is(PhaseType.END_OF_TURN)
|
||||
|| ((dmgByCardsInHand && phase.getPhase().isAfter(PhaseType.UPKEEP)))) {
|
||||
value = 1.5f * restDamage / enemy.getLife();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value > 0) { //more likely to burn with larger hand
|
||||
for (int i = 3; i < hand.size(); i++) {
|
||||
value *= 1.1f;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -56,16 +55,35 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
x = source.getCounters(CounterType.LOYALTY);
|
||||
}
|
||||
if (x == -1) {
|
||||
Player bestOpp = determineOppToKill(ai, sa, source, dmg);
|
||||
if (determineOppToKill(ai, sa, source, dmg) != null) {
|
||||
// we already know we can kill a player, so go for it
|
||||
return true;
|
||||
}
|
||||
// look for other value in this (damaging creatures or
|
||||
// creatures + player, e.g. Pestilence, etc.)
|
||||
return evaluateDamageAll(ai, sa, source, dmg) > 0;
|
||||
} else {
|
||||
int best = -1, best_x = -1;
|
||||
for (int i = 0; i < x; i++) {
|
||||
Player bestOpp = determineOppToKill(ai, sa, source, x);
|
||||
if (bestOpp != null) {
|
||||
// we can finish off a player, so go for it
|
||||
|
||||
// TODO: improve this by possibly damaging more creatures
|
||||
// on the battlefield belonging to other opponents at the same
|
||||
// time, if viable
|
||||
best_x = bestOpp.getLife();
|
||||
} else {
|
||||
// see if it's possible to get value from killing off creatures
|
||||
for (int i = 0; i <= x; i++) {
|
||||
final int value = evaluateDamageAll(ai, sa, source, i);
|
||||
if (value > best) {
|
||||
best = value;
|
||||
best_x = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best_x > 0) {
|
||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
source.setSVar("PayX", Integer.toString(best_x));
|
||||
@@ -79,8 +97,31 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
private Player determineOppToKill(Player ai, SpellAbility sa, Card source, int x) {
|
||||
// Attempt to determine which opponent can be finished off such that the most players
|
||||
// are killed at the same time, given X damage tops
|
||||
final String validP = sa.hasParam("ValidPlayers") ? sa.getParam("ValidPlayers") : "";
|
||||
int aiLife = ai.getLife();
|
||||
Player bestOpp = null; // default opponent, if all else fails
|
||||
|
||||
for (int dmg = 1; dmg <= x; dmg++) {
|
||||
// Don't kill yourself in the process
|
||||
if (validP.equals("Player") && aiLife <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)) {
|
||||
break;
|
||||
}
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
if ((validP.equals("Player") || validP.contains("Opponent"))
|
||||
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
||||
bestOpp = opp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestOpp;
|
||||
}
|
||||
|
||||
private int evaluateDamageAll(Player ai, SpellAbility sa, final Card source, int dmg) {
|
||||
Player opp = ai.getOpponent();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final CardCollection humanList = getKillableCreatures(sa, opp, dmg);
|
||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||
|
||||
@@ -98,12 +139,6 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if we can kill human, do it
|
||||
if ((validP.equals("Player") || validP.contains("Opponent"))
|
||||
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int minGain = 200; // The minimum gain in destroyed creatures
|
||||
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
|
||||
if (computerList.isEmpty()) {
|
||||
@@ -179,7 +214,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Evaluate creatures getting killed
|
||||
Player enemy = ai.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -261,7 +296,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Evaluate creatures getting killed
|
||||
Player enemy = ai.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -28,6 +20,9 @@ import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DamageDealAi extends DamageAiBase {
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
@@ -98,6 +93,29 @@ public class DamageDealAi extends DamageAiBase {
|
||||
source.setSVar("PayX", Integer.toString(dmg));
|
||||
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
|
||||
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
|
||||
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
|
||||
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
|
||||
if (sa.getTargetRestrictions().canTgtPlayer()) {
|
||||
int maxDmg = 0;
|
||||
Player maxDamaged = null;
|
||||
for (Player p : ai.getOpponents()) {
|
||||
if (p.canBeTargetedBy(sa)) {
|
||||
if (p.getCardsIn(ZoneType.Hand).size() > maxDmg) {
|
||||
maxDmg = p.getCardsIn(ZoneType.Hand).size();
|
||||
maxDamaged = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxDmg > 0 && maxDamaged != null) {
|
||||
if (shouldTgtP(ai, sa, maxDmg, false)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(maxDamaged);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +123,20 @@ public class DamageDealAi extends DamageAiBase {
|
||||
dmg += 2;
|
||||
}
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
if ("DiscardLands".equals(logic)) {
|
||||
dmg = 2;
|
||||
} else if (logic.startsWith("ProcRaid.")) {
|
||||
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
|
||||
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ai.getAttackedWithCreatureThisTurn()) {
|
||||
dmg = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
|
||||
}
|
||||
} else if ("WildHunt".equals(logic)) {
|
||||
// This dummy ability will just deal 0 damage, but holds the logic for the AI for Master of Wild Hunt
|
||||
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source);
|
||||
@@ -131,6 +160,18 @@ public class DamageDealAi extends DamageAiBase {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if ("NinThePainArtist".equals(logic)) {
|
||||
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
|
||||
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||
boolean doTarget = damageTargetAI(ai, sa, dmg, true);
|
||||
if (doTarget) {
|
||||
Card tgt = sa.getTargets().getFirstTargetedCard();
|
||||
if (tgt != null) {
|
||||
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceName.equals("Sorin, Grim Nemesis")) {
|
||||
@@ -157,7 +198,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -250,15 +291,19 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
hPlay = CardLists.getTargetableCards(hPlay, sa);
|
||||
|
||||
final List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||
List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.getSVar("Targeting").equals("Dies")
|
||||
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d) && !ComputerUtil.canRegenerate(ai, c)
|
||||
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d)
|
||||
&& !ComputerUtil.canRegenerate(ai, c)
|
||||
&& !(c.getSVar("SacMe").length() > 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
|
||||
|
||||
Card targetCard = null;
|
||||
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
||||
if (sa.getTargetRestrictions().canTgtPlaneswalker()) {
|
||||
@@ -345,7 +390,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
||||
|
||||
Player enemy = ai.getOpponent();
|
||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
|
||||
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
|
||||
@@ -377,7 +422,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
if ("Polukranos".equals(sa.getParam("AILogic"))) {
|
||||
int dmgTaken = 0;
|
||||
CardCollection humCreatures = ai.getOpponent().getCreaturesInPlay();
|
||||
CardCollection humCreatures = enemy.getCreaturesInPlay();
|
||||
Card lastTgt = null;
|
||||
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
||||
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
|
||||
@@ -633,7 +678,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
// this is for Triggered targets that are mandatory
|
||||
final boolean noPrevention = sa.hasParam("NoPrevention");
|
||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
System.out.println("damageChooseRequiredTargets " + ai + " " + sa);
|
||||
|
||||
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||
|
||||
@@ -3,10 +3,6 @@ package forge.ai.ability;
|
||||
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DamagePreventAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -42,7 +43,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
// temporarily disabled until AI is improved
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
* @return a CardCollection.
|
||||
*/
|
||||
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
|
||||
if (!list.isEmpty()) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@@ -216,7 +217,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
list.remove(c);
|
||||
}
|
||||
|
||||
final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponent());
|
||||
final CardCollection pref = CardLists.filterControlledBy(list, ComputerUtil.getOpponentFor(ai));
|
||||
final CardCollection forced = CardLists.filterControlledBy(list, ai);
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
|
||||
@@ -18,8 +18,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
// TODO: improve ai
|
||||
return true;
|
||||
}
|
||||
final String svarName = sa.getParam("Execute");
|
||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getHostCard().getSVar(svarName), sa.getHostCard());
|
||||
SpellAbility trigsa = null;
|
||||
if (sa.hasAdditionalAbility("Execute")) {
|
||||
trigsa = sa.getAdditionalAbility("Execute");
|
||||
} else {
|
||||
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (trigsa instanceof AbilitySub) {
|
||||
@@ -31,8 +35,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final String svarName = sa.getParam("Execute");
|
||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getHostCard().getSVar(svarName), sa.getHostCard());
|
||||
SpellAbility trigsa = null;
|
||||
if (sa.hasAdditionalAbility("Execute")) {
|
||||
trigsa = sa.getAdditionalAbility("Execute");
|
||||
} else {
|
||||
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||
}
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
@@ -45,8 +53,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final String svarName = sa.getParam("Execute");
|
||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getSVar(svarName), sa.getHostCard());
|
||||
SpellAbility trigsa = null;
|
||||
if (sa.hasAdditionalAbility("Execute")) {
|
||||
trigsa = sa.getAdditionalAbility("Execute");
|
||||
} else {
|
||||
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||
}
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiProps;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
@@ -49,7 +35,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection list;
|
||||
|
||||
if (abCost != null) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,6 +50,23 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
hasXCost = abCost.getCostMana() != null ? abCost.getCostMana().getAmountOfX() > 0 : false;
|
||||
}
|
||||
|
||||
if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (ph.getPlayerTurn() == ai || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
} else if ("AtEOT".equals(sa.getParam("AILogic"))) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (!ph.is(PhaseType.END_OF_TURN)) {
|
||||
return false;
|
||||
}
|
||||
} else if ("AtEOTIfNotAttacking".equals(sa.getParam("AILogic"))) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
if (!ph.is(PhaseType.END_OF_TURN) || ai.getAttackedWithCreatureThisTurn()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -78,6 +81,11 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
if ("MadSarkhanDragon".equals(logic)) {
|
||||
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
|
||||
} else if (logic != null && logic.startsWith("MinLoyalty.")) {
|
||||
int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
|
||||
if (source.getCounters(CounterType.LOYALTY) < minLoyalty) {
|
||||
return false;
|
||||
}
|
||||
} else if ("Polymorph".equals(logic)) {
|
||||
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
if (list.isEmpty()) {
|
||||
@@ -104,26 +112,14 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
final int cmcMax = ai.hasRevolt() ? 4 : 2;
|
||||
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
|
||||
}
|
||||
if (sa.hasParam("AITgts")) {
|
||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||
if (source.isEnchanted()) {
|
||||
if (source.getEnchantedBy(false).get(0).getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
final int value = ComputerUtilCard.evaluateCreature(source);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return ComputerUtilCard.evaluateCreature(c) > value + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
list = CardLists.getValidCards(list, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||
|
||||
list = CardLists.getNotKeyword(list, "Indestructible");
|
||||
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
|
||||
}
|
||||
if (!SpellAbilityAi.playReusable(ai, sa)) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
@@ -267,7 +263,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
} else if (sa.hasParam("Defined")) {
|
||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|
||||
|| ai.getCreaturesInPlay().size() < ai.getOpponent().getCreaturesInPlay().size()
|
||||
|| ai.getCreaturesInPlay().size() < ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size()
|
||||
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
||||
|| ai.getLife() <= 5)) {
|
||||
// Basic ai logic for Lethal Vapors
|
||||
@@ -300,6 +296,9 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection preferred = CardLists.getNotKeyword(list, "Indestructible");
|
||||
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
|
||||
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
|
||||
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
|
||||
}
|
||||
|
||||
// If NoRegen is not set, filter out creatures that have a
|
||||
// regeneration shield
|
||||
@@ -314,25 +313,8 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
});
|
||||
}
|
||||
|
||||
if (sa.hasParam("AITgts")) {
|
||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||
if (source.isEnchanted()) {
|
||||
if (source.getEnchantedBy(false).get(0).getController().equals(ai)) {
|
||||
preferred.clear();
|
||||
}
|
||||
} else {
|
||||
final int value = ComputerUtilCard.evaluateCreature(source);
|
||||
preferred = CardLists.filter(preferred, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return ComputerUtilCard.evaluateCreature(c) > value + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
preferred = CardLists.getValidCards(preferred, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||
}
|
||||
}
|
||||
// Filter AI-specific targets if provided
|
||||
preferred = ComputerUtil.filterAITgts(sa, ai, (CardCollection)preferred, true);
|
||||
|
||||
for (final Card c : preferred) {
|
||||
list.remove(c);
|
||||
@@ -453,4 +435,5 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
return tempoCheck;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
|
||||
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
Player opponent = ai.getOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
||||
Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
||||
|
||||
final int CREATURE_EVAL_THRESHOLD = 200;
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
@@ -26,7 +20,7 @@ public class DigAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
Player libraryOwner = ai;
|
||||
|
||||
@@ -47,6 +41,8 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||
return false;
|
||||
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
|
||||
return game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
// don't deck yourself
|
||||
@@ -103,7 +99,7 @@ public class DigAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -20,6 +21,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
double chance = .4; // 40 percent chance with instant speed stuff
|
||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
chance = .667; // 66.7% chance for sorcery speed (since it will
|
||||
@@ -29,7 +31,22 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
|
||||
Player libraryOwner = ai;
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if ("DontMillSelf".equals(logic)) {
|
||||
// A card that digs for specific things and puts everything revealed before it into graveyard
|
||||
// (e.g. Hermit Druid) - don't use it to mill itself and also make sure there's enough playable
|
||||
// material in the library after using it several times.
|
||||
// TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard?
|
||||
if (ai.getCardsIn(ZoneType.Library).size() < 20) {
|
||||
return false;
|
||||
}
|
||||
if ("Land.Basic".equals(sa.getParam("Valid"))
|
||||
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA).isEmpty()) {
|
||||
// We already have a mana-producing land in hand, so bail
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -27,10 +26,11 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for these costs
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
||||
}
|
||||
|
||||
final boolean humanHasHand = ai.getOpponent().getCardsIn(ZoneType.Hand).size() > 0;
|
||||
if (aiLogic.equals("VolrathsShapeshifter")) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
||||
}
|
||||
|
||||
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
|
||||
|
||||
if (tgt != null) {
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
@@ -84,7 +88,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("NumCards")) {
|
||||
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getOpponent()
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
|
||||
.getCardsIn(ZoneType.Hand).size());
|
||||
if (cardsToDiscard < 1) {
|
||||
return false;
|
||||
@@ -97,7 +101,35 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement support for Discard AI for cards with AnyNumber set to true.
|
||||
// TODO: Improve support for Discard AI for cards with AnyNumber set to true.
|
||||
if (sa.hasParam("AnyNumber")) {
|
||||
if ("DiscardUncastableAndExcess".equals(aiLogic)) {
|
||||
final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
|
||||
final int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||
int numDiscard = 0;
|
||||
int numOppInHand = 0;
|
||||
for (Player p : ai.getGame().getPlayers()) {
|
||||
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
|
||||
numOppInHand = p.getCardsIn(ZoneType.Hand).size();
|
||||
}
|
||||
}
|
||||
for (Card c : inHand) {
|
||||
if (c.equals(sa.getHostCard())) { continue; }
|
||||
if (c.hasSVar("DoNotDiscardIfAble") || c.hasSVar("IsReanimatorCard")) { continue; }
|
||||
if (c.isCreature() && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getSpellPermanent(), ai)) {
|
||||
numDiscard++;
|
||||
}
|
||||
if ((c.isLand() && numLandsOTB >= 5) || (c.getFirstSpellAbility() != null && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getFirstSpellAbility(), ai))) {
|
||||
if (numDiscard + 1 <= numOppInHand) {
|
||||
numDiscard++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numDiscard == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||
@@ -120,7 +152,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
|
||||
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
|
||||
return false;
|
||||
}
|
||||
@@ -139,7 +171,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
@@ -160,7 +192,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
}
|
||||
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getOpponent()
|
||||
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
|
||||
.getCardsIn(ZoneType.Hand).size());
|
||||
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -19,7 +20,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Random r = MyRandom.getRandom();
|
||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
@@ -42,7 +43,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -83,7 +84,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
}
|
||||
|
||||
return randomReturn;
|
||||
|
||||
@@ -86,7 +86,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) {
|
||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,16 @@ public class DrawAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (logic.startsWith("LifeLessThan.")) {
|
||||
// LifeLessThan logic presupposes activation as soon as possible in an
|
||||
// attempt to save the AI from dying
|
||||
return true;
|
||||
} else if (logic.equals("AtEndOfOppTurn")) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
}
|
||||
|
||||
// Don't use draw abilities before main 2 if possible
|
||||
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
@@ -204,6 +214,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final boolean drawback = sa.getParent() != null;
|
||||
final Game game = ai.getGame();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
|
||||
@@ -241,11 +252,9 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Logic for cards that require special handling
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) {
|
||||
if ("YawgmothsBargain".equals(logic)) {
|
||||
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
// Generic logic for all cards that do not need any special handling
|
||||
|
||||
@@ -312,6 +321,13 @@ public class DrawAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// we're trying to save ourselves from death
|
||||
// (e.g. Bargain), so target the opp anyway
|
||||
if (logic.startsWith("LifeLessThan.")) {
|
||||
int threshold = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
|
||||
sa.getTargets().add(oppA);
|
||||
return ai.getLife() < threshold;
|
||||
}
|
||||
}
|
||||
|
||||
boolean aiTarget = sa.canTarget(ai);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
@@ -15,8 +16,7 @@ import forge.ai.SpellApiToAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -48,6 +48,21 @@ public class EffectAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
boolean worthHolding = false;
|
||||
CardCollectionView oppCreatsLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield),
|
||||
Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.Presets.CREATURES));
|
||||
CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.Presets.TAPPED);
|
||||
|
||||
if (oppCreatsLandsTapped.size() >= 3 || oppCreatsLands.size() == oppCreatsLandsTapped.size()) {
|
||||
worthHolding = true;
|
||||
}
|
||||
if (!worthHolding) {
|
||||
return false;
|
||||
}
|
||||
randomReturn = true;
|
||||
}
|
||||
} else if (logic.equals("Fog")) {
|
||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||
return false;
|
||||
@@ -179,9 +194,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldPlay) {
|
||||
return true;
|
||||
}
|
||||
return shouldPlay;
|
||||
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
|
||||
if (game.getStack().isEmpty()) {
|
||||
return false;
|
||||
@@ -246,6 +259,12 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (logic.equals("CastFromGraveThisTurn")) {
|
||||
CardCollection list = new CardCollection(game.getCardsIn(ZoneType.Graveyard));
|
||||
list = CardLists.getValidCards(list, sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else { //no AILogic
|
||||
return false;
|
||||
@@ -281,4 +300,35 @@ public class EffectAi extends SpellAbilityAi {
|
||||
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
// E.g. Nova Pentacle
|
||||
if (aiLogic.equals("RedirectFromOppToCreature")) {
|
||||
// try to target the opponent's best targetable permanent, if able
|
||||
CardCollection oppPerms = CardLists.getValidCards(aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
if (!oppPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
// try to target the AI's worst targetable permanent, if able
|
||||
CardCollection aiPerms = CardLists.getValidCards(aiPlayer.getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
if (!aiPerms.isEmpty()) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
52
forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
Normal file
52
forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.*;
|
||||
import forge.game.card.*;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class ExploreAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Card shouldPutInGraveyard(CardCollection top, Player ai) {
|
||||
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
|
||||
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
|
||||
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||
|
||||
int maxCMCDiff = 1;
|
||||
int numLandsToStillNeedMore = 2;
|
||||
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
maxCMCDiff = aic.getIntProperty(AiProps.EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD);
|
||||
numLandsToStillNeedMore = aic.getIntProperty(AiProps.EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||
}
|
||||
|
||||
if (!top.isEmpty()) {
|
||||
Card topCard = top.getFirst();
|
||||
if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) {
|
||||
// We need more lands to improve our mana base, explore away the non-lands
|
||||
return topCard;
|
||||
}
|
||||
if (topCard.getCMC() - maxCMCDiff >= predictedMana && !topCard.hasSVar("DoNotDiscardIfAble")) {
|
||||
// We're not casting this in foreseeable future, put it in the graveyard
|
||||
return topCard;
|
||||
}
|
||||
}
|
||||
|
||||
// Put on top of the library (do not mark the card for placement in the graveyard)
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -22,6 +14,10 @@ import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
public class FightAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
@@ -103,6 +99,11 @@ public class FightAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
return checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (canPlayAI(ai, sa)) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
public class FogAi extends SpellAbilityAi {
|
||||
|
||||
@@ -34,6 +38,25 @@ public class FogAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
|
||||
int dmg = 0;
|
||||
for (Card atk : game.getCombat().getAttackersOf(ai)) {
|
||||
if (game.getCombat().isUnblocked(atk)) {
|
||||
dmg += atk.getNetCombatDamage();
|
||||
} else if (atk.hasKeyword("Trample")) {
|
||||
dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness);
|
||||
}
|
||||
}
|
||||
|
||||
if (dmg > ai.getLife() / 4) {
|
||||
return true;
|
||||
} else if (dmg >= 5) {
|
||||
return true;
|
||||
} else if (ai.getLife() < ai.getStartingLife() / 3) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Cast it if life is in danger
|
||||
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
||||
}
|
||||
@@ -58,7 +81,7 @@ public class FogAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
final Game game = aiPlayer.getGame();
|
||||
boolean chance;
|
||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getOpponent())) {
|
||||
if (game.getPhaseHandler().isPlayerTurn(ComputerUtil.getOpponentFor(sa.getActivatingPlayer()))) {
|
||||
chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE);
|
||||
} else {
|
||||
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -8,7 +9,7 @@ import forge.game.spellability.TargetRestrictions;
|
||||
public class GameLossAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (opp.cantLose()) {
|
||||
return false;
|
||||
}
|
||||
@@ -34,14 +35,14 @@ public class GameLossAi extends SpellAbilityAi {
|
||||
// (Final Fortune would need to attach it's delayed trigger to a
|
||||
// specific turn, which can't be done yet)
|
||||
|
||||
if (!mandatory && ai.getOpponent().cantLose()) {
|
||||
if (!mandatory && ComputerUtil.getOpponentFor(ai).cantLose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -22,7 +23,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
final Random r = MyRandom.getRandom();
|
||||
final int myLife = aiPlayer.getLife();
|
||||
Player opponent = aiPlayer.getOpponent();
|
||||
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
||||
final int hLife = opponent.getLife();
|
||||
|
||||
if (!aiPlayer.canGainLife()) {
|
||||
@@ -78,7 +79,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
||||
final boolean mandatory) {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -35,7 +38,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
|
||||
if (!lifeCritical) {
|
||||
// return super.willPayCosts(ai, sa, cost, source);
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||
@@ -60,7 +63,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
skipCheck |= ComputerUtilCost.isSacrificeSelfCost(cost) && !source.isCreature();
|
||||
|
||||
if (!skipCheck) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -80,6 +83,23 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE)
|
||||
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
||||
|
||||
// When life is critical but there is no immediate danger, try to wait until declare blockers
|
||||
// before using the lifegain ability if it's an ability on a creature with a detrimental activation cost
|
||||
if (lifeCritical
|
||||
&& sa.isAbility()
|
||||
&& sa.getHostCard() != null && sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts() != null
|
||||
&& (sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class) || sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))) {
|
||||
if (!game.getStack().isEmpty()) {
|
||||
SpellAbility saTop = game.getStack().peekAbility();
|
||||
if (saTop.getTargets() != null && Iterables.contains(saTop.getTargets().getTargetPlayers(), ai)) {
|
||||
return ComputerUtil.predictDamageFromSpell(saTop, ai) > 0;
|
||||
}
|
||||
}
|
||||
if (game.getCombat() == null) { return false; }
|
||||
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; }
|
||||
}
|
||||
|
||||
// Don't use lifegain before main 2 if possible
|
||||
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -22,7 +23,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
// Ability_Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final int myLife = ai.getLife();
|
||||
final Player opponent = ai.getOpponent();
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
final int hlife = opponent.getLife();
|
||||
final String amountStr = sa.getParam("LifeAmount");
|
||||
|
||||
@@ -93,7 +94,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (amount < myLife) {
|
||||
if (amount <= myLife) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +110,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final int myLife = ai.getLife();
|
||||
final Player opponent = ai.getOpponent();
|
||||
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
final int hlife = opponent.getLife();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -110,13 +104,28 @@ public class ManaEffectAi extends SpellAbilityAi {
|
||||
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
int numManaSrcs = manaSources.size();
|
||||
int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
|
||||
manaReceived *= sa.getParam("Produced").split(" ").length;
|
||||
|
||||
int selfCost = sa.getPayCosts().getCostMana() != null ? sa.getPayCosts().getCostMana().getMana().getCMC() : 0;
|
||||
byte producedColor = MagicColor.fromName(sa.getParam("Produced"));
|
||||
|
||||
String produced = sa.getParam("Produced");
|
||||
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
|
||||
|
||||
if ("ChosenX".equals(sa.getParam("Amount"))
|
||||
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
|
||||
CounterType ctrType = CounterType.KI; // Petalmane Baku
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
manaReceived = host.getCounters(ctrType);
|
||||
}
|
||||
|
||||
int searchCMC = numManaSrcs - selfCost + manaReceived;
|
||||
|
||||
if ("X".equals(sa.getParam("Produced"))) {
|
||||
@@ -156,6 +165,9 @@ public class ManaEffectAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
SpellAbility testSaNoCost = testSa.copyWithNoManaCost();
|
||||
if (testSaNoCost == null) {
|
||||
continue;
|
||||
}
|
||||
testSaNoCost.setActivatingPlayer(ai);
|
||||
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) {
|
||||
if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword("Haste")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -60,7 +62,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
boolean chance = false;
|
||||
|
||||
if (abTgt != null) {
|
||||
List<Card> list = CardLists.filter(ai.getOpponent().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||
List<Card> list = CardLists.filter(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
@@ -14,13 +17,13 @@ import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class PermanentAi extends SpellAbilityAi {
|
||||
|
||||
@@ -62,6 +65,8 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* -- not used anymore after Ixalan (Planeswalkers are now legendary, not unique by subtype) --
|
||||
if (card.isPlaneswalker()) {
|
||||
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.Presets.PLANEWALKERS);
|
||||
@@ -75,7 +80,7 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if (card.getType().hasSupertype(Supertype.World)) {
|
||||
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
|
||||
@@ -142,7 +147,8 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// don't play cards without being able to pay the upkeep for
|
||||
for (String ability : card.getKeywords()) {
|
||||
for (KeywordInterface inst : card.getKeywords()) {
|
||||
String ability = inst.getOriginal();
|
||||
if (ability.startsWith("UpkeepCost")) {
|
||||
final String[] k = ability.split(":");
|
||||
final String costs = k[1];
|
||||
@@ -175,22 +181,44 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
if (!hasCard) {
|
||||
dontCast = true;
|
||||
}
|
||||
} else if (param.equals("MaxControlled")) {
|
||||
// Only cast unless there are X or more cards like this on the battlefield under AI control already
|
||||
int numControlled = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())).size();
|
||||
} else if (param.startsWith("MaxControlled")) {
|
||||
// Only cast unless there are X or more cards like this on the battlefield under AI control already,
|
||||
CardCollection ctrld = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName()));
|
||||
|
||||
int numControlled = 0;
|
||||
if (param.endsWith("WithoutOppAuras")) {
|
||||
// Check that the permanet does not have any auras attached to it by the opponent (this assumes that if
|
||||
// the opponent cast an aura on the opposing permanent, it's not with good intentions, and thus it might
|
||||
// be better to have a pristine copy of the card - might not always be a correct assumption, but sounds
|
||||
// like a reasonable default for some cards).
|
||||
for (Card c : ctrld) {
|
||||
if (c.getEnchantedBy(false).isEmpty()) {
|
||||
numControlled++;
|
||||
} else {
|
||||
for (Card att : c.getEnchantedBy(false)) {
|
||||
if (!att.getController().isOpponentOf(ai)) {
|
||||
numControlled++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
numControlled = ctrld.size();
|
||||
}
|
||||
|
||||
if (numControlled >= Integer.parseInt(value)) {
|
||||
dontCast = true;
|
||||
}
|
||||
} else if (param.equals("NumManaSources")) {
|
||||
// Only cast if there are X or more mana sources controlled by the AI
|
||||
CardCollection m = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
if (m.size() < Integer.parseInt(value)) {
|
||||
dontCast = true;
|
||||
}
|
||||
} else if (param.equals("NumManaSourcesNextTurn")) {
|
||||
// Only cast if there are X or more mana sources controlled by the AI *or*
|
||||
// if there are X-1 mana sources in play but the AI has an extra land in hand
|
||||
CardCollection m = ComputerUtilMana.getAvailableMana(ai, true);
|
||||
CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||
int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0;
|
||||
if (card.getName().equals("Illusions of Grandeur")) {
|
||||
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -33,6 +34,10 @@ public class PhasesAi extends SpellAbilityAi {
|
||||
tgtCards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if (tgtCards.contains(source)) {
|
||||
// Protect it from something
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source);
|
||||
if (isThreatened) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Card def = tgtCards.get(0);
|
||||
// Phase this out if it might attack me, or before it can be
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.CardTypeView;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -22,6 +17,8 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PlayAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
@@ -59,6 +56,16 @@ public class PlayAi extends SpellAbilityAi {
|
||||
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
|
||||
}
|
||||
|
||||
if (source != null && source.hasKeyword("Hideaway") && source.hasRemembered()) {
|
||||
// AI is not very good at playing non-permanent spells this way, at least yet
|
||||
// (might be possible to enable it for Sorceries in Main1/Main2 if target is available,
|
||||
// but definitely not for most Instants)
|
||||
Card rem = (Card) source.getFirstRemembered();
|
||||
CardTypeView t = rem.getState(CardStateName.Original).getType();
|
||||
|
||||
return t.isPermanent() && !t.isLand();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -99,12 +106,13 @@ public class PlayAi extends SpellAbilityAi {
|
||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||
*/
|
||||
@Override
|
||||
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
||||
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
|
||||
final boolean isOptional,
|
||||
Player targetedPlayer) {
|
||||
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
for (SpellAbility s : c.getBasicSpells()) {
|
||||
for (SpellAbility s : c.getBasicSpells(c.getState(CardStateName.Original))) {
|
||||
Spell spell = (Spell) s;
|
||||
s.setActivatingPlayer(ai);
|
||||
// timing restrictions still apply
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -31,7 +32,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
|
||||
List<Card> list =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on
|
||||
// purpose
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@@ -147,9 +147,10 @@ public class ProtectAi extends SpellAbilityAi {
|
||||
if (s==null) {
|
||||
return false;
|
||||
} else {
|
||||
Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||
Combat combat = ai.getGame().getCombat();
|
||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, ai.getOpponent(), combat, true);
|
||||
float ratio = 1.0f * dmg / ai.getOpponent().getLife();
|
||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
||||
float ratio = 1.0f * dmg / opponent.getLife();
|
||||
Random r = MyRandom.getRandom();
|
||||
return r.nextFloat() < ratio;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ProtectAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostTapType;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -34,7 +21,13 @@ import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityRestriction;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PumpAi extends PumpAiBase {
|
||||
|
||||
@@ -82,16 +75,19 @@ public class PumpAi extends PumpAiBase {
|
||||
System.err.println("MoveCounter AiLogic without MoveCounter SubAbility!");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
} else if ("Aristocrat".equals(aiLogic)) {
|
||||
return doAristocratLogic(sa, ai);
|
||||
} else if (aiLogic.startsWith("AristocratCounters")) {
|
||||
return doAristocratWithCountersLogic(sa, ai);
|
||||
}
|
||||
|
||||
return super.checkAiLogic(ai, sa, aiLogic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
|
||||
final String logic) {
|
||||
// special Phase check for MoveCounter
|
||||
// special Phase check for various AI logics
|
||||
if (logic.equals("MoveCounter")) {
|
||||
if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
return true;
|
||||
@@ -101,6 +97,11 @@ public class PumpAi extends PumpAiBase {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (logic.equals("Aristocrat")) {
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(sa.getHostCard());
|
||||
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||
}
|
||||
@@ -112,7 +113,7 @@ public class PumpAi extends PumpAiBase {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
|
||||
return false;
|
||||
}
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN) && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -136,12 +137,16 @@ public class PumpAi extends PumpAiBase {
|
||||
final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : "";
|
||||
final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : "";
|
||||
|
||||
final String aiLogic = sa.getParam("AILogic");
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic);
|
||||
final boolean isBerserk = "Berserk".equals(aiLogic);
|
||||
|
||||
if ("MoveCounter".equals(aiLogic)) {
|
||||
if ("Pummeler".equals(aiLogic)) {
|
||||
return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa);
|
||||
} else if (aiLogic.startsWith("AristocratCounters")) {
|
||||
return true; // the preconditions to this are already tested in checkAiLogic
|
||||
} else if ("MoveCounter".equals(aiLogic)) {
|
||||
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
|
||||
|
||||
if (moveSA == null) {
|
||||
@@ -333,6 +338,23 @@ public class PumpAi extends PumpAiBase {
|
||||
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
|
||||
}
|
||||
|
||||
if ("ContinuousBonus".equals(aiLogic)) {
|
||||
// P/T bonus in a continuous static ability
|
||||
for (StaticAbility stAb : source.getStaticAbilities()) {
|
||||
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
attack += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddToughness")) {
|
||||
defense += AbilityUtils.calculateAmount(source, stAb.getParam("AddToughness"), stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddKeyword")) {
|
||||
keywords.addAll(Lists.newArrayList(stAb.getParam("AddKeyword").split(" & ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) {
|
||||
return false;
|
||||
}
|
||||
@@ -359,9 +381,21 @@ public class PumpAi extends PumpAiBase {
|
||||
|
||||
return true;
|
||||
}
|
||||
if (!card.getController().isOpponentOf(ai)
|
||||
&& ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
||||
if (!card.getController().isOpponentOf(ai)) {
|
||||
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
||||
return true;
|
||||
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
|
||||
|
||||
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
|
||||
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)
|
||||
|| game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
|
||||
if (!ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -371,6 +405,21 @@ public class PumpAi extends PumpAiBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("DebuffForXCounters".equals(sa.getParam("AILogic")) && sa.getTargetCard() != null) {
|
||||
// e.g. Skullmane Baku
|
||||
CounterType ctrType = CounterType.KI;
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not pay more counters than necessary to kill the targeted creature
|
||||
int chosenX = Math.min(source.getCounters(ctrType), sa.getTargetCard().getNetToughness());
|
||||
sa.setSVar("ChosenX", String.valueOf(chosenX));
|
||||
}
|
||||
|
||||
return true;
|
||||
} // pumpPlayAI()
|
||||
|
||||
@@ -405,10 +454,28 @@ public class PumpAi extends PumpAiBase {
|
||||
|
||||
CardCollection list;
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (sa.getParam("AILogic").equals("HighestPower")) {
|
||||
if (sa.getParam("AILogic").equals("HighestPower") || sa.getParam("AILogic").equals("ContinuousBonus")) {
|
||||
list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
CardLists.sortByPowerDesc(list);
|
||||
|
||||
// Try not to kill own creatures with this pump
|
||||
CardCollection canDieToPump = new CardCollection();
|
||||
for (Card c : list) {
|
||||
if (c.isCreature() && c.getController() == ai
|
||||
&& c.getNetToughness() - c.getTempToughnessBoost() + defense <= 0) {
|
||||
canDieToPump.add(c);
|
||||
}
|
||||
}
|
||||
list.removeAll(canDieToPump);
|
||||
|
||||
// Generally, don't pump anything that your opponents control
|
||||
if ("ContinuousBonus".equals(sa.getParam("AILogic"))) {
|
||||
// TODO: make it possible for the AI to use this logic to kill opposing creatures
|
||||
// when a toughness debuff is applied
|
||||
list = CardLists.filter(list, CardPredicates.isController(ai));
|
||||
}
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(list.get(0));
|
||||
return true;
|
||||
@@ -461,6 +528,25 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
// Detain target nonland permanent: don't target noncreature permanents that don't have
|
||||
// any activated abilities.
|
||||
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
|
||||
list = CardLists.filter(list, Predicates.or(CardPredicates.Presets.CREATURES, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
for (SpellAbility sa: card.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (ComputerUtil.activateForCost(sa, ai)) {
|
||||
return pumpMandatoryTarget(ai, sa);
|
||||
@@ -473,6 +559,17 @@ public class PumpAi extends PumpAiBase {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
}
|
||||
|
||||
if ("BetterCreatureThanSource".equals(sa.getParam("AILogic"))) {
|
||||
// Don't target cards that are not better in value than the targeting card
|
||||
final int sourceValue = ComputerUtilCard.evaluateCreature(source);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return card.isCreature() && ComputerUtilCard.evaluateCreature(card) > sourceValue + 30;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ("Snapcaster".equals(sa.getParam("AILogic"))) {
|
||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
|
||||
return false;
|
||||
@@ -695,8 +792,6 @@ public class PumpAi extends PumpAiBase {
|
||||
return true;
|
||||
} // pumpDrawbackAI()
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
//TODO Add logic here if necessary but I think the AI won't cast
|
||||
@@ -704,4 +799,248 @@ public class PumpAi extends PumpAiBase {
|
||||
//and the pump isn't mandatory
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean doAristocratLogic(final SpellAbility sa, final Player ai) {
|
||||
// A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final Card source = sa.getHostCard();
|
||||
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
|
||||
final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0;
|
||||
final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0;
|
||||
final boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible");
|
||||
final int selfEval = ComputerUtilCard.evaluateCreature(source);
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||
|
||||
if (numOtherCreats == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to save the card from death by pumping it if it's threatened with a damage spell
|
||||
if (isThreatened && (toughnessBonus > 0 || indestructible)) {
|
||||
SpellAbility saTop = game.getStack().peekAbility();
|
||||
|
||||
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
|
||||
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage();
|
||||
final int numCreatsToSac = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus));
|
||||
|
||||
if (numCreatsToSac > 1) { // probably not worth sacrificing too much
|
||||
return false;
|
||||
}
|
||||
|
||||
if (indestructible || (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg)) {
|
||||
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| card.hasSVar("SacMe")
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
|
||||
}
|
||||
}
|
||||
);
|
||||
if (sacFodder.size() >= numCreatsToSac) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (combat == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (combat.isAttacking(source)) {
|
||||
if (combat.getBlockers(source).isEmpty()) {
|
||||
// Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if
|
||||
// the opponent is tapped out or if we're willing to risk it (will currently risk it
|
||||
// in case it sacs less than half its creatures to deal lethal damage)
|
||||
|
||||
// TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully
|
||||
// for potential damage prevention, various effects like reducing damage to 0, etc.)
|
||||
|
||||
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
|
||||
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
|
||||
|
||||
final boolean isInfect = source.hasKeyword("Infect"); // Flesh-Eater Imp
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / powerBonus;
|
||||
|
||||
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
|
||||
return source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||
// than the card we attacked with.
|
||||
final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (sacTgts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
|
||||
final int DefP = indestructible ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
|
||||
|
||||
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||
}
|
||||
} else {
|
||||
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| card.hasSVar("SacMe")
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return !sacFodder.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) {
|
||||
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
|
||||
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||
|
||||
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
|
||||
if (numOtherCreats == 0) {
|
||||
// Cut short if there's nothing to sac at all
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||
if (isDeclareBlockers || isThreatened) {
|
||||
if (doAristocratLogic(sa, ai)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if anything is to be gained from the PutCounter subability
|
||||
if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) {
|
||||
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
|
||||
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter subability!");
|
||||
return false;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final int selfEval = ComputerUtilCard.evaluateCreature(source);
|
||||
|
||||
String typeToGainCtr = "";
|
||||
if (logic.contains(".")) {
|
||||
typeToGainCtr = logic.substring(logic.indexOf(".") + 1);
|
||||
}
|
||||
CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay()
|
||||
: CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr));
|
||||
relevantCreats.remove(source);
|
||||
if (relevantCreats.isEmpty()) {
|
||||
// No relevant creatures to sac
|
||||
return false;
|
||||
}
|
||||
|
||||
int numCtrs = AbilityUtils.calculateAmount(source, sa.getSubAbility().getParam("CounterNum"), sa.getSubAbility());
|
||||
|
||||
if (combat != null && combat.isAttacking(source) && isDeclareBlockers) {
|
||||
if (combat.getBlockers(source).isEmpty()) {
|
||||
// Unblocked. Check if we can deal lethal after receiving counters.
|
||||
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
|
||||
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
|
||||
|
||||
final boolean isInfect = source.hasKeyword("Infect");
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
// Check if there's anything that will die anyway that can be eaten to gain a perma-bonus
|
||||
final CardCollection forcedSacTgts = CardLists.filter(relevantCreats,
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|
||||
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat));
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!forcedSacTgts.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
|
||||
|
||||
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
|
||||
return source.getNetCombatDamage() < lethalDmg
|
||||
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||
// than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing
|
||||
// things that are also threatened to be destroyed anyway.
|
||||
final CardCollection sacTgts = CardLists.filter(relevantCreats,
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| ComputerUtilCard.evaluateCreature(card) < selfEval
|
||||
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (sacTgts.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
|
||||
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
|
||||
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
|
||||
|
||||
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||
}
|
||||
} else {
|
||||
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||
final boolean isBlocking = combat != null && combat.isBlocking(source);
|
||||
final CardCollection sacFodder = CardLists.filter(relevantCreats,
|
||||
new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||
|| card.hasSVar("SacMe")
|
||||
|| (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval)
|
||||
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return !sacFodder.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,13 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.card.MagicColor;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
@@ -181,7 +176,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final int newPower = card.getNetCombatDamage() + attack;
|
||||
//int defense = getNumDefense(sa);
|
||||
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
||||
@@ -207,6 +202,19 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
|
||||
if (ph.isPlayerTurn(opp) && combat != null
|
||||
&& Iterables.any(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))
|
||||
&& CombatUtil.canBlock(card)) {
|
||||
// Use defensively to destroy the opposing Flying creature when possible, or to block with an indestructible
|
||||
// creature buffed with Flying
|
||||
for (Card c : CardLists.filter(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))) {
|
||||
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)
|
||||
&& (card.getNetPower() >= c.getNetToughness() && card.getNetToughness() > c.getNetPower()
|
||||
|| ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, card))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ph.isPlayerTurn(opp) || !(CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
|
||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| newPower <= 0
|
||||
@@ -371,7 +379,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
|| !CombatUtil.canBlock(card)) {
|
||||
return false;
|
||||
}
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) {
|
||||
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
|
||||
if (ph.isPlayerTurn(ai)
|
||||
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
@@ -532,6 +540,15 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
else {
|
||||
final boolean addsKeywords = !keywords.isEmpty();
|
||||
if (addsKeywords) {
|
||||
|
||||
// If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization
|
||||
if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.")
|
||||
|| keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) {
|
||||
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, true);
|
||||
}
|
||||
}
|
||||
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
|
||||
@@ -48,7 +48,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
}
|
||||
|
||||
if (abCost != null && source.hasSVar("AIPreference")) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, true)) {
|
||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -27,7 +28,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
// ability is targeted
|
||||
sa.resetTargets();
|
||||
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
|
||||
|
||||
if (!canTgtHuman) {
|
||||
|
||||
@@ -17,6 +17,12 @@ public class RemoveFromCombatAi extends SpellAbilityAi {
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// AI should only activate this during Human's turn
|
||||
|
||||
if ("RemoveBestAttacker".equals(sa.getParam("AILogic"))) {
|
||||
if (aiPlayer.getGame().getCombat() != null && aiPlayer.getGame().getCombat().getDefenders().contains(aiPlayer)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - implement AI
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -17,7 +15,7 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if (tgt != null) {
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
@@ -27,7 +25,10 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("MaxX".equals(logic)) {
|
||||
if ("MaxX".equals(logic) || "MaxXAtOppEOT".equals(logic)) {
|
||||
if ("MaxXAtOppEOT".equals(logic) && !(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||
return false;
|
||||
}
|
||||
// Set PayX here to maximum value.
|
||||
final int max = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
source.setSVar("PayX", Integer.toString(max));
|
||||
@@ -48,7 +49,7 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
@@ -59,10 +60,10 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// setup subability to repeat
|
||||
final SpellAbility repeat = sa.getAdditonalAbility("RepeatSubAbility");
|
||||
final SpellAbility repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||
|
||||
if (repeat == null) {
|
||||
return false;
|
||||
return mandatory;
|
||||
}
|
||||
|
||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -15,6 +12,9 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class RepeatEachAi extends SpellAbilityAi {
|
||||
@@ -81,11 +81,11 @@ public class RepeatEachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
} else if ("AllPlayerLoseLife".equals(logic)) {
|
||||
final Card source = sa.getHostCard();
|
||||
AbilitySub repeat = sa.getAdditonalAbility("RepeatSubAbility");
|
||||
AbilitySub repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||
|
||||
String svar = repeat.getSVar(repeat.getParam("LifeAmount"));
|
||||
// replace RememberedPlayerCtrl with YouCtrl
|
||||
String svarYou = svar.replace("RememberedPlayer", "You");
|
||||
String svarYou = TextUtil.fastReplace(svar, "RememberedPlayer", "You");
|
||||
|
||||
// Currently all Cards with that are affect all player, including AI
|
||||
if (aiPlayer.canLoseLife()) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
public class RollPlanarDiceAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
@@ -107,7 +108,7 @@ public class RollPlanarDiceAi extends SpellAbilityAi {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
System.out.println(String.format("Unexpected AI hint parameter in card %s in RollPlanarDiceAi: %s.", plane.getName(), paramName));
|
||||
System.out.println(TextUtil.concatNoSpace("Unexpected AI hint parameter in card ", plane.getName(), " in RollPlanarDiceAi: ", paramName, "."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -59,7 +60,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final boolean destroy = sa.hasParam("Destroy");
|
||||
|
||||
Player opp = ai.getOpponent();
|
||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
@@ -72,7 +73,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
|
||||
|
||||
List<Card> list =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||
CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||
for (Card c : list) {
|
||||
if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) {
|
||||
return false;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
@@ -12,6 +13,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@@ -34,11 +36,11 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
||||
// Set PayX here to maximum value.
|
||||
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
source.setSVar("PayX", Integer.toString(xPay));
|
||||
valid = valid.replace("X", Integer.toString(xPay));
|
||||
valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
|
||||
}
|
||||
|
||||
CardCollection humanlist =
|
||||
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
CardCollection computerlist =
|
||||
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.Card.SplitCMCMode;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -18,8 +15,6 @@ import forge.util.MyRandom;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
public class ScryAi extends SpellAbilityAi {
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -49,6 +44,18 @@ public class ScryAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
||||
// if the Scry ability requires tapping and has a mana cost, it's best done at the end of opponent's turn
|
||||
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
|
||||
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
|
||||
// even if there's no mana cost.
|
||||
if (sa.getPayCosts() != null) {
|
||||
if (sa.getPayCosts().hasTapCost()
|
||||
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
|
||||
&& !SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
}
|
||||
|
||||
// in the playerturn Scry should only be done in Main1 or in upkeep if able
|
||||
if (ph.isPlayerTurn(ai)) {
|
||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.card.CardSplitType;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.Game;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardState;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -67,6 +63,13 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
return super.checkAiLogic(aiPlayer, sa, aiLogic);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// Gross generalization, but this always considers alternate
|
||||
@@ -78,6 +81,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||
final String mode = sa.getParam("Mode");
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final Game game = source.getGame();
|
||||
|
||||
if("Transform".equals(mode)) {
|
||||
@@ -86,7 +90,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
if (source.hasKeyword("CARDNAME can't transform")) {
|
||||
return false;
|
||||
}
|
||||
return shouldTransformCard(source, ai, ph);
|
||||
return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
|
||||
} else {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
sa.resetTargets();
|
||||
@@ -108,7 +112,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
for (final Card c : list) {
|
||||
if (shouldTransformCard(c, ai, ph)) {
|
||||
if (shouldTransformCard(c, ai, ph) || "Always".equals(logic)) {
|
||||
sa.getTargets().add(c);
|
||||
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
||||
break;
|
||||
@@ -126,7 +130,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return shouldTurnFace(list.get(0), ai, ph);
|
||||
return shouldTurnFace(list.get(0), ai, ph) || "Always".equals(logic);
|
||||
} else {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
sa.resetTargets();
|
||||
@@ -139,7 +143,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
for (final Card c : list) {
|
||||
if (shouldTurnFace(c, ai, ph)) {
|
||||
if (shouldTurnFace(c, ai, ph) || "Always".equals(logic)) {
|
||||
sa.getTargets().add(c);
|
||||
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
||||
break;
|
||||
@@ -166,12 +170,26 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
transformed.getCurrentState().copyFrom(card, card.getAlternateState());
|
||||
transformed.updateStateForView();
|
||||
|
||||
// TODO: compareCards assumes that a creature will transform into a creature. Need to improve this
|
||||
// for other things potentially transforming.
|
||||
return compareCards(card, transformed, ai, ph);
|
||||
|
||||
}
|
||||
|
||||
private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
|
||||
if (card.isFaceDown()) {
|
||||
// hidden agenda
|
||||
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
|
||||
&& card.getZone().is(ZoneType.Command)) {
|
||||
String chosenName = card.getNamedCard();
|
||||
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {
|
||||
if (cast.getController() == ai && cast.getName().equals(chosenName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// non-permanent facedown can't be turned face up
|
||||
if (!card.getRules().getType().isPermanent()) {
|
||||
return false;
|
||||
@@ -241,4 +259,9 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
// but for more cleaner way use Evaluate for check
|
||||
return valueCard <= valueTransformed;
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
// TODO: improve the AI for when it may want to transform something that's optional to transform
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -58,6 +61,20 @@ public class TapAi extends TapAiBase {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if ("TapForXCounters".equals(sa.getParam("AILogic"))) {
|
||||
// e.g. Waxmane Baku
|
||||
CounterType ctrType = CounterType.KI;
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int numTargetable = Math.min(sa.getHostCard().getCounters(ctrType), ai.getOpponents().getCreaturesInPlay().size());
|
||||
sa.setSVar("ChosenX", String.valueOf(numTargetable));
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
|
||||
return false;
|
||||
|
||||
@@ -2,11 +2,12 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -111,7 +112,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) {
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Game game = ai.getGame();
|
||||
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
||||
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);
|
||||
@@ -155,6 +156,11 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
});
|
||||
}
|
||||
|
||||
//try to exclude things that will already be tapped due to something on stack or because something is
|
||||
//already targeted in a parent or sub SA
|
||||
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap);
|
||||
tapList.removeAll(toExclude);
|
||||
|
||||
if (tapList.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -30,7 +31,7 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
// or during upkeep/begin combat?
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
||||
@@ -126,8 +127,8 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
validTappables = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
validTappables = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.SpellApiToAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardFactory;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
@@ -38,6 +26,11 @@ import forge.game.trigger.TriggerHandler;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.PaperToken;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -179,7 +172,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
*/
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = ai.getGame();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite tokens?
|
||||
@@ -233,7 +226,29 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
return MyRandom.getRandom().nextFloat() < .8;
|
||||
|
||||
double chance = 1.0F; // 100%
|
||||
boolean alwaysFromPW = true;
|
||||
boolean alwaysOnOppAttack = true;
|
||||
|
||||
if (ai.getController().isAI()) {
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
chance = (double)aic.getIntProperty(AiProps.TOKEN_GENERATION_ABILITY_CHANCE) / 100;
|
||||
alwaysFromPW = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_FROM_PLANESWALKER);
|
||||
alwaysOnOppAttack = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS);
|
||||
}
|
||||
|
||||
if (sa.getRestrictions() != null && sa.getRestrictions().isPwAbility() && alwaysFromPW) {
|
||||
return true;
|
||||
} else if (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
|
||||
&& ai.getGame().getCombat() != null
|
||||
&& !ai.getGame().getCombat().getAttackers().isEmpty()
|
||||
&& alwaysOnOppAttack) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return MyRandom.getRandom().nextFloat() <= chance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,13 +269,13 @@ public class TokenAi extends SpellAbilityAi {
|
||||
num = (num == null) ? "1" : num;
|
||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
||||
ai.getOpponent(), topStack.getHostCard(), sa);
|
||||
ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), sa);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
// only care about saving single creature for now
|
||||
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
|
||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||
list.add(token);
|
||||
list = CardLists.getValidCards(list, valid.split(","), ai.getOpponent(), topStack.getHostCard(), sa);
|
||||
list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), sa);
|
||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||
if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||
&& list.contains(token)) {
|
||||
@@ -278,7 +293,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
sa.getTargets().add(ai.getOpponent());
|
||||
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
@@ -414,7 +429,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
|
||||
final List<String> imageNames = new ArrayList<String>(1);
|
||||
if (tokenImage.equals("")) {
|
||||
imageNames.add(PaperToken.makeTokenFileName(colorDesc.replace(" ", ""), tokenPower, tokenToughness, tokenName));
|
||||
imageNames.add(PaperToken.makeTokenFileName(TextUtil.fastReplace(colorDesc, " ", ""), tokenPower, tokenToughness, tokenName));
|
||||
} else {
|
||||
imageNames.add(0, tokenImage);
|
||||
}
|
||||
@@ -436,9 +451,9 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
final String substitutedName = tokenName.equals("ChosenType") ? host.getChosenType() : tokenName;
|
||||
final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size()));
|
||||
final CardFactory.TokenInfo tokenInfo = new CardFactory.TokenInfo(substitutedName, imageName,
|
||||
final TokenInfo tokenInfo = new TokenInfo(substitutedName, imageName,
|
||||
cost, substitutedTypes, tokenKeywords, finalPower, finalToughness);
|
||||
Card token = CardFactory.makeOneToken(tokenInfo, ai);
|
||||
Card token = tokenInfo.makeOneToken(ai);
|
||||
|
||||
if (token == null) {
|
||||
return null;
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -66,7 +67,7 @@ public class UnattachAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card card = sa.getHostCard();
|
||||
final Player opp = ai.getOpponent();
|
||||
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||
// Check if there are any valid targets
|
||||
List<GameObject> targets = new ArrayList<GameObject>();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import forge.ai.*;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostTap;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
@@ -20,13 +23,17 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class UntapAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
final Card source = sa.getHostCard();
|
||||
if ("EOT".equals(sa.getParam("AILogic")) && (source.getGame().getPhaseHandler().getNextTurn() != ai
|
||||
if ("EOT".equals(aiLogic) && (source.getGame().getPhaseHandler().getNextTurn() != ai
|
||||
|| !source.getGame().getPhaseHandler().getPhase().equals(PhaseType.END_OF_TURN))) {
|
||||
return false;
|
||||
} else if ("PoolExtraMana".equals(aiLogic)) {
|
||||
return doPoolExtraManaLogic(ai, sa);
|
||||
}
|
||||
|
||||
return !("Never".equals(aiLogic));
|
||||
@@ -132,7 +139,7 @@ public class UntapAi extends SpellAbilityAi {
|
||||
Player targetController = ai;
|
||||
|
||||
if (sa.isCurse()) {
|
||||
targetController = ai.getOpponent();
|
||||
targetController = ComputerUtil.getOpponentFor(ai);
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -142,18 +149,50 @@ public class UntapAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection untapList = CardLists.filter(list, Presets.TAPPED);
|
||||
// For some abilities, it may be worth to target even an untapped card if we're targeting mostly for the subability
|
||||
boolean targetUntapped = false;
|
||||
if (sa.getSubAbility() != null) {
|
||||
SpellAbility subSa = sa.getSubAbility();
|
||||
if (subSa.getApi() == ApiType.RemoveFromCombat && "RemoveBestAttacker".equals(subSa.getParam("AILogic"))) {
|
||||
targetUntapped = true;
|
||||
}
|
||||
}
|
||||
|
||||
CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED);
|
||||
// filter out enchantments and planeswalkers, their tapped state doesn't
|
||||
// matter.
|
||||
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
|
||||
untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa);
|
||||
|
||||
// Try to avoid potential infinite recursion,
|
||||
// e.g. Kiora's Follower untapping another Kiora's Follower and repeating infinitely
|
||||
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostTap.class)) {
|
||||
CardCollection toRemove = new CardCollection();
|
||||
for (Card c : untapList) {
|
||||
for (SpellAbility ab : c.getAllSpellAbilities()) {
|
||||
if (ab.getApi() == ApiType.Untap
|
||||
&& ab.getPayCosts() != null
|
||||
&& ab.getPayCosts().hasOnlySpecificCostType(CostTap.class)
|
||||
&& ab.canTarget(source)) {
|
||||
toRemove.add(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
untapList.removeAll(toRemove);
|
||||
}
|
||||
|
||||
//try to exclude things that will already be untapped due to something on stack or because something is
|
||||
//already targeted in a parent or sub SA
|
||||
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, untapList, sa, ApiType.Untap);
|
||||
untapList.removeAll(toExclude);
|
||||
|
||||
sa.resetTargets();
|
||||
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
|
||||
Card choice = null;
|
||||
|
||||
if (untapList.isEmpty()) {
|
||||
// Animate untapped lands (Koth of the Hamer)
|
||||
// Animate untapped lands (Koth of the Hammer)
|
||||
if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.Animate && !list.isEmpty()
|
||||
&& ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
choice = ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false);
|
||||
@@ -165,17 +204,13 @@ public class UntapAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
//Untap Time Vault? - Yes please!
|
||||
for (Card c : untapList) {
|
||||
if (c.getName().equals("Time Vault")) {
|
||||
choice = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
choice = detectPriorityUntapTargets(untapList);
|
||||
|
||||
if (choice == null) {
|
||||
if (CardLists.getNotType(untapList, "Creature").isEmpty()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(untapList); // if only creatures take the best
|
||||
} else if (!sa.getPayCosts().hasManaCost() || sa.getRootAbility().isTrigger()) {
|
||||
} else if (!sa.getPayCosts().hasManaCost() || sa.getRootAbility().isTrigger()
|
||||
|| "Always".equals(sa.getParam("AILogic"))) {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(untapList, sa, false);
|
||||
}
|
||||
}
|
||||
@@ -300,4 +335,116 @@ public class UntapAi extends SpellAbilityAi {
|
||||
pl.addAll(ai.getAllies());
|
||||
return ComputerUtilCard.getBestAI(CardLists.filterControlledBy(list, pl));
|
||||
}
|
||||
|
||||
private static Card detectPriorityUntapTargets(final List<Card> untapList) {
|
||||
// untap Time Vault or another broken card? - Yes please!
|
||||
String[] priorityList = {"Time Vault", "Mana Vault", "Icy Manipulator", "Steel Overseer", "Grindclock", "Prototype Portal"};
|
||||
for (String name : priorityList) {
|
||||
for (Card c : untapList) {
|
||||
if (c.getName().equals(name)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, CardPredicates.hasKeyword("CARDNAME doesn't untap during your untap step."));
|
||||
if (!noAutoUntap.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(noAutoUntap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean doPoolExtraManaLogic(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final PhaseHandler ph = source.getGame().getPhaseHandler();
|
||||
final Game game = ai.getGame();
|
||||
|
||||
if (sa.getHostCard().isTapped()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if something is playable if we untap for an additional mana with this, then proceed
|
||||
CardCollection inHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
|
||||
// The AI is not very good at timing non-permanent spells this way, so filter them out
|
||||
// (it may actually be possible to enable this for sorceries, but that'll need some canPlay shenanigans)
|
||||
CardCollection playable = CardLists.filter(inHand, Presets.PERMANENTS);
|
||||
|
||||
CardCollection untappingCards = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
boolean hasUntapLandLogic = false;
|
||||
for (SpellAbility sa : card.getSpellAbilities()) {
|
||||
if ("PoolExtraMana".equals(sa.getParam("AILogic"))) {
|
||||
hasUntapLandLogic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasUntapLandLogic && card.isUntapped();
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: currently limited to Main 2, somehow improve to let the AI use this SA at other time?
|
||||
if (ph.is(PhaseType.MAIN2, ai)) {
|
||||
for (Card c : playable) {
|
||||
for (SpellAbility ab : c.getBasicSpells()) {
|
||||
if (!ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) {
|
||||
// TODO: Currently limited to predicting something that can be paid with any color,
|
||||
// can ideally be improved to work by color.
|
||||
ManaCostBeingPaid reduced = new ManaCostBeingPaid(ab.getPayCosts().getCostMana().getManaCostFor(ab), ab.getPayCosts().getCostMana().getRestiction());
|
||||
reduced.decreaseShard(ManaCostShard.GENERIC, untappingCards.size());
|
||||
if (ComputerUtilMana.canPayManaCost(reduced, ab, ai)) {
|
||||
CardCollection manaLandsTapped = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
Predicates.and(Presets.LANDS_PRODUCING_MANA, Presets.TAPPED));
|
||||
manaLandsTapped = CardLists.filter(manaLandsTapped, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return card.isValid(sa.getParam("ValidTgts"), ai, source, null);
|
||||
}
|
||||
});
|
||||
|
||||
if (!manaLandsTapped.isEmpty()) {
|
||||
// already have a tapped land, so agree to proceed with untapping it
|
||||
return true;
|
||||
}
|
||||
|
||||
// pool one additional mana by tapping a land to try to ramp to something
|
||||
CardCollection manaLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
Predicates.and(Presets.LANDS_PRODUCING_MANA, Presets.UNTAPPED));
|
||||
manaLands = CardLists.filter(manaLands, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
return card.isValid(sa.getParam("ValidTgts"), ai, source, null);
|
||||
}
|
||||
});
|
||||
|
||||
if (manaLands.isEmpty()) {
|
||||
// nothing to untap
|
||||
return false;
|
||||
}
|
||||
|
||||
Card landToPool = manaLands.getFirst();
|
||||
SpellAbility manaAb = landToPool.getManaAbilities().getFirst();
|
||||
|
||||
ComputerUtil.playNoStack(ai, manaAb, game);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no harm in doing this past declare blockers during the opponent's turn and right before our turn,
|
||||
// maybe we'll serendipitously untap into something like a removal spell or burn spell that'll help
|
||||
if (ph.getNextTurn() == ai
|
||||
&& (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// haven't found any immediate playable options
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user