mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 03:08:02 +00:00
Compare commits
908 Commits
forge-1.6.
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
646
.gitattributes
vendored
646
.gitattributes
vendored
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>forge</artifactId>
|
<artifactId>forge</artifactId>
|
||||||
<groupId>forge</groupId>
|
<groupId>forge</groupId>
|
||||||
<version>1.6.1</version>
|
<version>1.6.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>forge-ai</artifactId>
|
<artifactId>forge-ai</artifactId>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package forge;
|
package forge.ai;
|
||||||
|
|
||||||
public enum AIOption {
|
public enum AIOption {
|
||||||
USE_SIMULATION;
|
USE_SIMULATION;
|
||||||
@@ -17,14 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
import forge.card.CardTypeView;
|
import forge.card.CardTypeView;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
@@ -40,9 +35,14 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.Expressions;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
|
||||||
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
|
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
|
||||||
/**
|
/**
|
||||||
@@ -243,6 +243,19 @@ public class AiAttackController {
|
|||||||
return false;
|
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 checks to make sure that the computer player doesn't lose when the human player attacks
|
||||||
// this method is used by getAttackers()
|
// this method is used by getAttackers()
|
||||||
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
|
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
|
||||||
@@ -362,8 +375,12 @@ public class AiAttackController {
|
|||||||
blockersLeft--;
|
blockersLeft--;
|
||||||
continue;
|
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()) {
|
if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) {
|
||||||
@@ -424,13 +441,42 @@ public class AiAttackController {
|
|||||||
|
|
||||||
final Player opp = this.defendingOpponent;
|
final Player opp = this.defendingOpponent;
|
||||||
|
|
||||||
for (Card attacker : attackers) {
|
// if true, the AI will attempt to identify which blockers will already be taken,
|
||||||
if (!CombatUtil.canBeBlocked(attacker, this.blockers, null)
|
// thus attempting to predict how many creatures with evasion can actively block
|
||||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
boolean predictEvasion = false;
|
||||||
unblockedAttackers.add(attacker);
|
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) {
|
for (Card blocker : this.blockers) {
|
||||||
if (blocker.hasKeyword("CARDNAME can block any number of creatures.")
|
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.")) {
|
||||||
@@ -498,11 +544,16 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0));
|
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) {
|
if (null != entity) {
|
||||||
int n = defs.indexOf(entity);
|
int n = defs.indexOf(entity);
|
||||||
if (-1 == n) {
|
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;
|
return prefDefender;
|
||||||
} else {
|
} else {
|
||||||
return entity;
|
return entity;
|
||||||
@@ -544,10 +595,21 @@ public class AiAttackController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aggro options
|
||||||
boolean playAggro = false;
|
boolean playAggro = false;
|
||||||
|
int chanceToAttackToTrade = 0;
|
||||||
|
boolean tradeIfTappedOut = false;
|
||||||
|
int extraChanceIfOppHasMana = 0;
|
||||||
|
boolean tradeIfLowerLifePressure = false;
|
||||||
if (ai.getController().isAI()) {
|
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);
|
final boolean bAssault = this.doAssault(ai);
|
||||||
// TODO: detect Lightmine Field by presence of a card with a specific trigger
|
// TODO: detect Lightmine Field by presence of a card with a specific trigger
|
||||||
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
|
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
|
||||||
@@ -558,7 +620,11 @@ public class AiAttackController {
|
|||||||
|
|
||||||
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
||||||
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
|
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) {
|
if (attackMax == 0) {
|
||||||
// can't attack anymore
|
// can't attack anymore
|
||||||
@@ -590,7 +656,7 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mustAttack || attacker.getController().getMustAttackEntity() != null) {
|
if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) {
|
||||||
combat.addAttacker(attacker, defender);
|
combat.addAttacker(attacker, defender);
|
||||||
attackersLeft.remove(attacker);
|
attackersLeft.remove(attacker);
|
||||||
numForcedAttackers++;
|
numForcedAttackers++;
|
||||||
@@ -711,7 +777,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 the creature can attack next turn add it to counter attackers list
|
||||||
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
|
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
|
||||||
nextTurnAttackers.add(pCard);
|
nextTurnAttackers.add(pCard);
|
||||||
@@ -719,8 +798,13 @@ public class AiAttackController {
|
|||||||
humanForces += 1; // player forces they might use to attack
|
humanForces += 1; // player forces they might use to attack
|
||||||
}
|
}
|
||||||
// increment player forces that are relevant to an attritional attack - includes walls
|
// 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;
|
humanForcesForAttritionalAttack += 1;
|
||||||
|
if (predictEvasion) {
|
||||||
|
candidateAttackers.remove(potentialOppBlocker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -847,8 +931,18 @@ public class AiAttackController {
|
|||||||
if (ratioDiff > 0 && doAttritionalAttack) {
|
if (ratioDiff > 0 && doAttritionalAttack) {
|
||||||
this.aiAggression = 5; // attack at all costs
|
this.aiAggression = 5; // attack at all costs
|
||||||
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|
} 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.
|
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) {
|
} else if (ratioDiff >= 0 && this.attackers.size() > 1) {
|
||||||
this.aiAggression = 3; // attack expecting to make good trades or damage player.
|
this.aiAggression = 3; // attack expecting to make good trades or damage player.
|
||||||
} else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
|
} else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
|
||||||
@@ -996,7 +1090,8 @@ public class AiAttackController {
|
|||||||
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
||||||
if (!hasCombatEffect) {
|
if (!hasCombatEffect) {
|
||||||
for (String keyword : attacker.getKeywords()) {
|
for (String keyword : attacker.getKeywords()) {
|
||||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
if (keyword.equals("Wither") || keyword.equals("Infect")
|
||||||
|
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
|
||||||
hasCombatEffect = true;
|
hasCombatEffect = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1037,12 +1132,28 @@ public class AiAttackController {
|
|||||||
// and combat will have negative effects
|
// 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;
|
canKillAllDangerous = false;
|
||||||
canBeKilled = true;
|
canBeKilled = true;
|
||||||
canBeKilledByOne = true;
|
canBeKilledByOne = true;
|
||||||
@@ -1147,18 +1258,51 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
sa.setActivatingPlayer(c.getController());
|
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;
|
missTarget = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missTarget) {
|
if (missTarget) {
|
||||||
continue;
|
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
|
// TODO Improve when the AI wants to use Exert powers
|
||||||
shouldExert = true;
|
shouldExert = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,26 +19,20 @@ package forge.ai;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
|
import forge.card.CardStateName;
|
||||||
import forge.game.CardTraitBase;
|
import forge.game.CardTraitBase;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,8 +83,10 @@ public class AiBlockController {
|
|||||||
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
||||||
final List<Card> blockers = new ArrayList<>();
|
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) {
|
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);
|
blockers.add(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,8 +98,10 @@ public class AiBlockController {
|
|||||||
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
|
||||||
final List<Card> blockers = new ArrayList<>();
|
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) {
|
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);
|
blockers.add(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +149,7 @@ public class AiBlockController {
|
|||||||
for (final Card c : attackers) {
|
for (final Card c : attackers) {
|
||||||
sortedAttackers.add(c);
|
sortedAttackers.add(c);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (defender instanceof Player && defender.equals(ai)){
|
||||||
firstAttacker = combat.getAttackersOf(defender);
|
firstAttacker = combat.getAttackersOf(defender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,9 +347,9 @@ public class AiBlockController {
|
|||||||
final List<Card> firstStrikeBlockers = new ArrayList<>();
|
final List<Card> firstStrikeBlockers = new ArrayList<>();
|
||||||
final List<Card> blockGang = new ArrayList<>();
|
final List<Card> blockGang = new ArrayList<>();
|
||||||
for (Card blocker : blockers) {
|
for (Card blocker : blockers) {
|
||||||
if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) {
|
if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (blocker.hasFirstStrike() || blocker.hasDoubleStrike()) {
|
if (blocker.hasFirstStrike() || blocker.hasDoubleStrike()) {
|
||||||
firstStrikeBlockers.add(blocker);
|
firstStrikeBlockers.add(blocker);
|
||||||
}
|
}
|
||||||
@@ -445,9 +443,9 @@ public class AiBlockController {
|
|||||||
// The attacker will be killed
|
// The attacker will be killed
|
||||||
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
|
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
|
||||||
// only one blocker can be killed
|
// only one blocker can be killed
|
||||||
|| currentValue + addedValue - 50 <= evalAttackerValue
|
|| currentValue + addedValue - 50 <= evalAttackerValue
|
||||||
// or attacker is worth more
|
// or attacker is worth more
|
||||||
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
|
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
|
||||||
// or life is in danger
|
// or life is in danger
|
||||||
&& CombatUtil.canBlock(attacker, blocker, combat)) {
|
&& CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||||
// this is needed for attackers that can't be blocked by
|
// this is needed for attackers that can't be blocked by
|
||||||
@@ -497,7 +495,7 @@ public class AiBlockController {
|
|||||||
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
|
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
|
||||||
// The attacker will be killed
|
// The attacker will be killed
|
||||||
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
|
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
|
||||||
&& absorbedDamage3 + absorbedDamage2 > netCombatDamage)
|
&& absorbedDamage3 + absorbedDamage2 > netCombatDamage)
|
||||||
// only one blocker can be killed
|
// only one blocker can be killed
|
||||||
|| currentValue + addedValue2 + addedValue3 - 50 <= evalAttackerValue
|
|| currentValue + addedValue2 + addedValue3 - 50 <= evalAttackerValue
|
||||||
// or attacker is worth more
|
// or attacker is worth more
|
||||||
@@ -526,7 +524,56 @@ public class AiBlockController {
|
|||||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
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)
|
// 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>
|
* <p>
|
||||||
* makeTradeBlocks.
|
* makeTradeBlocks.
|
||||||
@@ -539,6 +586,39 @@ public class AiBlockController {
|
|||||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||||
List<Card> killingBlockers;
|
List<Card> killingBlockers;
|
||||||
|
|
||||||
|
// Parameters related to randomly trading when blocking (need to be enabled in the AI profile)
|
||||||
|
boolean enableRandomTrades = false;
|
||||||
|
boolean randomTradeIfBehindOnBoard = false;
|
||||||
|
boolean randomTradeIfCreatInHand = false;
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
aiCreatureCount = ComputerUtil.countUsefulCreatures(ai);
|
||||||
|
|
||||||
|
if (!attackersLeft.isEmpty()) {
|
||||||
|
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (final Card attacker : attackersLeft) {
|
for (final Card attacker : attackersLeft) {
|
||||||
|
|
||||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||||
@@ -546,16 +626,67 @@ public class AiBlockController {
|
|||||||
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
|
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||||
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
|
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
|
||||||
if (!killingBlockers.isEmpty() && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
|
||||||
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
if (!killingBlockers.isEmpty()) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final Card blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers);
|
final Card blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers);
|
||||||
combat.addBlocker(attacker, blocker);
|
boolean doTrade = false;
|
||||||
currentAttackers.remove(attacker);
|
|
||||||
|
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
|
// Always trade when life in danger
|
||||||
|
doTrade = true;
|
||||||
|
} else if (enableRandomTrades) {
|
||||||
|
// Randomly trade creatures with lower power and [hopefully] worse abilities, if enabled in profile
|
||||||
|
|
||||||
|
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
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, false, false);
|
||||||
|
int evalBlk = ComputerUtilCard.evaluateCreature(blocker, false, false);
|
||||||
|
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)) {
|
||||||
|
doTrade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doTrade) {
|
||||||
|
combat.addBlocker(attacker, blocker);
|
||||||
|
currentAttackers.remove(attacker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||||
@@ -760,6 +891,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) {
|
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
|
||||||
|
|
||||||
final List<Card> oldBlockers = combat.getAllBlockers();
|
final List<Card> oldBlockers = combat.getAllBlockers();
|
||||||
@@ -824,7 +1054,7 @@ public class AiBlockController {
|
|||||||
List<Card> chumpBlockers;
|
List<Card> chumpBlockers;
|
||||||
|
|
||||||
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
|
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;
|
diff = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -856,9 +1086,8 @@ public class AiBlockController {
|
|||||||
// When the AI holds some Fog effect, don't bother about lifeInDanger
|
// When the AI holds some Fog effect, don't bother about lifeInDanger
|
||||||
if (!ComputerUtil.hasAFogEffect(ai)) {
|
if (!ComputerUtil.hasAFogEffect(ai)) {
|
||||||
lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
|
lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
|
||||||
if (lifeInDanger) {
|
makeTradeBlocks(combat); // choose necessary trade blocks
|
||||||
makeTradeBlocks(combat); // choose necessary trade blocks
|
|
||||||
}
|
|
||||||
// if life is still in danger
|
// if life is still in danger
|
||||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
makeChumpBlocks(combat); // choose necessary chump blocks
|
makeChumpBlocks(combat); // choose necessary chump blocks
|
||||||
@@ -956,6 +1185,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
|
//Check for validity of blocks in case something slipped through
|
||||||
for (Card attacker : attackers) {
|
for (Card attacker : attackers) {
|
||||||
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
|
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
|
||||||
|
|||||||
@@ -41,17 +41,23 @@ import java.util.Set;
|
|||||||
public class AiCardMemory {
|
public class AiCardMemory {
|
||||||
|
|
||||||
private final Set<Card> memMandatoryAttackers;
|
private final Set<Card> memMandatoryAttackers;
|
||||||
|
private final Set<Card> memTrickAttackers;
|
||||||
private final Set<Card> memHeldManaSources;
|
private final Set<Card> memHeldManaSources;
|
||||||
|
private final Set<Card> memHeldManaSourcesForCombat;
|
||||||
private final Set<Card> memAttachedThisTurn;
|
private final Set<Card> memAttachedThisTurn;
|
||||||
private final Set<Card> memAnimatedThisTurn;
|
private final Set<Card> memAnimatedThisTurn;
|
||||||
private final Set<Card> memBouncedThisTurn;
|
private final Set<Card> memBouncedThisTurn;
|
||||||
|
private final Set<Card> memActivatedThisTurn;
|
||||||
|
|
||||||
public AiCardMemory() {
|
public AiCardMemory() {
|
||||||
this.memMandatoryAttackers = new HashSet<>();
|
this.memMandatoryAttackers = new HashSet<>();
|
||||||
this.memHeldManaSources = new HashSet<>();
|
this.memHeldManaSources = new HashSet<>();
|
||||||
|
this.memHeldManaSourcesForCombat = new HashSet<>();
|
||||||
this.memAttachedThisTurn = new HashSet<>();
|
this.memAttachedThisTurn = new HashSet<>();
|
||||||
this.memAnimatedThisTurn = new HashSet<>();
|
this.memAnimatedThisTurn = new HashSet<>();
|
||||||
this.memBouncedThisTurn = new HashSet<>();
|
this.memBouncedThisTurn = new HashSet<>();
|
||||||
|
this.memActivatedThisTurn = new HashSet<>();
|
||||||
|
this.memTrickAttackers = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,10 +67,13 @@ public class AiCardMemory {
|
|||||||
*/
|
*/
|
||||||
public enum MemorySet {
|
public enum MemorySet {
|
||||||
MANDATORY_ATTACKERS,
|
MANDATORY_ATTACKERS,
|
||||||
HELD_MANA_SOURCES,
|
TRICK_ATTACKERS,
|
||||||
|
HELD_MANA_SOURCES_FOR_MAIN2,
|
||||||
|
HELD_MANA_SOURCES_FOR_DECLBLK,
|
||||||
ATTACHED_THIS_TURN,
|
ATTACHED_THIS_TURN,
|
||||||
ANIMATED_THIS_TURN,
|
ANIMATED_THIS_TURN,
|
||||||
BOUNCED_THIS_TURN,
|
BOUNCED_THIS_TURN,
|
||||||
|
ACTIVATED_THIS_TURN,
|
||||||
//REVEALED_CARDS // stub, not linked to AI code yet
|
//REVEALED_CARDS // stub, not linked to AI code yet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +81,20 @@ public class AiCardMemory {
|
|||||||
switch (set) {
|
switch (set) {
|
||||||
case MANDATORY_ATTACKERS:
|
case MANDATORY_ATTACKERS:
|
||||||
return memMandatoryAttackers;
|
return memMandatoryAttackers;
|
||||||
case HELD_MANA_SOURCES:
|
case TRICK_ATTACKERS:
|
||||||
|
return memTrickAttackers;
|
||||||
|
case HELD_MANA_SOURCES_FOR_MAIN2:
|
||||||
return memHeldManaSources;
|
return memHeldManaSources;
|
||||||
|
case HELD_MANA_SOURCES_FOR_DECLBLK:
|
||||||
|
return memHeldManaSourcesForCombat;
|
||||||
case ATTACHED_THIS_TURN:
|
case ATTACHED_THIS_TURN:
|
||||||
return memAttachedThisTurn;
|
return memAttachedThisTurn;
|
||||||
case ANIMATED_THIS_TURN:
|
case ANIMATED_THIS_TURN:
|
||||||
return memAnimatedThisTurn;
|
return memAnimatedThisTurn;
|
||||||
case BOUNCED_THIS_TURN:
|
case BOUNCED_THIS_TURN:
|
||||||
return memBouncedThisTurn;
|
return memBouncedThisTurn;
|
||||||
|
case ACTIVATED_THIS_TURN:
|
||||||
|
return memActivatedThisTurn;
|
||||||
//case REVEALED_CARDS:
|
//case REVEALED_CARDS:
|
||||||
// return memRevealedCards;
|
// return memRevealedCards;
|
||||||
default:
|
default:
|
||||||
@@ -249,10 +264,13 @@ public class AiCardMemory {
|
|||||||
*/
|
*/
|
||||||
public void clearAllRemembered() {
|
public void clearAllRemembered() {
|
||||||
clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
|
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.ATTACHED_THIS_TURN);
|
||||||
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
|
clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
|
||||||
clearMemorySet(MemorySet.BOUNCED_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.
|
// 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) {
|
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
|
||||||
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, 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) {
|
public static void clearMemorySet(Player ai, MemorySet set) {
|
||||||
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
|
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,6 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
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.esotericsoftware.minlog.Log;
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
@@ -32,7 +24,6 @@ import com.google.common.base.Predicates;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.ai.ability.ChangeZoneAi;
|
import forge.ai.ability.ChangeZoneAi;
|
||||||
import forge.ai.simulation.SpellAbilityPicker;
|
import forge.ai.simulation.SpellAbilityPicker;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
@@ -40,44 +31,23 @@ import forge.card.mana.ManaCost;
|
|||||||
import forge.deck.CardPool;
|
import forge.deck.CardPool;
|
||||||
import forge.deck.Deck;
|
import forge.deck.Deck;
|
||||||
import forge.deck.DeckSection;
|
import forge.deck.DeckSection;
|
||||||
import forge.game.CardTraitBase;
|
import forge.game.*;
|
||||||
import forge.game.CardTraitPredicates;
|
|
||||||
import forge.game.Direction;
|
|
||||||
import forge.game.Game;
|
|
||||||
import forge.game.GameEntity;
|
|
||||||
import forge.game.GlobalRuleChange;
|
|
||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.ability.SpellApiBased;
|
import forge.game.ability.SpellApiBased;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.*;
|
||||||
import forge.game.cost.CostDiscard;
|
|
||||||
import forge.game.cost.CostPart;
|
|
||||||
import forge.game.cost.CostPutCounter;
|
|
||||||
import forge.game.cost.CostRemoveCounter;
|
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.replacement.ReplaceMoved;
|
import forge.game.replacement.ReplaceMoved;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
import forge.game.spellability.AbilityManaPart;
|
import forge.game.spellability.*;
|
||||||
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.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.trigger.WrappedAbility;
|
import forge.game.trigger.WrappedAbility;
|
||||||
@@ -88,6 +58,10 @@ import forge.util.Expressions;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
|
||||||
|
import java.security.InvalidParameterException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* AiController class.
|
* AiController class.
|
||||||
@@ -605,11 +579,32 @@ public class AiController {
|
|||||||
return null;
|
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);
|
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
|
||||||
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
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) {
|
for (Card c : manaSources) {
|
||||||
AiCardMemory.rememberCard(player, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES);
|
AiCardMemory.rememberCard(player, c, memSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,12 +719,31 @@ public class AiController {
|
|||||||
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
|
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
|
||||||
|
|
||||||
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
// 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;
|
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;
|
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)
|
// cast 0 mana cost spells first (might be a Mox)
|
||||||
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
|
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -737,9 +751,9 @@ public class AiController {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.getHostCard().hasSVar("FreeSpellAI")) {
|
if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||||
return -1;
|
return -1;
|
||||||
} else if (b.getHostCard().hasSVar("FreeSpellAI")) {
|
} else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,36 +766,48 @@ public class AiController {
|
|||||||
private int getSpellAbilityPriority(SpellAbility sa) {
|
private int getSpellAbilityPriority(SpellAbility sa) {
|
||||||
int p = 0;
|
int p = 0;
|
||||||
Card source = sa.getHostCard();
|
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();
|
final boolean noCreatures = ai.getCreaturesInPlay().isEmpty();
|
||||||
// puts creatures in front of spells
|
|
||||||
if (source.isCreature()) {
|
if (source != null) {
|
||||||
p += 1;
|
// puts creatures in front of spells
|
||||||
}
|
if (source.isCreature()) {
|
||||||
// don't play equipments before having any creatures
|
p += 1;
|
||||||
if (source.isEquipment() && noCreatures) {
|
}
|
||||||
p -= 9;
|
// don't play equipments before having any creatures
|
||||||
|
if (source.isEquipment() && noCreatures) {
|
||||||
|
p -= 9;
|
||||||
|
}
|
||||||
|
// 1. increase chance of using Surge effects
|
||||||
|
// 2. non-surged versions are usually inefficient
|
||||||
|
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) {
|
||||||
|
p += 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// artifacts and enchantments with effects that do not stack
|
||||||
|
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
|
||||||
|
p -= 9;
|
||||||
|
}
|
||||||
|
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||||
|
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
|
||||||
|
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use Surge and Prowl costs when able to
|
// use Surge and Prowl costs when able to
|
||||||
if (sa.isSurged() ||
|
if (sa.isSurged() ||
|
||||||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
||||||
p += 9;
|
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()) {
|
|
||||||
p -= 9;
|
|
||||||
}
|
|
||||||
// move snap-casted spells to front
|
|
||||||
if (source.isInZone(ZoneType.Graveyard)) {
|
|
||||||
if(sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
|
|
||||||
p += 50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// artifacts and enchantments with effects that do not stack
|
|
||||||
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
|
|
||||||
p -= 9;
|
|
||||||
}
|
|
||||||
// sort planeswalker abilities with most costly first
|
// sort planeswalker abilities with most costly first
|
||||||
if (sa.getRestrictions().isPwAbility()) {
|
if (sa.getRestrictions().isPwAbility()) {
|
||||||
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
||||||
@@ -801,11 +827,6 @@ public class AiController {
|
|||||||
p -= 9;
|
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
|
// try to cast mana ritual spells before casting spells to maximize potential mana
|
||||||
if ("ManaRitual".equals(sa.getParam("AILogic"))) {
|
if ("ManaRitual".equals(sa.getParam("AILogic"))) {
|
||||||
p += 9;
|
p += 9;
|
||||||
@@ -840,6 +861,8 @@ public class AiController {
|
|||||||
sourceCard = sa.getHostCard();
|
sourceCard = sa.getHostCard();
|
||||||
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) {
|
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) {
|
||||||
min = 1;
|
min = 1;
|
||||||
|
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
|
||||||
|
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,6 +881,9 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
if (prefCard == null) {
|
if (prefCard == null) {
|
||||||
prefCard = ComputerUtil.getCardPreference(player, sourceCard, "DiscardCost", validCards);
|
prefCard = ComputerUtil.getCardPreference(player, sourceCard, "DiscardCost", validCards);
|
||||||
|
if (prefCard != null && prefCard.hasSVar("DoNotDiscardIfAble")) {
|
||||||
|
prefCard = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (prefCard != null) {
|
if (prefCard != null) {
|
||||||
discardList.add(prefCard);
|
discardList.add(prefCard);
|
||||||
@@ -894,13 +920,29 @@ public class AiController {
|
|||||||
if (numLandsInHand > 0) {
|
if (numLandsInHand > 0) {
|
||||||
numLandsAvailable++;
|
numLandsAvailable++;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Discard unplayable card
|
//Discard unplayable card
|
||||||
if (validCards.get(0).getCMC() > numLandsAvailable) {
|
boolean discardedUnplayable = false;
|
||||||
discardList.add(validCards.get(0));
|
for (int j = 0; j < validCards.size(); j++) {
|
||||||
validCards.remove(validCards.get(0));
|
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);
|
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);
|
||||||
|
}
|
||||||
discardList.add(worst);
|
discardList.add(worst);
|
||||||
validCards.remove(worst);
|
validCards.remove(worst);
|
||||||
}
|
}
|
||||||
@@ -1059,6 +1101,20 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
|
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) {
|
if (landsWannaPlay != null) {
|
||||||
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
|
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
|
||||||
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
|
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
|
||||||
@@ -1066,10 +1122,12 @@ public class AiController {
|
|||||||
Card land = chooseBestLandToPlay(landsWannaPlay);
|
Card land = chooseBestLandToPlay(landsWannaPlay);
|
||||||
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|
||||||
|| player.cantLoseForZeroOrLessLife() ) {
|
|| player.cantLoseForZeroOrLessLife() ) {
|
||||||
game.PLAY_LAND_SURROGATE.setHostCard(land);
|
if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
|
||||||
final List<SpellAbility> abilities = Lists.newArrayList();
|
game.PLAY_LAND_SURROGATE.setHostCard(land);
|
||||||
abilities.add(game.PLAY_LAND_SURROGATE);
|
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||||
return abilities;
|
abilities.add(game.PLAY_LAND_SURROGATE);
|
||||||
|
return abilities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1135,103 @@ public class AiController {
|
|||||||
return singleSpellAbilityList(getSpellAbilityToPlay());
|
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);
|
||||||
|
|
||||||
|
// 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() {
|
private final SpellAbility getSpellAbilityToPlay() {
|
||||||
// if top of stack is owned by me
|
// if top of stack is owned by me
|
||||||
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
|
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
|
||||||
@@ -1306,7 +1461,7 @@ public class AiController {
|
|||||||
+ MyRandom.getRandom().nextInt(3);
|
+ MyRandom.getRandom().nextInt(3);
|
||||||
return Math.max(remaining, min) / 2;
|
return Math.max(remaining, min) / 2;
|
||||||
} else if ("LowestLoseLife".equals(logic)) {
|
} 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)) {
|
} else if ("HighestGetCounter".equals(logic)) {
|
||||||
return MyRandom.getRandom().nextInt(3);
|
return MyRandom.getRandom().nextInt(3);
|
||||||
} else if (source.hasSVar("EnergyToPay")) {
|
} else if (source.hasSVar("EnergyToPay")) {
|
||||||
@@ -1428,6 +1583,24 @@ public class AiController {
|
|||||||
} else {
|
} else {
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
CardCollectionView totap;
|
CardCollectionView totap;
|
||||||
if (isVehicle) {
|
if (isVehicle) {
|
||||||
totalP = type.split("withTotalPowerGE")[1];
|
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);
|
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped);
|
||||||
} else {
|
} else {
|
||||||
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
|
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import forge.LobbyPlayer;
|
|||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
import forge.util.FileUtil;
|
import forge.util.FileUtil;
|
||||||
|
|
||||||
|
import forge.util.TextUtil;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -52,7 +53,7 @@ public class AiProfileUtil {
|
|||||||
* @return the full relative path and file name for the given profile.
|
* @return the full relative path and file name for the given profile.
|
||||||
*/
|
*/
|
||||||
private static String buildFileName(final String profileName) {
|
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,45 @@ public enum AiProps { /** */
|
|||||||
DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */
|
DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */
|
||||||
MULLIGAN_THRESHOLD ("5"), /** */
|
MULLIGAN_THRESHOLD ("5"), /** */
|
||||||
PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"),
|
PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"),
|
||||||
|
HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED ("0"), /** */
|
||||||
CHEAT_WITH_MANA_ON_SHUFFLE ("false"),
|
CHEAT_WITH_MANA_ON_SHUFFLE ("false"),
|
||||||
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
|
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
|
||||||
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
|
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
|
||||||
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
|
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
|
||||||
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
|
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
|
||||||
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
|
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
|
||||||
PLAY_AGGRO ("false"), /** */
|
PLAY_AGGRO ("false"),
|
||||||
MIN_SPELL_CMC_TO_COUNTER ("0"),
|
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_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_OTHER_COUNTERSPELLS ("true"), /** */
|
||||||
ALWAYS_COUNTER_DAMAGE_SPELLS ("true"), /** */
|
ALWAYS_COUNTER_DAMAGE_SPELLS ("true"), /** */
|
||||||
ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS ("true"), /** */
|
ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS ("true"), /** */
|
||||||
ALWAYS_COUNTER_REMOVAL_SPELLS ("true"), /** */
|
ALWAYS_COUNTER_REMOVAL_SPELLS ("true"), /** */
|
||||||
|
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
|
||||||
|
ALWAYS_COUNTER_AURAS ("true"), /** */
|
||||||
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
|
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
|
||||||
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("false"), /** */
|
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
|
||||||
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
||||||
USE_BERSERK_AGGRESSIVELY ("false"), /** */
|
USE_BERSERK_AGGRESSIVELY ("false"), /** */
|
||||||
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
|
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
|
||||||
@@ -50,7 +75,29 @@ public enum AiProps { /** */
|
|||||||
STRIPMINE_MIN_LANDS_FOR_NO_TIMING_CHECK ("3"), /** */
|
STRIPMINE_MIN_LANDS_FOR_NO_TIMING_CHECK ("3"), /** */
|
||||||
STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK ("6"), /** */
|
STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK ("6"), /** */
|
||||||
STRIPMINE_MAX_LANDS_TO_ATTEMPT_MANALOCKING ("3"), /** */
|
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"); /** */
|
||||||
|
// Experimental features, must be removed after extensive testing and, ideally, defaulting
|
||||||
|
// <-- There are no experimental options here -->
|
||||||
|
|
||||||
private final String strDefaultVal;
|
private final String strDefaultVal;
|
||||||
|
|
||||||
|
|||||||
@@ -17,23 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
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.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.*;
|
||||||
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 forge.ai.ability.ProtectAi;
|
import forge.ai.ability.ProtectAi;
|
||||||
import forge.ai.ability.TokenAi;
|
import forge.ai.ability.TokenAi;
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
@@ -46,32 +32,17 @@ import forge.game.ability.AbilityFactory;
|
|||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.ability.effects.CharmEffect;
|
import forge.game.ability.effects.CharmEffect;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.CardPredicates.Presets;
|
||||||
import forge.game.card.CardUtil;
|
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.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.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
import forge.game.replacement.ReplacementLayer;
|
import forge.game.replacement.ReplacementLayer;
|
||||||
import forge.game.spellability.AbilityManaPart;
|
import forge.game.spellability.*;
|
||||||
import forge.game.spellability.AbilitySub;
|
|
||||||
import forge.game.spellability.SpellAbility;
|
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
|
||||||
import forge.game.spellability.TargetRestrictions;
|
|
||||||
import forge.game.staticability.StaticAbility;
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
@@ -79,7 +50,11 @@ import forge.game.zone.Zone;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
import forge.util.TextUtil;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,7 +167,7 @@ public class ComputerUtil {
|
|||||||
if (unless != null && !unless.endsWith(">")) {
|
if (unless != null && !unless.endsWith(">")) {
|
||||||
final int amount = AbilityUtils.calculateAmount(source, unless, sa);
|
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 the Unless isn't enough, this should be less likely to be used
|
||||||
if (amount > usableManaSources) {
|
if (amount > usableManaSources) {
|
||||||
@@ -310,11 +285,13 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
|
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
|
String prefDef = "";
|
||||||
if (activate != null) {
|
if (activate != null) {
|
||||||
|
prefDef = activate.getSVar("AIPreference");
|
||||||
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
|
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
|
||||||
for (String prefGroup : prefGroups) {
|
for (String prefGroup : prefGroups) {
|
||||||
final String[] prefValid = prefGroup.trim().split("\\$");
|
final String[] prefValid = prefGroup.trim().split("\\$");
|
||||||
if (prefValid[0].equals(pref)) {
|
if (prefValid[0].equals(pref) && !prefValid[1].startsWith("Special:")) {
|
||||||
final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","), activate.getController(), activate, null);
|
final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","), activate.getController(), activate, null);
|
||||||
CardCollection overrideList = null;
|
CardCollection overrideList = null;
|
||||||
|
|
||||||
@@ -411,6 +388,11 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Survival of the Fittest logic
|
||||||
|
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
|
||||||
|
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
|
||||||
|
}
|
||||||
|
|
||||||
// Discard lands
|
// Discard lands
|
||||||
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
|
final CardCollection landsInHand = CardLists.getType(typeList, "Land");
|
||||||
if (!landsInHand.isEmpty()) {
|
if (!landsInHand.isEmpty()) {
|
||||||
@@ -848,11 +830,11 @@ public class ComputerUtil {
|
|||||||
continue; // Won't play ability
|
continue; // Won't play ability
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c)) {
|
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c, sa)) {
|
||||||
continue; // Won't play ability
|
continue; // Won't play ability
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c)) {
|
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c, sa)) {
|
||||||
continue; // Won't play ability
|
continue; // Won't play ability
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,7 +850,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (final Exception ex) {
|
} 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 +889,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (final Exception ex) {
|
} 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,14 +908,23 @@ public class ComputerUtil {
|
|||||||
return true;
|
return true;
|
||||||
} else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
|
} else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
|
||||||
//Only play these main1 when the opponent has creatures (stealing and giving them haste)
|
//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;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
|
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()) {
|
if (card.getManaCost().isZero()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -973,7 +964,7 @@ public class ComputerUtil {
|
|||||||
return true;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (card.isCreature()) {
|
if (card.isCreature()) {
|
||||||
@@ -993,7 +984,7 @@ public class ComputerUtil {
|
|||||||
} // BuffedBy
|
} // BuffedBy
|
||||||
|
|
||||||
// get all cards the human controls with AntiBuffedBy
|
// 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) {
|
for (Card buffedcard : antibuffed) {
|
||||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||||
@@ -1033,11 +1024,11 @@ public class ComputerUtil {
|
|||||||
return ret;
|
return ret;
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, if life is possibly in danger, then this is fine.
|
// Otherwise, if life is possibly in danger, then this is fine.
|
||||||
Combat combat = new Combat(ai.getOpponent());
|
Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
|
||||||
CardCollectionView attackers = ai.getOpponent().getCreaturesInPlay();
|
CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||||
for (Card att : attackers) {
|
for (Card att : attackers) {
|
||||||
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
||||||
combat.addAttacker(att, att.getController().getOpponent());
|
combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AiBlockController aiBlock = new AiBlockController(ai);
|
AiBlockController aiBlock = new AiBlockController(ai);
|
||||||
@@ -1101,7 +1092,7 @@ public class ComputerUtil {
|
|||||||
return (sa.getHostCard().isCreature()
|
return (sa.getHostCard().isCreature()
|
||||||
&& sa.getPayCosts().hasTapCost()
|
&& sa.getPayCosts().hasTapCost()
|
||||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||||
|| !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||||
&& !sa.hasParam("ActivationPhases"));
|
&& !sa.hasParam("ActivationPhases"));
|
||||||
}
|
}
|
||||||
@@ -1145,7 +1136,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get all cards the human controls with AntiBuffedBy
|
// 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) {
|
for (Card buffedcard : antibuffed) {
|
||||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||||
@@ -1331,7 +1322,7 @@ public class ComputerUtil {
|
|||||||
if (tgt == null) {
|
if (tgt == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final Player enemy = ai.getOpponent();
|
final Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||||
if (!sa.canTarget(enemy)) {
|
if (!sa.canTarget(enemy)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1435,20 +1426,29 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
objects = canBeTargeted;
|
objects = canBeTargeted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
SpellAbility saviorWithSubs = saviour;
|
||||||
toughness = saviour.hasParam("NumDef") ?
|
ApiType saviorWithSubsApi = saviorWithSubs == null ? null : saviorWithSubs.getApi();
|
||||||
AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("NumDef"), saviour) : 0;
|
while (saviorWithSubs != null) {
|
||||||
final List<String> keywords = saviour.hasParam("KW") ?
|
ApiType curApi = saviorWithSubs.getApi();
|
||||||
Arrays.asList(saviour.getParam("KW").split(" & ")) : new ArrayList<String>();
|
if (curApi == ApiType.Pump || curApi == ApiType.PumpAll) {
|
||||||
if (keywords.contains("Indestructible")) {
|
toughness = saviorWithSubs.hasParam("NumDef") ?
|
||||||
grantIndestructible = true;
|
AbilityUtils.calculateAmount(saviorWithSubs.getHostCard(), saviorWithSubs.getParam("NumDef"), saviour) : 0;
|
||||||
}
|
final List<String> keywords = saviorWithSubs.hasParam("KW") ?
|
||||||
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
|
Arrays.asList(saviorWithSubs.getParam("KW").split(" & ")) : new ArrayList<String>();
|
||||||
grantShroud = true;
|
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) {
|
if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
|
||||||
if (saviour.getParam("CounterType").equals("P1P1")) {
|
if (saviour.getParam("CounterType").equals("P1P1")) {
|
||||||
toughness = AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("CounterNum"), saviour);
|
toughness = AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("CounterNum"), saviour);
|
||||||
@@ -1588,9 +1588,10 @@ public class ComputerUtil {
|
|||||||
// Destroy => regeneration/bounce/shroud
|
// Destroy => regeneration/bounce/shroud
|
||||||
else if ((threatApi == ApiType.Destroy || threatApi == ApiType.DestroyAll)
|
else if ((threatApi == ApiType.Destroy || threatApi == ApiType.DestroyAll)
|
||||||
&& (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
|
&& (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
|
||||||
&& !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone
|
&& !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone
|
||||||
|| saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
|| 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) {
|
for (final Object o : objects) {
|
||||||
if (o instanceof Card) {
|
if (o instanceof Card) {
|
||||||
final Card c = (Card) o;
|
final Card c = (Card) o;
|
||||||
@@ -1604,7 +1605,9 @@ public class ComputerUtil {
|
|||||||
continue;
|
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)
|
if ((tgt == null && !grantIndestructible)
|
||||||
|| (!grantShroud && !grantIndestructible)) {
|
|| (!grantShroud && !grantIndestructible)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1857,6 +1860,27 @@ public class ComputerUtil {
|
|||||||
public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) {
|
public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) {
|
||||||
boolean bottom = false;
|
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 allCards = player.getAllCards();
|
||||||
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
|
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
|
||||||
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
|
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
|
||||||
@@ -1871,8 +1895,9 @@ public class ComputerUtil {
|
|||||||
CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player)));
|
CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player)));
|
||||||
int numCards = allCreatures.size();
|
int numCards = allCreatures.size();
|
||||||
|
|
||||||
if (landsOTB.size() < 3 && landsInHand.isEmpty()) {
|
if (landsOTB.size() < maxLandsToScryLandsToTop && landsInHand.isEmpty()) {
|
||||||
if ((!c.isLand() && !manaArts.contains(c.getName())) || c.getManaAbilities().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
|
// 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
|
// on the battlefield is low, to try to improve the mana base early
|
||||||
bottom = true;
|
bottom = true;
|
||||||
@@ -1880,7 +1905,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (c.isLand()) {
|
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
|
// probably enough lands not to urgently need another one, so look for more gas instead
|
||||||
bottom = true;
|
bottom = true;
|
||||||
} else if (landsInHand.size() >= Math.max(cardsInHand.size() / 2, 2)) {
|
} else if (landsInHand.size() >= Math.max(cardsInHand.size() / 2, 2)) {
|
||||||
@@ -1898,16 +1923,15 @@ public class ComputerUtil {
|
|||||||
} else if (c.isCreature()) {
|
} else if (c.isCreature()) {
|
||||||
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
|
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
|
||||||
int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0;
|
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);
|
int maxControlledCMC = Aggregates.max(creaturesOTB, CardPredicates.Accessors.fnGetCmc);
|
||||||
|
|
||||||
if (ComputerUtilCard.evaluateCreature(c) < avgCreatureValue) {
|
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
|
// if there are more than five creatures and the creature is question is below average for
|
||||||
// the deck, scry it to the bottom
|
// the deck, scry it to the bottom
|
||||||
bottom = true;
|
bottom = true;
|
||||||
} else if (creaturesOTB.size() > 3 && c.getCMC() <= 3
|
} else if (creaturesOTB.size() > maxCreatsToScryLowCMCAway && c.getCMC() <= lowCMCThreshold
|
||||||
&& maxControlledCMC >= 4 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
|
&& maxControlledCMC >= lowCMCThreshold + 1 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
|
||||||
// if we are already at a stage when we have 4+ CMC creatures on the battlefield,
|
// 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
|
// probably worth it to scry away very low value creatures with low CMC
|
||||||
bottom = true;
|
bottom = true;
|
||||||
@@ -1915,6 +1939,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;
|
return bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2047,7 +2080,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (logic.equals("ChosenLandwalk")) {
|
else if (logic.equals("ChosenLandwalk")) {
|
||||||
for (Card c : ai.getOpponent().getLandsInPlay()) {
|
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
|
||||||
for (String t : c.getType()) {
|
for (String t : c.getType()) {
|
||||||
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
|
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
|
||||||
chosen = t;
|
chosen = t;
|
||||||
@@ -2065,7 +2098,7 @@ public class ComputerUtil {
|
|||||||
else if (kindOfType.equals("Land")) {
|
else if (kindOfType.equals("Land")) {
|
||||||
if (logic != null) {
|
if (logic != null) {
|
||||||
if (logic.equals("ChosenLandwalk")) {
|
if (logic.equals("ChosenLandwalk")) {
|
||||||
for (Card c : ai.getOpponent().getLandsInPlay()) {
|
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
|
||||||
for (String t : c.getType().getLandTypes()) {
|
for (String t : c.getType().getLandTypes()) {
|
||||||
if (!invalidTypes.contains(t)) {
|
if (!invalidTypes.contains(t)) {
|
||||||
chosen = t;
|
chosen = t;
|
||||||
@@ -2098,7 +2131,7 @@ public class ComputerUtil {
|
|||||||
case "Torture":
|
case "Torture":
|
||||||
return "Torture";
|
return "Torture";
|
||||||
case "GraceOrCondemnation":
|
case "GraceOrCondemnation":
|
||||||
return ai.getCreaturesInPlay().size() > ai.getOpponent().getCreaturesInPlay().size() ? "Grace"
|
return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace"
|
||||||
: "Condemnation";
|
: "Condemnation";
|
||||||
case "CarnageOrHomage":
|
case "CarnageOrHomage":
|
||||||
CardCollection cardsInPlay = CardLists
|
CardCollection cardsInPlay = CardLists
|
||||||
@@ -2683,4 +2716,53 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
package forge.ai;
|
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.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
@@ -29,15 +17,7 @@ import forge.game.GameObject;
|
|||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.CostPayEnergy;
|
import forge.game.cost.CostPayEnergy;
|
||||||
@@ -55,6 +35,12 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
import forge.util.MyRandom;
|
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 class ComputerUtilCard {
|
||||||
@@ -364,7 +350,12 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasEnchantmants || hasArtifacts) {
|
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);
|
return getCheapestPermanentAI(ae, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +368,28 @@ public class ComputerUtilCard {
|
|||||||
return getCheapestPermanentAI(list, null, false);
|
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);
|
||||||
|
|
||||||
|
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>() {
|
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(final Card a, final Card b) {
|
public int compare(final Card a, final Card b) {
|
||||||
@@ -399,6 +412,10 @@ public class ComputerUtilCard {
|
|||||||
return creatureEvaluator.evaluateCreature(c);
|
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) {
|
public static int evaluatePermanentList(final CardCollectionView list) {
|
||||||
int value = 0;
|
int value = 0;
|
||||||
for (int i = 0; i < list.size(); i++) {
|
for (int i = 0; i < list.size(); i++) {
|
||||||
@@ -482,7 +499,7 @@ public class ComputerUtilCard {
|
|||||||
*/
|
*/
|
||||||
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
||||||
AiBlockController aiBlk = new AiBlockController(ai);
|
AiBlockController aiBlk = new AiBlockController(ai);
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
Combat combat = new Combat(opp);
|
Combat combat = new Combat(opp);
|
||||||
//Use actual attackers if available, else consider all possible attackers
|
//Use actual attackers if available, else consider all possible attackers
|
||||||
Combat currentCombat = ai.getGame().getCombat();
|
Combat currentCombat = ai.getGame().getCombat();
|
||||||
@@ -845,9 +862,10 @@ public class ComputerUtilCard {
|
|||||||
List<String> chosen = new ArrayList<String>();
|
List<String> chosen = new ArrayList<String>();
|
||||||
Player ai = sa.getActivatingPlayer();
|
Player ai = sa.getActivatingPlayer();
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (sa.hasParam("AILogic")) {
|
if (sa.hasParam("AILogic")) {
|
||||||
final String logic = sa.getParam("AILogic");
|
final String logic = sa.getParam("AILogic");
|
||||||
|
|
||||||
if (logic.equals("MostProminentInHumanDeck")) {
|
if (logic.equals("MostProminentInHumanDeck")) {
|
||||||
chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
|
chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
|
||||||
}
|
}
|
||||||
@@ -873,7 +891,7 @@ public class ComputerUtilCard {
|
|||||||
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
|
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||||
}
|
}
|
||||||
else if (logic.equals("MostProminentHumanControls")) {
|
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")) {
|
else if (logic.equals("MostProminentPermanent")) {
|
||||||
chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
|
chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
|
||||||
@@ -897,7 +915,7 @@ public class ComputerUtilCard {
|
|||||||
String bestColor = Constant.GREEN;
|
String bestColor = Constant.GREEN;
|
||||||
for (byte color : MagicColor.WUBRG) {
|
for (byte color : MagicColor.WUBRG) {
|
||||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
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));
|
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||||
@@ -934,7 +952,7 @@ public class ComputerUtilCard {
|
|||||||
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
|
||||||
final Player ai = sa.getActivatingPlayer();
|
final Player ai = sa.getActivatingPlayer();
|
||||||
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final PhaseHandler ph = game.getPhaseHandler();
|
final PhaseHandler ph = game.getPhaseHandler();
|
||||||
final PhaseType phaseType = ph.getPhase();
|
final PhaseType phaseType = ph.getPhase();
|
||||||
@@ -1174,6 +1192,16 @@ public class ComputerUtilCard {
|
|||||||
final PhaseHandler phase = game.getPhaseHandler();
|
final PhaseHandler phase = game.getPhaseHandler();
|
||||||
final Combat combat = phase.getCombat();
|
final Combat combat = phase.getCombat();
|
||||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||||
|
|
||||||
|
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)) {
|
if (!c.canBeTargetedBy(sa)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1186,11 +1214,11 @@ public class ComputerUtilCard {
|
|||||||
/* -- currently disabled until better conditions are devised and the spell prediction is made smarter --
|
/* -- 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.
|
// 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();
|
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
|
// only hold mana sources once
|
||||||
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Pump);
|
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Pump);
|
||||||
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
||||||
aic.reserveManaSourcesForMain2(futureSpell);
|
aic.reserveManaSources(futureSpell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
@@ -1217,7 +1245,7 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
|
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
|
||||||
List<Card> oppCreatures = opp.getCreaturesInPlay();
|
List<Card> oppCreatures = opp.getCreaturesInPlay();
|
||||||
float chance = 0;
|
float chance = 0;
|
||||||
@@ -1234,6 +1262,28 @@ public class ComputerUtilCard {
|
|||||||
threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
|
threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
|
||||||
}
|
}
|
||||||
chance += threat;
|
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
|
//2. grant haste
|
||||||
@@ -1319,6 +1369,16 @@ public class ComputerUtilCard {
|
|||||||
if (combat.isAttacking(c) && opp.getLife() > 0) {
|
if (combat.isAttacking(c) && opp.getLife() > 0) {
|
||||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
|
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
|
||||||
int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, 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 (combat.isBlocked(c)) {
|
||||||
if (!c.hasKeyword("Trample")) {
|
if (!c.hasKeyword("Trample")) {
|
||||||
dmg = 0;
|
dmg = 0;
|
||||||
@@ -1331,8 +1391,11 @@ public class ComputerUtilCard {
|
|||||||
pumpedDmg = 0;
|
pumpedDmg = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pumpedDmg >= opp.getLife()) {
|
if (pumpedDmg > dmg) {
|
||||||
return true;
|
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
|
// 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
|
// considering all unblocked creatures after the blockers are already declared
|
||||||
@@ -1403,6 +1466,37 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return MyRandom.getRandom().nextFloat() < chance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1417,8 +1511,7 @@ public class ComputerUtilCard {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
|
public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
|
||||||
final Card c, final int toughness, final int power,
|
final Card c, int toughness, int power, final List<String> keywords) {
|
||||||
final List<String> keywords) {
|
|
||||||
Card pumped = CardFactory.copyCard(c, true);
|
Card pumped = CardFactory.copyCard(c, true);
|
||||||
pumped.setSickness(c.hasSickness());
|
pumped.setSickness(c.hasSickness());
|
||||||
final long timestamp = c.getGame().getNextTimestamp();
|
final long timestamp = c.getGame().getNextTimestamp();
|
||||||
@@ -1431,9 +1524,19 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Berserk
|
||||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||||
final int berserkPower = isBerserk ? c.getCurrentPower() : 0;
|
final int berserkPower = isBerserk ? c.getCurrentPower() : 0;
|
||||||
|
|
||||||
|
// 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.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
|
||||||
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
|
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
|
||||||
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);
|
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);
|
||||||
|
|||||||
@@ -17,14 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.game.CardTraitBase;
|
import forge.game.CardTraitBase;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
@@ -32,13 +28,7 @@ import forge.game.GlobalRuleChange;
|
|||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.CostPayment;
|
import forge.game.cost.CostPayment;
|
||||||
@@ -53,8 +43,12 @@ import forge.game.trigger.Trigger;
|
|||||||
import forge.game.trigger.TriggerHandler;
|
import forge.game.trigger.TriggerHandler;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.TextUtil;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -390,6 +384,18 @@ public class ComputerUtilCombat {
|
|||||||
return false;
|
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
|
// check for creatures that must be blocked
|
||||||
final List<Card> attackers = combat.getAttackersOf(ai);
|
final List<Card> attackers = combat.getAttackersOf(ai);
|
||||||
|
|
||||||
@@ -401,7 +407,11 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
if (blockers.isEmpty()) {
|
if (blockers.isEmpty()) {
|
||||||
if (!attacker.getSVar("MustBeBlocked").equals("")) {
|
if (!attacker.getSVar("MustBeBlocked").equals("")) {
|
||||||
return true;
|
boolean cond = !"attackingplayer".equalsIgnoreCase(attacker.getSVar("MustBeBlocked"))
|
||||||
|
|| combat.getDefenderByAttacker(attacker) instanceof Player;
|
||||||
|
if (cond) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (threateningCommanders.contains(attacker)) {
|
if (threateningCommanders.contains(attacker)) {
|
||||||
@@ -683,12 +693,21 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static boolean attackerWouldBeDestroyed(Player ai, final Card attacker, Combat combat) {
|
public static boolean attackerWouldBeDestroyed(Player ai, final Card attacker, Combat combat) {
|
||||||
final List<Card> blockers = combat.getBlockers(attacker);
|
final List<Card> blockers = combat.getBlockers(attacker);
|
||||||
|
int firstStrikeBlockerDmg = 0;
|
||||||
|
|
||||||
for (final Card defender : blockers) {
|
for (final Card defender : blockers) {
|
||||||
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
|
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
|
||||||
&& !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
|
&& !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
|
||||||
return true;
|
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);
|
return ComputerUtilCombat.totalDamageOfBlockers(attacker, blockers) >= ComputerUtilCombat.getDamageToKill(attacker);
|
||||||
@@ -781,7 +800,7 @@ public class ComputerUtilCombat {
|
|||||||
if (validBlocked.contains(".withLesserPower")) {
|
if (validBlocked.contains(".withLesserPower")) {
|
||||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
// 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$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
|
||||||
validBlocked = validBlocked.replace(".withLesserPower", "");
|
validBlocked = TextUtil.fastReplace(validBlocked, ".withLesserPower", "");
|
||||||
if (defender.getCurrentPower() <= attacker.getCurrentPower()) {
|
if (defender.getCurrentPower() <= attacker.getCurrentPower()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -795,7 +814,7 @@ public class ComputerUtilCombat {
|
|||||||
if (validBlocker.contains(".withLesserPower")) {
|
if (validBlocker.contains(".withLesserPower")) {
|
||||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||||
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
|
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
|
||||||
validBlocker = validBlocker.replace(".withLesserPower", "");
|
validBlocker = TextUtil.fastReplace(validBlocker, ".withLesserPower", "");
|
||||||
if (defender.getCurrentPower() >= attacker.getCurrentPower()) {
|
if (defender.getCurrentPower() >= attacker.getCurrentPower()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -854,9 +873,12 @@ public class ComputerUtilCombat {
|
|||||||
public static int predictPowerBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
|
public static int predictPowerBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
|
||||||
int power = 0;
|
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")) {
|
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
||||||
power -= attacker.getAmountOfKeyword("Flanking");
|
power -= attacker.getAmountOfKeyword("Flanking");
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Serene Master switches power with attacker
|
// Serene Master switches power with attacker
|
||||||
if (blocker.getName().equals("Serene Master")) {
|
if (blocker.getName().equals("Serene Master")) {
|
||||||
@@ -889,7 +911,7 @@ public class ComputerUtilCombat {
|
|||||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
|
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
|
||||||
continue;
|
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)) {
|
if (!blocker.isValid(valid, card.getController(), card, null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1176,7 +1198,10 @@ public class ComputerUtilCombat {
|
|||||||
* a {@link forge.game.combat.Combat} object.
|
* a {@link forge.game.combat.Combat} object.
|
||||||
* @return a int.
|
* @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;
|
int power = 0;
|
||||||
|
|
||||||
power += attacker.getKeywordMagnitude("Bushido");
|
power += attacker.getKeywordMagnitude("Bushido");
|
||||||
@@ -1216,27 +1241,29 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
// look out for continuous static abilities that only care for attacking
|
// look out for continuous static abilities that only care for attacking
|
||||||
// creatures
|
// creatures
|
||||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
if (!withoutCombatStaticAbilities) {
|
||||||
for (final Card card : cardList) {
|
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final Card card : cardList) {
|
||||||
final Map<String, String> params = stAb.getMapParams();
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
if (!params.get("Mode").equals("Continuous")) {
|
final Map<String, String> params = stAb.getMapParams();
|
||||||
continue;
|
if (!params.get("Mode").equals("Continuous")) {
|
||||||
}
|
continue;
|
||||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
}
|
||||||
continue;
|
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
||||||
}
|
continue;
|
||||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
}
|
||||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||||
continue;
|
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||||
}
|
continue;
|
||||||
if (params.containsKey("AddPower")) {
|
}
|
||||||
if (params.get("AddPower").equals("X")) {
|
if (params.containsKey("AddPower")) {
|
||||||
power += CardFactoryUtil.xCount(card, card.getSVar("X"));
|
if (params.get("AddPower").equals("X")) {
|
||||||
} else if (params.get("AddPower").equals("Y")) {
|
power += CardFactoryUtil.xCount(card, card.getSVar("X"));
|
||||||
power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
|
} else if (params.get("AddPower").equals("Y")) {
|
||||||
} else {
|
power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
|
||||||
power += Integer.valueOf(params.get("AddPower"));
|
} else {
|
||||||
|
power += Integer.valueOf(params.get("AddPower"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1305,9 +1332,13 @@ public class ComputerUtilCombat {
|
|||||||
} else {
|
} else {
|
||||||
String bonus = new String(source.getSVar(att));
|
String bonus = new String(source.getSVar(att));
|
||||||
if (bonus.contains("TriggerCount$NumBlockers")) {
|
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
|
} 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);
|
power += CardFactoryUtil.xCount(source, bonus);
|
||||||
|
|
||||||
@@ -1372,6 +1403,10 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
|
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
|
||||||
, boolean withoutAbilities) {
|
, 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;
|
int toughness = 0;
|
||||||
|
|
||||||
//check Exalted only for the first attacker
|
//check Exalted only for the first attacker
|
||||||
@@ -1400,37 +1435,39 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
// look out for continuous static abilities that only care for attacking
|
// look out for continuous static abilities that only care for attacking
|
||||||
// creatures
|
// creatures
|
||||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
if (!withoutCombatStaticAbilities) {
|
||||||
for (final Card card : cardList) {
|
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final Card card : cardList) {
|
||||||
final Map<String, String> params = stAb.getMapParams();
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
if (!params.get("Mode").equals("Continuous")) {
|
final Map<String, String> params = stAb.getMapParams();
|
||||||
continue;
|
if (!params.get("Mode").equals("Continuous")) {
|
||||||
}
|
continue;
|
||||||
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
|
}
|
||||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
|
||||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||||
continue;
|
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||||
}
|
continue;
|
||||||
if (params.containsKey("AddToughness")) {
|
}
|
||||||
if (params.get("AddToughness").equals("X")) {
|
if (params.containsKey("AddToughness")) {
|
||||||
toughness += CardFactoryUtil.xCount(card, card.getSVar("X"));
|
if (params.get("AddToughness").equals("X")) {
|
||||||
} else if (params.get("AddToughness").equals("Y")) {
|
toughness += CardFactoryUtil.xCount(card, card.getSVar("X"));
|
||||||
toughness += CardFactoryUtil.xCount(card, card.getSVar("Y"));
|
} else if (params.get("AddToughness").equals("Y")) {
|
||||||
} else {
|
toughness += CardFactoryUtil.xCount(card, card.getSVar("Y"));
|
||||||
toughness += Integer.valueOf(params.get("AddToughness"));
|
} else {
|
||||||
}
|
toughness += Integer.valueOf(params.get("AddToughness"));
|
||||||
}
|
}
|
||||||
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
|
}
|
||||||
final String valid = params.get("Affected").replace("untapped", "Creature");
|
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
|
||||||
if (!attacker.isValid(valid, card.getController(), card, null)
|
final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
|
||||||
|| attacker.hasKeyword("Vigilance")) {
|
if (!attacker.isValid(valid, card.getController(), card, null)
|
||||||
continue;
|
|| attacker.hasKeyword("Vigilance")) {
|
||||||
}
|
continue;
|
||||||
// remove the bonus, because it will no longer be granted
|
}
|
||||||
if (params.containsKey("AddToughness")) {
|
// remove the bonus, because it will no longer be granted
|
||||||
toughness -= Integer.valueOf(params.get("AddToughness"));
|
if (params.containsKey("AddToughness")) {
|
||||||
}
|
toughness -= Integer.valueOf(params.get("AddToughness"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1517,9 +1554,9 @@ public class ComputerUtilCombat {
|
|||||||
} else {
|
} else {
|
||||||
String bonus = new String(source.getSVar(def));
|
String bonus = new String(source.getSVar(def));
|
||||||
if (bonus.contains("TriggerCount$NumBlockers")) {
|
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
|
} 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);
|
toughness += CardFactoryUtil.xCount(source, bonus);
|
||||||
}
|
}
|
||||||
@@ -1674,6 +1711,10 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
|
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
|
||||||
final boolean withoutAbilities) {
|
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
|
// Can activate transform ability
|
||||||
if (!withoutAbilities) {
|
if (!withoutAbilities) {
|
||||||
attacker = canTransform(attacker);
|
attacker = canTransform(attacker);
|
||||||
@@ -1725,10 +1766,10 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
if (attacker.toughnessAssignsDamage()) {
|
if (attacker.toughnessAssignsDamage()) {
|
||||||
attackerDamage = attacker.getNetToughness()
|
attackerDamage = attacker.getNetToughness()
|
||||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||||
} else {
|
} else {
|
||||||
attackerDamage = attacker.getNetPower()
|
attackerDamage = attacker.getNetPower()
|
||||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
int possibleDefenderPrevention = 0;
|
int possibleDefenderPrevention = 0;
|
||||||
@@ -1741,11 +1782,16 @@ public class ComputerUtilCombat {
|
|||||||
// consider Damage Prevention/Replacement
|
// consider Damage Prevention/Replacement
|
||||||
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
|
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
|
||||||
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, 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)
|
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
||||||
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
||||||
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
|
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 (blocker.hasKeyword("Double Strike")) {
|
||||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||||
@@ -1914,6 +1960,10 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
|
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
|
||||||
final boolean withoutAbilities) {
|
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
|
// Can activate transform ability
|
||||||
if (!withoutAbilities) {
|
if (!withoutAbilities) {
|
||||||
attacker = canTransform(attacker);
|
attacker = canTransform(attacker);
|
||||||
@@ -1947,10 +1997,10 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
if (attacker.toughnessAssignsDamage()) {
|
if (attacker.toughnessAssignsDamage()) {
|
||||||
attackerDamage = attacker.getNetToughness()
|
attackerDamage = attacker.getNetToughness()
|
||||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||||
} else {
|
} else {
|
||||||
attackerDamage = attacker.getNetPower()
|
attackerDamage = attacker.getNetPower()
|
||||||
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities);
|
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
int possibleDefenderPrevention = 0;
|
int possibleDefenderPrevention = 0;
|
||||||
@@ -1975,7 +2025,7 @@ public class ComputerUtilCombat {
|
|||||||
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
|
||||||
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
|
||||||
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
|
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 (attacker.hasKeyword("Double Strike")) {
|
||||||
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
|
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
|
||||||
@@ -2192,6 +2242,7 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public final static int getDamageToKill(final Card c) {
|
public final static int getDamageToKill(final Card c) {
|
||||||
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
|
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
|
||||||
|
|
||||||
if ((killDamage > c.getPreventNextDamageTotalShields())
|
if ((killDamage > c.getPreventNextDamageTotalShields())
|
||||||
&& c.hasSVar("DestroyWhenDamaged")) {
|
&& c.hasSVar("DestroyWhenDamaged")) {
|
||||||
killDamage = 1 + c.getPreventNextDamageTotalShields();
|
killDamage = 1 + c.getPreventNextDamageTotalShields();
|
||||||
@@ -2431,6 +2482,91 @@ public class ComputerUtilCombat {
|
|||||||
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
|
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
|
||||||
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
|
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 (String kw : atk.getKeywords()) {
|
||||||
|
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")) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ public class ComputerUtilCost {
|
|||||||
return false;
|
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.
|
// check the sa what the PaymentDecision is.
|
||||||
// ignore Loyality abilities with Zero as Cost
|
// ignore Loyality abilities with Zero as Cost
|
||||||
if (sa != null && !CounterType.LOYALTY.equals(type)) {
|
if (sa != null && !CounterType.LOYALTY.equals(type)) {
|
||||||
@@ -231,13 +240,15 @@ public class ComputerUtilCost {
|
|||||||
* the source
|
* the source
|
||||||
* @return true, if successful
|
* @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) {
|
if (cost == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostSacrifice) {
|
if (part instanceof CostSacrifice) {
|
||||||
final CostSacrifice sac = (CostSacrifice) part;
|
final CostSacrifice sac = (CostSacrifice) part;
|
||||||
|
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||||
|
|
||||||
if (sac.payCostFromSource() && source.isCreature()) {
|
if (sac.payCostFromSource() && source.isCreature()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -246,10 +257,19 @@ public class ComputerUtilCost {
|
|||||||
if (type.equals("CARDNAME")) {
|
if (type.equals("CARDNAME")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(","), source.getController(), source, null);
|
final CardCollection sacList = new CardCollection();
|
||||||
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
|
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
||||||
return false;
|
|
||||||
|
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,14 +287,15 @@ public class ComputerUtilCost {
|
|||||||
* is the gain important enough?
|
* is the gain important enough?
|
||||||
* @return true, if successful
|
* @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) {
|
if (cost == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostSacrifice) {
|
if (part instanceof CostSacrifice) {
|
||||||
final CostSacrifice sac = (CostSacrifice) part;
|
final CostSacrifice sac = (CostSacrifice) part;
|
||||||
|
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||||
|
|
||||||
final String type = sac.getType();
|
final String type = sac.getType();
|
||||||
|
|
||||||
if (type.equals("CARDNAME")) {
|
if (type.equals("CARDNAME")) {
|
||||||
@@ -286,9 +307,19 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final CardCollection sacList = new CardCollection();
|
||||||
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
|
||||||
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
|
|
||||||
return false;
|
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +367,7 @@ public class ComputerUtilCost {
|
|||||||
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
|
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
|
||||||
String type = part.getType();
|
String type = part.getType();
|
||||||
String totalP = type.split("withTotalPowerGE")[1];
|
String totalP = type.split("withTotalPowerGE")[1];
|
||||||
type = type.replace("+withTotalPowerGE" + totalP, "");
|
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
|
||||||
CardCollection exclude = CardLists.getValidCards(
|
CardCollection exclude = CardLists.getValidCards(
|
||||||
new CardCollection(ai.getCardsIn(ZoneType.Battlefield)), type.split(";"),
|
new CardCollection(ai.getCardsIn(ZoneType.Battlefield)), type.split(";"),
|
||||||
source.getController(), source, sa);
|
source.getController(), source, sa);
|
||||||
@@ -364,8 +395,8 @@ public class ComputerUtilCost {
|
|||||||
* the source
|
* the source
|
||||||
* @return true, if successful
|
* @return true, if successful
|
||||||
*/
|
*/
|
||||||
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source) {
|
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
|
||||||
return checkSacrificeCost(ai, cost, source, true);
|
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -561,12 +592,12 @@ public class ComputerUtilCost {
|
|||||||
|
|
||||||
return checkLifeCost(payer, cost, source, 4, sa)
|
return checkLifeCost(payer, cost, source, 4, sa)
|
||||||
&& checkDamageCost(payer, cost, source, 4)
|
&& checkDamageCost(payer, cost, source, 4)
|
||||||
&& (isMine || checkSacrificeCost(payer, cost, source))
|
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
|
||||||
&& (isMine || checkDiscardCost(payer, cost, source))
|
&& (isMine || checkDiscardCost(payer, cost, source))
|
||||||
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||||
&& (!source.getName().equals("Perplex") || 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("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) {
|
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.*;
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.common.collect.Multimap;
|
|
||||||
|
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
@@ -18,11 +14,7 @@ import forge.game.Game;
|
|||||||
import forge.game.GameActionUtil;
|
import forge.game.GameActionUtil;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardUtil;
|
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostAdjustment;
|
import forge.game.cost.CostAdjustment;
|
||||||
@@ -41,7 +33,6 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.TextUtil;
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
@@ -203,41 +194,70 @@ public class ComputerUtilMana {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getHostCard() != null && sa.getApi() == ApiType.Animate) {
|
if (sa.getHostCard() != null) {
|
||||||
// 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.getApi() == ApiType.Animate) {
|
||||||
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
|
||||||
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard()
|
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
||||||
&& ma.getPayCosts().hasTapCost()) {
|
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard()
|
||||||
continue;
|
&& ma.getPayCosts().hasTapCost()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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().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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpellAbility paymentChoice = ma;
|
||||||
|
|
||||||
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
|
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
|
||||||
// to attempt to make the spell uncounterable when possible.
|
// to attempt to make the spell uncounterable when possible.
|
||||||
if ((toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)
|
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
||||||
&& ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
|
||||||
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
|
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
|
||||||
for (SpellAbility ab : saList) {
|
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
|
||||||
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
|
||||||
return ab;
|
continue;
|
||||||
|
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
|
||||||
|
for (SpellAbility ab : saList) {
|
||||||
|
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
||||||
|
paymentChoice = ab;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final String typeRes = cost.getSourceRestriction();
|
final String typeRes = cost.getSourceRestriction();
|
||||||
if (StringUtils.isNotBlank(typeRes) && !ma.getHostCard().getType().hasStringType(typeRes)) {
|
if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canPayShardWithSpellAbility(toPay, ai, ma, sa, checkCosts)) {
|
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts)) {
|
||||||
return ma;
|
return paymentChoice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -802,7 +822,7 @@ public class ComputerUtilMana {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isManaSourceReserved returns true if sourceCard is reserved as a mana source for payment
|
// 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
|
// being considered for casting) is high priority, then mana source reservation will be
|
||||||
// ignored.
|
// ignored.
|
||||||
private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) {
|
private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) {
|
||||||
@@ -816,8 +836,21 @@ public class ComputerUtilMana {
|
|||||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
|
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
|
// 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.
|
// AI profile variable.
|
||||||
if (sa.getSVar("LowPriorityAI").equals("")) {
|
if (sa.getSVar("LowPriorityAI").equals("")) {
|
||||||
if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) {
|
if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) {
|
||||||
@@ -825,16 +858,16 @@ public class ComputerUtilMana {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
|
|
||||||
if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) {
|
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 {
|
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.
|
// This mana source is held elsewhere for a Main Phase 2 spell.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,8 +1125,65 @@ public class ComputerUtilMana {
|
|||||||
return cost;
|
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
|
//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 CardCollectionView list = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand));
|
||||||
final List<Card> manaSources = CardLists.filter(list, new Predicate<Card>() {
|
final List<Card> manaSources = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -1200,7 +1290,7 @@ public class ComputerUtilMana {
|
|||||||
System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources);
|
System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources);
|
||||||
}
|
}
|
||||||
return sortedManaSources;
|
return sortedManaSources;
|
||||||
} // getAvailableMana()
|
} // getAvailableManaSources()
|
||||||
|
|
||||||
//This method is currently used by AI to estimate mana available
|
//This method is currently used by AI to estimate mana available
|
||||||
private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final Player ai, boolean checkPlayable) {
|
private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final Player ai, boolean checkPlayable) {
|
||||||
@@ -1221,7 +1311,7 @@ public class ComputerUtilMana {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loop over all current available mana sources
|
// 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) {
|
if (DEBUG_MANA_PAYMENT) {
|
||||||
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor sourceCard = " + sourceCard);
|
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor sourceCard = " + sourceCard);
|
||||||
}
|
}
|
||||||
@@ -1264,7 +1354,7 @@ public class ComputerUtilMana {
|
|||||||
Card crd = replacementEffect.getHostCard();
|
Card crd = replacementEffect.getHostCard();
|
||||||
String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement"));
|
String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement"));
|
||||||
if (repType.contains("Chosen")) {
|
if (repType.contains("Chosen")) {
|
||||||
repType = repType.replace("Chosen", MagicColor.toShortString(crd.getChosenColor()));
|
repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor()));
|
||||||
}
|
}
|
||||||
mp.setManaReplaceType(repType);
|
mp.setManaReplaceType(repType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CounterType;
|
||||||
|
import forge.game.cost.CostPayEnergy;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
public class CreatureEvaluator implements Function<Card, Integer> {
|
public class CreatureEvaluator implements Function<Card, Integer> {
|
||||||
@@ -19,6 +21,10 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int evaluateCreature(final Card c) {
|
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;
|
int value = 80;
|
||||||
if (!c.isToken()) {
|
if (!c.isToken()) {
|
||||||
value += addValue(20, "non-token"); // tokens should be worth less than actual cards
|
value += addValue(20, "non-token"); // tokens should be worth less than actual cards
|
||||||
@@ -34,9 +40,13 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
value += addValue(power * 15, "power");
|
if (considerPT) {
|
||||||
value += addValue(toughness * 10, "toughness: " + toughness);
|
value += addValue(power * 15, "power");
|
||||||
value += addValue(c.getCMC() * 5, "cmc");
|
value += addValue(toughness * 10, "toughness: " + toughness);
|
||||||
|
}
|
||||||
|
if (considerCMC) {
|
||||||
|
value += addValue(c.getCMC() * 5, "cmc");
|
||||||
|
}
|
||||||
|
|
||||||
// Evasion keywords
|
// Evasion keywords
|
||||||
if (c.hasKeyword("Flying")) {
|
if (c.hasKeyword("Flying")) {
|
||||||
@@ -91,6 +101,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
value += addValue(power * 15, "infect");
|
value += addValue(power * 15, "infect");
|
||||||
}
|
}
|
||||||
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
|
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
|
||||||
|
value += addValue(c.getKeywordMagnitude("Afflict") * 5, "afflict");
|
||||||
}
|
}
|
||||||
|
|
||||||
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
|
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
|
||||||
@@ -99,6 +110,14 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
|
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
|
||||||
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
|
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
|
// Defensive Keywords
|
||||||
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
||||||
value += addValue(5, "reach");
|
value += addValue(5, "reach");
|
||||||
@@ -127,7 +146,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
if (c.hasStartOfKeyword("PreventAllDamageBy")) {
|
if (c.hasStartOfKeyword("PreventAllDamageBy")) {
|
||||||
value += addValue(10, "prevent-dmg");
|
value += addValue(10, "prevent-dmg");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bad keywords
|
// Bad keywords
|
||||||
if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
|
if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
|
||||||
value -= subValue((power * 9) + 40, "defender");
|
value -= subValue((power * 9) + 40, "defender");
|
||||||
@@ -184,7 +203,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
|
|
||||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||||
if (sa.isAbility()) {
|
if (sa.isAbility()) {
|
||||||
value += addValue(10, "sa: " + sa);
|
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!c.getManaAbilities().isEmpty()) {
|
if (!c.getManaAbilities().isEmpty()) {
|
||||||
@@ -203,9 +222,38 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
if (!c.getEncodedCards().isEmpty()) {
|
if (!c.getEncodedCards().isEmpty()) {
|
||||||
value += addValue(24, "encoded");
|
value += addValue(24, "encoded");
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
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) {
|
protected int addValue(int value, String text) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import java.io.BufferedReader;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import forge.StaticData;
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
|
import forge.StaticData;
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
@@ -23,14 +29,21 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardFactory;
|
import forge.game.card.CardFactory;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
|
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.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.PlayerZone;
|
import forge.game.zone.PlayerZone;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.item.IPaperCard;
|
import forge.item.IPaperCard;
|
||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
|
import forge.util.TextUtil;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
public abstract class GameState {
|
public abstract class GameState {
|
||||||
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
|
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
|
||||||
@@ -48,17 +61,38 @@ public abstract class GameState {
|
|||||||
private String humanCounters = "";
|
private String humanCounters = "";
|
||||||
private String computerCounters = "";
|
private String computerCounters = "";
|
||||||
|
|
||||||
|
private boolean puzzleCreatorState = false;
|
||||||
|
|
||||||
private final Map<ZoneType, String> humanCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
|
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<ZoneType, String> aiCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
|
||||||
|
|
||||||
private final Map<Integer, Card> idToCard = new HashMap<>();
|
private final Map<Integer, Card> idToCard = new HashMap<>();
|
||||||
private final Map<Card, Integer> cardToAttachId = 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> 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 Map<String, String> abilityString = new HashMap<>();
|
||||||
|
|
||||||
|
private final Set<Card> cardsReferencedByID = new HashSet<>();
|
||||||
|
|
||||||
private String tChangePlayer = "NONE";
|
private String tChangePlayer = "NONE";
|
||||||
private String tChangePhase = "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() {
|
public GameState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,18 +101,31 @@ public abstract class GameState {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder sb = new StringBuilder();
|
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()) {
|
if (!humanCounters.isEmpty()) {
|
||||||
sb.append(String.format("humancounters=%s\n", humanCounters));
|
sb.append(TextUtil.concatNoSpace("humancounters=", humanCounters, "\n"));
|
||||||
}
|
}
|
||||||
if (!computerCounters.isEmpty()) {
|
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(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
|
||||||
sb.append(String.format("activephase=%s\n", tChangePhase));
|
sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
|
||||||
appendCards(humanCardTexts, "human", sb);
|
appendCards(humanCardTexts, "human", sb);
|
||||||
appendCards(aiCardTexts, "ai", sb);
|
appendCards(aiCardTexts, "ai", sb);
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
@@ -86,13 +133,13 @@ public abstract class GameState {
|
|||||||
|
|
||||||
private void appendCards(Map<ZoneType, String> cardTexts, String categoryPrefix, StringBuilder sb) {
|
private void appendCards(Map<ZoneType, String> cardTexts, String categoryPrefix, StringBuilder sb) {
|
||||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
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 {
|
public void initFromGame(Game game) throws Exception {
|
||||||
FCollectionView<Player> players = game.getPlayers();
|
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) {
|
if (players.size() != 2) {
|
||||||
throw new Exception("Game not supported");
|
throw new Exception("Game not supported");
|
||||||
}
|
}
|
||||||
@@ -110,12 +157,52 @@ public abstract class GameState {
|
|||||||
tChangePhase = game.getPhaseHandler().getPhase().toString();
|
tChangePhase = game.getPhaseHandler().getPhase().toString();
|
||||||
aiCardTexts.clear();
|
aiCardTexts.clear();
|
||||||
humanCardTexts.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()) {
|
for (ZoneType zone : ZONES.keySet()) {
|
||||||
// Init texts to empty, so that restoring will clear the state
|
// Init texts to empty, so that restoring will clear the state
|
||||||
// if the zone had no cards in it (e.g. empty hand).
|
// if the zone had no cards in it (e.g. empty hand).
|
||||||
aiCardTexts.put(zone, "");
|
aiCardTexts.put(zone, "");
|
||||||
humanCardTexts.put(zone, "");
|
humanCardTexts.put(zone, "");
|
||||||
for (Card card : game.getCardsIn(zone)) {
|
for (Card card : game.getCardsIn(zone)) {
|
||||||
|
if (card.getName().equals("Puzzle Goal") && card.getOracleText().contains("New Puzzle")) {
|
||||||
|
puzzleCreatorState = true;
|
||||||
|
}
|
||||||
if (card instanceof DetachedCardEffect) {
|
if (card instanceof DetachedCardEffect) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -140,6 +227,11 @@ public abstract class GameState {
|
|||||||
if (c.isCommander()) {
|
if (c.isCommander()) {
|
||||||
newText.append("|IsCommander");
|
newText.append("|IsCommander");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cardsReferencedByID.contains(c)) {
|
||||||
|
newText.append("|Id:").append(c.getId());
|
||||||
|
}
|
||||||
|
|
||||||
if (zoneType == ZoneType.Battlefield) {
|
if (zoneType == ZoneType.Battlefield) {
|
||||||
if (c.isTapped()) {
|
if (c.isTapped()) {
|
||||||
newText.append("|Tapped");
|
newText.append("|Tapped");
|
||||||
@@ -147,6 +239,16 @@ public abstract class GameState {
|
|||||||
if (c.isSick()) {
|
if (c.isSick()) {
|
||||||
newText.append("|SummonSick");
|
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()) {
|
if (c.isFaceDown()) {
|
||||||
newText.append("|FaceDown");
|
newText.append("|FaceDown");
|
||||||
if (c.isManifested()) {
|
if (c.isManifested()) {
|
||||||
@@ -155,11 +257,10 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||||
newText.append("|Transformed");
|
newText.append("|Transformed");
|
||||||
}
|
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||||
Map<CounterType, Integer> counters = c.getCounters();
|
newText.append("|Flipped");
|
||||||
if (!counters.isEmpty()) {
|
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||||
newText.append("|Counters:");
|
newText.append("|Meld");
|
||||||
newText.append(countersToString(counters));
|
|
||||||
}
|
}
|
||||||
if (c.getEquipping() != null) {
|
if (c.getEquipping() != null) {
|
||||||
newText.append("|Attaching:").append(c.getEquipping().getId());
|
newText.append("|Attaching:").append(c.getEquipping().getId());
|
||||||
@@ -169,10 +270,66 @@ public abstract class GameState {
|
|||||||
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!c.getEnchantedBy(false).isEmpty() || !c.getEquippedBy(false).isEmpty() || !c.getFortifiedBy(false).isEmpty()) {
|
if (c.getDamage() > 0) {
|
||||||
newText.append("|Id:").append(c.getId());
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
cardTexts.put(zoneType, newText.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +343,7 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
first = false;
|
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();
|
return counterString.toString();
|
||||||
}
|
}
|
||||||
@@ -298,6 +455,12 @@ public abstract class GameState {
|
|||||||
abilityString.put(categoryName.substring("ability".length()), categoryValue);
|
abilityString.put(categoryName.substring("ability".length()), categoryValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (categoryName.endsWith("precast")) {
|
||||||
|
if (isHuman)
|
||||||
|
precastHuman = categoryValue;
|
||||||
|
else
|
||||||
|
precastAI = categoryValue;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
System.out.println("Unknown key: " + categoryName);
|
System.out.println("Unknown key: " + categoryName);
|
||||||
}
|
}
|
||||||
@@ -318,6 +481,13 @@ public abstract class GameState {
|
|||||||
|
|
||||||
idToCard.clear();
|
idToCard.clear();
|
||||||
cardToAttachId.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;
|
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
|
||||||
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
|
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
|
||||||
@@ -331,22 +501,363 @@ public abstract class GameState {
|
|||||||
if (!computerCounters.isEmpty()) {
|
if (!computerCounters.isEmpty()) {
|
||||||
applyCountersToGameEntity(ai, computerCounters);
|
applyCountersToGameEntity(ai, computerCounters);
|
||||||
}
|
}
|
||||||
|
|
||||||
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
|
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
|
||||||
|
|
||||||
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
game.getTriggerHandler().setSuppressAllTriggers(true);
|
||||||
|
|
||||||
setupPlayerState(humanLife, humanCardTexts, human);
|
setupPlayerState(humanLife, humanCardTexts, human);
|
||||||
setupPlayerState(computerLife, aiCardTexts, ai);
|
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.getStack().setResolving(false);
|
||||||
|
|
||||||
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
|
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);
|
||||||
|
spellDef = spellDef.substring(0, spellDef.indexOf(":"));
|
||||||
|
} else if (spellDef.contains("->")) {
|
||||||
|
String tgtDef = spellDef.substring(spellDef.indexOf("->") + 2);
|
||||||
|
tgtID = parseTargetInScript(tgtDef);
|
||||||
|
spellDef = spellDef.substring(0, spellDef.indexOf("->"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
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(",");
|
String[] allCounterStrings = counterString.split(",");
|
||||||
for (final String counterPair : allCounterStrings) {
|
for (final String counterPair : allCounterStrings) {
|
||||||
String[] pair = counterPair.split("=", 2);
|
String[] pair = counterPair.split("=", 2);
|
||||||
@@ -356,7 +867,6 @@ public abstract class GameState {
|
|||||||
|
|
||||||
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
|
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
|
||||||
// Lock check static as we setup player state
|
// Lock check static as we setup player state
|
||||||
final Game game = p.getGame();
|
|
||||||
|
|
||||||
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
||||||
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
|
||||||
@@ -384,9 +894,13 @@ public abstract class GameState {
|
|||||||
Map<CounterType, Integer> counters = c.getCounters();
|
Map<CounterType, Integer> counters = c.getCounters();
|
||||||
// Note: Not clearCounters() since we want to keep the counters
|
// Note: Not clearCounters() since we want to keep the counters
|
||||||
// var as-is.
|
// var as-is.
|
||||||
c.setCounters(new HashMap<CounterType, Integer>());
|
c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
||||||
p.getZone(ZoneType.Hand).add(c);
|
p.getZone(ZoneType.Hand).add(c);
|
||||||
if (c.isAura()) {
|
if (c.isAura()) {
|
||||||
|
// 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);
|
||||||
|
|
||||||
p.getGame().getAction().moveToPlay(c, null);
|
p.getGame().getAction().moveToPlay(c, null);
|
||||||
} else {
|
} else {
|
||||||
p.getGame().getAction().moveToPlay(c, null);
|
p.getGame().getAction().moveToPlay(c, null);
|
||||||
@@ -401,25 +915,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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,6 +948,11 @@ public abstract class GameState {
|
|||||||
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
|
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
|
||||||
} else {
|
} else {
|
||||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode);
|
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);
|
c = Card.fromPaperCard(pc, player);
|
||||||
if (setCode != null) {
|
if (setCode != null) {
|
||||||
hasSetCurSet = true;
|
hasSetCurSet = true;
|
||||||
@@ -463,6 +963,13 @@ public abstract class GameState {
|
|||||||
for (final String info : cardinfo) {
|
for (final String info : cardinfo) {
|
||||||
if (info.startsWith("Tapped")) {
|
if (info.startsWith("Tapped")) {
|
||||||
c.tap();
|
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:")) {
|
} else if (info.startsWith("Counters:")) {
|
||||||
applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1));
|
applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1));
|
||||||
} else if (info.startsWith("SummonSick")) {
|
} else if (info.startsWith("SummonSick")) {
|
||||||
@@ -474,6 +981,10 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
} else if (info.startsWith("Transformed")) {
|
} else if (info.startsWith("Transformed")) {
|
||||||
c.setState(CardStateName.Transformed, true);
|
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")) {
|
} else if (info.startsWith("IsCommander")) {
|
||||||
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
|
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
|
||||||
c.setCommander(true);
|
c.setCommander(true);
|
||||||
@@ -488,6 +999,28 @@ public abstract class GameState {
|
|||||||
} else if (info.startsWith("Ability:")) {
|
} else if (info.startsWith("Ability:")) {
|
||||||
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
|
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
|
||||||
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
|
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("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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package forge.ai;
|
|||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import forge.AIOption;
|
|
||||||
import forge.LobbyPlayer;
|
import forge.LobbyPlayer;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.player.IGameEntitiesFactory;
|
import forge.game.player.IGameEntitiesFactory;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package forge.ai;
|
|||||||
import java.security.InvalidParameterException;
|
import java.security.InvalidParameterException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import forge.card.CardStateName;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
@@ -46,7 +47,6 @@ import forge.game.player.PlayerController;
|
|||||||
import forge.game.player.PlayerView;
|
import forge.game.player.PlayerView;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
import forge.game.spellability.*;
|
import forge.game.spellability.*;
|
||||||
import forge.game.trigger.Trigger;
|
|
||||||
import forge.game.trigger.WrappedAbility;
|
import forge.game.trigger.WrappedAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
@@ -115,7 +115,23 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
if (ability.getApi() != null) {
|
if (ability.getApi() != null) {
|
||||||
switch (ability.getApi()) {
|
switch (ability.getApi()) {
|
||||||
case ChooseNumber:
|
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:
|
case BidLife:
|
||||||
return 0;
|
return 0;
|
||||||
default:
|
default:
|
||||||
@@ -179,8 +195,8 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
|
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
|
||||||
final SpellAbility sa = wrapper.getWrappedAbility();
|
final SpellAbility sa = wrapper.getWrappedAbility();
|
||||||
final Trigger regtrig = wrapper.getTrigger();
|
//final Trigger regtrig = wrapper.getTrigger();
|
||||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
|
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -844,21 +860,40 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
@Override
|
@Override
|
||||||
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
|
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
|
||||||
if (sa.hasParam("AILogic")) {
|
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");
|
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")) {
|
if (logic.equals("MostProminentInComputerDeck")) {
|
||||||
return ComputerUtilCard.getMostProminentCardName(player.getCardsIn(ZoneType.Library));
|
return ComputerUtilCard.getMostProminentCardName(aiLibrary);
|
||||||
} else if (logic.equals("MostProminentInHumanDeck")) {
|
} else if (logic.equals("MostProminentInHumanDeck")) {
|
||||||
return ComputerUtilCard.getMostProminentCardName(player.getOpponent().getCardsIn(ZoneType.Library));
|
return ComputerUtilCard.getMostProminentCardName(oppLibrary);
|
||||||
} else if (logic.equals("MostProminentCreatureInComputerDeck")) {
|
} 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);
|
return ComputerUtilCard.getMostProminentCardName(cards);
|
||||||
} else if (logic.equals("BestCreatureInComputerDeck")) {
|
} else if (logic.equals("BestCreatureInComputerDeck")) {
|
||||||
return ComputerUtilCard.getBestCreatureAI(player.getCardsIn(ZoneType.Library)).getName();
|
return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
|
||||||
} else if (logic.equals("RandomInComputerDeck")) {
|
} else if (logic.equals("RandomInComputerDeck")) {
|
||||||
return Aggregates.random(player.getCardsIn(ZoneType.Library)).getName();
|
return Aggregates.random(aiLibrary).getName();
|
||||||
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
|
} 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);
|
return ComputerUtilCard.getMostProminentCardName(cards);
|
||||||
|
} else if (logic.equals("CursedScroll")) {
|
||||||
|
return SpecialCardAi.CursedScroll.chooseCard(player, sa);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
|
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());
|
||||||
|
|||||||
@@ -17,28 +17,20 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai;
|
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.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.card.CardFactoryUtil;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
@@ -46,8 +38,18 @@ import forge.game.phase.PhaseType;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerPredicates;
|
import forge.game.player.PlayerPredicates;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.staticability.StaticAbility;
|
||||||
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Aggregates;
|
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
|
* Special logic for individual cards
|
||||||
@@ -73,8 +75,8 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Black Lotus and Lotus Bloom
|
// Black Lotus and Lotus Bloom
|
||||||
public static class BlackLotus {
|
public static class BlackLotus {
|
||||||
public static boolean consider(Player ai, SpellAbility sa, ManaCostBeingPaid cost) {
|
public static boolean consider(final Player ai, final SpellAbility sa, final ManaCostBeingPaid cost) {
|
||||||
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true);
|
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||||
int numManaSrcs = manaSources.size();
|
int numManaSrcs = manaSources.size();
|
||||||
|
|
||||||
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN,
|
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN,
|
||||||
@@ -102,7 +104,7 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Bonds of Faith
|
// Bonds of Faith
|
||||||
public static class BondsOfFaith {
|
public static class BondsOfFaith {
|
||||||
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;
|
Card chosen = null;
|
||||||
|
|
||||||
List<Card> aiHumans = CardLists.filter(list, new Predicate<Card>() {
|
List<Card> aiHumans = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@@ -139,10 +141,26 @@ public class SpecialCardAi {
|
|||||||
return chosen;
|
return chosen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// 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
|
// Chain of Smog
|
||||||
public static class ChainOfSmog {
|
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()) {
|
if (ai.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||||
// to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point)
|
// 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
|
// TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a
|
||||||
@@ -165,11 +183,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
|
// Desecration Demon
|
||||||
public static class DesecrationDemon {
|
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
|
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"))));
|
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"))));
|
||||||
boolean hasUsefulBlocker = false;
|
boolean hasUsefulBlocker = false;
|
||||||
|
|
||||||
@@ -193,7 +266,7 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Donate
|
// Donate
|
||||||
public static class 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(
|
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
|
||||||
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
||||||
if (donateTarget != null) {
|
if (donateTarget != null) {
|
||||||
@@ -229,7 +302,7 @@ public class SpecialCardAi {
|
|||||||
return false;
|
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")));
|
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
|
||||||
if (donateTarget != null) {
|
if (donateTarget != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -243,9 +316,159 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Force of Will
|
// Force of Will
|
||||||
public static class ForceOfWill {
|
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));
|
CardCollection blueCards = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.isColor(MagicColor.BLUE));
|
||||||
|
|
||||||
boolean isExileMode = false;
|
boolean isExileMode = false;
|
||||||
@@ -272,8 +495,9 @@ public class SpecialCardAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guilty Conscience
|
||||||
public static class GuiltyConscience {
|
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;
|
Card chosen = null;
|
||||||
|
|
||||||
List<Card> aiStuffies = CardLists.filter(list, new Predicate<Card>() {
|
List<Card> aiStuffies = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@@ -307,11 +531,149 @@ public class SpecialCardAi {
|
|||||||
return chosen;
|
return chosen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 possibly other similar cards using AILogic LivingDeath)
|
// Living Death (and possibly other similar cards using AILogic LivingDeath)
|
||||||
public static class LivingDeath {
|
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 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);
|
CardCollection aiCreaturesInGY = CardLists.filter(ai.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES);
|
||||||
|
|
||||||
if (aiCreaturesInGY.isEmpty()) {
|
if (aiCreaturesInGY.isEmpty()) {
|
||||||
@@ -348,13 +710,67 @@ public class SpecialCardAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if we get more value out of this than our opponent does (hopefully), go for it
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Necropotence
|
||||||
public static class 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();
|
Game game = ai.getGame();
|
||||||
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
int computerHandSize = ai.getZone(ZoneType.Hand).size();
|
||||||
int maxHandSize = ai.getMaxHandSize();
|
int maxHandSize = ai.getMaxHandSize();
|
||||||
@@ -407,9 +823,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
|
// Nykthos, Shrine to Nyx
|
||||||
public static class NykthosShrineToNyx {
|
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();
|
Game game = ai.getGame();
|
||||||
PhaseHandler ph = game.getPhaseHandler();
|
PhaseHandler ph = game.getPhaseHandler();
|
||||||
if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||||
@@ -428,7 +869,7 @@ public class SpecialCardAi {
|
|||||||
final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
|
final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
|
||||||
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai);
|
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)) {
|
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
|
||||||
ManaCost cost = testSa.getPayCosts().getTotalMana();
|
ManaCost cost = testSa.getPayCosts().getTotalMana();
|
||||||
@@ -468,7 +909,7 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Phyrexian Dreadnought
|
// Phyrexian Dreadnought
|
||||||
public static class PhyrexianDreadnought {
|
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));
|
choices.sort(Collections.reverseOrder(ComputerUtilCard.EvaluateCreatureComparator));
|
||||||
int power = 0;
|
int power = 0;
|
||||||
List<Card> toKeep = Lists.newArrayList();
|
List<Card> toKeep = Lists.newArrayList();
|
||||||
@@ -492,11 +933,11 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Sarkhan the Mad
|
// Sarkhan the Mad
|
||||||
public static class SarkhanTheMad {
|
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;
|
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?
|
// 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();
|
CardCollection creatures = ai.getCreaturesInPlay();
|
||||||
boolean hasValidTgt = !CardLists.filter(creatures, new Predicate<Card>() {
|
boolean hasValidTgt = !CardLists.filter(creatures, new Predicate<Card>() {
|
||||||
@@ -513,7 +954,7 @@ public class SpecialCardAi {
|
|||||||
return false;
|
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 minLife = weakestOpp.getLife();
|
||||||
|
|
||||||
int dragonPower = 0;
|
int dragonPower = 0;
|
||||||
@@ -526,9 +967,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
|
// Timetwister
|
||||||
public static class 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();
|
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||||
int maxOppHandSize = 0;
|
int maxOppHandSize = 0;
|
||||||
|
|
||||||
@@ -549,9 +1104,45 @@ public class SpecialCardAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Volrath's Shapeshifter
|
||||||
|
public static class VolrathsShapeshifter {
|
||||||
|
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||||
|
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 ((topGY != null && !topGY.isCreature()) || creatHand != null) {
|
||||||
|
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 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();
|
Card source = sa.getHostCard();
|
||||||
Game game = source.getGame();
|
Game game = source.getGame();
|
||||||
|
|
||||||
@@ -610,7 +1201,7 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Yawgmoth's Bargain
|
// Yawgmoth's Bargain
|
||||||
public static class YawgmothsBargain {
|
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();
|
Game game = ai.getGame();
|
||||||
PhaseHandler ph = game.getPhaseHandler();
|
PhaseHandler ph = game.getPhaseHandler();
|
||||||
|
|
||||||
@@ -653,7 +1244,7 @@ public class SpecialCardAi {
|
|||||||
|
|
||||||
// Yawgmoth's Will (can potentially be expanded for other broadly similar effects too)
|
// Yawgmoth's Will (can potentially be expanded for other broadly similar effects too)
|
||||||
public static class YawgmothsWill {
|
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);
|
CardCollectionView cardsInGY = ai.getCardsIn(ZoneType.Graveyard);
|
||||||
if (cardsInGY.size() == 0) {
|
if (cardsInGY.size() == 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public abstract class SpellAbilityAi {
|
|||||||
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
|
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.BidLife, BidLifeAi.class)
|
.put(ApiType.BidLife, BidLifeAi.class)
|
||||||
.put(ApiType.Bond, BondAi.class)
|
.put(ApiType.Bond, BondAi.class)
|
||||||
.put(ApiType.Branch, AlwaysPlayAi.class)
|
.put(ApiType.Branch, AlwaysPlayAi.class)
|
||||||
|
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
|
||||||
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
||||||
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
||||||
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
|
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
|
||||||
@@ -71,6 +72,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.ExchangeControlVariant, CannotPlayAi.class)
|
.put(ApiType.ExchangeControlVariant, CannotPlayAi.class)
|
||||||
.put(ApiType.ExchangePower, PowerExchangeAi.class)
|
.put(ApiType.ExchangePower, PowerExchangeAi.class)
|
||||||
.put(ApiType.ExchangeZone, ZoneExchangeAi.class)
|
.put(ApiType.ExchangeZone, ZoneExchangeAi.class)
|
||||||
|
.put(ApiType.Explore, AlwaysPlayAi.class)
|
||||||
.put(ApiType.Fight, FightAi.class)
|
.put(ApiType.Fight, FightAi.class)
|
||||||
.put(ApiType.FlipACoin, FlipACoinAi.class)
|
.put(ApiType.FlipACoin, FlipACoinAi.class)
|
||||||
.put(ApiType.Fog, FogAi.class)
|
.put(ApiType.Fog, FogAi.class)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -21,7 +22,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Random r = MyRandom.getRandom();
|
final Random r = MyRandom.getRandom();
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
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 TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
@@ -87,7 +88,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ai.getOpponent());
|
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||||
}
|
}
|
||||||
|
|
||||||
return randomReturn;
|
return randomReturn;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
public class AlwaysPlayAi extends SpellAbilityAi {
|
public class AlwaysPlayAi extends SpellAbilityAi {
|
||||||
@@ -13,4 +14,9 @@ public class AlwaysPlayAi extends SpellAbilityAi {
|
|||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
package forge.ai.ability;
|
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.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import forge.ai.AiCardMemory;
|
import forge.ai.*;
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.ComputerUtilCost;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -38,6 +25,10 @@ import forge.game.trigger.TriggerHandler;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -86,13 +77,13 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
num = (num == null) ? "1" : num;
|
num = (num == null) ? "1" : num;
|
||||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
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));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||||
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
|
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
|
||||||
Card animatedCopy = becomeAnimated(source, sa);
|
Card animatedCopy = becomeAnimated(source, sa);
|
||||||
list.add(animatedCopy);
|
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);
|
topStack);
|
||||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||||
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
|
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||||
@@ -186,9 +177,19 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||||
return false;
|
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())) {
|
||||||
|
bFlag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (bFlag) {
|
||||||
|
this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||||
|
}
|
||||||
return bFlag; // All of the defined stuff is animated, not very useful
|
return bFlag; // All of the defined stuff is animated, not very useful
|
||||||
} else {
|
} else {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
// AI currently disabled for these costs
|
// AI currently disabled for these costs
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||||
@@ -250,6 +250,18 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return false;
|
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()) {
|
if (!c.isEnchanted()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -580,8 +592,8 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||||
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
|
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||||
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
|
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||||
|
|
||||||
String kws = stabMap.get("AddKeyword");
|
String kws = stabMap.get("AddKeyword");
|
||||||
if (kws != null) {
|
if (kws != null) {
|
||||||
@@ -702,30 +714,6 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return true;
|
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.
|
* Attach preference.
|
||||||
*
|
*
|
||||||
@@ -874,8 +862,8 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||||
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
|
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||||
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
|
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||||
|
|
||||||
grantingAbilities |= stabMap.containsKey("AddAbility");
|
grantingAbilities |= stabMap.containsKey("AddAbility");
|
||||||
|
|
||||||
@@ -1077,10 +1065,10 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
// make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around
|
// 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);
|
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);
|
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach);
|
||||||
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
if (futureSpell != null && futureSpell.getHostCard() != null) {
|
||||||
aic.reserveManaSourcesForMain2(futureSpell);
|
aic.reserveManaSources(futureSpell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
@@ -16,7 +17,7 @@ public class BalanceAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
int diff = 0;
|
int diff = 0;
|
||||||
// TODO Add support for multiplayer logic
|
// 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 humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
||||||
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
|
|||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (tgt.canTgtCreature()) {
|
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);
|
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ public final class BondAi extends SpellAbilityAi {
|
|||||||
* <p>
|
* <p>
|
||||||
* bondCanPlayAI.
|
* bondCanPlayAI.
|
||||||
* </p>
|
* </p>
|
||||||
|
* @param aiPlayer
|
||||||
|
* a {@link forge.game.player.Player} object.
|
||||||
* @param sa
|
* @param sa
|
||||||
* a {@link forge.game.spellability.SpellAbility} object.
|
* a {@link forge.game.spellability.SpellAbility} object.
|
||||||
* @param af
|
*
|
||||||
* a {@link forge.game.ability.AbilityFactory} object.
|
|
||||||
*
|
|
||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@@ -53,4 +53,9 @@ public final class BondAi extends SpellAbilityAi {
|
|||||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||||
return ComputerUtilCard.getBestCreatureAI(options);
|
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;
|
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.Predicate;
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -22,11 +12,7 @@ import forge.game.GameObject;
|
|||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.CardPredicates.Presets;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
@@ -40,6 +26,9 @@ import forge.game.spellability.AbilitySub;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
public class ChangeZoneAi extends SpellAbilityAi {
|
public class ChangeZoneAi extends SpellAbilityAi {
|
||||||
/*
|
/*
|
||||||
@@ -48,7 +37,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
* idea to re-factor ChangeZoneAi into more specific effects since it is really doing
|
* idea to re-factor ChangeZoneAi into more specific effects since it is really doing
|
||||||
* too much: blink/bounce/exile/tutor/Raise Dead/Surgical Extraction/......
|
* 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
|
@Override
|
||||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||||
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
|
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
|
||||||
@@ -64,23 +57,45 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
return false;
|
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);
|
return super.checkAiLogic(ai, sa, aiLogic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
|
||||||
// Checks for "return true" unlike checkAiLogic()
|
// Checks for "return true" unlike checkAiLogic()
|
||||||
|
|
||||||
|
multipleCardsToChoose.clear();
|
||||||
String aiLogic = sa.getParam("AILogic");
|
String aiLogic = sa.getParam("AILogic");
|
||||||
if (aiLogic != null) {
|
if (aiLogic != null) {
|
||||||
if (aiLogic.equals("Always")) {
|
if (aiLogic.equals("Always")) {
|
||||||
return true;
|
return true;
|
||||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||||
return this.doSacAndUpgradeLogic(aiPlayer, sa);
|
return this.doSacAndUpgradeLogic(aiPlayer, sa);
|
||||||
|
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||||
|
return this.doSacAndReturnFromGraveLogic(aiPlayer, sa);
|
||||||
} else if (aiLogic.equals("Necropotence")) {
|
} else if (aiLogic.equals("Necropotence")) {
|
||||||
return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
|
return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
|
||||||
} else if (aiLogic.equals("SameName")) { // Declaration in Stone
|
} else if (aiLogic.equals("SameName")) { // Declaration in Stone
|
||||||
return this.doSameNameLogic(aiPlayer, sa);
|
return this.doSameNameLogic(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)) {
|
if (isHidden(sa)) {
|
||||||
@@ -166,7 +181,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
ZoneType origin = null;
|
ZoneType origin = null;
|
||||||
final Player opponent = ai.getOpponent();
|
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||||
|
|
||||||
if (sa.hasParam("Origin")) {
|
if (sa.hasParam("Origin")) {
|
||||||
@@ -182,7 +197,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
// AI currently disabled for these costs
|
// AI currently disabled for these costs
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
|
||||||
&& !(destination.equals("Battlefield") && !source.isLand())) {
|
&& !(destination.equals("Battlefield") && !source.isLand())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -367,7 +382,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
// if putting cards from hand to library and parent is drawing cards
|
// if putting cards from hand to library and parent is drawing cards
|
||||||
// make sure this will actually do something:
|
// make sure this will actually do something:
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Player opp = aiPlayer.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||||
if (tgt != null && tgt.canTgtPlayer()) {
|
if (tgt != null && tgt.canTgtPlayer()) {
|
||||||
boolean isCurse = sa.isCurse();
|
boolean isCurse = sa.isCurse();
|
||||||
if (isCurse && sa.canTarget(opp)) {
|
if (isCurse && sa.canTarget(opp)) {
|
||||||
@@ -428,7 +443,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
Iterable<Player> pDefined;
|
Iterable<Player> pDefined;
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if ((tgt != null) && tgt.canTgtPlayer()) {
|
if ((tgt != null) && tgt.canTgtPlayer()) {
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (sa.isCurse()) {
|
if (sa.isCurse()) {
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
@@ -547,8 +562,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
private static Card chooseCreature(final Player ai, CardCollection list) {
|
private static Card chooseCreature(final Player ai, CardCollection list) {
|
||||||
// Creating a new combat for testing purposes.
|
// Creating a new combat for testing purposes.
|
||||||
Combat combat = new Combat(ai.getOpponent());
|
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||||
for (Card att : ai.getOpponent().getCreaturesInPlay()) {
|
Combat combat = new Combat(opponent);
|
||||||
|
for (Card att : opponent.getCreaturesInPlay()) {
|
||||||
combat.addAttacker(att, ai);
|
combat.addAttacker(att, ai);
|
||||||
}
|
}
|
||||||
AiBlockController block = new AiBlockController(ai);
|
AiBlockController block = new AiBlockController(ai);
|
||||||
@@ -647,6 +663,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return false;
|
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();
|
final AbilitySub subAb = sa.getSubAbility();
|
||||||
@@ -666,6 +696,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||||
|
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
|
if (aiLogic.equals("SurvivalOfTheFittest")) {
|
||||||
|
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden(sa)) {
|
if (isHidden(sa)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -828,7 +864,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
&& !currCombat.getBlockers(attacker).isEmpty()) {
|
&& !currCombat.getBlockers(attacker).isEmpty()) {
|
||||||
ComputerUtilCard.sortByEvaluateCreature(blockers);
|
ComputerUtilCard.sortByEvaluateCreature(blockers);
|
||||||
Combat combat = new Combat(ai);
|
Combat combat = new Combat(ai);
|
||||||
combat.addAttacker(attacker, ai.getOpponent());
|
combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
|
||||||
for (Card blocker : blockers) {
|
for (Card blocker : blockers) {
|
||||||
combat.addBlocker(attacker, blocker);
|
combat.addBlocker(attacker, blocker);
|
||||||
}
|
}
|
||||||
@@ -1000,6 +1036,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return false;
|
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)) {
|
if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1284,6 +1324,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (sa.getTargetRestrictions() == null) {
|
if (sa.getTargetRestrictions() == null) {
|
||||||
// Just in case of Defined cases
|
// Just in case of Defined cases
|
||||||
if (!mandatory && sa.hasParam("AttachedTo")) {
|
if (!mandatory && sa.hasParam("AttachedTo")) {
|
||||||
@@ -1319,6 +1363,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else if ("WorstCard".equals(logic)) {
|
} else if ("WorstCard".equals(logic)) {
|
||||||
return ComputerUtilCard.getWorstAI(fetchList);
|
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()) {
|
if (fetchList.isEmpty()) {
|
||||||
@@ -1458,8 +1512,34 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
// But just in case it does, just select the first option
|
// But just in case it does, just select the first option
|
||||||
return Iterables.getFirst(options, null);
|
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();
|
Card source = sa.getHostCard();
|
||||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParam("AILogic");
|
||||||
@@ -1623,6 +1703,45 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return true;
|
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) {
|
private static void rememberBouncedThisTurn(Player ai, Card c) {
|
||||||
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import forge.ai.*;
|
||||||
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.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -28,6 +15,9 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
public class ChangeZoneAllAi extends SpellAbilityAi {
|
public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
@@ -73,14 +63,25 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
|
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
|
||||||
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
|
||||||
|
|
||||||
// Living Death AI
|
|
||||||
if ("LivingDeath".equals(sa.getParam("AILogic"))) {
|
if ("LivingDeath".equals(sa.getParam("AILogic"))) {
|
||||||
|
// Living Death AI
|
||||||
return SpecialCardAi.LivingDeath.consider(ai, sa);
|
return SpecialCardAi.LivingDeath.consider(ai, sa);
|
||||||
}
|
} else if ("Timetwister".equals(sa.getParam("AILogic"))) {
|
||||||
|
// Timetwister AI
|
||||||
// Timetwister AI
|
|
||||||
if ("Timetwister".equals(sa.getParam("AILogic"))) {
|
|
||||||
return SpecialCardAi.Timetwister.consider(ai, sa);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO improve restrictions on when the AI would want to use this
|
// TODO improve restrictions on when the AI would want to use this
|
||||||
@@ -130,15 +131,39 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
computerType = new CardCollection();
|
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)
|
if ((CardLists.getNotType(oppType, "Creature").size() == 0)
|
||||||
&& (CardLists.getNotType(computerType, "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)) {
|
.evaluateCreatureList(oppType)) {
|
||||||
return false;
|
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
|
// permanents are more valuable
|
||||||
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + 3) >= ComputerUtilCard
|
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
|
||||||
.evaluatePermanentList(oppType)) {
|
.evaluatePermanentList(oppType)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -180,6 +205,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
// minimum card advantage unless the hand will be fully reloaded
|
// minimum card advantage unless the hand will be fully reloaded
|
||||||
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
|
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());
|
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
|
||||||
}
|
}
|
||||||
} else if (origin.equals(ZoneType.Stack)) {
|
} else if (origin.equals(ZoneType.Stack)) {
|
||||||
@@ -231,8 +264,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
* </p>
|
* </p>
|
||||||
* @param sa
|
* @param sa
|
||||||
* a {@link forge.game.spellability.SpellAbility} object.
|
* a {@link forge.game.spellability.SpellAbility} object.
|
||||||
* @param af
|
* @param aiPlayer
|
||||||
* a {@link forge.game.ability.AbilityFactory} object.
|
* a {@link forge.game.player.Player} object.
|
||||||
*
|
*
|
||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import java.util.List;
|
|||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilAbility;
|
import forge.ai.ComputerUtilAbility;
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
@@ -122,7 +124,7 @@ public class ChooseCardAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else if (aiLogic.equals("Duneblast")) {
|
} else if (aiLogic.equals("Duneblast")) {
|
||||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||||
CardCollection oppCreatures = ai.getOpponent().getCreaturesInPlay();
|
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
|
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
|
||||||
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
|
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import com.google.common.collect.Iterables;
|
|||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.StaticData;
|
import forge.StaticData;
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.card.CardDb;
|
import forge.card.CardDb;
|
||||||
import forge.card.CardRules;
|
import forge.card.CardRules;
|
||||||
import forge.card.CardSplitType;
|
import forge.card.CardSplitType;
|
||||||
@@ -36,29 +33,16 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParam("AILogic");
|
||||||
if (logic.equals("MomirAvatar")) {
|
if (logic.equals("MomirAvatar")) {
|
||||||
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
|
return SpecialCardAi.MomirVigAvatar.consider(ai, sa);
|
||||||
return false;
|
} else if (logic.equals("CursedScroll")) {
|
||||||
}
|
return SpecialCardAi.CursedScroll.consider(ai, sa);
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (tgt.canOnlyTgtOpponent()) {
|
if (tgt.canOnlyTgtOpponent()) {
|
||||||
sa.getTargets().add(ai.getOpponent());
|
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||||
} else {
|
} else {
|
||||||
sa.getTargets().add(ai);
|
sa.getTargets().add(ai);
|
||||||
}
|
}
|
||||||
@@ -78,6 +62,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||||
|
|
||||||
return ComputerUtilCard.getBestAI(options);
|
return ComputerUtilCard.getBestAI(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilAbility;
|
import forge.ai.ComputerUtilAbility;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
@@ -53,7 +52,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("Addle".equals(sourceName)) {
|
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 false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -62,7 +61,7 @@ public class ChooseColorAi extends SpellAbilityAi {
|
|||||||
if (logic.equals("MostExcessOpponentControls")) {
|
if (logic.equals("MostExcessOpponentControls")) {
|
||||||
for (byte color : MagicColor.WUBRG) {
|
for (byte color : MagicColor.WUBRG) {
|
||||||
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
|
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));
|
ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
|
||||||
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// milling against Tamiyo is pointless
|
// milling against Tamiyo is pointless
|
||||||
if (owner.isCardInCommand("Tamiyo, the Moon Sage emblem")) {
|
if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) {
|
||||||
return allow;
|
return allow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -16,8 +17,9 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
|||||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (sa.canTarget(aiPlayer.getOpponent())) {
|
Player opp = ComputerUtil.getOpponentFor(aiPlayer);
|
||||||
sa.getTargets().add(aiPlayer.getOpponent());
|
if (sa.canTarget(opp)) {
|
||||||
|
sa.getTargets().add(opp);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.List;
|
|||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
@@ -51,7 +52,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +64,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (sa.canTarget(ai.getOpponent())) {
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
sa.getTargets().add(ai.getOpponent());
|
if (sa.canTarget(opp)) {
|
||||||
|
sa.getTargets().add(opp);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -15,6 +13,9 @@ import forge.game.player.Player;
|
|||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class CloneAi extends SpellAbilityAi {
|
public class CloneAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -131,13 +132,19 @@ public class CloneAi extends SpellAbilityAi {
|
|||||||
* cloneTgtAI.
|
* cloneTgtAI.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param af
|
|
||||||
* a {@link forge.game.ability.AbilityFactory} object.
|
|
||||||
* @param sa
|
* @param sa
|
||||||
* a {@link forge.game.spellability.SpellAbility} object.
|
* a {@link forge.game.spellability.SpellAbility} object.
|
||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
private boolean cloneTgtAI(final SpellAbility sa) {
|
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
|
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||||
// two are the only things
|
// two are the only things
|
||||||
// that clone a target. Those can just use SVar:RemAIDeck:True until
|
// 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.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
|||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|
||||||
CardCollection list =
|
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
|
// AI won't try to grab cards that are filtered out of AI decks on
|
||||||
// purpose
|
// purpose
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
|||||||
@@ -227,16 +227,18 @@ public class ControlGainAi extends SpellAbilityAi {
|
|||||||
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.isCreature())
|
if (t != null) {
|
||||||
creatures--;
|
if (t.isCreature())
|
||||||
if (t.isPlaneswalker())
|
creatures--;
|
||||||
planeswalkers--;
|
if (t.isPlaneswalker())
|
||||||
if (t.isLand())
|
planeswalkers--;
|
||||||
lands--;
|
if (t.isLand())
|
||||||
if (t.isArtifact())
|
lands--;
|
||||||
artifacts--;
|
if (t.isArtifact())
|
||||||
if (t.isEnchantment())
|
artifacts--;
|
||||||
enchantments--;
|
if (t.isEnchantment())
|
||||||
|
enchantments--;
|
||||||
|
}
|
||||||
|
|
||||||
if (!sa.canTarget(t)) {
|
if (!sa.canTarget(t)) {
|
||||||
list.remove(t);
|
list.remove(t);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
|||||||
import forge.ai.SpecialCardAi;
|
import forge.ai.SpecialCardAi;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -25,9 +26,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
|||||||
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||||
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
|
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
|
||||||
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
|
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
|
||||||
|
|
||||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||||
|
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||||
|
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.chkAIDrawback(sa, aiPlayer);
|
return super.chkAIDrawback(sa, aiPlayer);
|
||||||
@@ -37,5 +39,17 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
|||||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
||||||
return spells.get(0);
|
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;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.AiController;
|
import forge.ai.*;
|
||||||
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.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
@@ -25,6 +16,8 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
public class CounterAi extends SpellAbilityAi {
|
public class CounterAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -43,7 +36,7 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
// AI currently disabled for these costs
|
// AI currently disabled for these costs
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||||
@@ -93,8 +86,9 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
|
||||||
|
|
||||||
if (unlessCost != null && !unlessCost.endsWith(">")) {
|
if (unlessCost != null && !unlessCost.endsWith(">")) {
|
||||||
// Is this Usable Mana Sources? Or Total Available Mana?
|
Player opp = tgtSA.getActivatingPlayer();
|
||||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||||
|
|
||||||
int toPay = 0;
|
int toPay = 0;
|
||||||
boolean setPayX = false;
|
boolean setPayX = false;
|
||||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||||
@@ -137,6 +131,10 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
if (tgtCMC < minCMC) {
|
if (tgtCMC < minCMC) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if ("NullBrooch".equals(logic)) {
|
||||||
|
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +144,31 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
boolean ctrCmc0ManaPerms = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS);
|
boolean ctrCmc0ManaPerms = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS);
|
||||||
boolean ctrDamageSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_DAMAGE_SPELLS);
|
boolean ctrDamageSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_DAMAGE_SPELLS);
|
||||||
boolean ctrRemovalSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_REMOVAL_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);
|
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);
|
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)) {
|
if (tgtSA != null && tgtCMC < aic.getIntProperty(AiProps.MIN_SPELL_CMC_TO_COUNTER)) {
|
||||||
boolean dontCounter = true;
|
dontCounter = true;
|
||||||
Card tgtSource = tgtSA.getHostCard();
|
Card tgtSource = tgtSA.getHostCard();
|
||||||
if ((tgtSource != null && tgtCMC == 0 && tgtSource.isPermanent() && !tgtSource.getManaAbilities().isEmpty() && ctrCmc0ManaPerms)
|
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.DealDamage || tgtSA.getApi() == ApiType.LoseLife || tgtSA.getApi() == ApiType.DamageAll && ctrDamageSpells)
|
||||||
|| (tgtSA.getApi() == ApiType.Counter && ctrOtherCounters)
|
|| (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.Destroy || tgtSA.getApi() == ApiType.DestroyAll || tgtSA.getApi() == ApiType.Sacrifice
|
||||||
|| tgtSA.getApi() == ApiType.SacrificeAll && ctrRemovalSpells)) {
|
|| tgtSA.getApi() == ApiType.SacrificeAll && ctrRemovalSpells)) {
|
||||||
dontCounter = false;
|
dontCounter = false;
|
||||||
@@ -166,17 +181,21 @@ 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())) {
|
|
||||||
dontCounter = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dontCounter) {
|
// should not refrain from countering a CMC X spell if that's the only CMC
|
||||||
return false;
|
// 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;
|
return toReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +231,9 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
if (unlessCost != null) {
|
if (unlessCost != null) {
|
||||||
// Is this Usable Mana Sources? Or Total Available Mana?
|
Player opp = tgtSA.getActivatingPlayer();
|
||||||
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
|
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
|
||||||
|
|
||||||
int toPay = 0;
|
int toPay = 0;
|
||||||
boolean setPayX = false;
|
boolean setPayX = false;
|
||||||
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import forge.game.combat.CombatUtil;
|
|||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.cost.CostRemoveCounter;
|
import forge.game.cost.CostRemoveCounter;
|
||||||
|
import forge.game.cost.CostSacrifice;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -225,13 +226,42 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
|
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) {
|
if (playAggro) {
|
||||||
// aggro profiles ignore conservative play for this AI logic
|
// aggro profiles ignore conservative play for this AI logic
|
||||||
return true;
|
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) {
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +356,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
|
||||||
if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +403,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)) {
|
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -491,7 +528,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
boolean immediately = ComputerUtil.playImmediately(ai, sa);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,6 +707,13 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||||
preferred = false;
|
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()) {
|
if (list.isEmpty()) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -35,10 +37,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
final String type = sa.getParam("CounterType");
|
final String type = sa.getParam("CounterType");
|
||||||
final String amountStr = sa.getParam("CounterNum");
|
final String amountStr = sa.getParam("CounterNum");
|
||||||
final String valid = sa.getParam("ValidCards");
|
final String valid = sa.getParam("ValidCards");
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
final boolean curse = sa.isCurse();
|
final boolean curse = sa.isCurse();
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
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);
|
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
@@ -51,13 +54,19 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
Player pl = curse ? ai.getOpponent() : ai;
|
Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
|
||||||
sa.getTargets().add(pl);
|
sa.getTargets().add(pl);
|
||||||
|
|
||||||
hList = CardLists.filterControlledBy(hList, pl);
|
hList = CardLists.filterControlledBy(hList, pl);
|
||||||
@@ -138,6 +147,6 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
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) {
|
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
|
||||||
int restDamage = d;
|
int restDamage = d;
|
||||||
final Game game = comp.getGame();
|
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)) {
|
if (!sa.canTarget(enemy)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -70,8 +77,11 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
|||||||
value = 1.0f * restDamage / enemy.getLife();
|
value = 1.0f * restDamage / enemy.getLife();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (phase.isPlayerTurn(enemy) && phase.is(PhaseType.END_OF_TURN)) {
|
if (phase.isPlayerTurn(enemy)) {
|
||||||
value = 1.5f * restDamage / enemy.getLife();
|
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
|
if (value > 0) { //more likely to burn with larger hand
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -56,16 +55,35 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
x = source.getCounters(CounterType.LOYALTY);
|
x = source.getCounters(CounterType.LOYALTY);
|
||||||
}
|
}
|
||||||
if (x == -1) {
|
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;
|
return evaluateDamageAll(ai, sa, source, dmg) > 0;
|
||||||
} else {
|
} else {
|
||||||
int best = -1, best_x = -1;
|
int best = -1, best_x = -1;
|
||||||
for (int i = 0; i < x; i++) {
|
Player bestOpp = determineOppToKill(ai, sa, source, x);
|
||||||
final int value = evaluateDamageAll(ai, sa, source, i);
|
if (bestOpp != null) {
|
||||||
if (value > best) {
|
// we can finish off a player, so go for it
|
||||||
best = value;
|
|
||||||
best_x = i;
|
// 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 (best_x > 0) {
|
||||||
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
if (sa.getSVar(damage).equals("Count$xPaid")) {
|
||||||
source.setSVar("PayX", Integer.toString(best_x));
|
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) {
|
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);
|
final CardCollection humanList = getKillableCreatures(sa, opp, dmg);
|
||||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||||
|
|
||||||
@@ -98,62 +139,56 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we can kill human, do it
|
int minGain = 200; // The minimum gain in destroyed creatures
|
||||||
if ((validP.equals("Player") || validP.contains("Opponent"))
|
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
|
||||||
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
if (computerList.isEmpty()) {
|
||||||
return 1;
|
minGain = 10; // nothing to lose
|
||||||
}
|
// no creatures to lose and player can be damaged
|
||||||
|
// so do it if it's helping!
|
||||||
int minGain = 200; // The minimum gain in destroyed creatures
|
// ----------------------------
|
||||||
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
|
// needs future improvement on pestilence :
|
||||||
if (computerList.isEmpty()) {
|
// what if we lose creatures but can win by repeated activations?
|
||||||
minGain = 10; // nothing to lose
|
// that tactic only works if there are creatures left to keep pestilence in play
|
||||||
// no creatures to lose and player can be damaged
|
// and can kill the player in a reasonable amount of time (no more than 2-3 turns?)
|
||||||
// so do it if it's helping!
|
if (validP.equals("Player")) {
|
||||||
// ----------------------------
|
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
|
||||||
// needs future improvement on pestilence :
|
// When using Pestilence to hurt players, do it at
|
||||||
// what if we lose creatures but can win by repeated activations?
|
// the end of the opponent's turn only
|
||||||
// that tactic only works if there are creatures left to keep pestilence in play
|
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|
||||||
// and can kill the player in a reasonable amount of time (no more than 2-3 turns?)
|
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||||
if (validP.equals("Player")) {
|
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
|
||||||
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
|
// Need further improvement : if able to kill immediately with repeated activations, do not wait
|
||||||
// When using Pestilence to hurt players, do it at
|
// for phases! Will also need to implement considering repeated activations for killed creatures!
|
||||||
// the end of the opponent's turn only
|
// || (ai.sa.getPayCosts(). ??? )
|
||||||
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|
{
|
||||||
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
// would take zero damage, and hurt opponent, do it!
|
||||||
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
|
if (ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)<1) {
|
||||||
// Need further improvement : if able to kill immediately with repeated activations, do not wait
|
return 1;
|
||||||
// for phases! Will also need to implement considering repeated activations for killed creatures!
|
}
|
||||||
// || (ai.sa.getPayCosts(). ??? )
|
// enemy is expected to die faster than AI from damage if repeated
|
||||||
{
|
if (ai.getLife() > ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)
|
||||||
// would take zero damage, and hurt opponent, do it!
|
* ((opp.getLife() + ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) - 1)
|
||||||
if (ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)<1) {
|
/ ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
||||||
return 1;
|
// enemy below 10 life, go for it!
|
||||||
}
|
if ((opp.getLife() < 10)
|
||||||
// enemy is expected to die faster than AI from damage if repeated
|
&& (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) >= 1)) {
|
||||||
if (ai.getLife() > ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)
|
return 1;
|
||||||
* ((opp.getLife() + ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) - 1)
|
}
|
||||||
/ ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
// At least half enemy remaining life can be removed in one go
|
||||||
// enemy below 10 life, go for it!
|
// worth doing even if enemy still has high health - one more copy of spell to win!
|
||||||
if ((opp.getLife() < 10)
|
if (opp.getLife() <= 2 * ComputerUtilCombat.predictDamageTo(opp, dmg, source, false)) {
|
||||||
&& (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) >= 1)) {
|
return 1;
|
||||||
return 1;
|
}
|
||||||
}
|
}
|
||||||
// At least half enemy remaining life can be removed in one go
|
}
|
||||||
// worth doing even if enemy still has high health - one more copy of spell to win!
|
}
|
||||||
if (opp.getLife() <= 2 * ComputerUtilCombat.predictDamageTo(opp, dmg, source, false)) {
|
}
|
||||||
return 1;
|
} else {
|
||||||
}
|
minGain = 100; // safety for errors in evaluate creature
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
minGain = 100; // safety for errors in evaluate creature
|
|
||||||
}
|
|
||||||
} else if (sa.getSubAbility() != null && ai.getGame().getPhaseHandler().isPreCombatMain() && computerList.isEmpty()
|
} else if (sa.getSubAbility() != null && ai.getGame().getPhaseHandler().isPreCombatMain() && computerList.isEmpty()
|
||||||
&& opp.getCreaturesInPlay().size() > 1 && !ai.getCreaturesInPlay().isEmpty()) {
|
&& opp.getCreaturesInPlay().size() > 1 && !ai.getCreaturesInPlay().isEmpty()) {
|
||||||
minGain = 126; // prepare for attack
|
minGain = 126; // prepare for attack
|
||||||
}
|
}
|
||||||
|
|
||||||
return ComputerUtilCard.evaluateCreatureList(humanList) - ComputerUtilCard.evaluateCreatureList(computerList)
|
return ComputerUtilCard.evaluateCreatureList(humanList) - ComputerUtilCard.evaluateCreatureList(computerList)
|
||||||
@@ -179,7 +214,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate creatures getting killed
|
// Evaluate creatures getting killed
|
||||||
Player enemy = ai.getOpponent();
|
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
@@ -261,7 +296,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate creatures getting killed
|
// Evaluate creatures getting killed
|
||||||
Player enemy = ai.getOpponent();
|
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||||
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
|
||||||
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameObject;
|
import forge.game.GameObject;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -28,6 +20,9 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class DamageDealAi extends DamageAiBase {
|
public class DamageDealAi extends DamageAiBase {
|
||||||
@Override
|
@Override
|
||||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||||
@@ -98,6 +93,29 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
source.setSVar("PayX", Integer.toString(dmg));
|
source.setSVar("PayX", Integer.toString(dmg));
|
||||||
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
|
} 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
|
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;
|
dmg += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
if ("DiscardLands".equals(logic)) {
|
if ("DiscardLands".equals(logic)) {
|
||||||
dmg = 2;
|
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)) {
|
} 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
|
// 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);
|
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source);
|
||||||
@@ -157,7 +186,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,8 +374,8 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
||||||
|
|
||||||
Player enemy = ai.getOpponent();
|
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||||
|
|
||||||
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
|
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()
|
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
|
||||||
if (tgt.canTgtCreatureAndPlayer() && this.shouldTgtP(ai, sa, dmg, noPrevention)){
|
if (tgt.canTgtCreatureAndPlayer() && this.shouldTgtP(ai, sa, dmg, noPrevention)){
|
||||||
@@ -377,7 +406,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
}
|
}
|
||||||
if ("Polukranos".equals(sa.getParam("AILogic"))) {
|
if ("Polukranos".equals(sa.getParam("AILogic"))) {
|
||||||
int dmgTaken = 0;
|
int dmgTaken = 0;
|
||||||
CardCollection humCreatures = ai.getOpponent().getCreaturesInPlay();
|
CardCollection humCreatures = enemy.getCreaturesInPlay();
|
||||||
Card lastTgt = null;
|
Card lastTgt = null;
|
||||||
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
||||||
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
|
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
|
||||||
@@ -633,7 +662,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
// this is for Triggered targets that are mandatory
|
// this is for Triggered targets that are mandatory
|
||||||
final boolean noPrevention = sa.hasParam("NoPrevention");
|
final boolean noPrevention = sa.hasParam("NoPrevention");
|
||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
System.out.println("damageChooseRequiredTargets " + ai + " " + sa);
|
System.out.println("damageChooseRequiredTargets " + ai + " " + sa);
|
||||||
|
|
||||||
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import forge.ai.SpecialCardAi;
|
import forge.ai.SpecialCardAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
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.Player;
|
||||||
import forge.game.player.PlayerCollection;
|
import forge.game.player.PlayerCollection;
|
||||||
import forge.game.player.PlayerPredicates;
|
import forge.game.player.PlayerPredicates;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class DamagePreventAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class DamagePreventAllAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -42,7 +43,7 @@ public class DebuffAi extends SpellAbilityAi {
|
|||||||
final Cost cost = sa.getPayCosts();
|
final Cost cost = sa.getPayCosts();
|
||||||
|
|
||||||
// temporarily disabled until AI is improved
|
// temporarily disabled until AI is improved
|
||||||
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) {
|
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ public class DebuffAi extends SpellAbilityAi {
|
|||||||
* @return a CardCollection.
|
* @return a CardCollection.
|
||||||
*/
|
*/
|
||||||
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
|
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);
|
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@@ -216,7 +217,7 @@ public class DebuffAi extends SpellAbilityAi {
|
|||||||
list.remove(c);
|
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 CardCollection forced = CardLists.filterControlledBy(list, ai);
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import forge.ai.AiController;
|
import forge.ai.*;
|
||||||
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.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.cost.CostSacrifice;
|
import forge.game.cost.CostSacrifice;
|
||||||
@@ -49,7 +35,7 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
CardCollection list;
|
CardCollection list;
|
||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +64,11 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
if ("MadSarkhanDragon".equals(logic)) {
|
if ("MadSarkhanDragon".equals(logic)) {
|
||||||
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
|
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)) {
|
} else if ("Polymorph".equals(logic)) {
|
||||||
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
@@ -267,7 +258,7 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
} else if (sa.hasParam("Defined")) {
|
} else if (sa.hasParam("Defined")) {
|
||||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||||
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|
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)
|
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|
||||||
|| ai.getLife() <= 5)) {
|
|| ai.getLife() <= 5)) {
|
||||||
// Basic ai logic for Lethal Vapors
|
// Basic ai logic for Lethal Vapors
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||||
final Card source = sa.getHostCard();
|
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;
|
final int CREATURE_EVAL_THRESHOLD = 200;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import forge.ai.SpellAbilityAi;
|
|||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
@@ -26,7 +25,7 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Card host = sa.getHostCard();
|
final Card host = sa.getHostCard();
|
||||||
Player libraryOwner = ai;
|
Player libraryOwner = ai;
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (mandatory && sa.canTarget(opp)) {
|
if (mandatory && sa.canTarget(opp)) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -20,6 +21,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
Card source = sa.getHostCard();
|
Card source = sa.getHostCard();
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
double chance = .4; // 40 percent chance with instant speed stuff
|
double chance = .4; // 40 percent chance with instant speed stuff
|
||||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||||
chance = .667; // 66.7% chance for sorcery speed (since it will
|
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);
|
final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||||
|
|
||||||
Player libraryOwner = ai;
|
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()) {
|
if (sa.usesTargeting()) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -109,7 +126,7 @@ public class DigUntilAi extends SpellAbilityAi {
|
|||||||
// or none in play with one in library, oath
|
// or none in play with one in library, oath
|
||||||
return creaturesInLibrary.size() > 2
|
return creaturesInLibrary.size() > 2
|
||||||
|| (creaturesInBattlefield.size() == 0 && creaturesInLibrary.size() > 0);
|
|| (creaturesInBattlefield.size() == 0 && creaturesInLibrary.size() > 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilAbility;
|
|
||||||
import forge.ai.ComputerUtilCost;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
@@ -27,10 +23,11 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
|
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
if (abCost != null) {
|
if (abCost != null) {
|
||||||
// AI currently disabled for these costs
|
// AI currently disabled for these costs
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +50,11 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
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 (tgt != null) {
|
||||||
if (!discardTargetAI(ai, sa)) {
|
if (!discardTargetAI(ai, sa)) {
|
||||||
@@ -84,7 +85,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
if (sa.hasParam("NumCards")) {
|
if (sa.hasParam("NumCards")) {
|
||||||
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
|
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
|
||||||
// Set PayX here to maximum value.
|
// 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());
|
.getCardsIn(ZoneType.Hand).size());
|
||||||
if (cardsToDiscard < 1) {
|
if (cardsToDiscard < 1) {
|
||||||
return false;
|
return false;
|
||||||
@@ -120,7 +121,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
|
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -139,7 +140,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (!discardTargetAI(ai, sa)) {
|
if (!discardTargetAI(ai, sa)) {
|
||||||
if (mandatory && sa.canTarget(opp)) {
|
if (mandatory && sa.canTarget(opp)) {
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
@@ -160,7 +161,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
|
||||||
// Set PayX here to maximum value.
|
// 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());
|
.getCardsIn(ZoneType.Hand).size());
|
||||||
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
|
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -19,7 +20,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Random r = MyRandom.getRandom();
|
final Random r = MyRandom.getRandom();
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
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 TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
@@ -83,7 +84,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ai.getOpponent());
|
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||||
}
|
}
|
||||||
|
|
||||||
return randomReturn;
|
return randomReturn;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +126,16 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
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
|
// Don't use draw abilities before main 2 if possible
|
||||||
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||||
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||||
@@ -204,6 +214,7 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final boolean drawback = sa.getParent() != null;
|
final boolean drawback = sa.getParent() != null;
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||||
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
|
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
|
||||||
@@ -241,10 +252,8 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Logic for cards that require special handling
|
// Logic for cards that require special handling
|
||||||
if (sa.hasParam("AILogic")) {
|
if ("YawgmothsBargain".equals(logic)) {
|
||||||
if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) {
|
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
||||||
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic logic for all cards that do not need any special handling
|
// Generic logic for all cards that do not need any special handling
|
||||||
@@ -312,6 +321,13 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
return true;
|
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);
|
boolean aiTarget = sa.canTarget(ai);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
@@ -15,8 +16,7 @@ import forge.ai.SpellApiToAi;
|
|||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -48,6 +48,21 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
randomReturn = true;
|
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")) {
|
} else if (logic.equals("Fog")) {
|
||||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||||
return false;
|
return false;
|
||||||
@@ -179,9 +194,7 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldPlay) {
|
return shouldPlay;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
|
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
|
||||||
if (game.getStack().isEmpty()) {
|
if (game.getStack().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -246,6 +259,12 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
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
|
} else { //no AILogic
|
||||||
return false;
|
return false;
|
||||||
@@ -281,4 +300,35 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
return randomReturn;
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardPredicates;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.util.Aggregates;
|
||||||
|
|
||||||
public class FogAi extends SpellAbilityAi {
|
public class FogAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -34,6 +38,25 @@ public class FogAi extends SpellAbilityAi {
|
|||||||
return false;
|
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
|
// Cast it if life is in danger
|
||||||
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
||||||
}
|
}
|
||||||
@@ -58,7 +81,7 @@ public class FogAi extends SpellAbilityAi {
|
|||||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
final Game game = aiPlayer.getGame();
|
final Game game = aiPlayer.getGame();
|
||||||
boolean chance;
|
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);
|
chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE);
|
||||||
} else {
|
} else {
|
||||||
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -8,7 +9,7 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
public class GameLossAi extends SpellAbilityAi {
|
public class GameLossAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (opp.cantLose()) {
|
if (opp.cantLose()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -34,14 +35,14 @@ public class GameLossAi extends SpellAbilityAi {
|
|||||||
// (Final Fortune would need to attach it's delayed trigger to a
|
// (Final Fortune would need to attach it's delayed trigger to a
|
||||||
// specific turn, which can't be done yet)
|
// specific turn, which can't be done yet)
|
||||||
|
|
||||||
if (!mandatory && ai.getOpponent().cantLose()) {
|
if (!mandatory && ComputerUtil.getOpponentFor(ai).cantLose()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ai.getOpponent());
|
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -22,7 +23,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
final Random r = MyRandom.getRandom();
|
final Random r = MyRandom.getRandom();
|
||||||
final int myLife = aiPlayer.getLife();
|
final int myLife = aiPlayer.getLife();
|
||||||
Player opponent = aiPlayer.getOpponent();
|
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
||||||
final int hLife = opponent.getLife();
|
final int hLife = opponent.getLife();
|
||||||
|
|
||||||
if (!aiPlayer.canGainLife()) {
|
if (!aiPlayer.canGainLife()) {
|
||||||
@@ -78,7 +79,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
final boolean mandatory) {
|
final boolean mandatory) {
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostRemoveCounter;
|
||||||
|
import forge.game.cost.CostSacrifice;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -35,7 +38,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (!lifeCritical) {
|
if (!lifeCritical) {
|
||||||
// return super.willPayCosts(ai, sa, cost, source);
|
// return super.willPayCosts(ai, sa, cost, source);
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
|
||||||
@@ -60,7 +63,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
|||||||
skipCheck |= ComputerUtilCost.isSacrificeSelfCost(cost) && !source.isCreature();
|
skipCheck |= ComputerUtilCost.isSacrificeSelfCost(cost) && !source.isCreature();
|
||||||
|
|
||||||
if (!skipCheck) {
|
if (!skipCheck) {
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +83,23 @@ public class LifeGainAi extends SpellAbilityAi {
|
|||||||
lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE)
|
lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE)
|
||||||
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
|
&& 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
|
// Don't use lifegain before main 2 if possible
|
||||||
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
|
||||||
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilAbility;
|
import forge.ai.ComputerUtilAbility;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -22,7 +23,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
// Ability_Cost abCost = sa.getPayCosts();
|
// Ability_Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final int myLife = ai.getLife();
|
final int myLife = ai.getLife();
|
||||||
final Player opponent = ai.getOpponent();
|
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||||
final int hlife = opponent.getLife();
|
final int hlife = opponent.getLife();
|
||||||
final String amountStr = sa.getParam("LifeAmount");
|
final String amountStr = sa.getParam("LifeAmount");
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (amount < myLife) {
|
if (amount <= myLife) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final int myLife = ai.getLife();
|
final int myLife = ai.getLife();
|
||||||
final Player opponent = ai.getOpponent();
|
final Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||||
final int hlife = opponent.getLife();
|
final int hlife = opponent.getLife();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import forge.card.ColorSet;
|
|||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.cost.CostRemoveCounter;
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -110,13 +109,28 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
|
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
|
||||||
final Card host = sa.getHostCard();
|
final Card host = sa.getHostCard();
|
||||||
|
|
||||||
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true);
|
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
|
||||||
int numManaSrcs = manaSources.size();
|
int numManaSrcs = manaSources.size();
|
||||||
int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
|
int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
|
||||||
manaReceived *= sa.getParam("Produced").split(" ").length;
|
manaReceived *= sa.getParam("Produced").split(" ").length;
|
||||||
|
|
||||||
int selfCost = sa.getPayCosts().getCostMana() != null ? sa.getPayCosts().getCostMana().getMana().getCMC() : 0;
|
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;
|
int searchCMC = numManaSrcs - selfCost + manaReceived;
|
||||||
|
|
||||||
if ("X".equals(sa.getParam("Produced"))) {
|
if ("X".equals(sa.getParam("Produced"))) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -60,7 +62,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
|||||||
boolean chance = false;
|
boolean chance = false;
|
||||||
|
|
||||||
if (abTgt != null) {
|
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.getTargetableCards(list, sa);
|
||||||
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
|
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -- not used anymore after Ixalan (Planeswalkers are now legendary, not unique by subtype) --
|
||||||
if (card.isPlaneswalker()) {
|
if (card.isPlaneswalker()) {
|
||||||
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||||
CardPredicates.Presets.PLANEWALKERS);
|
CardPredicates.Presets.PLANEWALKERS);
|
||||||
@@ -75,7 +77,7 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (card.getType().hasSupertype(Supertype.World)) {
|
if (card.getType().hasSupertype(Supertype.World)) {
|
||||||
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
|
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
|
||||||
@@ -175,22 +177,44 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
if (!hasCard) {
|
if (!hasCard) {
|
||||||
dontCast = true;
|
dontCast = true;
|
||||||
}
|
}
|
||||||
} else if (param.equals("MaxControlled")) {
|
} else if (param.startsWith("MaxControlled")) {
|
||||||
// Only cast unless there are X or more cards like this on the battlefield under AI control already
|
// 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();
|
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)) {
|
if (numControlled >= Integer.parseInt(value)) {
|
||||||
dontCast = true;
|
dontCast = true;
|
||||||
}
|
}
|
||||||
} else if (param.equals("NumManaSources")) {
|
} else if (param.equals("NumManaSources")) {
|
||||||
// Only cast if there are X or more mana sources controlled by the AI
|
// 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)) {
|
if (m.size() < Integer.parseInt(value)) {
|
||||||
dontCast = true;
|
dontCast = true;
|
||||||
}
|
}
|
||||||
} else if (param.equals("NumManaSourcesNextTurn")) {
|
} else if (param.equals("NumManaSourcesNextTurn")) {
|
||||||
// Only cast if there are X or more mana sources controlled by the AI *or*
|
// 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
|
// 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;
|
int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0;
|
||||||
if (card.getName().equals("Illusions of Grandeur")) {
|
if (card.getName().equals("Illusions of Grandeur")) {
|
||||||
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.
|
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
|
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||||
*/
|
*/
|
||||||
@Override
|
@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) {
|
Player targetedPlayer) {
|
||||||
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
@@ -31,7 +32,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
|
|||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|
||||||
List<Card> list =
|
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
|
// AI won't try to grab cards that are filtered out of AI decks on
|
||||||
// purpose
|
// purpose
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
|||||||
@@ -147,9 +147,10 @@ public class ProtectAi extends SpellAbilityAi {
|
|||||||
if (s==null) {
|
if (s==null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||||
Combat combat = ai.getGame().getCombat();
|
Combat combat = ai.getGame().getCombat();
|
||||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, ai.getOpponent(), combat, true);
|
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
||||||
float ratio = 1.0f * dmg / ai.getOpponent().getLife();
|
float ratio = 1.0f * dmg / opponent.getLife();
|
||||||
Random r = MyRandom.getRandom();
|
Random r = MyRandom.getRandom();
|
||||||
return r.nextFloat() < ratio;
|
return r.nextFloat() < ratio;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class ProtectAllAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
package forge.ai.ability;
|
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.Predicate;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import forge.ai.*;
|
||||||
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.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.card.CardPredicates.Presets;
|
||||||
import forge.game.card.CardUtil;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.card.CounterType;
|
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
|
import forge.game.cost.CostRemoveCounter;
|
||||||
import forge.game.cost.CostTapType;
|
import forge.game.cost.CostTapType;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -35,6 +22,11 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.spellability.SpellAbilityRestriction;
|
import forge.game.spellability.SpellAbilityRestriction;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
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 {
|
public class PumpAi extends PumpAiBase {
|
||||||
|
|
||||||
@@ -82,16 +74,17 @@ public class PumpAi extends PumpAiBase {
|
|||||||
System.err.println("MoveCounter AiLogic without MoveCounter SubAbility!");
|
System.err.println("MoveCounter AiLogic without MoveCounter SubAbility!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if ("Aristocrat".equals(aiLogic)) {
|
||||||
|
return doAristocratLogic(sa, ai);
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.checkAiLogic(ai, sa, aiLogic);
|
return super.checkAiLogic(ai, sa, aiLogic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
|
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
|
||||||
final String logic) {
|
final String logic) {
|
||||||
// special Phase check for MoveCounter
|
// special Phase check for various AI logics
|
||||||
if (logic.equals("MoveCounter")) {
|
if (logic.equals("MoveCounter")) {
|
||||||
if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) {
|
if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -101,6 +94,11 @@ public class PumpAi extends PumpAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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);
|
return super.checkPhaseRestrictions(ai, sa, ph);
|
||||||
}
|
}
|
||||||
@@ -112,7 +110,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
|
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
|
||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +139,9 @@ public class PumpAi extends PumpAiBase {
|
|||||||
final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic);
|
final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic);
|
||||||
final boolean isBerserk = "Berserk".equals(aiLogic);
|
final boolean isBerserk = "Berserk".equals(aiLogic);
|
||||||
|
|
||||||
if ("MoveCounter".equals(aiLogic)) {
|
if ("Pummeler".equals(aiLogic)) {
|
||||||
|
return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa);
|
||||||
|
} else if ("MoveCounter".equals(aiLogic)) {
|
||||||
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
|
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
|
||||||
|
|
||||||
if (moveSA == null) {
|
if (moveSA == null) {
|
||||||
@@ -359,9 +359,12 @@ public class PumpAi extends PumpAiBase {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!card.getController().isOpponentOf(ai)
|
if (!card.getController().isOpponentOf(ai)) {
|
||||||
&& ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
||||||
return true;
|
return true;
|
||||||
|
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -371,6 +374,21 @@ public class PumpAi extends PumpAiBase {
|
|||||||
return false;
|
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;
|
return true;
|
||||||
} // pumpPlayAI()
|
} // pumpPlayAI()
|
||||||
|
|
||||||
@@ -461,6 +479,22 @@ 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;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
if (ComputerUtil.activateForCost(sa, ai)) {
|
if (ComputerUtil.activateForCost(sa, ai)) {
|
||||||
return pumpMandatoryTarget(ai, sa);
|
return pumpMandatoryTarget(ai, sa);
|
||||||
@@ -694,8 +728,6 @@ public class PumpAi extends PumpAiBase {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} // pumpDrawbackAI()
|
} // pumpDrawbackAI()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
@@ -704,4 +736,123 @@ public class PumpAi extends PumpAiBase {
|
|||||||
//and the pump isn't mandatory
|
//and the pump isn't mandatory
|
||||||
return true;
|
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 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) {
|
||||||
|
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 = Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus));
|
||||||
|
|
||||||
|
if (numCreatsToSac > 1) { // probably not worth sacrificing too much
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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 = (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 = 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final Combat combat = game.getCombat();
|
final Combat combat = game.getCombat();
|
||||||
final PhaseHandler ph = game.getPhaseHandler();
|
final PhaseHandler ph = game.getPhaseHandler();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final int newPower = card.getNetCombatDamage() + attack;
|
final int newPower = card.getNetCombatDamage() + attack;
|
||||||
//int defense = getNumDefense(sa);
|
//int defense = getNumDefense(sa);
|
||||||
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
||||||
@@ -207,6 +207,19 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
|
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)))
|
if (ph.isPlayerTurn(opp) || !(CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
|
||||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||||
|| newPower <= 0
|
|| newPower <= 0
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public class PumpAllAi extends PumpAiBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (abCost != null && source.hasSVar("AIPreference")) {
|
if (abCost != null && source.hasSVar("AIPreference")) {
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, true)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ public class PumpAllAi extends PumpAiBase {
|
|||||||
valid = sa.getParam("ValidCards");
|
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 comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
|
||||||
CardCollection human = CardLists.getValidCards(opp.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;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -27,7 +28,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
|||||||
// ability is targeted
|
// ability is targeted
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
|
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
|
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
|
||||||
|
|
||||||
if (!canTgtHuman) {
|
if (!canTgtHuman) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
|
|
||||||
import forge.ai.AiController;
|
import forge.ai.AiController;
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.PlayerControllerAi;
|
import forge.ai.PlayerControllerAi;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -17,7 +18,7 @@ public class RepeatAi extends SpellAbilityAi {
|
|||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
if (!opp.canBeTargetedBy(sa)) {
|
if (!opp.canBeTargetedBy(sa)) {
|
||||||
@@ -48,7 +49,7 @@ public class RepeatAi extends SpellAbilityAi {
|
|||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
|
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (sa.canTarget(opp)) {
|
if (sa.canTarget(opp)) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
|
|
||||||
public class RepeatEachAi extends SpellAbilityAi {
|
public class RepeatEachAi extends SpellAbilityAi {
|
||||||
@@ -85,7 +86,7 @@ public class RepeatEachAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
String svar = repeat.getSVar(repeat.getParam("LifeAmount"));
|
String svar = repeat.getSVar(repeat.getParam("LifeAmount"));
|
||||||
// replace RememberedPlayerCtrl with YouCtrl
|
// 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
|
// Currently all Cards with that are affect all player, including AI
|
||||||
if (aiPlayer.canLoseLife()) {
|
if (aiPlayer.canLoseLife()) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
public class RollPlanarDiceAi extends SpellAbilityAi {
|
public class RollPlanarDiceAi extends SpellAbilityAi {
|
||||||
/* (non-Javadoc)
|
/* (non-Javadoc)
|
||||||
@@ -107,7 +108,7 @@ public class RollPlanarDiceAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -59,7 +60,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final boolean destroy = sa.hasParam("Destroy");
|
final boolean destroy = sa.hasParam("Destroy");
|
||||||
|
|
||||||
Player opp = ai.getOpponent();
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (!opp.canBeTargetedBy(sa)) {
|
if (!opp.canBeTargetedBy(sa)) {
|
||||||
@@ -72,7 +73,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
|||||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
|
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
|
||||||
|
|
||||||
List<Card> list =
|
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) {
|
for (Card c : list) {
|
||||||
if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) {
|
if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
@@ -12,6 +13,7 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
@@ -34,11 +36,11 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
|||||||
// Set PayX here to maximum value.
|
// Set PayX here to maximum value.
|
||||||
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||||
source.setSVar("PayX", Integer.toString(xPay));
|
source.setSVar("PayX", Integer.toString(xPay));
|
||||||
valid = valid.replace("X", Integer.toString(xPay));
|
valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection humanlist =
|
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 =
|
CardCollection computerlist =
|
||||||
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.Card.SplitCMCMode;
|
import forge.game.card.Card.SplitCMCMode;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
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.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -18,8 +15,6 @@ import forge.util.MyRandom;
|
|||||||
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
|
||||||
|
|
||||||
public class ScryAi extends SpellAbilityAi {
|
public class ScryAi extends SpellAbilityAi {
|
||||||
|
|
||||||
/* (non-Javadoc)
|
/* (non-Javadoc)
|
||||||
@@ -49,6 +44,18 @@ public class ScryAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
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
|
// in the playerturn Scry should only be done in Main1 or in upkeep if able
|
||||||
if (ph.isPlayerTurn(ai)) {
|
if (ph.isPlayerTurn(ai)) {
|
||||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.card.CardSplitType;
|
import forge.card.CardSplitType;
|
||||||
|
import forge.card.CardStateName;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.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.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
@@ -67,6 +63,19 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
if (aiLogic.equals("CheckCondition")) {
|
||||||
|
SpellAbility saCopy = sa.copy();
|
||||||
|
saCopy.setActivatingPlayer(aiPlayer);
|
||||||
|
return saCopy.getConditions().areMet(saCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.checkAiLogic(aiPlayer, sa, aiLogic);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||||
// Gross generalization, but this always considers alternate
|
// Gross generalization, but this always considers alternate
|
||||||
@@ -78,6 +87,7 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||||
final String mode = sa.getParam("Mode");
|
final String mode = sa.getParam("Mode");
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
final Game game = source.getGame();
|
final Game game = source.getGame();
|
||||||
|
|
||||||
if("Transform".equals(mode)) {
|
if("Transform".equals(mode)) {
|
||||||
@@ -86,7 +96,7 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
if (source.hasKeyword("CARDNAME can't transform")) {
|
if (source.hasKeyword("CARDNAME can't transform")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return shouldTransformCard(source, ai, ph);
|
return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
|
||||||
} else {
|
} else {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -108,7 +118,7 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (final Card c : list) {
|
for (final Card c : list) {
|
||||||
if (shouldTransformCard(c, ai, ph)) {
|
if (shouldTransformCard(c, ai, ph) || "Always".equals(logic)) {
|
||||||
sa.getTargets().add(c);
|
sa.getTargets().add(c);
|
||||||
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
||||||
break;
|
break;
|
||||||
@@ -126,7 +136,7 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return shouldTurnFace(list.get(0), ai, ph);
|
return shouldTurnFace(list.get(0), ai, ph) || "Always".equals(logic);
|
||||||
} else {
|
} else {
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -139,7 +149,7 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (final Card c : list) {
|
for (final Card c : list) {
|
||||||
if (shouldTurnFace(c, ai, ph)) {
|
if (shouldTurnFace(c, ai, ph) || "Always".equals(logic)) {
|
||||||
sa.getTargets().add(c);
|
sa.getTargets().add(c);
|
||||||
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
|
||||||
break;
|
break;
|
||||||
@@ -165,13 +175,27 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
Card transformed = CardUtil.getLKICopy(card);
|
Card transformed = CardUtil.getLKICopy(card);
|
||||||
transformed.getCurrentState().copyFrom(card, card.getAlternateState());
|
transformed.getCurrentState().copyFrom(card, card.getAlternateState());
|
||||||
transformed.updateStateForView();
|
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);
|
return compareCards(card, transformed, ai, ph);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
|
private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
|
||||||
if (card.isFaceDown()) {
|
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
|
// non-permanent facedown can't be turned face up
|
||||||
if (!card.getRules().getType().isPermanent()) {
|
if (!card.getRules().getType().isPermanent()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -241,4 +265,9 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
// but for more cleaner way use Evaluate for check
|
// but for more cleaner way use Evaluate for check
|
||||||
return valueCard <= valueTransformed;
|
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.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CounterType;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostPart;
|
||||||
|
import forge.game.cost.CostRemoveCounter;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
@@ -58,6 +61,20 @@ public class TapAi extends TapAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
sa.resetTargets();
|
||||||
if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
|
if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) {
|
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();
|
final Game game = ai.getGame();
|
||||||
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
||||||
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);
|
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
|||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCombat;
|
import forge.ai.ComputerUtilCombat;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -30,7 +31,7 @@ public class TapAllAi extends SpellAbilityAi {
|
|||||||
// or during upkeep/begin combat?
|
// or during upkeep/begin combat?
|
||||||
|
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
|
|
||||||
if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
||||||
@@ -126,8 +127,8 @@ public class TapAllAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
sa.getTargets().add(ai.getOpponent());
|
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||||
validTappables = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
|
validTappables = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mandatory) {
|
if (mandatory) {
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
package forge.ai.ability;
|
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.base.Predicate;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.ai.SpellApiToAi;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameEntity;
|
import forge.game.GameEntity;
|
||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
|
||||||
import forge.game.card.CardFactory;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.CostPart;
|
import forge.game.cost.CostPart;
|
||||||
import forge.game.cost.CostPutCounter;
|
import forge.game.cost.CostPutCounter;
|
||||||
@@ -38,6 +25,11 @@ import forge.game.trigger.TriggerHandler;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.item.PaperToken;
|
import forge.item.PaperToken;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -179,7 +171,7 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
|
|
||||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||||
return false; // prevent infinite tokens?
|
return false; // prevent infinite tokens?
|
||||||
@@ -233,7 +225,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 +268,13 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
num = (num == null) ? "1" : num;
|
num = (num == null) ? "1" : num;
|
||||||
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
|
||||||
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
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));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||||
// only care about saving single creature for now
|
// only care about saving single creature for now
|
||||||
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
|
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
|
||||||
ComputerUtilCard.sortByEvaluateCreature(list);
|
ComputerUtilCard.sortByEvaluateCreature(list);
|
||||||
list.add(token);
|
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));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
|
||||||
if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
|
if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
|
||||||
&& list.contains(token)) {
|
&& list.contains(token)) {
|
||||||
@@ -278,7 +292,7 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (tgt.canOnlyTgtOpponent()) {
|
if (tgt.canOnlyTgtOpponent()) {
|
||||||
sa.getTargets().add(ai.getOpponent());
|
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
|
||||||
} else {
|
} else {
|
||||||
sa.getTargets().add(ai);
|
sa.getTargets().add(ai);
|
||||||
}
|
}
|
||||||
@@ -414,7 +428,7 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final List<String> imageNames = new ArrayList<String>(1);
|
final List<String> imageNames = new ArrayList<String>(1);
|
||||||
if (tokenImage.equals("")) {
|
if (tokenImage.equals("")) {
|
||||||
imageNames.add(PaperToken.makeTokenFileName(colorDesc.replace(" ", ""), tokenPower, tokenToughness, tokenName));
|
imageNames.add(PaperToken.makeTokenFileName(TextUtil.fastReplace(colorDesc, " ", ""), tokenPower, tokenToughness, tokenName));
|
||||||
} else {
|
} else {
|
||||||
imageNames.add(0, tokenImage);
|
imageNames.add(0, tokenImage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi {
|
|||||||
valid = sa.getParam("ValidCards");
|
valid = sa.getParam("ValidCards");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -66,7 +67,7 @@ public class UnattachAllAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Card card = sa.getHostCard();
|
final Card card = sa.getHostCard();
|
||||||
final Player opp = ai.getOpponent();
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
// Check if there are any valid targets
|
// Check if there are any valid targets
|
||||||
List<GameObject> targets = new ArrayList<GameObject>();
|
List<GameObject> targets = new ArrayList<GameObject>();
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
@@ -13,6 +11,7 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostTap;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerCollection;
|
import forge.game.player.PlayerCollection;
|
||||||
@@ -20,6 +19,8 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class UntapAi extends SpellAbilityAi {
|
public class UntapAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||||
@@ -132,7 +133,7 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
Player targetController = ai;
|
Player targetController = ai;
|
||||||
|
|
||||||
if (sa.isCurse()) {
|
if (sa.isCurse()) {
|
||||||
targetController = ai.getOpponent();
|
targetController = ComputerUtil.getOpponentFor(ai);
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
|
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
|
||||||
@@ -148,6 +149,24 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
|
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
|
||||||
untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
|
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
|
||||||
Card choice = null;
|
Card choice = null;
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ public class GameCopier {
|
|||||||
newCard.setSemiPermanentToughnessBoost(c.getSemiPermanentToughnessBoost());
|
newCard.setSemiPermanentToughnessBoost(c.getSemiPermanentToughnessBoost());
|
||||||
newCard.setDamage(c.getDamage());
|
newCard.setDamage(c.getDamage());
|
||||||
|
|
||||||
newCard.setChangedCardTypes(c.getChangedCardTypes());
|
newCard.setChangedCardTypes(c.getChangedCardTypesMap());
|
||||||
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
|
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
|
||||||
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
|
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
|
||||||
for (String kw : c.getHiddenExtrinsicKeywords())
|
for (String kw : c.getHiddenExtrinsicKeywords())
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetChoices;
|
import forge.game.spellability.TargetChoices;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
public class GameSimulator {
|
public class GameSimulator {
|
||||||
public static boolean COPY_STACK = false;
|
public static boolean COPY_STACK = false;
|
||||||
@@ -136,7 +137,7 @@ public class GameSimulator {
|
|||||||
System.err.println(sa.getDescription() + "->" + desc);
|
System.err.println(sa.getDescription() + "->" + desc);
|
||||||
}
|
}
|
||||||
// FIXME: This is a hack that makes testManifest pass - figure out why it's needed.
|
// FIXME: This is a hack that makes testManifest pass - figure out why it's needed.
|
||||||
desc = desc.replace("Unmanifest {0}", "Unmanifest no cost");
|
desc = TextUtil.fastReplace(desc, "Unmanifest {0}", "Unmanifest no cost");
|
||||||
for (SpellAbility cSa : hostCard.getSpellAbilities()) {
|
for (SpellAbility cSa : hostCard.getSpellAbilities()) {
|
||||||
if (desc.equals(cSa.getDescription())) {
|
if (desc.equals(cSa.getDescription())) {
|
||||||
return cSa;
|
return cSa;
|
||||||
@@ -204,7 +205,7 @@ public class GameSimulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Support multiple opponents.
|
// TODO: Support multiple opponents.
|
||||||
Player opponent = aiPlayer.getOpponent();
|
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
||||||
resolveStack(simGame, opponent);
|
resolveStack(simGame, opponent);
|
||||||
|
|
||||||
// TODO: If this is during combat, before blockers are declared,
|
// TODO: If this is during combat, before blockers are declared,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import forge.ai.CreatureEvaluator;
|
|||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
import forge.game.combat.Combat;
|
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import forge.game.spellability.AbilitySub;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityCondition;
|
import forge.game.spellability.SpellAbilityCondition;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
public class SpellAbilityPicker {
|
public class SpellAbilityPicker {
|
||||||
private Game game;
|
private Game game;
|
||||||
@@ -285,7 +286,7 @@ public class SpellAbilityPicker {
|
|||||||
saString = sa.toString();
|
saString = sa.toString();
|
||||||
String cardName = sa.getHostCard().getName();
|
String cardName = sa.getHostCard().getName();
|
||||||
if (!cardName.isEmpty()) {
|
if (!cardName.isEmpty()) {
|
||||||
saString = saString.replace(cardName, "<$>");
|
saString = TextUtil.fastReplace(saString, cardName, "<$>");
|
||||||
}
|
}
|
||||||
if (saString.length() > 40) {
|
if (saString.length() > 40) {
|
||||||
saString = saString.substring(0, 40) + "...";
|
saString = saString.substring(0, 40) + "...";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>forge</artifactId>
|
<artifactId>forge</artifactId>
|
||||||
<groupId>forge</groupId>
|
<groupId>forge</groupId>
|
||||||
<version>1.6.1</version>
|
<version>1.6.3</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>forge-core</artifactId>
|
<artifactId>forge-core</artifactId>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package forge;
|
|||||||
import forge.item.*;
|
import forge.item.*;
|
||||||
import forge.util.FileUtil;
|
import forge.util.FileUtil;
|
||||||
import forge.util.ImageUtil;
|
import forge.util.ImageUtil;
|
||||||
|
import forge.util.TextUtil;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -104,17 +105,17 @@ public final class ImageKeys {
|
|||||||
// AE -> Ae and Ae -> AE for older cards with different file names
|
// AE -> Ae and Ae -> AE for older cards with different file names
|
||||||
// on case-sensitive file systems
|
// on case-sensitive file systems
|
||||||
if (filename.contains("Ae")) {
|
if (filename.contains("Ae")) {
|
||||||
file = findFile(dir, filename.replace("Ae", "AE"));
|
file = findFile(dir, TextUtil.fastReplace(filename, "Ae", "AE"));
|
||||||
if (file != null) { return file; }
|
if (file != null) { return file; }
|
||||||
} else if (filename.contains("AE")) {
|
} else if (filename.contains("AE")) {
|
||||||
file = findFile(dir, filename.replace("AE", "Ae"));
|
file = findFile(dir, TextUtil.fastReplace(filename, "AE", "Ae"));
|
||||||
if (file != null) { return file; }
|
if (file != null) { return file; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// some S00 cards are really part of 6ED
|
// some S00 cards are really part of 6ED
|
||||||
String s2kAlias = getSetFolder("S00");
|
String s2kAlias = getSetFolder("S00");
|
||||||
if (filename.startsWith(s2kAlias)) {
|
if (filename.startsWith(s2kAlias)) {
|
||||||
file = findFile(dir, filename.replace(s2kAlias, getSetFolder("6ED")));
|
file = findFile(dir, TextUtil.fastReplace(filename, s2kAlias, getSetFolder("6ED")));
|
||||||
if (file != null) { return file; }
|
if (file != null) { return file; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -637,9 +637,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cardRarity == CardRarity.Unknown) {
|
if (cardRarity == CardRarity.Unknown) {
|
||||||
System.err.println(String.format("An unknown card found when loading Forge decks: \"%s\" Forge does not know of such a card's existence. Have you mistyped the card name?", cardName));
|
System.err.println(TextUtil.concatWithSpace("An unknown card found when loading Forge decks:", TextUtil.enclosedDoubleQuote(cardName), "Forge does not know of such a card's existence. Have you mistyped the card name?"));
|
||||||
} else {
|
} else {
|
||||||
System.err.println(String.format("An unsupported card was requested: \"%s\" from \"%s\" set. We're sorry, but you cannot use this card yet.", request.cardName, cardEdition.getName()));
|
System.err.println(TextUtil.concatWithSpace("An unsupported card was requested:", TextUtil.enclosedDoubleQuote(request.cardName), "from", TextUtil.enclosedDoubleQuote(cardEdition.getName()), "set. We're sorry, but you cannot use this card yet."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);
|
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ import forge.card.CardDb.SetPreference;
|
|||||||
import forge.deck.CardPool;
|
import forge.deck.CardPool;
|
||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
import forge.item.SealedProduct;
|
import forge.item.SealedProduct;
|
||||||
import forge.util.Aggregates;
|
import forge.util.*;
|
||||||
import forge.util.FileSection;
|
|
||||||
import forge.util.FileUtil;
|
|
||||||
import forge.util.IItemReader;
|
|
||||||
import forge.util.MyRandom;
|
|
||||||
import forge.util.storage.StorageBase;
|
import forge.util.storage.StorageBase;
|
||||||
import forge.util.storage.StorageReaderBase;
|
import forge.util.storage.StorageReaderBase;
|
||||||
import forge.util.storage.StorageReaderFolder;
|
import forge.util.storage.StorageReaderFolder;
|
||||||
@@ -307,7 +303,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
|||||||
enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH));
|
enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH));
|
||||||
} catch (IllegalArgumentException ignored) {
|
} catch (IllegalArgumentException ignored) {
|
||||||
// ignore; type will get UNKNOWN
|
// ignore; type will get UNKNOWN
|
||||||
System.err.println(String.format("Ignoring unknown type in set definitions: name: %s; type: %s", res.name, type));
|
System.err.println(TextUtil.concatWithSpace("Ignoring unknown type in set definitions: name:", TextUtil.addSuffix(res.name, ";"), "type:", type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.type = enumType;
|
res.type = enumType;
|
||||||
@@ -393,7 +389,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
|||||||
public CardEdition getEditionByCodeOrThrow(final String code) {
|
public CardEdition getEditionByCodeOrThrow(final String code) {
|
||||||
final CardEdition set = this.get(code);
|
final CardEdition set = this.get(code);
|
||||||
if (null == set) {
|
if (null == set) {
|
||||||
throw new RuntimeException(String.format("Edition with code '%s' not found", code));
|
throw new RuntimeException(TextUtil.concatWithSpace("Edition with code", TextUtil.enclosedSingleQuote(code), "not found"));
|
||||||
}
|
}
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forge.card;
|
package forge.card;
|
||||||
|
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
|
import forge.util.TextUtil;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -89,7 +90,7 @@ final class CardFace implements ICardFace {
|
|||||||
void setPtText(String value) {
|
void setPtText(String value) {
|
||||||
final int slashPos = value.indexOf('/');
|
final int slashPos = value.indexOf('/');
|
||||||
if (slashPos == -1) {
|
if (slashPos == -1) {
|
||||||
throw new RuntimeException(String.format("Creature '%s' has bad p/t stats", this.getName()));
|
throw new RuntimeException(TextUtil.concatWithSpace("Creature", TextUtil.enclosedSingleQuote(this.getName()),"has bad p/t stats"));
|
||||||
}
|
}
|
||||||
boolean negPower = value.charAt(0) == '-';
|
boolean negPower = value.charAt(0) == '-';
|
||||||
boolean negToughness = value.charAt(slashPos + 1) == '-';
|
boolean negToughness = value.charAt(slashPos + 1) == '-';
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package forge.card;
|
|||||||
|
|
||||||
import java.util.StringTokenizer;
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
import forge.util.TextUtil;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import forge.card.mana.IParserManaCost;
|
import forge.card.mana.IParserManaCost;
|
||||||
@@ -217,10 +218,10 @@ public final class CardRules implements ICardCharacteristics {
|
|||||||
public void setVanguardProperties(String pt) {
|
public void setVanguardProperties(String pt) {
|
||||||
final int slashPos = pt == null ? -1 : pt.indexOf('/');
|
final int slashPos = pt == null ? -1 : pt.indexOf('/');
|
||||||
if (slashPos == -1) {
|
if (slashPos == -1) {
|
||||||
throw new RuntimeException(String.format("Vanguard '%s' has bad hand/life stats", this.getName()));
|
throw new RuntimeException(TextUtil.concatWithSpace("Vanguard", TextUtil.enclosedSingleQuote(this.getName()), "has bad hand/life stats"));
|
||||||
}
|
}
|
||||||
this.deltaHand = Integer.parseInt(pt.substring(0, slashPos).replace("+", ""));
|
this.deltaHand = Integer.parseInt(TextUtil.fastReplace(pt.substring(0, slashPos), "+", ""));
|
||||||
this.deltaLife = Integer.parseInt(pt.substring(slashPos+1).replace("+", ""));
|
this.deltaLife = Integer.parseInt(TextUtil.fastReplace(pt.substring(slashPos+1), "+", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Downloadable image
|
// Downloadable image
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ public enum CardStateName {
|
|||||||
Meld,
|
Meld,
|
||||||
Cloned,
|
Cloned,
|
||||||
LeftSplit,
|
LeftSplit,
|
||||||
RightSplit;
|
RightSplit,
|
||||||
|
OriginalText; // backup state for cards like Volrath's Shapeshifter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Write javadoc for this method.
|
* TODO: Write javadoc for this method.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user